【Unity】ベルレ法で物体を動かす

ベルレ法を使って物体の位置を計算する方法です。

記号の定義

本記事の数式で使用する記号は以下の定義基づいています。
必要に応じて参照しながら読み進めてください。

記号 定義
{p_{previous}} 前フレームの位置
{p_{current}} 現フレームの位置
{p_{next}} 次フレームの予測位置
{dt} 前フレームから今のフレームまでの時間
{F_{current}} 現フレームでかかっている力
{m} 質量

ベルレ法とは?

ベルレ法とは現在の位置と1フレーム前の位置から1フレーム後の位置を計算するための手法です。
式としては以下のようになります。

 { \displaystyle
p_{next} = p_{current} + (p_{current} - p_{previous}) \tag{1}
}

要するに前のフレームから今のフレームに掛けて動いたのと同じ速さで次のフレームも動くと仮定しているだけです。
実際にはこれに今のフレームでかかった力による移動距離の項が追加されて以下の式で表されます。

 { \displaystyle
p_{next} = p_{current} + (p_{current} - p_{previous}) + \frac{dt^{2}}{m}F_{current} \tag{2}
}

ベルレ法のメリット・デメリット

ベルレ法のメリットとしては、前フレームの位置情報だけあれば次のフレームの位置を計算できるという点が挙げられます。
運動方程式を用いた方法では初期位置や初期の力から正確な現在位置を求められますが、
前フレームからの差分を求めるような処理については計算が複雑になりすぎてしまいます。
そのため結果的にベルレ法を使ったほうが処理負荷が軽くなりやすいのもメリットであるといえます。

デメリットは物理的に正確な計算ではないという点です。
特に、DeltaTimeが大きくブレる環境だとおかしな結果になってしまいます(これについては後述します)。

実装(DeltaTimeにTime.deltaTimeを使う版)

上述の内容をそのまま実装すると以下のようになります。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField, Tooltip("質量")]
    private float _mass = 1.0f;
    [SerializeField, Tooltip("クリック時にかかる力の大きさ(N)")]
    private float _forceWhenClick = 50;

    [SerializeField]
    private Rigidbody _rigidbody;

    private Vector3 _previousPosition;

    private void Start () 
    {
        _previousPosition = transform.position;
    }
    
    private void Update ()
    {
        var force = Vector3.zero;
        
        // 重力
        force += Vector3.down * 9.8f * _mass;
        
        // クリックによる力
        if (Input.GetMouseButtonDown(0)) {
            force = Vector3.up * _forceWhenClick;
            _rigidbody.AddForce(Vector3.up * _forceWhenClick);
        }
        
        var position = transform.position;

        // 位置を計算
        var nextPosition = Verlet(transform.position, _previousPosition, force, _mass, Time.deltaTime);
        if (nextPosition.y < 0) {
            // 0より下の位置にはいかないように
            nextPosition.y = 0;
        }

        _previousPosition = position;
        transform.position = nextPosition;
    }

    private static Vector3 Verlet(Vector3 position, Vector3 previousPosition, Vector3 force, float mass, float deltaTimeSec)
    {
        // ベルレ法で次の位置を求める
        position = position + (position - previousPosition) + force * deltaTimeSec * deltaTimeSec / mass;

        return position;
    }
}

力は重力だけを適用し、クリックされたら上向きの力を加えるようにしています。

実装(DeltaTimeを固定する版)

前節の方法でもある程度まともに動くのですが、1フレームの時間をTime.deltaTimeから取得している点で問題が起こりえます。
例えばあるフレームにだけ時間が掛かったりするとそこから先はずっと早く動き続けたりしてしまいます。

これを解決するためにはVerlet()案数に与えるdeltaTimeSecを固定してしまいます。

using UnityEngine;

public class Example : MonoBehaviour
{
    [SerializeField, Tooltip("質量")]
    private float _mass = 1.0f;
    [SerializeField, Tooltip("クリック時にかかる力の大きさ(N)")]
    private float _forceWhenClick = 50;

    [SerializeField]
    private Rigidbody _rigidbody;

    private Vector3 _previousPosition;

    private void Start () 
    {
        _previousPosition = transform.position;
        Application.targetFrameRate = 60;
    }
    
    private void Update ()
    {
        var force = Vector3.zero;
        
        // 重力
        force += Vector3.down * 9.8f * _mass;
        
        // クリックによる力
        if (Input.GetMouseButtonDown(0)) {
            force = Vector3.up * _forceWhenClick;
            _rigidbody.AddForce(Vector3.up * _forceWhenClick);
        }
        
        var position = transform.position;

        // 位置を計算
        var nextPosition = Verlet(transform.position, _previousPosition, force, _mass, 1.0f / Application.targetFrameRate);
        if (nextPosition.y < 0) {
            // 0より下の位置にはいかないように
            nextPosition.y = 0;
        }

        _previousPosition = position;
        transform.position = nextPosition;
    }

    private static Vector3 Verlet(Vector3 position, Vector3 previousPosition, Vector3 force, float mass, float deltaTimeSec)
    {
        // ベルレ法で次の位置を求める
        position = position + (position - previousPosition) + force * deltaTimeSec * deltaTimeSec / mass;

        return position;
    }
}

実際にフレームにかかった時間ではないので正確性は失われますが、
元々ベルレ法自体が物理的な正確性を必要とするところには使えないはずなので問題ないかと思います。

結果

前節のスクリプトを適当なオブジェクトにアタッチして再生します。
クリックすると上向きに_forceニュートンの力が加わります。

f:id:halya_11:20200306163603g:plain
結果

参考

himaxoff.blog111.fc2.com