エフアンダーバー

個人開発の記録

【Unity】 SmoothDampの謎

ユニティちゃんのリパッケージ作業中、SmoothDampの挙動がどうも自分の理解と違うことに気づいたので調べてみました。

www.f-sp.com

SmoothDampとは?

スクリプトリファレンスによれば、

Gradually changes a vector towards a desired goal over time. The vector is smoothed by some spring-damper like function, which will never overshoot. The most common use is for smoothing a follow camera.

和訳すると、

時間の経過とともに目標位置へ向けてベクトルを徐々に変化させます。 ベクトルはあるバネ-ダンパ系のような関数により滑らかに変化し、目標位置を通り過ぎることはありません。最も一般的な用途はカメラの追尾を滑らかにすることです。

要は目標位置が移動する場合の補間です。

ここでは、最もよく使われるであろうVector3のものを前提に話を進めますが、 同様のメソッドがMathf, Vector2, Vector4にもそれぞれあります。

各パラメータの意味は以下の通り。

パラメータ 説明(スクリプトリファレンスの和訳)
current Vector3 現在位置。
target Vector3 目標位置。
currentVelocity ref Vector3 現在速度、この値は関数を呼び出す度に変更されます。
smoothTime float 目標位置への到達にかけるおおよその時間。小さい値ほど目標位置へ素早く到達します。
maxSpeed float 必要であれば速さの最大値を制限します。
deltaTime float この関数の最後の呼び出しからの経過時間。デフォルトはTime.deltaTimeです。

現在速度を保存するためのフィールドを用意して、 SmoothDampで現在位置を更新し続けることにより、 目標位置へと滑らかに移動しつづけるオブジェクトを作成できます。

using UnityEngine;

public class Sample : MonoBehaviour
{
    public Transform Target;

    public float SmoothTime;

    private Vector3 velocity;

    private void Update()
    {
        transform.position = Vector3.SmoothDamp(transform.position, Target.position, ref velocity, SmoothTime);
    }
}

動作検証コード

で、何が予想と違ったかというと次のコードを実行した時の結果です。

using System;
using UnityEngine;

public class SmoothDamp : MonoBehaviour
{
    private void Start()
    {
        Vector3 current = Vector3.zero;
        Vector3 target = Vector3.one;
        Vector3 velocity = Vector3.zero;
        float smoothTime = 0.1f;
        float maxSpeed = Single.PositiveInfinity;
        float deltaTime = smoothTime;

        Vector3 result = Vector3.SmoothDamp(current, target, ref velocity, smoothTime, maxSpeed, deltaTime);

        Debug.Log(result);     // (0.6, 0.6, 0.6)
        Debug.Log(velocity);   // (5.9, 5.9, 5.9)
    }
}

smoothTimedeltaTimeの値を同じにして、座標(0,0,0)から座標(1,1,1)へと移動させるコードです。

移動にかける時間と経過時間を同じにしているので、結果の座標は(1,1,1)に、速度は(0,0,0)になって欲しかったのですが、実際の値はだいぶ違います。

改めてスクリプトリファレンスを見直してみると、 spring-damper like function(バネ-ダンパ系のような関数)とか、Approximately the time(おおよその時間)といった怪しげな記述が。

どういうことなのかもう少し調べてみることにします。

動作コード

入力と出力を眺めてもよくわからなかったのでコードを直接読むことにします。

SmoothDampのコードは次のようになっているようです(変数名は私が適当に付けました)。

public static Vector3 SmoothDamp(Vector3 current, Vector3 target, ref Vector3 currentVelocity, float smoothTime, float maxSpeed, float deltaTime)
{
    smoothTime = Mathf.Max(0.0001f, smoothTime);

    float a = 2.0f / smoothTime;
    float b = a * deltaTime;
    float c = (float)(1.0 / (1.0 + b + 0.48 * b * b + 0.235 * b * b * b));

    Vector3 delta = current - target;
    Vector3 originalTarget = target;
    float maxLength = maxSpeed * smoothTime;
    Vector3 clampedDelta = Vector3.ClampMagnitude(delta, maxLength);
    target = current - clampedDelta;

    Vector3 distance = (currentVelocity + a * clampedDelta) * deltaTime;
    currentVelocity = (currentVelocity - a * distance) * c;
    Vector3 newPosition = target + (clampedDelta + distance) * c;

    if (Vector3.Dot(originalTarget - current, newPosition - originalTarget) > 0.0)
    {
        newPosition = originalTarget;
        currentVelocity = (newPosition - originalTarget) / deltaTime;
    }

    return newPosition;
}

重要でない部分を省いて、なんとなく整理してみるとこんな感じ。

public static Vector3 SmoothDamp(Vector3 current, Vector3 target, ref Vector3 currentVelocity, float smoothTime, float maxSpeed, float deltaTime)
{
    float t = (deltaTime / smoothTime) * 2.0f;
    float s = Mathf.Exp(-t);
    float u = s * t * 2.0f;
    float v = s * (1.0f - t);
    Vector3 nextVelocity = ((target - current) / smoothTime) * u + currentVelocity * v;
    Vector3 position = Vector3.Lerp(current, target, 1.0f - (u + v)) + currentVelocity * deltaTime * s;
        
    currentVelocity = nextVelocity;

    return position;
}

うん、わからん。

バネ-ダンパ系という話だったので減衰振動っぽい式が出てくるかなと思っていたのですがいまいち読み解けず。 自分の知識ではどうにもなりそうにないのでこの辺で投げっぱなしときます。

余談ですが、元コードの最後の部分。

newPosition = originalTarget;
currentVelocity = (newPosition - originalTarget) / deltaTime;

なんかバグっぽいような気がしますね。

おわりに

いろいろ調べてはみたのですが、結局よくわからなかったので知識ある人が通りかかることを期待して記事にしてみました。 もし理解できた方がいればコメントいただけるとありがたいです。

今後しばらくはため込んだUnityネタを書いていくつもりです。 比較的書くのが楽なものから消化しているので、小ネタ系が少し続くかも。



執筆時のUnityのバージョン:5.6.0f3