TT Lab
はじめる
学ぶ 学習パス コース

物理エンジンの骨組み

箱を五つ積んで安定させる

TT Labで続きを見る

目標

めり込んだ重なりを、位置補正で戻し、反復回数で山の固さを調節し、箱5つが積み重なって安定する過程を、絵に残します。このラボが、コース2の締めくくりです。

なぜ重要なのか

力積は、速度を直すだけで、すでにめり込んだ重なりを戻せません。重なりに気づく時点が、すでに重なったあとだからです。そのため、位置を別に押す必要がありますが、ここに、2つのつまみがあります。1ステップで何パーセントを戻すか(percent)と、無視する余裕(slop)です。一度にすべて押すと弾かれ、余裕を置かないと、微細に震えます。重なりを正確に0にしようとしないほうが、むしろ安定するという点が、この層の核心です。

そして、接触が複数あれば、互いに絡み合います。上を直すと下が押しつぶされ、下を直すと上が浮きます。正確に解くには連立方程式が必要ですが、高価なので、エンジンは、同じ巡回を何度も繰り返して近似します。反復回数が、そのまま、山がどれだけ固く見えるかを決めます。

ステップ

  1. /root/stackに、ツールボックスを置きます。
  2. /root/stack/correct.pyに、位置補正を作ります。
  3. /root/stack/out/03-sink.txtに、補正なしで沈む深さを書きます。
  4. /root/stack/out/04-fixed.txtに、補正をオンにした深さを書きます。
  5. /root/stack/out/05-iters.txtに、反復回数1・4・10の結果を書きます。
  6. /root/stack/out/06-stack.txtとstack.pngに、5つを積んだ結果を書きます。
  7. /root/stack/out/f00.pngからf11.pngまでとindex.htmlに、積み重なる過程を書きます。

参考

描画のツールボックスを置く

/root/stack/gfxlib.pyを例のとおりに保存し、/root/stack/check.pyでテストパターンを描いて、/root/stack/out/00-check.pngを作ってください。パターンは、64x64の黒い背景に、(0,0)から(63,63)まで、白(255,255,255)の対角線を引き、そのあとに、(0,32)から(63,32)まで、赤(255,0,0)の横線を重ねて引いたものです。

このラボからは、PNGエンコーダーを作り直しません。最初のラボで手で作ったものと同じコードを、ツールとして配ります。ここで学ぶのは、ファイル形式ではないからです。

ラボのPodにはボリュームがないので、前のラボで作ったファイルが残っていません。そのため、ラボごとに、ツールボックスを置き直すことから始めます。

Canvas(w, h, bg)を作り、line(x0, y0, x1, y1, rgb)で2本の線を引いたあと、write_png(path)で保存してください。交差点(32,32)が赤になるには、横線をあとで引く必要があります。

このラボでは、積み重なる過程の絵を描くのに使います。

位置補正

/root/stack/correct.pyにcorrect(depth, slop, percent, inv_m1, inv_m2)を作ってください。보정량 = max(depth - slop, 0) / (inv_m1 + inv_m2) * percentを求めて、(보정량*inv_m1, 보정량*inv_m2)の2つの実数を返します(プレースホルダーは、補正量です)。物体1は、その分だけ-n方向に、物体2は、+n方向に移すという意味です。逆質量の和が0なら、(0.0, 0.0)を返してください。

max(depth - slop, 0)が、スロップのすべてです。重なりが余裕より浅ければ、何もしません。これがないと、重なりを正確に0にしようとする補正が、毎ステップごく小さな値で起き続けて、物体が微細に震えます。

percentは、1ステップで何パーセントを戻すかです。1.0なら、一度にすべて押し出しますが、そうすると、物体が弾かれて、次のステップでまたぶつかる振動が生じます。普通は、0.2から0.8を使います。

逆質量で割ったあと、あらためてそれぞれの逆質量を掛ける理由は、軽いほうがより多く動くようにするためです。片方が動かない物体(逆質量0)なら、反対側がすべて動きます。

補正がないと、沈んだまま固まる

半分の高さが0.5の箱1つを、y=1.5、v=0から落として、g=-9.8、dt=1/60で、600ステップ回してください。1ステップは、(1)v += g*dt、(2)反復10回で、0.5 - y > 0かつv < 0ならv = 0、(3)y += v*dtで、位置補正は行いません。最後のめり込みの深さ0.5 - yを、/root/stack/out/03-sink.txtに、depth_final=として書いてください(負なら0)。

速度を0にしても、すでにめり込んだ分は、そのまま残ります。重なりに気づく時点が、すでに重なったあとだからです。

そして、その状態で、次のステップの重力がもう一度速度を与え、位置が少し下がり、また0になります。深さが、ある値で止まりますが、それが、このステップの答えです。

600ステップ後の深さは、0.02より大きくなります。箱の高さの数パーセントにあたる値ですが、箱を複数積むと、この誤差が積み重なって、山が目に見えて低くなります。

v = 0にするのは、反発係数0の力積と同じです。床の逆質量が0なので、箱の側だけが変わります。

補正をオンにすると、スロップまで戻ってくる

同じ条件に、(4)位置補正の段階を加えてください。位置の更新のあとで、depth = 0.5 - yを求めて、depth > slopなら、y += percent*(depth - slop)です(slop=0.01、percent=0.2)。600ステップ後の深さを、/root/stack/out/04-fixed.txtに、depth_final=として、ステップ3の値との差を、improved=として書いてください。

補正は、位置だけを動かします。速度もあわせて変えると、補正が物体を押す力になって、山が自分で跳ね上がります。

深さが、スロップの近くで止まります。0にならないのが正常です。ごく薄く重なった状態にしておくほうが、むしろ安定するからです。

improvedは、3단계 깊이 - 4단계 깊이です(プレースホルダーは、ステップ3の深さとステップ4の深さです)。正の値でなければならず、おおよそステップ3の深さの半分を超えます。

床は動かないので、箱の側だけを動かします。箱どうしなら、逆質量の比率で分けて、両側を押す必要があります。

反復回数が山の固さを決める

半分の高さが0.5の箱3つを、y = 0.5 + i + 1.0(i=0,1,2)から落として、600ステップ回しますが、反復回数だけを1、4、10に変えて、それぞれの最大のめり込みの深さを、/root/stack/out/05-iters.txtに、iters1=、iters4=、iters10=として書いてください。位置補正はオンにします(slop=0.01、percent=0.2)。

接触は3つです。床と箱0、箱0と箱1、箱1と箱2です。反復の中で、この順序で解きます。

箱どうしの接触は、次のとおりです。重なり1.0 - (y[i+1] - y[i])が0より大きく、相対速度v[i+1] - v[i]が負なら、j = -(v[i+1]-v[i]) / (inv_i + inv_{i+1})を求めて、v[i] -= j*inv_i、v[i+1] += j*inv_{i+1}です。すべての箱の逆質量は、1です。

位置補正も、3つの接触すべてにかけます。箱どうしは、m = percent*(depth-slop)/(inv_i+inv_{i+1})を求めて、下側をm*inv_iだけ下げ、上側をm*inv_{i+1}だけ上げます。

最大のめり込みの深さは、3つの接触の深さのうち、最も大きい値です。反復を増やすほど、小さくなる必要があります。それが、このステップで見たいことです。

5つを積む

箱5つを、y = 0.5 + 2.0 + i*1.6(i=0..4)から落として、反復15回で900ステップ回し、最後の高さ5つと最大のめり込みの深さを、/root/stack/out/06-stack.txtに、y1=からy5=と、max_depth=として書いてください。そして、最後の状態を、/root/stack/out/stack.png(256x256、背景(20,20,28))に描いてください。箱は、横の半幅が1.5で、画面座標は、sx = int(x*25.6 + 128)、sy = int(255 - y*25.6)で、色は、下から、(230,120,110)・(230,190,110)・(150,220,120)・(120,190,230)・(190,140,230)で、最後にsy=255の行を白で引いて、床を示します。

最初の高さをずらしておくと、箱が順番に降りてきて、積み重なる様子が出ます。同じ間隔にすると、一度に落ちて、見るものがありません。

箱1つは、画面で、横が89から166、縦は、int(255-(y+0.5)*25.6)からint(255-(y-0.5)*25.6)までの、四角形です。Canvasのfill_rectを使えば、一行です。

一番下の箱は、スロップの分だけめり込んでいるので、四角形がsy=255まで下りてきます。そのため、床の行を先に引くと、箱がそれを覆ってしまいます。床は最後に引いてください。

最後の高さは、おおよそ0.47、1.44、2.41、3.38、4.37の近くで出ます。ちょうど0.5、1.5、2.5ではない理由は、スロップの分だけ重なったまま安定するからです。

最大のめり込みの深さは、0.05を超えてはいけません。超えるなら、反復回数が足りないか、位置補正が抜けています。

積み重なる過程を12枚にする

同じシミュレーションで、0ステップ目から15ステップごとに状態を撮って、/root/stack/out/f00.pngからf11.pngまで、12枚を作り、12個の名前をすべて含めた/root/stack/out/index.htmlを作って、python3 -m http.server 8080 -d /root/stack/outで確認してください。絵のルールは、ステップ6と同じです。

0、15、30、...、165ステップ目の状態を保存しておいて、あとで一度に描けば済みます。シミュレーションを12回やり直す必要はありません。

最後の2枚が、ほとんど同じなら、正常です。山が安定したことを、数字で確認する方法が、まさにそれです。フレームの間の差が、0に収束するか、ということです。

逆に、最初の1枚と最後の1枚は、はっきり違う必要があります。最初の1枚は、箱が空中に散らばっていて、最後の1枚は、床に積み重なっています。

HTMLは、前のラボと同じ方式で、<img>1つとsetIntervalで済みます。