学ぶ ・ 3D と映像の数学

3D の運動学 — 着順から「走り」を逆算する

3D シミュレーションの馬は物理法則で走っているのではなく、「決まったゴール時刻に必ず着く」ように設計された式で動いています。その式の作り方と性質 (単調性・連続性・最大リード) を、微分を使って確かめます。

このページで学ぶこと
  • Zig の「出来」から各馬の走破タイム TiT_i を決める式
  • 発走直後の加速を表す「時間のゆがみ」τ(t)\tau(t)
  • 脚質を表す 2 次式 f(u)=u+s u(1−u)f(u) = u + s\,u(1-u) と、それが必ず前に進む (単調) 条件
  • 逃げ馬と追込馬の差が最大になる時刻と、その大きさ
  • ゴール時刻の補間、着差 (馬身) の換算、ゴール後のなめらかな減速

設計の考え方 — 結果から過程を作る

ふつうの物理シミュレーションは「力 → 加速度 → 速度 → 位置」と計算し、着順は結果として決まります。Keiba Vision の 3D は逆で、

  1. 着順と着差は 先に Zig のモデル が決める
  2. その結果に 必ず一致する ように、途中の動きを式で作る

という順番です。こうすると、物理の細かい調整なしに「確率モデルどおりの結果」と「それらしい展開」を両立できます。

走破タイム

Zig の race_once が返す各馬の出来を xix_i、最も良い出来を xmaxx_\text{max} とします。

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);
B=Dvˉ,Ti=B (1+0.12 (xmax−xi))B = \frac{D}{\bar v}, \qquad T_i = B\,\big(1 + 0.12\,(x_\text{max} - x_i)\big)
  • vˉ\bar v … 平均速度。芝 16.6 m/s、ダート 15.9 m/s、ばんえい 1.7 m/s
  • 1 着馬は T=BT = B (芝 2,000m なら 2000/16.6=120.482000 / 16.6 = 120.48 秒、約 2 分 0 秒 5)
  • 出来の差 0.03 は 120.48×0.12×0.03=0.434120.48 \times 0.12 \times 0.03 = 0.434 秒の差 → 約 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 つの部品に分けて読みます。

x(t)=D f(u(t)),u(t)=τ(t)T−a,τ(t)=t−a(1−e−t/a),a=1.1x(t) = D\,f\big(u(t)\big), \qquad u(t) = \frac{\tau(t)}{T - a}, \qquad \tau(t) = t - a\big(1 - e^{-t/a}\big), \quad a = 1.1

部品 1: 時間のゆがみ τ(t)\tau(t) — 止まった状態から加速する

τ\tau の微分 (時間あたりの増え方) は

τ′(t)=1−e−t/a\tau'(t) = 1 - e^{-t/a}

で、t=0t = 0 では 0、tt が大きくなると 1 に近づきます。つまり「レースの進み具合」が、最初は止まっていて、約 3a≈3.33a \approx 3.3 秒でほぼ全速になる、という加速を表しています。

t≫at \gg a では e−t/a≈0e^{-t/a} \approx 0 なので τ(t)≈t−a\tau(t) \approx t - a。加速にかかった分、ちょうど aa 秒だけ遅れる ことになります。だから uu の分母は T−aT - a です。ゴール (t=Tt = T) では

u(T)=T−a+ae−T/aT−a=1+a e−T/aT−au(T) = \frac{T - a + a e^{-T/a}}{T - a} = 1 + \frac{a\,e^{-T/a}}{T - a}

2 分のレースなら e−120/1.1≈4×10−48e^{-120/1.1} \approx 4 \times 10^{-48} なので、誤差は実質 0。どの馬も、ぴったり自分の TiT_i にゴールします。

部品 2: 脚質の 2 次式 f(u)f(u)

f(u)=u+s u(1−u)f(u) = u + s\,u(1 - u)

ss は脚質ごとの定数です。

const STYLE_SHAPE = [0.14, 0.07, -0.05, -0.12]; // 逃げ・先行・差し・追込

性質を確かめましょう。

  • f(0)=0f(0) = 0、f(1)=1f(1) = 1 … スタートとゴールは ss に関係なく同じ
  • f(1/2)=1/2+s/4f(1/2) = 1/2 + s/4 … 中間点で、逃げ (s=0.14s = 0.14) は 3.5% 前、追込 (s=−0.12s = -0.12) は 3% 後ろ
  • 微分 f′(u)=1+s(1−2u)f'(u) = 1 + s(1 - 2u) … 序盤 (u≈0u \approx 0) の速さは 1+s1 + s 倍、終盤 (u≈1u \approx 1) は 1−s1 - s 倍

逃げ馬は「序盤 14% 速く、終盤 14% 遅い」、追込馬は「序盤 12% 遅く、終盤 12% 速い」という、脚質らしい走りになります。

単調性の証明 — 馬が後ろに下がらないこと

f′(u)=1+s(1−2u)f'(u) = 1 + s(1 - 2u) は uu の 1 次関数なので、0≤u≤10 \le u \le 1 での最小値は端の u=0u = 0 か u=1u = 1 でとります。値は 1+s1 + s と 1−s1 - s なので、

∣s∣<1  ⟹  f′(u)>0(0≤u≤1)|s| < 1 \;\Longrightarrow\; f'(u) > 0 \quad (0 \le u \le 1)

アプリの ss は最大でも ∣s∣=0.14|s| = 0.14 なので、どの馬も常に前に進みます。もし ∣s∣≥1|s| \ge 1 にすると、どこかで速度が 0 以下になり、馬が止まったり後退したりします。

部品 3: ゴール後の減速

ゴールした瞬間の速度は、f′(1)=1−sf'(1) = 1 - s と τ′≈1\tau' \approx 1 から

vend=D (1−s)T−av_\text{end} = \frac{D\,(1 - s)}{T - a}

ゴール後の経過時間を Δ\Delta として

x=D+5 vend (1−e−Δ/5)x = D + 5\,v_\text{end}\,\big(1 - e^{-\Delta/5}\big)

で、Δ=0\Delta = 0 での位置は DD、速度 (微分) は 5vend×15=vend5 v_\text{end} \times \tfrac15 = v_\text{end} です。位置も速度もゴールの前後でつながる (数学では C1C^1 級と呼びます) ので、ゴール板を過ぎた瞬間にカクッと減速することがありません。そのあと約 5 秒ずつ指数関数的にゆっくりになり、最終的にゴールから 5vend5 v_\text{end} (約 80m) 先で止まります。

逃げ馬のリードはどこまで広がるか

2 頭の走破タイムが同じ (TA=TBT_A = T_B) なら uA=uB=uu_A = u_B = u で、差は

xA−xB=D (sA−sB) u(1−u)x_A - x_B = D\,(s_A - s_B)\,u(1 - u)

u(1−u)u(1-u) は u=1/2u = 1/2 で最大値 1/41/4 をとる放物線なので、最大のリードは D(sA−sB)/4D(s_A - s_B)/4、レースのちょうど真ん中 です。

逃げ (s=0.14s = 0.14) と追込 (s=−0.12s = -0.12)、2,000m なら 2000×0.26/4=1302000 \times 0.26 / 4 = 130m。実際の計算でも、出来の差 0.03 (追込の方が強い) の場合で最大 126.4m のリードを作り、追込馬がゴール 27m 手前 (119.05 秒) で逆転し、3 馬身差で勝ちます。

130m は実際の競馬より大きすぎる

実際のレースで先頭と最後方の差は、2,000m の中間点でも 20〜40m (10〜20 馬身) 程度です。今の ss は展開を分かりやすく見せるために大きめにしてあり、映像としては「逃げ馬が大逃げしている」状態になります。現実に近づけるなら、たとえば最大リードを 30m にしたい場合、sA−sB=4×30/2000=0.06s_A - s_B = 4 \times 30 / 2000 = 0.06 になるよう、ss を約 1/4 (逃げ 0.03、追込 −0.03 など) にします。

ゴール時刻の補間

3D は 1 秒間に約 60 回 (フレーム) しか位置を計算しません。4 倍速なら 1 フレームでレース時間が最大 0.05×4=0.20.05 \times 4 = 0.2 秒進み、その間に馬は約 3.3m (1.4 馬身) 動きます。「ゴールを越えたフレームの時刻」をそのまま使うと、着差がフレーム単位に丸められてしまいます。

h.finishT = h.s > prev ? this.t - dt * (h.s - goal) / (h.s - prev) : this.t;

前のフレームの位置 xprevx_\text{prev} と今の位置 xx の間を直線で結び、ゴール GG を通る時刻を求めます (線形補間)。

t∗=t−Δt⋅x−Gx−xprevt^* = t - \Delta t \cdot \frac{x - G}{x - x_\text{prev}}

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;
  // ...
}

前の馬とのゴール時刻の差 Δt\Delta t に、1 着馬の平均速度 v1=D/T1v_1 = D / T_1 を掛けて距離にし、1 馬身 = 2.4m で割ります。

ℓ=Δt⋅v12.4\ell = \frac{\Delta t \cdot v_1}{2.4}

0.35 馬身未満は「ハナ」「アタマ」「クビ」、10 馬身以上は「大差」、その間は 1/41/4 馬身単位に四捨五入 (⌊4ℓ+0.5⌋/4\lfloor 4\ell + 0.5 \rfloor / 4) して「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 外へずらす」を繰り返します。先に決めた馬の位置は変えない 貪欲法 (その場で最善を選ぶ方法) で、全体として最適とは限りませんが、毎フレーム O(n2)O(n^2) 程度の軽い計算で済みます。

実際のレーンは目標へ「1 秒あたり 1.6 の割合」で近づきます。これは時定数 1/1.6≈0.631/1.6 \approx 0.63 秒の指数関数的な接近 (次のページの カメラ と同じ考え方) で、馬が横に瞬間移動するのを防いでいます。

まとめ
  • 着順は Zig が決め、3D はそれに合う動きを式で作る
  • Ti=B(1+0.12(xmax−xi))T_i = B(1 + 0.12(x_\text{max} - x_i))。出来 0.03 の差 ≒ 3 馬身
  • τ(t)=t−a(1−e−t/a)\tau(t) = t - a(1 - e^{-t/a}) で止まった状態から加速し、ちょうど aa 秒遅れる
  • f(u)=u+s u(1−u)f(u) = u + s\,u(1-u) は ∣s∣<1|s| < 1 なら単調。逃げは前半速く後半遅い
  • 最大リード D(sA−sB)/4D(s_A - s_B)/4 はレースの中間点。今の値 (130m) は現実より大きい
  • ゴール時刻は線形補間、着差は Δt⋅v1/2.4\Delta t \cdot v_1 / 2.4 馬身、ゴール後の減速は C1C^1 連続
練習問題3,000m の最大リード

3,000m で逃げ (s=0.14s = 0.14) と差し (s=−0.05s = -0.05) が同じタイムで走るとき、最大リードは何 m で、レースの何秒後ですか (芝、vˉ=16.6\bar v = 16.6)。

答えを見る

3000×0.19/4=142.53000 \times 0.19 / 4 = 142.5m。u=1/2u = 1/2 のときなので τ=(T−a)/2\tau = (T - a)/2。T=3000/16.6=180.7T = 3000/16.6 = 180.7 秒、τ=89.8\tau = 89.8 秒、t≈τ+a=90.9t \approx \tau + a = 90.9 秒後。

練習問題減速の距離

芝 2,000m、T=120.48T = 120.48 秒の差し馬 (s=−0.05s = -0.05) は、ゴール後に最終的に何 m 先で止まりますか。

答えを見る

vend=2000×1.05/(120.48−1.1)=17.59v_\text{end} = 2000 \times 1.05 / (120.48 - 1.1) = 17.59 m/s。止まる位置はゴールから 5×17.59≈885 \times 17.59 \approx 88m 先です。

練習問題着差の換算

1 着が 120.48 秒 (2,000m)、2 着が 0.15 秒遅れのとき、着差の表示は何になりますか。

答えを見る

v1=2000/120.48=16.60v_1 = 2000 / 120.48 = 16.60 m/s。ℓ=0.15×16.60/2.4=1.04\ell = 0.15 \times 16.60 / 2.4 = 1.04 馬身。1/41/4 単位に丸めて「1」(1 馬身)。