地図/力学/当たり判定と物理

速度を捨てる — Verlet

位置と速度を別々の変数で持つと、pos += vel; vel += accel で一歩進む。拘束で位置を直接ずらしたとき、速度はその修正を知らないまま前の向きで残り、辻褄が合わなくなる。Verlet 積分は速度という変数を持たない。前のフレームの位置を覚えておき、今の位置との差をそのまま速度の代わりに使う。

オイラー:  vel += accel;  pos += vel        速度を陽に持つ
Verlet:   next = pos + (pos − prev) + accel  速度は (pos − prev) に隠れている
          prev = pos;  pos = next

Verlet 積分は Loup Verlet が 1967 年、分子動力学のシミュレーションのために整理したもの。位置の履歴から速度を逆算するので、位置を直接いじる拘束ソルバと相性がいい。点を掴んで動かすと (pos − prev) が勝手に更新されて速度になる。布やロープの物理でよく使われる。

点を一つ、速度を持たせず重力 g だけ毎フレーム足して落とす。前の位置 px, py と今の位置 x, y の差が速度になる。

状態として持つのは位置 x, y と前フレームの位置 px, py だけで、vx, vy は毎回その差から作り直している。床で跳ね返すときも速度を反転せず、prev を今の位置の向こう側へ鏡映して、次の差が上向きになるようにする。px = x - 2.5 の初期ずれがそのまま横向きの初速になり、点は斜めに放られて落ちる。

二点の距離を一定に保ちたいとき、Verlet なら点の位置を引き寄せる/押し離すだけで済む。次のフレームに (pos − prev) がその修正を新しい速度として拾うので、速度を別に更新する必要がない。二点をつなぐ一本のリンクをちょうど rest の長さに保つ拘束は、いまの距離 d と目標 rest のずれを測り、二点を半分ずつ寄せたり離したりする。

// 点 i と i+1 の距離を rest に近づける(半分ずつ動かす)
const dx = xs[i + 1] - xs[i]
const dy = ys[i + 1] - ys[i]
const d = Math.hypot(dx, dy) || 1e-6
const diff = ((d - rest) / d) * 0.5
xs[i] += dx * diff
ys[i] += dy * diff
xs[i + 1] -= dx * diff
ys[i + 1] -= dy * diff

(d - rest) / d が目標に対するずれの割合で、0.5 倍して両端へ振り分ける。位置を直接いじるだけで、引き寄せた勢いは (pos − prev) に乗って次のフレームの速度として残る。

Verlet と距離拘束が噛み合うのは、どちらも位置だけを操作するから。速度を持つ系では拘束で位置を動かすと速度との整合を別途取り直す必要があるが、Verlet は速度を (pos − prev) から事後に読むので、位置を直した結果がそのまま速度に反映される。距離拘束を多数並べて反復で緩和する手法は Thomas Jakobsen が 2001 年、ゲーム『Hitman』の布・ロープ表現として広めた。

この拘束をリンクの数だけ並べて鎖にする。先頭をピンで留めて横に揺らすと、各リンクは自分の長さを保とうとするだけで、全体が振り子のように動く。

同じ二点拘束をリンクの数だけ並べ、それを iter 回くり返す。一回だけだとあるリンクを直したぶん隣が狂うので、何度も緩和すると全体がだんだん辻褄を合わせる。iter を 1 にすると鎖が伸びてゴムのようにたわみ、増やすほど硬い鎖になる。剛性は反復回数で決まる。先頭 xs[0] は毎回ピンの位置へ上書きされ、ここが固定端になる。速度をどこにも書いていないのに鎖が揺れ、ピンを止めてもしばらく余韻が残る。揺れは全部 (pos − prev) の差に入っている。