3D の運動学 — 着順から「走り」を逆算する
3D シミュレーションの馬は物理法則で走っているのではなく、「決まったゴール時刻に必ず着く」ように設計された式で動いています。その式の作り方と性質 (単調性・連続性・最大リード) を、微分を使って確かめます。
- Zig の「出来」から各馬の走破タイム を決める式
- 発走直後の加速を表す「時間のゆがみ」
- 脚質を表す 2 次式 と、それが必ず前に進む (単調) 条件
- 逃げ馬と追込馬の差が最大になる時刻と、その大きさ
- ゴール時刻の補間、着差 (馬身) の換算、ゴール後のなめらかな減速
設計の考え方 — 結果から過程を作る
ふつうの物理シミュレーションは「力 → 加速度 → 速度 → 位置」と計算し、着順は結果として決まります。Keiba Vision の 3D は逆で、
- 着順と着差は 先に Zig のモデル が決める
- その結果に 必ず一致する ように、途中の動きを式で作る
という順番です。こうすると、物理の細かい調整なしに「確率モデルどおりの結果」と「それらしい展開」を両立できます。
走破タイム
Zig の race_once が返す各馬の出来を 、最も良い出来を とします。
const vAvg = isBanei ? 1.7 : this.dirt ? 15.9 : 16.6;
const base = this.distance / vAvg;
// ...
const T = base * (1 + (best - scores[i]) * 0.12);- … 平均速度。芝 16.6 m/s、ダート 15.9 m/s、ばんえい 1.7 m/s
- 1 着馬は (芝 2,000m なら 秒、約 2 分 0 秒 5)
- 出来の差 0.03 は 秒の差 → 約 7.2m → 約 3 馬身
位置の式
progress(h, t) {
const acc = 1.1;
const tw = t - acc * (1 - Math.exp(-t / acc));
const u = tw / (h.T - acc);
if (u < 1) return this.distance * (u + h.shape * u * (1 - u));
// ゴール後はゆっくり減速
const vEnd = this.distance * (1 - h.shape) / (h.T - acc);
const dt = (u - 1) * (h.T - acc);
return this.distance + vEnd * 5 * (1 - Math.exp(-dt / 5));
}これを 3 つの部品に分けて読みます。
部品 1: 時間のゆがみ — 止まった状態から加速する
の微分 (時間あたりの増え方) は
で、 では 0、 が大きくなると 1 に近づきます。つまり「レースの進み具合」が、最初は止まっていて、約 秒でほぼ全速になる、という加速を表しています。
では なので 。加速にかかった分、ちょうど 秒だけ遅れる ことになります。だから の分母は です。ゴール () では
2 分のレースなら なので、誤差は実質 0。どの馬も、ぴったり自分の にゴールします。
部品 2: 脚質の 2 次式
は脚質ごとの定数です。
const STYLE_SHAPE = [0.14, 0.07, -0.05, -0.12]; // 逃げ・先行・差し・追込性質を確かめましょう。
- 、 … スタートとゴールは に関係なく同じ
- … 中間点で、逃げ () は 3.5% 前、追込 () は 3% 後ろ
- 微分 … 序盤 () の速さは 倍、終盤 () は 倍
逃げ馬は「序盤 14% 速く、終盤 14% 遅い」、追込馬は「序盤 12% 遅く、終盤 12% 速い」という、脚質らしい走りになります。
は の 1 次関数なので、 での最小値は端の か でとります。値は と なので、
アプリの は最大でも なので、どの馬も常に前に進みます。もし にすると、どこかで速度が 0 以下になり、馬が止まったり後退したりします。
部品 3: ゴール後の減速
ゴールした瞬間の速度は、 と から
ゴール後の経過時間を として
で、 での位置は 、速度 (微分) は です。位置も速度もゴールの前後でつながる (数学では 級と呼びます) ので、ゴール板を過ぎた瞬間にカクッと減速することがありません。そのあと約 5 秒ずつ指数関数的にゆっくりになり、最終的にゴールから (約 80m) 先で止まります。
逃げ馬のリードはどこまで広がるか
2 頭の走破タイムが同じ () なら で、差は
は で最大値 をとる放物線なので、最大のリードは 、レースのちょうど真ん中 です。
逃げ () と追込 ()、2,000m なら m。実際の計算でも、出来の差 0.03 (追込の方が強い) の場合で最大 126.4m のリードを作り、追込馬がゴール 27m 手前 (119.05 秒) で逆転し、3 馬身差で勝ちます。
実際のレースで先頭と最後方の差は、2,000m の中間点でも 20〜40m (10〜20 馬身) 程度です。今の は展開を分かりやすく見せるために大きめにしてあり、映像としては「逃げ馬が大逃げしている」状態になります。現実に近づけるなら、たとえば最大リードを 30m にしたい場合、 になるよう、 を約 1/4 (逃げ 0.03、追込 −0.03 など) にします。
ゴール時刻の補間
3D は 1 秒間に約 60 回 (フレーム) しか位置を計算しません。4 倍速なら 1 フレームでレース時間が最大 秒進み、その間に馬は約 3.3m (1.4 馬身) 動きます。「ゴールを越えたフレームの時刻」をそのまま使うと、着差がフレーム単位に丸められてしまいます。
h.finishT = h.s > prev ? this.t - dt * (h.s - goal) / (h.s - prev) : this.t;前のフレームの位置 と今の位置 の間を直線で結び、ゴール を通る時刻を求めます (線形補間)。
1 フレームの間、速度はほぼ一定なので、誤差は 1/1000 秒以下になります。
着差 — 秒を馬身に直す
export function margin(gapMeters) {
const l = gapMeters / 2.4;
if (l < 0.08) return "ハナ";
if (l < 0.18) return "アタマ";
if (l < 0.35) return "クビ";
if (l >= 10) return "大差";
const q = Math.round(l * 4) / 4;
// ...
}前の馬とのゴール時刻の差 に、1 着馬の平均速度 を掛けて距離にし、1 馬身 = 2.4m で割ります。
0.35 馬身未満は「ハナ」「アタマ」「クビ」、10 馬身以上は「大差」、その間は 馬身単位に四捨五入 () して「1 1/4」のように表示します。
横の位置 (レーン) — 貪欲法
for (let tries = 0; tries < 20; tries++) {
const clash = placed.some(o => Math.abs(o.s - h.s) < HORSE_LEN * 1.15 && Math.abs(o.lane - want) < 1.25);
if (!clash) break;
want += 1.3;
}
// ...
h.lane += (want - h.lane) * Math.min(1, dt * 1.6);前を走る馬から順に「内ラチから 1m の位置に行きたい。でも前後 2.9m 以内・横 1.25m 以内に馬がいたら 1.3m 外へずらす」を繰り返します。先に決めた馬の位置は変えない 貪欲法 (その場で最善を選ぶ方法) で、全体として最適とは限りませんが、毎フレーム 程度の軽い計算で済みます。
実際のレーンは目標へ「1 秒あたり 1.6 の割合」で近づきます。これは時定数 秒の指数関数的な接近 (次のページの カメラ と同じ考え方) で、馬が横に瞬間移動するのを防いでいます。
- 着順は Zig が決め、3D はそれに合う動きを式で作る
- 。出来 0.03 の差 ≒ 3 馬身
- で止まった状態から加速し、ちょうど 秒遅れる
- は なら単調。逃げは前半速く後半遅い
- 最大リード はレースの中間点。今の値 (130m) は現実より大きい
- ゴール時刻は線形補間、着差は 馬身、ゴール後の減速は 連続
3,000m で逃げ () と差し () が同じタイムで走るとき、最大リードは何 m で、レースの何秒後ですか (芝、)。
答えを見る
m。 のときなので 。 秒、 秒、 秒後。
芝 2,000m、 秒の差し馬 () は、ゴール後に最終的に何 m 先で止まりますか。
答えを見る
m/s。止まる位置はゴールから m 先です。
1 着が 120.48 秒 (2,000m)、2 着が 0.15 秒遅れのとき、着差の表示は何になりますか。
答えを見る
m/s。 馬身。 単位に丸めて「1」(1 馬身)。