徳島ゲーム開発ごっこ 技術ブログ

ゲームを作るために役に立ったり立たなかったりする技術を学んでいきます!

【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()で変化するものが多い場合は検討してもいいかもしれませんね。

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

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