Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

貝茲、曲線與路徑

貝茲曲線是一種自然幾何形狀的數學近似。我們用它們來以最少的資訊,高度靈活地表示一條曲線。

不同於更抽象的數學概念,貝茲曲線是專為工業設計而創造的。它們是圖形軟體產業中常用的工具。

它們依賴於 插值 (我們在前一篇文章中已介紹),結合多個步驟來建立平滑的曲線。為了更好理解貝茲曲線的運作方式,讓我們從最簡單的形式──二次貝茲曲線開始。

二次貝茲曲線

取三個點,這是二次貝茲曲線所需的最小數量:

../../_images/bezier_quadratic_points.png

要在這三個點之間繪製一條曲線,我們首先對由這三個點形成的兩個線段的端點進行插值(範圍從 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)

然後將 q0q1 做插值,得到一個隨著曲線移動的點 r

var r = q0.lerp(q1, t)
return r

這種曲線稱為 二次貝茲曲線

../../_images/bezier_quadratic_points2.gif

(圖片來源:維基百科)

三次貝茲曲線

在前述例子的基礎上,若以四個點進行插值,我們便能取得更多控制。

../../_images/bezier_cubic_points.png

首先我們使用一個帶有四個參數的函式,分別輸入四個點 p0p1p2p3

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

以下是完整函式:

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

結果會是一條在四個點之間平滑插值的曲線:

../../_images/bezier_cubic_points.gif

(圖片來源:維基百科)

備註

三次貝茲插值在 3D 中的用法也相同,只需將 Vector2 換成 Vector3 即可。

新增控制點

在三次貝茲的基礎上,我們可以藉由改變其中兩個點的方式,來自由調整曲線的形狀。我們不直接儲存 p0p1p2p3,而是這樣規劃:

  • point0 = p0:第一個點,起點

  • control0 = p1 - p0:第一個控制點的相對向量

  • control1 = p3 - p2:第二個控制點的相對向量

  • point1 = p3:第二個點,終點

這樣我們就有了兩個點及兩個作為相對向量的控制點。如果你曾用過圖形或動畫軟體,這應該看起來很熟悉:

../../_images/bezier_cubic_handles.png

這就是圖形軟體向使用者呈現貝茲曲線的方式,也是它們在 Godot 裡運作與顯示的方式。

Curve2D、Curve3D、Path 與 Path2D

有兩種物件可以存放曲線:Curve3DCurve2D (分別對應 3D 與 2D)。

這些曲線類別可以包含多個點,允許建立較長的路徑。也能將它們設為節點:Path3DPath2D (分別對應 3D 與 2D):

../../_images/bezier_path_2d.png

但它們的使用方式未必直覺,下方將說明貝茲曲線最常見的使用情境。

取樣評估

只對曲線進行取樣評估是一種做法,但多數情境下實用性有限。貝茲曲線的一大缺點是:如果你從 t = 0 以固定速度走到 t = 1,實際插值的速度*並不*會均勻。速度會受 p0p1p2p3 之間距離的插值影響,數學上沒有簡單方式能以固定速度遍歷曲線。

我們來看個範例,以下是偽程式碼:

var t = 0.0

func _process(delta):
    t += delta
    position = _cubic_bezier(p0, p1, p2, p3, t)
../../_images/bezier_interpolation_speed.gif

正如你所見,即便 t 以固定速率遞增,圓的移動速度(像素/秒)仍會變化。這讓貝茲曲線在實際應用時不容易直接拿來用。

繪製

繪製貝茲曲線(或基於曲線的物件)是很常見的用途,但同樣不簡單。大多數情況下,貝茲曲線都必須轉換為一系列線段。不過這麼做時通常會遇到如何避免產生過多線段的困難。

原因在於曲線某些區段(特別是轉角處)可能需要大量點,而其他區段則不需要:

../../_images/bezier_point_amount.png

另外,如果兩個控制點都是 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)

如此輸出就能以固定速度移動:

../../_images/bezier_interpolation_baked.gif