特殊な物理シミュレーションを利用したデジタル時計 Clock を公開しました。
以下、一見不可解な動きがどのようにして実現されているかの解説となります。
まだ作品を見ていない人は、先に動きを観察して仕組みを考えてみるとより楽しめると思います。
・
・
・
寄せられた予想
- 逆再生っぽい
- 徐々に力を加えてくっつけている
逆再生はかなり近いです。というか半分くらい合っています。が、普通に逆再生するとこういう動きにならないことには多くの人は気付いていたようです。
一方で重力以外の外力は加えておらず、その意味では完全に自然な(=物理的に説明がつく)見た目になっています。
仕組み
時刻が完成してからの計算
何も難しいことはなく[1]、普通の物理シミュレーションを行っています。
時刻が完成するまでの計算
あらかじめ計算した結果を逆回しで再生しています。
ただし、計算自体も逆向きに行っています。
結果として順向きのシミュレーションに見えます。
逆向きの物理シミュレーション
通常の物理シミュレーションでは、時刻 t
における状態から時刻 t+\Delta t
における状態を計算します。
しかし、今回作成した特殊な物理エンジンでは、時刻 t
における状態から時刻 t-\Delta t
における状態を計算しています。因果が逆転した世界における物理をシミュレーションしているということになります。
反転した時間が流れる世界における物理は現実の物理とは異なるので、一般的な物理エンジンでは再現することができません[2]。
逆問題を解く
通常解かれる問題の解(出力)を入力とし、元の入力を求める問題を一般に逆問題といいます。剛体物理における状態は位置と速度なので、時刻 t
における位置と速度を与えるような時刻 t-\Delta t
における位置と速度を求めることが問題になります。
衝突がない場合
最初に衝突がない場合を考えてみます。外力は重力だけとし、剛体は等加速度運動するものとします。
時刻 t
における位置を x(t)
、速度を v(t)
とします。
適当な離散化の下で、時刻 t+\Delta t
における位置と速度は次のようになります。
v(t+\Delta t) &= v(t) + \Delta t g \\
x(t+\Delta t) &= x(t) + \Delta t v(t)
g
は重力加速度です。これより、次のように位置と速度を逆算することができます。
v(t-\Delta t) &= v(t) - \Delta tg \\
x(t-\Delta t) &= x(t) - \Delta tv(t)
ここで、逆向きの速度 w(t)=-v(t)
を考えてみると、上の式は次のように書くことができます。
w(t-\Delta t) &= w(t) + \Delta tg \\
x(t-\Delta t) &= x(t) + \Delta tw(t)
なんと、最初の式において v
と w
に置き換えただけの結果となりました。これは運動が完全に同じ法則に従って発生していることを意味します。言い換えると、衝突が発生していない場合は、逆回しになった動画を見ても人間は逆再生であると気付くことができないということになります。
衝突がある場合
ボールが反発係数 e
で壁に衝突したときのことを考えます。簡単にするために1次元で考え、正の方向に詰まっている壁にボールが速度 v\ (\gt0)
で衝突したとします。反発係数の定義より、ボールは速度 -ev
で跳ね返ります。
この問題の逆を解きます。すなわち、衝突後の速度 v'\ (\lt0)
が与えられたときの衝突前の速度 v
を求めます。これは簡単に解くことができ、v=-(1/e)v'
となります。よく見ると、反発係数が e
から 1/e
に変わっただけで、根本的な法則は何も変わっていません。つまり、反発係数を 1 より大きく設定するだけで、既存の物理エンジンでも同じ計算ができることになります。
摩擦がある場合
箱が床を滑っている状態を考えます。床から受けている垂直抗力が N\ (>0)
のとき、箱に働く最大の摩擦力の大きさは摩擦係数を \mu
として[3] \mu N
で表されます。
水平方向の速度 v(t)\ (>0)
が十分に大きい場合、時間 \Delta t
の間摩擦によって速度は次のように変化します。
v(t+\Delta t) = v(t) - \Delta t\mu N
一方、水平方向の速度がある程度小さい場合、摩擦によって速度は完全にゼロになります。
v(t+\Delta t) = 0
この逆問題は場合分けが必要で、少々厄介です。v(t)
を単に v
と表記し、v(t+\Delta t)
を v'
と表記します。
v'\neq0
の場合
この場合、摩擦力は速度をゼロにしようと最大限仕事をしたが、結果として速度をゼロにできなかったということになります。つまり、元々の速度は v'
を摩擦力の限界まで加速させた速度、すなわち
v = \begin{cases}
v'+\Delta t\mu N & (v'\gt0) \\
v'-\Delta t\mu N & (v'\lt0)
\end{cases}
v=0
の場合
この場合、摩擦力が頑張った結果速度がゼロになったということになります。しかし、どの程度頑張ったのかは結果から推測することはできず、解を一意に定めることができません。順方向の物理は基本的に将来の状態が一意的に定まりますが、逆方向ではこうした任意性が生じます。
-\Delta t\mu N \leq v \leq \Delta t\mu N
今回作成した物理エンジンでは、この場合は v=0
となるように計算しています[4]。
既にある程度ややこしいですが、ここまで説明した内容は1次元の2体問題で、しかも片方は質量が無限大で静止しています。実用的にするにはさらに複雑なケースに対応させる必要があります。
真・逆向きの物理シミュレーション
上記の内容を多体問題に拡張し、反復的に衝突力を求めるソルバを作成します。
全部解説するととんでもない分量になるので省きますが、基本の構造は普通の物理エンジンと同じで、衝突応答のためのソルバを改造することで実現できます。
実は垂直抗力にも任意性がある
一般的な物理エンジンでは、jittering(剛体が安定せず震えること)を防ぐために、一定未満の速度で衝突した場合に反発係数をゼロとして扱う処理が含まれています。
そのため、衝突後の速度がゼロであった場合でも、ある速度の閾値を限度として衝突前の速度を上げることが可能になり、垂直抗力の大きさに任意性が生じます。
今回は、低い確率で衝突前の速度が非ゼロになるように計算しています。
重力の処理が難しい
通常、物理エンジンは
- 衝突検出
- 外力を加える(重力など)
- 拘束を解く(衝突、ジョイントなど)
- 位置を動かす
という順に処理を行うのですが、逆問題を解く場合は衝突時の速度がクリティカルに重要になってきます。よって、外力を加えてから拘束を解くと、本来速度がゼロのところに外力による速度が加わってしまい、正しい計算ができなくなります。
そのため、次のように順番を変えて計算を行っています。
- 衝突検出
- 衝突の目標速度(跳ね返り、摩擦)を計算する
- 重力を加える
- 衝突を解く
- 位置を動かす
こうすることで、重力が衝突後の速度に余分な影響を与えないようにすることができます。
余談
思いついたときから「多分これを作れる人は他にほとんどいないだろうな」と思っていたのですが、なんとおのさんがこの解説記事を書く前に仕組みを当てて、さらに3次元版の実装まで済ませてしまいました。
@shr_id
昨日のデモについて考えました
1.剛体を完成状態にする
2.摩擦や衝突時の反発など減衰処理で逆に増幅する特殊処理にして、剛体シミュ行う
3.ステップごとにシミュレーション結果の剛体の位置や角度を保存する
4.保存結果を逆再生する
5.初期状態まで逆再生したら普通の剛体シミュに移行する— おの (@qEouo) February 11, 2022
めちゃめちゃびっくりしましたが、おのさんは一から物理エンジンを全部組める方なのである意味納得でした。それでも相当大変だったと思います。色々考えてくれてありがとうございます。