自由落下
$\mathbf{g}$ を重力の定数とすれば、加速度に関して下記の式が成り立つ。
\frac{d^2\mathbf{x}}{dt^2} = \mathbf{g}
上式の積分から
\begin{eqnarray}
\mathbf{v}_{\rm Integral}(t) &=& \mathbf{v}_{0} + \mathbf{g} \dot{} t,\\
\mathbf{x}_{\rm Integral}(t) &=& \mathbf{x}_{0} + \mathbf{v}_{0} \dot{} t + \frac{1}{2} \mathbf{g} \dot{} t^2 .
\end{eqnarray}
一方で、数値計算においては、時間が連続でない(離散的)ので、あるフレーム $f$ における速度や位置は
\begin{align*}
\mathbf{v}_{{\rm Discrete},f} &= \mathbf{v}_{{\rm D},f-1} + \mathbf{g} \dot{} (T_{f} - T_{f-1}),\\
\mathbf{x}_{{\rm Discrete},f} &= \mathbf{x}_{{\rm D},f-1} + \mathbf{v}_{{\rm D},f-1} \dot{} (T_{f} - T_{f-1})
\end{align*}
とも表現できる。
今回はボールの自由落下を上記の2つの積分による運動と、Unity の物理エンジンによる自由落下を比較してみることにする。
Unity Project : [Github] note-nota/UniformAcceleration
自由落下の比較
時間方向の離散間隔を変えて、2つの動画を GIF にしてみました。2つの動画共に左から順に【Unity Rigidbody による落下】、【積分で計算した方程式による落下】、【離散的時間積分による落下】の計算をしたボールになります。
フレーム間隔:0.20[s]
フレーム間隔:0.66[s]
- 【Unity Rigidbody による落下】と【積分で計算した方程式による落下】は比較的よく似ている
- 多少【積分で計算した方程式による落下】の方が遅い
- フレーム間隔が大きくなると【離散的時間積分による落下】のズレが大きくなる
フレーム間隔を UI から操作したかったので、コルーチンを組んで時間間隔を変更できるように実装してます。(slider.onValueChanged
を使ってることで、動かしている間に書き換わっていくのは実装上は良くない気もしてます…が、一旦…ね。)
void Start()
{
slider.onValueChanged.AddListener(var =>
{
deltaTimeText.text = GetDeltaTimeTextFormat(var);
StartTimer(var);
});
}
void StartTimer(float seconds)
{
if (timer != null) StopCoroutine(timer);
timer = CountTime(seconds);
StartCoroutine(timer);
}
IEnumerator CountTime(float seconds)
{
while (true)
{
OnFrameEvent.Invoke();
var now = Time.time;
Debug.Log("WaitTime :" + (now - lasttime));
lasttime = now;
yield return new WaitForSeconds(seconds);
}
}
Rigidbody による落下は rigidbody.useGravity
の操作だけ。
protected override void StartAction()
{
ActionFrame();
rigidbody.useGravity = true;
frameCtr.SetFrameEvent(ActionFrame);
}
protected override void ResetAction()
{
base.ResetAction();
rigidbody.useGravity = false;
rigidbody.velocity = Vector3.zero;
}
…おっと、rigidbody.velocity
の初期化も忘れずに…。
積分による方程式系は
public override void Update(float time)
{
nowVelocity = GRAVITY * (time - startTime) + initVelicity;
nowPosition = GRAVITY * Mathf.Pow(time - startTime, 2.0f) / 2 + initVelicity * (time - startTime) + initPosition;
}
最初の位置・速度を保持し、経過時間から求められる。
一方、時間積分の方は
public override void Update(float time)
{
nowPosition += nowVelocity * (time - lastTime);
nowVelocity += GRAVITY * (time - lastTime);
lastTime = time;
}
ひとつ前のフレームの値に足し上げていく操作を行う。
簡単な説明
基本的には Unity の Rigidbody による自由落下と積分した方程式による落下は同じになるハズ…、と思ったんですがね?ちょっと差が出てるので、ココは今後も考えてみたいと思います。
~~ チョット考え中 ~~
考えられる原因の候補
- フレームレートの違い
- 重力定数の違い
- 累積誤差の違い
- 計算実行順による違い
~~~ 一旦、保留 ~~~
さてさて、以降は【離散的時間積分による落下】について何が起きていたかを見ていきたいと思います。
下に2つのグラフを作成しました。1つ目は落下速度のグラフ。2つ目は位置のグラフ。方程式によるグラフと時間間隔の違う2つの状態をプロットしてます。
落下速度に関するグラフを見ると、0.20 [s] も 0.66 [s] もともに方程式と同じ値をとっていることが分かる。階段状になっているのは、時間間隔を空けて計算しているためで、各フレームにおいては計算された速度は方程式に一致している。
一方で、位置に関するグラフを見れば、時間間隔が長い方が方程式からのズレが大きいことがうかがえる。ある時間での落下位置を順に並べれば、【方程式】<【0.20 [s]】<【0.66 [s]】となる。
0.20 [s] と 0.66 [s] それぞれある時間での落下位置と速度との関係を図示してみよう。
上の図で赤い矢印の落下位置の計算を、速度のグラフで表現すると赤い面積になる。これは、GravityMoveDeltime.cs
での nowPosition += nowVelocity * (time - lastTime);
からも同じ様に導かれる。
0.66 [s] においても同様に、青い矢印での落下位置の計算には、速度に関するグラフの青い面積の部分によって計算される。
さて、話を戻せば、速度に関しては 0.20 [s] であれ 0.66 [s] であれどちらも各フレームにおいて方程式と同じ値をとっている。一方で、落下位置に関しては、0.66 [s] の方が方程式から離れた値をとっている。これは、先ほどの面積で考えるととてもわかりやすい。次の図は方程式の系統で同じことを考えた図です。
上の黄色い矢印を求めるために、下の図での黄色い三角形の面積を考える必要があります。
ということは、時間フレームを切った【離散的時間積分による落下】で正確な落下を表現するには、「時間間隔を“極めて”小さく区切って三角形に近い形になるようにすればよい」ことが分かります。今回作成した Unity のプロジェクトで時間間隔を小さくして確かめてみてください。
(とは言いつつ、やはり時間間隔を小さくする限界があるますが、雰囲気だけでも感じてもらえればと思います。)