貝茲、曲線與路徑
貝茲曲線是一種自然幾何形狀的數學近似。我們用它們來以最少的資訊,高度靈活地表示一條曲線。
不同於更抽象的數學概念,貝茲曲線是專為工業設計而創造的。它們是圖形軟體產業中常用的工具。
它們依賴於 插值 (我們在前一篇文章中已介紹),結合多個步驟來建立平滑的曲線。為了更好理解貝茲曲線的運作方式,讓我們從最簡單的形式──二次貝茲曲線開始。
二次貝茲曲線
取三個點,這是二次貝茲曲線所需的最小數量:
要在這三個點之間繪製一條曲線,我們首先對由這三個點形成的兩個線段的端點進行插值(範圍從 0 到 1)。這樣當我們將 t 從 0 變到 1,就會得到兩個沿線段移動的點。
func _quadratic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, t: float):
var q0 = p0.lerp(p1, t)
var q1 = p1.lerp(p2, t)
private Vector2 QuadraticBezier(Vector2 p0, Vector2 p1, Vector2 p2, float t)
{
Vector2 q0 = p0.Lerp(p1, t);
Vector2 q1 = p1.Lerp(p2, t);
}
然後將 q0 和 q1 做插值,得到一個隨著曲線移動的點 r。
var r = q0.lerp(q1, t)
return r
Vector2 r = q0.Lerp(q1, t);
return r;
這種曲線稱為 二次貝茲曲線。
(圖片來源:維基百科)
三次貝茲曲線
在前述例子的基礎上,若以四個點進行插值,我們便能取得更多控制。
首先我們使用一個帶有四個參數的函式,分別輸入四個點 p0、p1、p2 和 p3:
func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
public Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
}
接著對每一對點進行線性插值,將其減少為三個點:
var q0 = p0.lerp(p1, t)
var q1 = p1.lerp(p2, t)
var q2 = p2.lerp(p3, t)
Vector2 q0 = p0.Lerp(p1, t);
Vector2 q1 = p1.Lerp(p2, t);
Vector2 q2 = p2.Lerp(p3, t);
然後將這三個點進一步縮減為兩個點:
var r0 = q0.lerp(q1, t)
var r1 = q1.lerp(q2, t)
Vector2 r0 = q0.Lerp(q1, t);
Vector2 r1 = q1.Lerp(q2, t);
再縮減為一個點:
var s = r0.lerp(r1, t)
return s
Vector2 s = r0.Lerp(r1, t);
return s;
以下是完整函式:
func _cubic_bezier(p0: Vector2, p1: Vector2, p2: Vector2, p3: Vector2, t: float):
var q0 = p0.lerp(p1, t)
var q1 = p1.lerp(p2, t)
var q2 = p2.lerp(p3, t)
var r0 = q0.lerp(q1, t)
var r1 = q1.lerp(q2, t)
var s = r0.lerp(r1, t)
return s
private Vector2 CubicBezier(Vector2 p0, Vector2 p1, Vector2 p2, Vector2 p3, float t)
{
Vector2 q0 = p0.Lerp(p1, t);
Vector2 q1 = p1.Lerp(p2, t);
Vector2 q2 = p2.Lerp(p3, t);
Vector2 r0 = q0.Lerp(q1, t);
Vector2 r1 = q1.Lerp(q2, t);
Vector2 s = r0.Lerp(r1, t);
return s;
}
結果會是一條在四個點之間平滑插值的曲線:
(圖片來源:維基百科)
備註
三次貝茲插值在 3D 中的用法也相同,只需將 Vector2 換成 Vector3 即可。
新增控制點
在三次貝茲的基礎上,我們可以藉由改變其中兩個點的方式,來自由調整曲線的形狀。我們不直接儲存 p0、p1、p2、p3,而是這樣規劃:
point0 = p0:第一個點,起點control0 = p1 - p0:第一個控制點的相對向量control1 = p3 - p2:第二個控制點的相對向量point1 = p3:第二個點,終點
這樣我們就有了兩個點及兩個作為相對向量的控制點。如果你曾用過圖形或動畫軟體,這應該看起來很熟悉:
這就是圖形軟體向使用者呈現貝茲曲線的方式,也是它們在 Godot 裡運作與顯示的方式。
Curve2D、Curve3D、Path 與 Path2D
有兩種物件可以存放曲線:Curve3D 和 Curve2D (分別對應 3D 與 2D)。
這些曲線類別可以包含多個點,允許建立較長的路徑。也能將它們設為節點:Path3D 和 Path2D (分別對應 3D 與 2D):
但它們的使用方式未必直覺,下方將說明貝茲曲線最常見的使用情境。
取樣評估
只對曲線進行取樣評估是一種做法,但多數情境下實用性有限。貝茲曲線的一大缺點是:如果你從 t = 0 以固定速度走到 t = 1,實際插值的速度*並不*會均勻。速度會受 p0、p1、p2、p3 之間距離的插值影響,數學上沒有簡單方式能以固定速度遍歷曲線。
我們來看個範例,以下是偽程式碼:
var t = 0.0
func _process(delta):
t += delta
position = _cubic_bezier(p0, p1, p2, p3, t)
private float _t = 0.0f;
public override void _Process(double delta)
{
_t += (float)delta;
Position = CubicBezier(p0, p1, p2, p3, _t);
}
正如你所見,即便 t 以固定速率遞增,圓的移動速度(像素/秒)仍會變化。這讓貝茲曲線在實際應用時不容易直接拿來用。
繪製
繪製貝茲曲線(或基於曲線的物件)是很常見的用途,但同樣不簡單。大多數情況下,貝茲曲線都必須轉換為一系列線段。不過這麼做時通常會遇到如何避免產生過多線段的困難。
原因在於曲線某些區段(特別是轉角處)可能需要大量點,而其他區段則不需要:
另外,如果兩個控制點都是 0, 0 (記得它們是相對向量),那貝茲曲線就變成一條直線(此時產生過多點就很浪費資源)。
在繪製貝茲曲線之前,通常要先進行 細分。這通常透過遞迴或分治法分割曲線,直到曲率小於某個門檻為止。
Curve 類別透過 Curve2D.tessellate() 函式(可選遞迴階段數 stages 與角度容差 tolerance 參數)來提供細分功能。如此一來,基於曲線繪圖就更容易了。
遍歷
最後一個常見用途是沿曲線遍歷。但如前所述,若要以固定速度遍歷曲線,會有困難。
為了簡化操作,需要先將曲線 烘焙 為等距的點。如此一來就能用常規插值(甚至可進一步用三次插值)進行近似。只需使用 Curve3D.sample_baked() 搭配 Curve2D.get_baked_length() 方法。首次呼叫任一方法時,曲線會在內部自動烘焙。
固定速度遍歷曲線,可以用以下偽程式碼實現:
var t = 0.0
func _process(delta):
t += delta
position = curve.sample_baked(t * curve.get_baked_length(), true)
private float _t = 0.0f;
public override void _Process(double delta)
{
_t += (float)delta;
Position = curve.SampleBaked(_t * curve.GetBakedLength(), true);
}
如此輸出就能以固定速度移動: