三次曲線(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:
Post a Comment