読者です 読者をやめる 読者になる 読者になる

【Unity】美しくパトロールさせたいその2~ベジェ曲線を用いて滑らかなループを作る

巡回経路シリーズ第三弾。
前回の記事の続きである。
【Unity】美しくパトロールさせたい~ベジェ曲線を試してみる - 奇跡のヌードル帝国

目的

巡回経路のプログラムにベジェ曲線を用いて、経路の点と点の間において自然な曲線の経路を辿らせよう、という試みは先日行ったばかりだが、これには問題がある。
前回記事の動画を見ても明らかではあるが、この方法では巡回するポイントにおいて不自然に曲がってしまう。
f:id:VirtualNoodle:20161005140531p:plain

マッコウクジラさんに水中を泳いでもらって経路をトレースしたが、巡回点でカクカクしているのは明らかである。
この問題点を解消するのが目的である。

実装

四次ベジェ曲線にする。指定できるのは各辺の真ん中の点のみ。

ソース

理屈の項目で何故そうしているのか説明するが、四次ベジェ曲線の制御点三つのうちの真ん中の制御点どうしの差のベクトルを正規化して、適当に大きさを与えることで残り二つの制御点を生成している。
テスト用のコードがそのまんま残ってるけど気にしない。

 
using UnityEngine;
using System.Collections;

public class SmoothBezierPatrol : MonoBehaviour {
    const float margin = 0.3f;

    public Vector3[] checkpoints;
    public Vector3[] ctrlPoints;
    private Vector3[] startCtrlPts;
    private Vector3[] endCtrlPts;

    private float time;
    private int whereToGo;

    public GameObject blue;
    public GameObject red;
    public GameObject green;
    public GameObject yellow;

    private Vector3 BezierCurve4(Vector3 pt1, Vector3 pt2, Vector3 ctrlPt,Vector3 ctrlPt2,Vector3 ctrlPt3, float t)
    {
        if (t > 1.0f)
            t = 1.0f;
        //14641
        Vector3 result = new Vector3();
        float cmp = 1.0f - t;

        result.x = cmp * cmp * cmp * cmp * pt1.x + 
            4.0f * cmp * cmp * cmp * t * ctrlPt.x + 
            6.0f * cmp * cmp * t * t * ctrlPt2.x + 
            4.0f * cmp * t * t * t * ctrlPt3.x + 
            t * t * t * t * pt2.x;
        result.y = cmp * cmp * cmp * cmp * pt1.y + 4.0f * cmp * cmp * cmp * t * ctrlPt.y + 6.0f * cmp * cmp * t * t * ctrlPt2.y + 4.0f * cmp * t * t * t * ctrlPt3.y + t * t * t * t * pt2.y;
        result.z = cmp * cmp * cmp * cmp * pt1.z + 4.0f * cmp * cmp * cmp * t * ctrlPt.z + 6.0f * cmp * cmp * t * t * ctrlPt2.z + 4.0f * cmp * t * t * t * ctrlPt3.z + t * t * t * t * pt2.z;

        return result;
    }

    // Use this for initialization
    void Start () {
        time = 0.0f;
        whereToGo = 0;
        startCtrlPts = new Vector3[checkpoints.Length];
        endCtrlPts = new Vector3[checkpoints.Length];

        for (int i = 1; i <= checkpoints.Length; i++) {
            int tmp = i % checkpoints.Length;
            Vector3 dir = ctrlPoints[tmp] - ctrlPoints[i - 1];
            startCtrlPts[tmp] = checkpoints[tmp] + (dir.normalized * Vector3.Distance(ctrlPoints[tmp], checkpoints[tmp]) / 5.0f);
            endCtrlPts[i - 1] = checkpoints[tmp] + (dir.normalized * Vector3.Distance(ctrlPoints[i - 1], checkpoints[tmp]) / 5.0f * -1.0f);

            //testCode
            Instantiate(red, startCtrlPts[tmp], Quaternion.identity);
            Instantiate(blue, endCtrlPts[i - 1], Quaternion.identity);

        }

        for (int i = 0; i < checkpoints.Length; i++) {
            Instantiate(yellow, checkpoints[i], Quaternion.identity);
            Instantiate(green, ctrlPoints[i], Quaternion.identity);
        }

	}
	
	// Update is called once per frame
	void Update () {
        if (checkpoints.Length == 0)
            return;


        if (checkpoints.Length <= this.whereToGo)
            whereToGo = 0;

        this.transform.position = BezierCurve4(checkpoints[whereToGo], checkpoints[(whereToGo + 1) % checkpoints.Length], startCtrlPts[whereToGo], ctrlPoints[whereToGo],endCtrlPts[whereToGo],time);

        if (Vector3.Distance(transform.position, checkpoints[(whereToGo + 1) % checkpoints.Length]) < margin)
        {
            Debug.Log(whereToGo);
            whereToGo++;
            time = 0.0f;
        }

        time += Time.deltaTime / 2.0f;
    }
}
感想

台風で授業が休講になったので暇を持て余した結果。
三回にわたって巡回プログラムを書いてきたけど、流石にもうネタが無い。
下に一応理屈の解説を書いてみたけど、こんなもん読む人いるんだろうか。
マッコウクジラさんもいい感じに泳ぐ事ができてよかったよかった。

理屈

適当に良く知らないまま証明したので誤りを含むかもしれない

明らかな事ではあるが、今回の目的は巡回点で経路が滑らかであれば良い。
要は巡回点に侵入する時と出ていく時で微分値が一致すればそれでなんとなく滑らかに見えるだろう。

という訳で、簡単の為に2次ベジェ曲線の式でパラメータxについて考える。


{ \displaystyle
 x = (1 - t)^2 x_s + 2(1 - t)tx_c + t^2x_e\\
 x_s : 始点 x_c : 制御点 x_e : 終点
}

こいつを時間で微分してやると

{ \displaystyle
 x = (2t - 2) x_s - 2  x_c + 2tx_e
}

ベジェ曲線を二つ繋ぐわけだから、導関数を二つ用意してやる。繋ぐときにはaが前、bが後ろになる。
{ \displaystyle
 x_a = (2t - 2) x_{sa} - 2  x_{ca} + 2tx_{ea}\\
 x_b = (2t - 2) x_{sb} - 2  x_{cb} + 2tx_{eb}
}

で、こいつらの時間パラメータは独立で、上の式のt = 1の時点が下の式のt = 0に対応する。
繋げた後の曲線で、制御点での左側微分係数が上の式でのt = 1、右側微分係数が下の式でのt = 0に対応するので、極限をとってやると
{ \displaystyle
 \lim_{t \to 1} x_a =  - 2 x_{ca} + 2x_{ea} \\
 \displaystyle
 \lim_{t \to 0} x_b = - 2 x_{sb} + 2 x_{cb} 
}

先述のように微分可能であれば良い。
2つのベジェ曲線の終点と始点は同じなので
{\displaystyle x_{ea} = x_{sb} = x_{p}}
とおいて、条件を満たすように式を整理すると
{ \displaystyle
 \frac{1} {2}(x_{ca} + x_{cb}) = x_p
}

となればよい、ということになる。
要は通る点の後ろと前の制御点が直線上に存在すればいいのだ。

以上、2次ベジェ曲線の場合について証明したが、この関係は一般的に成り立つ。
wikipediaによれば一般的なベジェ曲線
{\displaystyle P(t) = \sum_{i = 0}^{N - 1} B_i J_{i,(N - 1)}(t) }
と表される。
Jはバーンスタイン基底関数のブレンディング関数であり
{\displaystyle J_{i,n}(t) = \left(\begin{array}{ccc} n \\ i \end{array}\right)t^i(1 - t)^{n - i}}

である。

バーンスタイン基底関数の導関数

{\displaystyle 
\begin{equation}
\begin{split}
b'_{v,n}(x)& = n(b_{v-1,n-1}(x) - b_{v,n-1}(x))\\
& = n(\left(\begin{array}{ccc} n-1 \\ v-1 \end{array}\right)x ^ {v-1}  (1-x)^{n-v} - \left(\begin{array}{ccc} n-1 \\ v \end{array}\right)x^v  (1 - x)^{n-v-1})
\end{split}
\end{equation}
}

と表される。Jもバーンスタイン基底関数なのでこんな感じになる。
これらを利用してベジェ曲線の式の端の微分係数を求めると

{\displaystyle 
P'(0) = (N - 1)(B_1  - B_0) \\

P'(1) = (N - 1)(B_N-1 - B_N-2 )

}

となり、後は2次の時と同じ議論で証明できる。