【Unity】Lerpを用いたスムージングの問題点とMathf.SmoothDampによる解決策

UnityでLerpを用いてスムージングする際の問題点とMathf.SmoothDampによる解決策についてまとめました。

Unity2020.2.7

Lerpでスムージング

例えばキャラクターを追従するようなカメラを作る時には、目標の地点に徐々に近く処理を書くことになります。
この処理でよく見るのがLerpを使った方法です。

Transform target; // 追従するキャラクターのTransform(目標)
transform.position = Vector3.Lerp(transform.position, target.position, Time.deltaTime * speed);

これを使うとバネのように徐々に対象に接近する動きが表現できます。

Lerpによるスムージングの問題点

Lerpによるスムージングは手軽で処理負荷も低いのですが、目標の動きが滑らかでないと綺麗にスムージングできないという欠点があります。
これを確認するために以下のコードを記述します。

using UnityEngine;

public class Lerp : MonoBehaviour
{
    private const float TotalTime = 5.0f;
    private const float Speed = 3;
    private float _currentTime;
    private float _currentValue;
    private float _targetValue = 1.0f;

    private void Update()
    {
        _currentTime += Time.deltaTime;

        if (_currentTime > TotalTime)
        {
            return;
        }

        // 一定時間を超えたら目標値を変える
        if (_currentTime > TotalTime / 3.0f)
        {
            _targetValue = 3.0f;
        }
        if (_currentTime > TotalTime * 2.0f / 3.0f)
        {
            _targetValue = 9.0f;
        }

        // Lerp
        _currentValue = Mathf.Lerp(_currentValue, _targetValue, Time.deltaTime * Speed);

        // 軌跡にGameObjectを配置
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.GetComponent<MeshRenderer>().material.color = Color.red;
        cube.transform.localScale *= 0.1f;
        cube.transform.position = new Vector3(_currentTime * 3, _currentValue, 0);
    }
}

最初は1を目標としてLerpしますが、途中で目標を3、さらに途中で9に変更します。
これの実行結果は以下のとおりです。

f:id:halya_11:20210525143120p:plain
Lerp

目標が切り替わった時点で急に値が変化してしまっていることがわかります。
例えばこれを前述のカメラに適用してしまうと、カメラが急な動きをしてなんともイケてない感じになります。

また、第3引数の値が1を超えた時に計算が正確にできないという問題もあります。

SmoothDampを使う

Lerpの代わりにSmoothDampメソッドを使うと滑らかなスムージングが実現できます。
前述のスクリプトをSmoothDampを用いた形に修正します。

using UnityEngine;

public class SmoothDamp : MonoBehaviour
{
    private const float TotalTime = 5.0f;
    private const float SmoothTime = 0.7f;
    private const float MaxSpeed = Mathf.Infinity;
    private float _currentTime;
    private float _currentValue;
    private float _currentVelocity;
    private float _targetValue = 1.0f;

    private void Update()
    {
        _currentTime += Time.deltaTime;

        if (_currentTime > TotalTime)
        {
            return;
        }

        // 一定時間を超えたら目標値を変える
        if (_currentTime > TotalTime / 3.0f)
        {
            _targetValue = 3.0f;
        }
        if (_currentTime > TotalTime * 2.0f / 3.0f)
        {
            _targetValue = 9.0f;
        }

        // SmoothDamp
        _currentValue = Mathf.SmoothDamp(_currentValue, _targetValue, ref _currentVelocity, SmoothTime, MaxSpeed,
            Time.deltaTime);

        // 軌跡にGameObjectを配置
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        cube.GetComponent<MeshRenderer>().material.color = Color.green;
        cube.transform.localScale *= 0.1f;
        cube.transform.position = new Vector3(_currentTime * 3, _currentValue, 0);
    }
}

これの実行結果は以下のとおりです。

f:id:halya_11:20210525143346p:plain
SmoothDamp

値の変化が滑らかになっていることがわかります。

パラメータを変える

さてSmoothDampにはいくつか引数があるのでこれを変えて挙動を確認します。

まずSmoothTimeを上げるとより滑らかにスムージングされます。
以下はSmoothTimeを1.2に変えた結果です。

f:id:halya_11:20210525143545p:plain
SmoothTime = 1.2

またMaxSpeedを変えると最高速度を制限することができます。

Lerpとの比較

Lerpと比較すると以下の通りとなります。

f:id:halya_11:20210525144503g:plain
Lerpとの比較

SmoothDamp系メソッド

UnityではSmoothDamp系のメソッドがいくつか用意されています。
以下にそれらを一覧化しておきます。

SmoothDampって何者なのか?

さてそれではこのSmoothDampメソッドとは一体なんなのでしょうか?
まずこのソースコードUnityCsReferenceからそのまま転載します。

public static float SmoothDamp(float current, float target, ref float currentVelocity, float smoothTime, [uei.DefaultValue("Mathf.Infinity")]  float maxSpeed, [uei.DefaultValue("Time.deltaTime")]  float deltaTime)
{
    // Based on Game Programming Gems 4 Chapter 1.10
    smoothTime = Mathf.Max(0.0001F, smoothTime);
    float omega = 2F / smoothTime;

    float x = omega * deltaTime;
    float exp = 1F / (1F + x + 0.48F * x * x + 0.235F * x * x * x);
    float change = current - target;
    float originalTo = target;

    // Clamp maximum speed
    float maxChange = maxSpeed * smoothTime;
    change = Mathf.Clamp(change, -maxChange, maxChange);
    target = current - change;

    float temp = (currentVelocity + omega * change) * deltaTime;
    currentVelocity = (currentVelocity - omega * temp) * exp;
    float output = target + (change + temp) * exp;

    // Prevent overshooting
    if (originalTo - current > 0.0F == output > originalTo)
    {
        output = originalTo;
        currentVelocity = (output - originalTo) / deltaTime;
    }

    return output;
}

それなりに複雑な処理であることはわかりますが、このままでは理解できません。

実はこれはGame Programming Gems 4のChapter 1.10に載っているアルゴリズムをベースとしています。
Critically Damped Ease-In/Ease-Out Smoothingというタイトルでスムージングについて記述されています。

本ブログでは必要に駆られるまでこれ以上深入りしませんが、興味がある方はそちらを見ていただければと思います。

参考

docs.huihoo.com