うら干物書き

ゲームを作っています。

【Unity】LerpとSlerpと数学を知らない僕

 こういう質問をされました。
「Vector3の関数にLerp()Slerp()というものがあるんですけど、違いって何なんですか」

 僕はこう答えました。
「そうだなぁ、SlerpLerpにSが足されているだろ。LerpよりS(すごい)ってことさ!」

 King of 適当な返しです。
 若い子には、僕のような駄目な大人にはなって欲しくないな、と思いました。
 少しLerp()Slerp()について調べます……。

この記事にはUnity5.5.1f1を使用しています。

Lerpってなんぞ、Slerpってなんぞ

 えーっと、確かLerp()にしろSlerp()にしろ、数値αから数値βに変化するとした際に、αからβへの変化途中の補間した数値を取得するものだったかな。
 ちなみにほかんは「人類補完計画」のほかんではなくて補間の方だよ。

LerpとSlerpの違いは?

 補間する際の計算式が違ったような気がするだよなぁ。
 Lerpが線形補間で、Slerpが球面線形補間だったけな。

 線形補間はわかりやすいんだけど、球面線形補間がなんか難しかった気が……。

 うーん、そうだなぁ。例をあげるとしたら、
 大豆から醤油に変化する流れがあるとして、
 
 大豆から醤油になるのが線形補間で、
 大豆から味噌を経由して醤油になるのが球面線形補間かなぁ。

f:id:urahimono:20170208001043p:plain

 ……。

 うん、絶対違うね。
 上の図は見なかったことにしてください。

Lerp(線形補間)ってどんな補間方法をするの?

 Wikipedia先生曰く、
線形補間 - Wikipedia

 な、なんか小難しい計算式が出てきたなぁ。
 数値を直線的に変化させているようですな。
 0*が20に変化するとすると、Lerp()を使って補間値を求めると、3割ぐらい変化したときは数値は6ぐらいになっているでしょうし、半分ぐらい変化したときは10ぐらいになっていることでしょう。

 実際Unity上でこの2つのオブジェクトの位置をLerp()で補間を取ると、以下の画像のようになりました。
f:id:urahimono:20170208001124p:plain
f:id:urahimono:20170208001132p:plain

 ColorにもLerp()がありますので、そちらも一緒に使って見ましょう。
f:id:urahimono:20170208001144p:plain
f:id:urahimono:20170208001153p:plain

 いい感じですね。
 そう、線形補間は何となく分かっているんですよ。
 計算式を書けといわれれば、何とか書けますしね。

 問題は球面線形補間の方です。

Slerp(球面線形補間)ってどんな補間方法をするの?

 とりあえず、先ほどLerp()で補間をとっていた箇所をSlerp()に書き換えてみましょう。
 どうなるかな。
 
f:id:urahimono:20170208001215p:plain

 何この分けわかんない補間…・・・。
 この画面はXY平面上のものになっていますが、これをXYZの3D空間で見ると何か分かるのかもしれません、見てみましょう。

f:id:urahimono:20170208001226p:plain

 おおー、Z軸方向に弧を描いていますね。
 まるでコンパス描いたかのような補間をしていますね。
 これが球面線形補間なんですね。

 ど、どういう計算式なんだろうか。
 中学後半以降の数学の授業は基本寝ていた僕にはさっぱりですよ。
 こういうときは、頭のいい人が作成しているホームページに頼るしかない!。
 というわけで、教えて、○×つくろーどっとコム先生ー!!

マルペケつくろーどっとコム
その57 クォータニオンを"使わない"球面線形補間

 さすがですよ。ありましたよ。
 ていうか、最初からここを頼れば良かった……。
 学生時代から引き続きお世話になります。

Slerpってどんな時に使うの?

 で、結局Slerp()って何に使うのさ。
 そもそもLerp()と違ってSlerp()Vector3Quaternionぐらいしかないですからね。
 回転系で使いそうですけど。
 えーと、何か情報ないかなぁ。

 あった、Qiitaにあった。

qiita.com

 コメント欄にすごい詳しく書いてた。

 なんとなくわかった気がする!

(おまけ)UnityでLerpやSlerpってどう使うの?

 今回は補間挙動を調べる際にも使った、Vector3ColorLerp()処理をスクリプトで書いてみます。

docs.unity3d.com
docs.unity3d.com

 Lerp()またはSlerp()を使う際は、変化値を時間で制御する場合と相性がいいですね。
 そして、変化後の目標値とは別に開始値も保持してい置く必要があります。

 では、早速スクリプトを書いていきます。

using UnityEngine;

public class LerpTest : MonoBehaviour
{
    [SerializeField, Range( 0.0f, 10.0f )]
    private float   m_time      = 0.0f;

    [SerializeField]
    private Vector3 m_targetPosition    = Vector3.zero;

    [SerializeField]
    private Color   m_startColor        = Color.white;
    [SerializeField]
    private Color   m_targetColor       = Color.white;


    private float   m_startTime         = 0.0f;
    private Vector3 m_startPosition     = Vector3.zero;
    

    private Color CurrentColor
    {
        set
        {
            var render = GetComponent<Renderer>();
            if( render == null )
            {
                return;
            }

            if( render.material == null )
            {
                return;
            }

            render.material.color   = value;
        }

    }


    void Start()
    {
        m_startTime     = Time.time;
        m_startPosition = transform.position;
    }

    void Update()
    {
        float timeStep  = m_time > 0.0f ? ( Time.time - m_startTime ) / m_time : 1.0f;
        timeStep        = Mathf.Clamp01( timeStep );

        transform.position  = Vector3.Lerp( m_startPosition, m_targetPosition, timeStep );
        CurrentColor        = Color.Lerp( m_startColor, m_targetColor, timeStep );
    }

} // class LerpTest

 第三引数に指定する時間は、0.0 ~ 1.0に正規化する必要があります。
 今回はMathf.Clamp01()を使用して、0.0未満、1.0より大きくならないようにしていますが、Lerp()内でClampしてくれるはずなので必要ないかもしれません。
 ClampしないVector3.LerpUnclamped()なんてのもあります。

docs.unity3d.com

 実行するとこんな感じです。
f:id:urahimono:20170208001612g:plain [アニメーション]

 ただ、メンバ変数として持たないといけない値が多い気がします。
 開始時間やら開始位置やら、Lerp()処理のためだけにこれだけ変数を使うと、ちょっとクラスがごちゃごちゃしてしまいそうですね。
 コルーチンと相性がいいかもしれません。
 コルーチンを使う形に改良してみましょう。

using UnityEngine;
using System.Collections;

public class LerpTest : MonoBehaviour
{
    [SerializeField, Range( 0.0f, 10.0f )]
    private float   m_time      = 0.0f;

    [SerializeField]
    private Vector3 m_targetPosition    = Vector3.zero;

    [SerializeField]
    private Color   m_startColor        = Color.white;
    [SerializeField]
    private Color   m_targetColor       = Color.white;


    void Start()
    {
        StartCoroutine( UpdatePosition( transform.position, m_targetPosition, m_time ) );
        StartCoroutine( UpdateColor( m_startColor, m_targetColor, m_time ) );
    }


    private IEnumerator UpdatePosition( Vector3 i_startPosition, Vector3 i_targetPosition, float i_time )
    {
        float   startTime = Time.time;

        do
        {
            float timeStep = i_time > 0.0f ? ( Time.time - startTime ) / i_time : 1.0f;
            transform.position = Vector3.Lerp( i_startPosition, i_targetPosition, timeStep );

            yield return null;
        }
        while( Time.time < startTime + i_time );
    }

    private IEnumerator UpdateColor( Color i_startColor, Color i_targetColor, float i_time )
    {
        var render = GetComponent<Renderer>();
        if( render == null )
        {
            yield break;
        }

        if( render.material == null )
        {
            yield break;
        }


        float startTime = Time.time;

        do
        {
            float timeStep          = i_time > 0.0f ? ( Time.time - startTime ) / i_time : 1.0f;
            render.material.color   = Color.Lerp( i_startColor, i_targetColor, timeStep );

            yield return null;
        }
        while( Time.time < startTime + i_time );
    }

} // class LerpTest

 コルーチンを使うことで、メンバ変数で持っていたものをローカル変数に変更することが出来ました。
 同時にLerp()で変化するものが多い場合は検討してもいいかもしれませんね。

 それにしても、数学系のことになると、他人の情報ばかりになるなぁ。
 自分でも勉強しなくちゃいけませんな!

 とりあえず中学数学から復習しようかな……。