使用 3D 變換
前言
如果您以前從未製作過3D遊戲, 那麼一開始在三維環境中進行旋轉可能會讓人感到困惑. 從2D來的人, 自然的思維方式就是類似於 "噢, 它就像2D旋轉一樣, 只是現在旋轉發生在X,Y和Z軸上" .
起初這似乎很簡單。對於簡單的遊戲,這種思維方式甚至可能足夠了。不幸的是,這往往是不正確的。
三維角度通常被稱為“歐拉角”。
歐拉角是由數學家萊昂哈德•歐拉在 1700 年代初引入的。
這種代表三維旋轉的方式在當時是開創性的, 但在遊戲開發中使用時有一些缺點(這畢竟是一個戴著滑稽帽子的傢伙想出來的). 本文的主旨是解釋其原因, 並概述在編寫3D遊戲時處理變換的最佳做法.
歐拉角的問題
雖然看起來很直觀, 每個軸都有一個旋轉, 但事實是它就是不實用.
軸順序
這樣的主要原因是沒有一種 單一 的從角度建構方向的方法. 沒有一個標準的數學函式可以將所有角度放在一起並產生實際的3D旋轉. 從角度產生方向的唯一方法是以 任意順序 按角度旋轉物體角度.
這可以通過先旋轉 X , 然後 Y , 然後旋轉 Z 來完成. 或者, 你可以先以旋轉 Y , 然後旋轉 Z , 最後旋轉 X . 怎樣都行, 但根據順序不同, 物件的最終方向 不一定是相同的 . 事實上, 這意味著有多種方法可以從3個不同的角度建構方向, 具體取決於 旋轉的順序 .
下圖是一個萬向結(來自維琪百科), 它有視覺化的旋轉軸(以XYZ順序). 如你所見, 每個軸的方向取決於前一個軸的旋轉方向:
你可能想知道這是如何影響你的. 我們來看一個實際的範例:
想像一下, 你正在做一個第一人稱控制器(例如FPS遊戲). 向左和向右移動滑鼠可以控制與地面平行的視角, 同時上下移動可以調整遊戲角色上下的視野.
為了實作希望的效果, 必須先在 Y 軸上應用旋轉(在這種情況下為 "up(向上)", 因為Godot中Y軸指向正上方(" Y-Up" 方向)), 然後在 X 軸上旋轉.
如果我們首先在 X 軸上應用旋轉, 然後再在 Y 軸上應用旋轉, 則效果會不理想:
根據所需的遊戲型別或效果, 您想要應用軸旋轉的順序可能會有所不同. 因此, 在X,Y和Z中應用旋轉是不夠的: 您還需要 旋轉順序 .
插值
使用歐拉角的另一個問題是插值. 設想您想在兩個不同的相機或敵人位置(包括旋轉)之間轉換. 解決這個問題的一個合乎邏輯的方法是從一個位置插值到下一個位置. 人們會期望它看起來像這樣:
但是, 在使用角度時, 這並不總是有預期的效果:
相機實際上旋轉去了相反的方向!
這可能有幾個原因:
旋轉不會線性對應到方向, 因此它們插值並不總是會形成最短路徑(即從
270到0的度數與從270開始到360的度數不同, 即使角度是相同的).萬向節鎖死 正在發揮作用(第一個和最後一個旋轉的軸對齊, 因此失去了一個自由度). 請參閱 維琪百科關於Gimbal Lock 的頁面 以瞭解這個問題的詳細解釋.
對歐拉角說不
總而言之,在 Godot 中用於遊戲時,您**不應該使用** Node3D 節點的 rotation 屬性。它的存在主要是供編輯器使用,為了與 2D 引擎保持一致,以及用於簡單旋轉 (通常只是一個軸,或在有限情況下甚至兩個軸)。不論您有多麼想,不要使用它。
相反, 有一個更好的方法來解決你的旋轉問題.
變換的介紹
Godot 使用 Transform3D 資料型別來表示方向。每個 Node3D 節點都包含一個 transform 屬性,如果父節點是 Node3D-derived type,它會相對於父節點的變換。
也可以通過 global_transform 屬性存取世界座標變換.
變換擁有一個基 class_Basis`(transform.basis 子屬性),它由三個 :ref:`class_Vector3 向量組成。這些向量可以通過 transform.basis 屬性存取,也可以使用 transform.basis.x、transform.basis.y、transform.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
// Due to technical limitations on structs in C# the default
// constructor will contain zero values for all fields.
var defaultBasis = new Basis();
GD.Print(defaultBasis); // prints: ((0, 0, 0), (0, 0, 0), (0, 0, 0))
// Instead we can use the Identity property.
var identityBasis = Basis.Identity;
GD.Print(identityBasis.X); // prints: (1, 0, 0)
GD.Print(identityBasis.Y); // prints: (0, 1, 0)
GD.Print(identityBasis.Z); // prints: (0, 0, 1)
// The Identity basis is equivalent to:
var basis = new Basis(Vector3.Right, Vector3.Up, Vector3.Back);
GD.Print(basis); // prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))
這也類似於一個 3x3 單位矩陣。
遵循OpenGL慣例, X 是 右 軸, Y 是 上 軸, Z 是 前 軸.
連同 基底,一個 變換 也具有一個 原點。這是一個 Vector3,指定距離實際原點 (0, 0, 0) 有多遠。將 基底 與 原點 組合,一個 變換 就能有效率地表示空間中的獨特位移、旋轉和縮放。
視覺化變換的一種方法是在“本地空間”模式下查看該物件的 3D 小工具。
小工具的箭頭顯示的是基的 X、Y、Z 軸(分別為紅色、綠色、藍色),小工具的中心位於該物件的原點。
有關向量和變換在數學方面的更多資訊, 請閱讀 向量數學 教學.
操作變換
當然, 變換並不像角度那樣容易控制, 並且有它自己的問題.
可以對變換進行旋轉,方法是將基與另一個基相乘(稱作累加),或者使用其旋轉方法。
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)
Transform3D transform = Transform;
Vector3 axis = new Vector3(1, 0, 0); // Or Vector3.Right
float rotationAmount = 0.1f;
// Rotate the transform around the X axis by 0.1 radians.
transform.Basis = new Basis(axis, rotationAmount) * transform.Basis;
// shortened
transform.Basis = transform.Basis.Rotated(axis, rotationAmount);
Transform = transform;
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 the transform around the X axis by 0.1 radians.
Rotate(new Vector3(1, 0, 0), 0.1f);
// shortened
RotateX(0.1f);
這會相對於父節點來旋轉節點.
要相對於物件空間旋轉(節點自己的變換), 請使用下面的方法:
# Rotate around the object's local X axis by 0.1 radians.
rotate_object_local(Vector3(1, 0, 0), 0.1)
// Rotate around the object's local X axis by 0.1 radians.
RotateObjectLocal(new Vector3(1, 0, 0), 0.1f);
軸必須以物件的本地座標系來定義。例如,若要繞物件的本地 X、Y 或 Z 軸旋轉,請分別使用 Vector3.RIGHT (X 軸)、 Vector3.UP (Y 軸)、以及 Vector3.FORWARD (Z 軸)。
精度誤差
對變換執行連續的操作將導致由於浮點錯誤導致的精度損失. 這意味著每個軸的比例可能不再精確地為 1.0 , 並且它們可能不完全相互為 90 度.
如果一個變換每影格旋轉一次, 它最終會隨著時間的推移開始變形. 這是不可避免的.
有兩種不同的方法來處理這個問題. 首先是在一段時間後對變換進行 正交正規化(orthonormalize) 處理(如果每影格修改一次, 則可能每影格一次):
transform = transform.orthonormalized()
transform = transform.Orthonormalized();
這將使所有的軸再次擁有有 1.0 的長度並且彼此成 90 度角. 但是, 應用於變換的任何縮放都將丟失.
建議您不要縮放要操縱的節點, 而是縮放其子節點(如MeshInstance). 如果您必須縮放節點, 則在最後重新應用它:
transform = transform.orthonormalized()
transform = transform.scaled(scale)
transform = transform.Orthonormalized();
transform = transform.Scaled(scale);
獲取資訊
現在你可能在想: "好吧, 但是我怎麼從變換中獲得角度?" . 答案又一次是: 沒有必要. 你必須盡最大努力停止用角度思考.
想像一下, 你需要朝你的遊戲角色面對的方向射擊子彈. 只需使用向前的軸(通常為 Z 或 -Z ).
bullet.transform = transform
bullet.speed = transform.basis.z * BULLET_SPEED
bullet.Transform = transform;
bullet.LinearVelocity = transform.Basis.Z * BulletSpeed;
敵人在看著遊戲角色嗎? 為此判斷你可以使用點積(請參閱 向量數學 教學以獲取對點積的解釋):
# 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)
// Get the direction vector from player to enemy
Vector3 direction = enemy.Transform.Origin - player.Transform.Origin;
if (direction.Dot(enemy.Transform.Basis.Z) > 0)
{
enemy.ImWatchingYou(player);
}
向左平移:
# Remember that +X is right
if Input.is_action_pressed("strafe_left"):
translate_object_local(-transform.basis.x)
// Remember that +X is right
if (Input.IsActionPressed("strafe_left"))
{
TranslateObjectLocal(-Transform.Basis.X);
}
跳躍:
# Keep in mind Y is up-axis
if Input.is_action_just_pressed("jump"):
velocity.y = JUMP_SPEED
move_and_slide()
// Keep in mind Y is up-axis
if (Input.IsActionJustPressed("jump"))
velocity.Y = JumpSpeed;
MoveAndSlide();
所有常見的行為和邏輯都可以用向量來完成.
設定資訊
當然, 有些情況下你想要將一些資訊賦予到變換上. 想像一下第一人稱控制器或環繞旋轉的相機. 那些肯定是用角度來完成的, 因為你 確實希望 變換以特定的順序進行.
遇到這種狀況時,請將角度與旋轉維持在變換 外部,並於每一幀重新設定。不要嘗試從變換中讀回角度再重複使用,因為變換本來就不是這樣設計的。
環顧四周,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
// accumulators
private float _rotationX = 0f;
private float _rotationY = 0f;
public override void _Input(InputEvent @event)
{
if (@event is InputEventMouseMotion mouseMotion)
{
// modify accumulated mouse rotation
_rotationX += mouseMotion.Relative.X * LookAroundSpeed;
_rotationY += mouseMotion.Relative.Y * LookAroundSpeed;
// reset rotation
Transform3D transform = Transform;
transform.Basis = Basis.Identity;
Transform = transform;
RotateObjectLocal(Vector3.Up, _rotationX); // first rotate about Y
RotateObjectLocal(Vector3.Right, _rotationY); // then rotate about 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)
// Convert basis to quaternion, keep in mind scale is lost
var a = new Quaternion(transform.Basis);
var b = new Quaternion(transform2.Basis);
// Interpolate using spherical-linear interpolation (SLERP).
var c = a.Slerp(b, 0.5f); // find halfway point between a and b
// Apply back
transform.Basis = new Basis(c);
Quaternion 型別參考資料具有關於該資料型別的更多資訊 (它也能執行變換累加、變換點等,儘管這些用法較少見)。如果您多次對四元數進行插值或應用運算,請記住它們最終需要被正規化。否則,它們也會遇到數值精確度誤差。
四元數在處理相機/路徑/等東西的移動軌跡時很有用. 插值的結果總會是正確且平滑的.
變換是你的朋友
對於大多數初學者來說, 習慣於使用變換可能需要一些時間. 但是, 一旦你習慣了它們, 你會欣賞他們的簡單而有力.
不要猶豫,在 Godot 的任何線上社群網站上尋求幫助,一旦你變得足夠自信,請幫助其他人!