Up to date
This page is up to date for Godot 4.2
.
If you still find outdated information, please open an issue.
貝塞爾、曲線和路徑¶
貝茲曲線是一種自然幾何形狀的數學近似. 我們用它們來代表一個曲線, 含有盡可能少的資訊, 保持高水平的靈活性.
不像抽象的數學概念, 貝茲曲線是為工業設計. 它們是圖形軟體行業中的流行工具.
它們依賴於 插值, 我們在上一篇文章中看到, 如何結合多個步驟來建立平滑的曲線. 為了更好地理解貝茲曲線的工作原理, 我們從最簡單的形式開始: 二次貝茲曲線.
二次貝茲曲線¶
取三個點, 這是建立二次貝茲曲線所需的最小值:
要在它們之間畫一條曲線,我們首先使用 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;
}
結果將是在所有四個點之間的平滑曲線插值:
(圖像來源: 維琪百科)
備註
三次貝塞爾插值在三維中也是一樣的,只需使用 Vector3
代替 Vector2
。
新增控制點¶
在三次貝塞爾的基礎上,我們可以通過改變兩個點的工作方式來自由地控制曲線的形狀。我們不使用 p0
、p1
、p2
、p3
,而是將它們儲存為:
point0 = p0
:是第一個點,即源control0 = p1 - p0
:是相對於第一個控制點的向量control1 = p3 - p2
:是相對於第二個控制點的向量point1 = p3
:是第二個點,即終點
使用這種方式, 有兩個點和兩個控制點, 它們是各自點的相對向量. 如果你以前用過圖形或動畫軟體, 這可能看起來很熟悉:
這就是圖形軟體如何向使用者呈現貝茲曲線, 以及它們在Godot引擎內的工作原理.
Curve2D、Curve3D、Path 以及 Path2D¶
有兩個物件包含曲線 Curve3D 和 :ref:`Curve2D <class_Curve2D>`(分別代表 3D 和 2D)。
它們可以包含幾個點,允許更長的路徑。也可以將它們設定為節點:Path 和 :ref:`Path2D <class_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
參數). 這樣一來, 基於曲線畫東西就比較容易了.
走訪¶
最後曲線最常見的用例是走訪. 因為之前提到關於勻速的內容, 這也是困難的.
為了操作起來更方便,需要先把曲線 烘焙 成若干等距的點。這樣就可以用常規的插值操作(還可以使用立方選項進一步優化)來進行近似估值了。要實作這樣的效果,只需呼叫 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)
private float _t = 0.0f;
public override void _Process(double delta)
{
_t += (float)delta;
Position = curve.SampleBaked(_t * curve.GetBakedLength(), true);
}
並且輸出, 然後勻速移動: