Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

貝塞爾、曲線和路徑

貝茲曲線是一種自然幾何形狀的數學近似. 我們用它們來代表一個曲線, 含有盡可能少的資訊, 保持高水平的靈活性.

不像抽象的數學概念, 貝茲曲線是為工業設計. 它們是圖形軟體行業中的流行工具.

它們依賴於 插值, 我們在上一篇文章中看到, 如何結合多個步驟來建立平滑的曲線. 為了更好地理解貝茲曲線的工作原理, 我們從最簡單的形式開始: 二次貝茲曲線.

二次貝茲曲線

取三個點, 這是建立二次貝茲曲線所需的最小值:

../../_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

(圖像來源: 維琪百科)

備註

三次貝塞爾插值在三維中也是一樣的,只需使用 Vector3 代替 Vector2

新增控制點

在三次貝塞爾的基礎上,我們可以通過改變兩個點的工作方式來自由地控制曲線的形狀。我們不使用 p0p1p2p3,而是將它們儲存為:

  • point0 = p0:是第一個點,即源

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

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

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

使用這種方式, 有兩個點和兩個控制點, 它們是各自點的相對向量. 如果你以前用過圖形或動畫軟體, 這可能看起來很熟悉:

../../_images/bezier_cubic_handles.png

這就是圖形軟體如何向使用者呈現貝茲曲線, 以及它們在Godot引擎內的工作原理.

Curve2D、Curve3D、Path 以及 Path2D

有兩個物件包含曲線 Curve3D 和 :ref:`Curve2D <class_Curve2D>`(分別代表 3D 和 2D)。

它們可以包含幾個點,允許更長的路徑。也可以將它們設定為節點:Path 和 :ref:`Path2D <class_Path2D>`(在 3D 和 2D 內都適用):

../../_images/bezier_path_2d.png

然而它們的使用方法可能不是很直觀,下面是對貝茲曲線最常見用例的描述。

估值

一種選擇是直接估值,不過在大多數情況下都不是很有用。貝茲曲線最大的缺點是如果你以恒定的速度沿著它走,從 t = 0t = 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 參數). 這樣一來, 基於曲線畫東西就比較容易了.

走訪

最後曲線最常見的用例是走訪. 因為之前提到關於勻速的內容, 這也是困難的.

為了操作起來更方便,需要先把曲線 烘焙 成若干等距的點。這樣就可以用常規的插值操作(還可以使用立方選項進一步優化)來進行近似估值了。要實作這樣的效果,只需呼叫 Curve.interpolate_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