Unity - 自己的三次曲線(Cubic Curve)自己畫 - Bézier Curve, Hermite Curve...

想法
  三次曲線(Cubic curve)可以應用的地方很多,像是讓物體跟隨一條路徑移動,例如平台遊戲的移動跳板;或者是讓攝影機跟著這條路徑移動,達到類似運鏡的效果。

  網路上也有許多寫得還不錯的插件可以使用,而且功能都滿完整的,如果單純要使用這樣的效果,推薦可以直接選一個下載來用,會比自己重頭寫一個來的方便快速



  國外幾個別人分享寫好的Component,例如:Hermite曲線
http://wiki.unity3d.com/index.php?title=Hermite_Spline_Controller
http://wiki.unity3d.com/index.php/Spline_Controller

  以前是因為想要製作橫向卷軸射擊遊戲,讓敵人從畫面外出現的時候不是只有直線前進,才開始接觸到Bézier曲線的公式(參考http://en.wikipedia.org/wiki/B%C3%A9zier_curve),不過當時沒有深入研究就是了,最近則是有點時間,於是就打算稍微深入一點來了解它運作的方式。

  我目前書上看到的前兩個曲線公式(Bézier、Hermite),雖然兩個公式稍有不同,Bézier的三次曲線使用兩個Control point來調整,Hermite使用端點的切線,在製作自己的Component上想要套用哪個公式其實都沒關係,因為這兩個線段之間其實是可以轉換的,所以使用自己喜歡的即可。

Cubic Bézier curve:B(t) = (1-t)^3 * P0 + 3*(1-t)^2*t * P1 + 3*(1-t)*t^2 * P2 + t^3 * P3
Cubic Hermite curve:P(t) = (2t^3 - 3t^2 + 1)P0 + (t^3 - 2t^2 + t)M0 + (-2t^3 + 3t^2)P1 + (t^3 - t^2)M1

  根據線段公式,Bézier曲線在端點跟控制點形成的切線,把這切線的向量乘上1/3套用在Hermite上,這兩條線會是一樣的。

  所以Hermite曲線的P0(h)、M0(h)、P1(h)、M1(h)換算成Bézier曲線的P0(b)、P1(b)、P2(b)、P3(b)後可以帶入Bézier曲線公式,兩條線會相同。

  P0(b) = P0(h)
  P1(b) = P0(h) + M0(h) * 1/3
  P2(b) = P1(h) - M1(h) * 1/3
  P3(b) = P1(h)

  曲線的製作不難,把公式套用上去即可取得目前位置,但是目前這些都只是單條線段,雖然可以使用,但是應用上卻稍不如Spline來的好用,而Spline也可以看做是好幾段Curve組合而成的一條線段,使用上就比單條線段更靈活。




Bézier spline


int curveCount = (points.Count-1)/3; //Curve線段的數量
float currentTime = Mathf.PingPong (Time.time, (float)curveCount); //目前的時間,這邊使用PingPong來讓座標在線段上頭尾來回移動
int index = Mathf.FloorToInt(currentTime); //目前在第幾條Curve
float t = currentTime - index; //目前的t值

//一條Curve的尾端是下一條Curve的頭
//P[0],P[1],P[2],P[3]第一條,P[3],P[4],P[5],P[6]
Vector3 p0 = points[index*3]; //頭
Vector3 p1 = points[index*3+1]; //Control point
Vector3 p2 = points[index*3+2]; //Control point
Vector3 p3 = points[index*3+3]; //尾

//使用Bézier curve公式 B(t) = (1-t)^3 * P0 + 3*(1-t)^2*t * P1 + 3*(1-t)*t^2 * P2 + t^3 * P3
//取得目前t值在曲線上的位置
float omt = 1.0f - t;
Vector3 currentPosition = p0 * Mathf.Pow(omt, 3) + p1 * (3 * omt * omt * t) + p2 * (3 * omt * t * t) + p3 * Mathf.Pow(t, 3);

  因為只是單純的把數個Curve連起來,所以只能確保有C0的連續性,如果線段要有C1或C2的連續,就需要調整兩條Curve接點的切線方向跟長度相同,這樣線段在接點處才不會出現尖銳的轉折。




Catmull-Rom spline



  依據曲線(http://en.wikipedia.org/wiki/Cubic_Hermite_spline)的公式來計算Tangent,但因為index+-1的關係,所以points陣列的index是從1~n-1,所以Spline頭尾兩個端點線段沒連到。
Vector3 p0 = points[index];
Vector3 p1 = points[index+1];
Vector3 m0 = (points[index+1] - points[index-1]) / ((index+1)-(index-1));
Vector3 m1 = (points[(index+1)+1] - points[(index+1)-1]) / ((index+1+1)-(index+1-1));

//這邊換算使用Bézier曲線的公式
float omt = 1.0f - t;
Vector3 currentPosition = p0 * Mathf.Pow(omt, 3) + (p0 + m0/3) * (3 * omt * omt * t) + (p1 - m1/3) * (3 * omt * t * t) + p1 * Mathf.Pow(t, 3);




Cardinal spline

Cardinal spline (tension=0)


Cardinal spline (tension=0.5)


Cardinal spline (tension=1)

  Cardinal spline跟Catmull-Rom的公式差不多,只是在切線前面多了一個係數,這個係數0的時候其實就是Catmull-Rom。
Vector3 p0 = points[index];
Vector3 p1 = points[index+1];
Vector3 m0 = (1-tension)*(points[index+1] - points[index-1]) / ((index+1)-(index-1));
Vector3 m1 = (1-tension)*(points[(index+1)+1] - points[(index+1)-1]) / ((index+1+1)-(index+1-1));




Finite difference

   同樣依據曲線(http://en.wikipedia.org/wiki/Cubic_Hermite_spline)的公式來計算。
Vector3 p0 = points[index];
Vector3 p1 = points[index+1];
Vector3 m0 = (points[index+1] - points[index-1]) / 2*((index+1)-(index)) + (points[index] - points[index-1]) / 2*((index)-(index-1));
Vector3 m1 = (points[(index+1)+1] - points[(index+1)-1]) / 2*(((index+1)+1)-(index+1)) + (points[(index+1)] - points[(index+1)-1]) / 2*((index+1)-((index+1)-1));
//同樣換算成Bézier曲線來求座標
float omt = 1.0f - t;
Vector3 currentPosition = p0 * Mathf.Pow(omt, 3) + (p0 + m0/3) * (3 * omt * omt * t) + (p1 - m1/3) * (3 * omt * t * t) + p1 * Mathf.Pow(t, 3);




Uniform B-Spline


  依據B-Spline的定義(http://en.wikipedia.org/wiki/B-spline)以及曲線的公式來看。

//四個點一組
int i0 = index+0;
int i1 = index+1;
int i2 = index+2;
int i3 = index+3;

Vector3 p0 = Mathf.Pow(t, 3) * (1/6f) * (points[i0]*(-1)+points[i1]*3-points[i2]*3+points[i3]);
Vector3 p1 = Mathf.Pow(t, 2) * (1/6f) * (points[i0]*3-points[i1]*6+points[i2]*3);
Vector3 p2 = Mathf.Pow(t, 1) * (1/6f) * (points[i0]*(-3)+points[i2]*3);
Vector3 p3 = 1 * (1/6f) * (points[i0]+points[i1]*4+points[i2]);

Vector3 currentPosition = p0+p1+p2+p3; //相加取得S(t)




差異

   是說一般有C2連續性的線段,當線段中一個點移除的時候,整條線段都會受到影響,而B-Spline就不會有這樣的問題。

  不過如果不是非常要求,其實沒有什麼太大的關係,除非是即時配置端點,可能會造成非預期的路徑之外,如果是預先製作好線段再讓遊戲中物件去跑,製作當下調整就好了,所以也沒有什麼問題。




Bicubic surfaces





  這邊使用的是Bézier surface,使用總共16個點來形成一塊patch,根據公式(http://en.wikipedia.org/wiki/B%C3%A9zier_surface)來計算點的位置。

  這邊我使用points[16]來設定16個點,0123第一排四個點,4567第二排四個點依此類推,然後就用這16個點直接來計算了,以(u,v)來計算取得曲面上的位置。

float omu = 1f - u;
Vector3 p0u = omu*omu*omu*points[0] + 3*omu*omu*u*points[4] + 3*omu*u*u*points[8] + u*u*u*points[12];
Vector3 p1u = omu*omu*omu*points[1] + 3*omu*omu*u*points[5] + 3*omu*u*u*points[9] + u*u*u*points[13];
Vector3 p2u = omu*omu*omu*points[2] + 3*omu*omu*u*points[6] + 3*omu*u*u*points[10] + u*u*u*points[14];
Vector3 p3u = omu*omu*omu*points[3] + 3*omu*omu*u*points[7] + 3*omu*u*u*points[11] + u*u*u*points[15];

float omv = 1f - v;
Vector3 currentPosition = omv*omv*omv*p0u + 3*omv*omv*v*p1u + 3*omv*v*v*p2u + v*v*v*p3u;




實作測試

  使用Catmull-Rom Spline預先製作好一條路線,然後讓飛機跟著路線走。





結論

  雖然不同的公式會讓相同座標點形成的線段稍有不同,但是因為都可以調整,所以我覺得在使用上其實沒太大的差別,選一個自己喜歡的公式使用即可,線段的差異可以靠增加或減少幾個控制點來達到所需的曲線。

  不過Bézier或Hermite spline除了連結的點的資訊之外,每一段curve都還需要兩個control點來調整,而其他的像是Catmull-Rom或B-Spline只需要連結的點作為資訊來源即可形成曲線,也許讓玩家在調整線段上畫面會比較簡潔一點,不過也是要看製作上有怎樣的需求囉。

1 comment:

Justin John said...
This comment has been removed by the author.

Post a Comment