Up to date

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

使用 3D 變換

前言

如果您以前從未製作過3D遊戲, 那麼一開始在三維環境中進行旋轉可能會讓人感到困惑. 從2D來的人, 自然的思維方式就是類似於 "噢, 它就像2D旋轉一樣, 只是現在旋轉發生在X,Y和Z軸上" .

起初這似乎很簡單。對於簡單的遊戲,這種思維方式甚至可能足夠了。不幸的是,這往往是不正確的。

三維角度通常被稱為“歐拉角”。

../../_images/transforms_euler.png

歐拉角是由數學家萊昂哈德•歐拉在 1700 年代初引入的。

../../_images/transforms_euler_himself.png

這種代表三維旋轉的方式在當時是開創性的, 但在遊戲開發中使用時有一些缺點(這畢竟是一個戴著滑稽帽子的傢伙想出來的). 本文的主旨是解釋其原因, 並概述在編寫3D遊戲時處理變換的最佳做法.

歐拉角的問題

雖然看起來很直觀, 每個軸都有一個旋轉, 但事實是它就是不實用.

軸順序

這樣的主要原因是沒有一種 單一 的從角度建構方向的方法. 沒有一個標準的數學函式可以將所有角度放在一起並產生實際的3D旋轉. 從角度產生方向的唯一方法是以 任意順序 按角度旋轉物體角度.

這可以通過先旋轉 X , 然後 Y , 然後旋轉 Z 來完成. 或者, 你可以先以旋轉 Y , 然後旋轉 Z , 最後旋轉 X . 怎樣都行, 但根據順序不同, 物件的最終方向 不一定是相同的 . 事實上, 這意味著有多種方法可以從3個不同的角度建構方向, 具體取決於 旋轉的順序 .

下圖是一個萬向結(來自維琪百科), 它有視覺化的旋轉軸(以XYZ順序). 如你所見, 每個軸的方向取決於前一個軸的旋轉方向:

../../_images/transforms_gimbal.gif

你可能想知道這是如何影響你的. 我們來看一個實際的範例:

想像一下, 你正在做一個第一人稱控制器(例如FPS遊戲). 向左和向右移動滑鼠可以控制與地面平行的視角, 同時上下移動可以調整遊戲角色上下的視野.

為了實作希望的效果, 必須先在 Y 軸上應用旋轉(在這種情況下為 "up(向上)", 因為Godot中Y軸指向正上方(" Y-Up" 方向)), 然後在 X 軸上旋轉.

../../_images/transforms_rotate1.gif

如果我們首先在 X 軸上應用旋轉, 然後再在 Y 軸上應用旋轉, 則效果會不理想:

../../_images/transforms_rotate2.gif

根據所需的遊戲型別或效果, 您想要應用軸旋轉的順序可能會有所不同. 因此, 在X,Y和Z中應用旋轉是不夠的: 您還需要 旋轉順序 .

插值

使用歐拉角的另一個問題是插值. 設想您想在兩個不同的相機或敵人位置(包括旋轉)之間轉換. 解決這個問題的一個合乎邏輯的方法是從一個位置插值到下一個位置. 人們會期望它看起來像這樣:

../../_images/transforms_interpolate1.gif

但是, 在使用角度時, 這並不總是有預期的效果:

../../_images/transforms_interpolate2.gif

相機實際上旋轉去了相反的方向!

這可能有幾個原因:

  • 旋轉不會線性對應到方向, 因此它們插值並不總是會形成最短路徑(即從 2700 的度數與從 270 開始到 360 的度數不同, 即使角度是相同的).

  • 萬向節鎖死 正在發揮作用(第一個和最後一個旋轉的軸對齊, 因此失去了一個自由度). 請參閱 維琪百科關於Gimbal Lock 的頁面 以瞭解這個問題的詳細解釋.

對歐拉角說不

所有這些的結論是, 你 不應該 在遊戲中使用Godot class_Spatial 節點的 rotation 屬性. 它主要用在編輯器中, 為了與2D引擎一致, 並且用於簡單的旋轉(通常只有一個軸, 或者, 在有限的情況下, 兩個). 你可能會受到誘惑, 但不要使用它.

相反, 有一個更好的方法來解決你的旋轉問題.

變換的介紹

Godot裡的方向使用 class_Transform 資料型別. 每一個 class_Spatial 節點都包含一個 transform 屬性, 如果該父類是一個空間衍生型別, 則該屬性相對依賴於父類變換.

也可以通過 global_transform 屬性存取世界座標變換.

變換擁有一個基 class_Basis`(transform.basis 子屬性),它由三個 :ref:`class_Vector3 向量組成。這些向量可以通過 transform.basis 屬性存取,也可以使用 transform.basis.xtransform.basis.ytransform.basis.z 直接存取。每個向量指向它的軸被旋轉的方向,因此它們可以有效地描述節點的總旋轉。比例(只要它三個軸長度是一致的)也可以從軸的長度推斷出來。一個*基*也可以被解釋為一個 3x3 矩陣並像 transform.basis[x][y] 這樣使用。

預設的基(未經修改)類似於:

var basis = Basis()
# Contains the following default values:
basis.x = Vector3(1, 0, 0) # Vector pointing along the X axis
basis.y = Vector3(0, 1, 0) # Vector pointing along the Y axis
basis.z = Vector3(0, 0, 1) # Vector pointing along the Z axis

這也類似於一個 3x3 單位矩陣。

遵循OpenGL慣例, X 軸, Y 軸, Z 軸.

變換除了*基*以外還有一個*原點*。這是一個 Vector3,用於指定該變換距離實際原點 (0, 0, 0) 有多遠。*變換*是*基*與*原點*的組合,可以有效地表示空間中特定的平移、旋轉和縮放。

../../_images/transforms_camera.png

視覺化變換的一種方法是在“本地空間”模式下查看該物件的 3D 小工具。

../../_images/transforms_local_space.png

小工具的箭頭顯示的是基的 XYZ 軸(分別為紅色、綠色、藍色),小工具的中心位於該物件的原點。

../../_images/transforms_gizmo.png

有關向量和變換在數學方面的更多資訊, 請閱讀 向量數學 教學.

操作變換

當然, 變換並不像角度那樣容易控制, 並且有它自己的問題.

可以對變換進行旋轉,方法是將基與另一個基相乘(稱作累加),或者使用其旋轉方法。

var axis = Vector3(1, 0, 0) # Or Vector3.RIGHT
var rotation_amount = 0.1
# Rotate the transform around the X axis by 0.1 radians.
transform.basis = Basis(axis, rotation_amount) * transform.basis
# shortened
transform.basis = transform.basis.rotated(axis, rotation_amount)

Spatial中的一種方法簡化了這個操作:

# Rotate the transform around the X axis by 0.1 radians.
rotate(Vector3(1, 0, 0), 0.1)
# shortened
rotate_x(0.1)

這會相對於父節點來旋轉節點.

要相對於物件空間旋轉(節點自己的變換), 請使用下面的方法:

# Rotate around the object's local X axis by 0.1 radians.
rotate_object_local(Vector3(1, 0, 0), 0.1)

精度誤差

對變換執行連續的操作將導致由於浮點錯誤導致的精度損失. 這意味著每個軸的比例可能不再精確地為 1.0 , 並且它們可能不完全相互為 90 度.

如果一個變換每影格旋轉一次, 它最終會隨著時間的推移開始變形. 這是不可避免的.

有兩種不同的方法來處理這個問題. 首先是在一段時間後對變換進行 正交正規化(orthonormalize) 處理(如果每影格修改一次, 則可能每影格一次):

transform = transform.orthonormalized()

這將使所有的軸再次擁有有 1.0 的長度並且彼此成 90 度角. 但是, 應用於變換的任何縮放都將丟失.

建議您不要縮放要操縱的節點, 而是縮放其子節點(如MeshInstance). 如果您必須縮放節點, 則在最後重新應用它:

transform = transform.orthonormalized()
transform = transform.scaled(scale)

獲取資訊

現在你可能在想: "好吧, 但是我怎麼從變換中獲得角度?" . 答案又一次是: 沒有必要. 你必須盡最大努力停止用角度思考.

想像一下, 你需要朝你的遊戲角色面對的方向射擊子彈. 只需使用向前的軸(通常為 Z-Z ).

bullet.transform = transform
bullet.speed = transform.basis.z * BULLET_SPEED

敵人在看著遊戲角色嗎? 為此判斷你可以使用點積(請參閱 向量數學 教學以獲取對點積的解釋):

# Get the direction vector from player to enemy
var direction = enemy.transform.origin - player.transform.origin
if direction.dot(enemy.transform.basis.z) > 0:
    enemy.im_watching_you(player)

向左平移:

# Remember that +X is right
if Input.is_action_pressed("strafe_left"):
    translate_object_local(-transform.basis.x)

跳躍:

# Keep in mind Y is up-axis
if Input.is_action_just_pressed("jump"):
    velocity.y = JUMP_SPEED

move_and_slide()

所有常見的行為和邏輯都可以用向量來完成.

設定資訊

當然, 有些情況下你想要將一些資訊賦予到變換上. 想像一下第一人稱控制器或環繞旋轉的相機. 那些肯定是用角度來完成的, 因為你 確實希望 變換以特定的順序進行.

For such cases, keep the angles and rotations outside the transform and set them every frame. Don't try to retrieve and reuse them because the transform is not meant to be used this way.

環顧四周,FPS風格的範例:

# accumulators
var rot_x = 0
var rot_y = 0

func _input(event):
    if event is InputEventMouseMotion and event.button_mask & 1:
        # modify accumulated mouse rotation
        rot_x += event.relative.x * LOOKAROUND_SPEED
        rot_y += event.relative.y * LOOKAROUND_SPEED
        transform.basis = Basis() # reset rotation
        rotate_object_local(Vector3(0, 1, 0), rot_x) # first rotate in Y
        rotate_object_local(Vector3(1, 0, 0), rot_y) # then rotate in X

如您所見, 在這種情況下, 保持外部旋轉更為簡單, 然後使用變換作為 最後的 方向.

用四元數插值

用四元數能有效率地完成兩個變換之間的插值. 有關四元數如何工作的更多資訊可以在互聯網上的其他地方找到. 在實際應用中, 瞭解它們的主要用途是做最短路插值就足夠了. 同樣, 如果你有兩個旋轉, 四元數將平滑地使用最近的軸在它們之間進行插值.

將旋轉轉換為四元數很簡單.

# Convert basis to quaternion, keep in mind scale is lost
var a = Quaternion(transform.basis)
var b = Quaternion(transform2.basis)
# Interpolate using spherical-linear interpolation (SLERP).
var c = a.slerp(b,0.5) # find halfway point between a and b
# Apply back
transform.basis = Basis(c)

class_Quat 型別引用有更多關於資料型別的資訊(它還可以做變換累加, 變換點等, 不過這個使用頻率較低). 如果你多次對四元數進行插值或應用操作, 請記住它們最終需要被正規化. 否則, 會帶來數值精度誤差影響.

四元數在處理相機/路徑/等東西的移動軌跡時很有用. 插值的結果總會是正確且平滑的.

變換是你的朋友

對於大多數初學者來說, 習慣於使用變換可能需要一些時間. 但是, 一旦你習慣了它們, 你會欣賞他們的簡單而有力.

不要猶豫, 在Godot的任何 線上社區 網站上尋求幫助, 一旦你變得足夠自信, 請幫助其他人!