Up to date

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

矩陣與變換

前言

在閱讀本教學之前,我們推薦你從頭到尾閱讀並且理解 向量數學 教學,因為本教學需要一點向量的知識。

這個教學介紹的是*變換*以及我們如何在 Godot 中使用矩陣表示它。這不是完整深入的矩陣指南。變換大多數時候被應用為平移、旋轉、縮放,所以我們將會關注如何用矩陣表示這些變換。

雖然這個指南主要關注於 2D,使用 Transform2DVector2,但是 3D 中的工作方式也十分相似。

備註

正如之前的教學中提到的,要記住在 Godot 中,2D 的 Y 軸是*向下*的。這與學校裡教的線性代數正好相反,在那裡 Y 軸是向上的。

備註

這裡的慣例是 X 軸用紅色、Y 軸用綠色、Z 軸用藍色。本教學中的顏色都遵循這個慣例,不過我們也在原點向量上使用藍色。

矩陣分量和單位矩陣

單位矩陣代表一個沒有平移、沒有旋轉、沒有縮放的變換。讓我們開始看看單位矩陣以及它的分量如何與它的視覺表現相聯繫吧。

../../_images/identity.png

矩陣有行和列,變換矩陣對它們有特定的約定。

在上圖中,我們可以看到紅色的 X 向量由矩陣的第一列表示,綠色的 Y 向量則由第二列表示。改變列就會改變這些向量。我們將在接下來的幾個例子中看到如何操作它們。

您不必擔心直接操作行, 因為我們通常使用列. 然而, 你可以把矩陣的行看作是表示哪些向量有助於在給定的方向上移動.

當我們指定一個值例如 t.x.y , 這是X列向量的Y分量. 換句話說, 是這個矩陣的左下角. 類似地, t.x.x 是左上角, t.y.x 是右上角, 然後 t.y.y 是右下角, 在這裡 t 是一個 Transform2D.

縮放變換矩陣

應用一個縮放是最容易理解的操作之一. 讓我們開始吧, 把Godot logo放置於我們的向量之下, 這樣我們可以直觀得看出應用於這些物件上的效果:

../../_images/identity-godot.png

現在, 為了縮放矩陣, 我們唯一需要做的就是將每個矩陣分量乘以我們想要的縮放比例. 來將它縮放兩倍吧,1乘以2變成了2,0乘以2變成了0, 所以我們最後得到了這個:

../../_images/scale.png

要在程式碼中做到這件事. 我們可以簡單地乘上每個向量:

var t = Transform2D()
# Scale
t.x *= 2
t.y *= 2
transform = t # Change the node's transform to what we calculated.

如果我們想要回到它原來的尺度, 我們可以對每個分量乘以0.5. 這幾乎就是縮放一個變換矩陣的全部了.

要從一個已經存在的變換矩陣中計算物件的縮放尺度, 你可以對每個列向量使用 length() 方法.

備註

在實際的專案中, 你可以使用 scaled() 方法去執行縮放.

建立內容

我們將以與前面相同的方式開始, 在標識矩陣下使用Godot徽標:

../../_images/identity-godot.png

舉個例子,假設我們想順時針旋轉 Godot 旗標 90 度。現在,X 軸指向右邊,Y 軸向下。如果我們在頭腦中旋轉這些,我們就會在邏輯上看到,新的 X 軸應該向下,新的 Y 軸應該指向左邊。

你可以想像, 你抓住了godot的圖示和它的向量, 然後旋轉它的中心. 無論你在哪裡完成旋轉, 向量的方向決定了矩陣是什麼.

我們需要在法線座標中表示“下”和“左”,因此我們將 X 設為 (0, 1),將 Y 設為 (-1, 0)。這些也正是 Vector2.DOWN 和 Vector2.LEFT 的值,當我們這樣做時,就會得到旋轉物件想要的結果:

../../_images/rotate1.png

如果你很難理解上面的內容, 那就試試這個練習: 剪一個正方形的紙, 在上面畫X和Y向量, 把它放在圖表紙上, 然後旋轉它並記下端點.

要在程式碼中執行旋轉, 我們需要能夠以程式設計方式計算值. 這幅圖像顯示了從旋轉角度計算變換矩陣所需的公式. 如果這部分看起來很複雜, 別擔心, 我保證這是你需要知道的最難的事情.

../../_images/rotate2.png

備註

Godot 用弧度表示所有的旋轉,不用角度。完整轉一圈是 TAUPI*2 弧度,90 度的四分之一圈是 TAU/4PI/2 弧度。使用 TAU 通常會讓程式碼更易讀。

備註

有趣的事實:在 Godot 中,不僅 Y 是*朝下*的,旋轉也是順時針的。這意味著所有的數學和三角函式的行為都與 Y 朝上的 CCW 坐標系相同,因為這些差異“相互抵消”了。你可以認為在這兩個坐標系中的旋轉都是“從 X 到 Y”。

為了執行 0.5 弧度的旋轉(約 28.65 度),我們只需將 0.5 代入上面的公式中,然後計算出實際值應該是什麼:

../../_images/rotate3.png

這是在程式碼中完成的方法(將腳本放在 Node2D 上):

var rot = 0.5 # The rotation to apply.
var t = Transform2D()
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
transform = t # Change the node's transform to what we calculated.

要從現有的變換矩陣中計算物件的旋轉,可以使用 atan2(t.x.y, t.x.x),其中 t 是 Transform2D。

備註

在實際專案中,可以使用 rotated() 方法進行旋轉。

變換矩陣的基

到目前為止,我們只使用 xy 向量,它們負責表示旋轉、縮放和/或剪切(高級,會在文末提及)。X 和 Y 向量合稱變換矩陣的*基*(Basis)。“基”和“基向量”都是非常重要的術語。

你可能已經注意到 Transform2D 實際上有三個 Vector2 值:xyorigin。其中 origin 值不是基的一部分,而是變換的一部分,我們需要用它來表示位置。從現在開始,我們將在所有例子中記錄原點向量。您可以將原點看作另一列,但把它認為是完全獨立的通常更好。

請注意在 3D 中,Godot 有一個單獨的 Basis 結構,裡面包含矩陣基的三個 Vector3 的值。因為程式碼可能變得複雜,因此將它們從 class_Transform`(由一個 :ref:`class_Basis 和一個額外的原點 Vector3 組成)中拆分出來是值得的。

變換矩陣的平移

origin 向量的修改稱為對變換矩陣的*平移*。平移其實上是“移動”物件的一個技術術語,但它不會包含任何旋轉。

讓我們通過一個例子來幫助理解這一點。我們將像上次一樣從恒等變換開始,但這次我們將記錄原點向量。

../../_images/identity-origin.png

如果希望物件移動到 (1, 2) 的位置,只需將其 origin 向量設定為 (1, 2):

../../_images/translate.png

還有一個 translated() 方法,它執行的是與直接增加或更改 origin 不同的操作。這個 translated() 方法將讓該物件相對於它自己的旋轉進行平移。例如,順時針旋轉了 90 度的物件如果用 Vector2.UP 呼叫了 translated(),那麼它就會向右移動。

備註

Godot 的 2D 使用基於像素的座標,所以在實際專案中,你會想要轉換成數百個單位。

融會貫通

我們將把到目前為止提到的所有內容都應用到一個變換上。接下來,使用 Sprite 節點建立一個簡單的專案,並使用 Godot 徽標作為其紋理資源。

讓我們將平移設定為 (350, 150),旋轉設為 -0.5 rad,縮放設為 3。我把螢幕截圖和重現程式碼都發出來了,但我鼓勵您不看程式碼來嘗試重現螢幕截圖!

../../_images/putting-all-together.png
var t = Transform2D()
# Translation
t.origin = Vector2(350, 150)
# Rotation
var rot = -0.5 # The rotation to apply.
t.x.x = cos(rot)
t.y.y = cos(rot)
t.x.y = sin(rot)
t.y.x = -sin(rot)
# Scale
t.x *= 3
t.y *= 3
transform = t # Change the node's transform to what we calculated.

剪切變換矩陣(高級)

備註

如果您只想瞭解如何*使用*變換矩陣,請隨意跳過本教學的這一節。本節探討變換矩陣的一個不常用的方面,目的是為了你建立對它們的理解。

Node2D 提供了開箱即用的剪切屬性。

您可能已經注意到,變換的自由度比上述操作的組合要多。2D 變換矩陣的基在兩個 Vector2 值中總共有四個數,而旋轉值和縮放的 Vector2 只有三個數字。缺失自由度的高級概念稱為*剪切*(Shearing)。

通常,您將始終擁有彼此垂直的基向量。但是,剪切在某些情況下可能很有用,瞭解剪切可以幫助您理解變換的工作原理。

為了直觀地向您展示它的外觀, 讓我們在Godot徽標上疊加一個網格:

../../_images/identity-grid.png

此網格上的每個點都是通過將基向量相加而獲得的。右下角是 X + Y,而右上角是 X - Y。如果我們更改基向量,整個柵格也會隨之移動,因為柵格是由基向量組成的。無論我們對基向量做什麼更改,柵格上目前平行的所有直線都將保持平行。

例如, 讓我們將Y設定為(1,1):

../../_images/shear.png
var t = Transform2D()
# Shear by setting Y to (1, 1)
t.y = Vector2.ONE
transform = t # Change the node's transform to what we calculated.

備註

不能在編輯器中設定Transform2D的原始值, 所以想要剪切物件, 必須使用程式碼.

由於向量不再垂直, 因此物件已被剪切. 柵格的底部中心(相對於自身為(0,1))現在位於世界位置(1,1).

物件內部座標在紋理中稱為UV座標, 因此我們借用此處的術語. 要從相對位置找到世界位置, 公式為U*X+V*Y, 其中U和V是數字,X和Y是基向量.

柵格的右下角始終位於UV位置(1,1), 位於世界位置(2,1), 該位置是從X*1+Y*1(即(1,0)+(1,1)或(1+1,0+1)或(2,1)計算得出的. 這與我們觀察到的圖像右下角的位置相吻合.

同樣, 柵格的右上角始終位於UV位置(1, -1), 位於世界位置(0, -1), 該位置是從X*1+Y*-1計算得出的,X*1+Y*-1是(1,0)-(1,1)或(1-1,0-1)或(0, -1). 這與我們觀察到的圖像右上角的位置相吻合.

希望您現在完全瞭解變換矩陣如何影響物件,以及基向量之間的關係以及物件的“UV”或“內部座標”如何更改其世界位置。

備註

在Godot中, 所有變換數學運算都是相對於父節點完成的. 當我們提到 "世界位置" 時, 如果節點有父節點, 那麼它將相對於節點的父位置.

如果你想要更多的解釋,你可以查看 3Blue1Brown 關於線性變換的精彩影片:http://www.bilibili.com/video/BV1ys411472E?p=4

變換的實際應用

在實際專案中,您通常會通過將多個 Node2Dclass_Spatial 節點設定為彼此的父級來處理變換中的變換。

但是, 有時手動計算我們需要的值非常有用. 我們將介紹如何使用 Transform2DCLASS_Transform 手動計算節點轉換.

在變換之間轉換位置

在許多情況下,您可能需要將某個位置轉換為變換前或者變換後的位置。例如,如果您有一個相對於玩家的位置並想要搜尋世界(相對於玩家來說是父級)位置,或者如果您有一個世界位置並想知道它相對於玩家的位置。

通過“xform”方法,我們可以找到相對於玩家的向量如果定義在世界空間中的話應該是什麼:

# World space vector 100 units below the player.
print(transform * Vector2(0, 100))

我們可以使用 "xform_inv" 方法來搜尋世界空間位置(如果它是相對於玩家定義的):

# Where is (0, 100) relative to the player?
print(Vector2(0, 100) * transform)

備註

如果您事先知道變換位於 (0, 0) 處,則可以改用“basis_xform”或“basis_xform_inv”方法,這將跳過處理平移的過程。

相對於物件本身移動物件

一種常見的操作,尤其是在 3D 遊戲中,是相對於自身移動對象。例如,在第一人稱射擊遊戲中,當您按下 W 鍵時,您希望角色向前移動(-Z 軸)。

由於基向量是相對於父物件的方向,而原點向量是相對於父物件的位置,因此我們可以簡單地將基向量的倍數相加,以相對於物件本身移動物件。

此程式碼會讓物件向它自己的右邊移動 100 個單位:

transform.origin += transform.x * 100

要在 3D 中移動,需要將“x”替換為“basis.x”。

備註

在實際工程中,您可以使用 3D 中的 translate_object_local 或者 2D 中的 move_local_xmove_local_y 來實作。

將變換應用於變換

關於轉換, 需要瞭解的最重要的事情之一是如何將幾個轉換一起使用. 父節點的變換會影響其所有子節點. 讓我們來剖析一個例子.

在此圖像中, 子節點的元件名稱後面有一個 "2", 以將其與父節點區分開來. 這麼多數字可能看起來有點令人不知所措, 但請記住, 每個數位都會顯示兩次(在箭頭旁邊和矩陣中), 而且幾乎一半的數字都是零.

../../_images/apply.png

這裡進行的唯一轉換是父節點的比例為(2,1), 子節點的比例為(0.5,0.5), 兩個節點都指定了位置.

所有子變換都受父變換的影響。子物件的比例為 (0.5, 0.5),因此您會認為它是 1:1 的比例正方形,確實如此,但僅相對於父物件。子物件的 X 向量最終在世界空間中為 (1, 0),因為它是由父物件的基礎向量縮放的。類似地,子節點的 origin 向量被設定為(1,1),但由於父節點的基向量,這實際上會在世界空間中移動 (2, 1)。

要手動計算子變換的世界空間變換, 我們將使用以下程式碼:

# Set up transforms like in the image, except make positions be 100 times bigger.
var parent = Transform2D(Vector2(2, 0), Vector2(0, 1), Vector2(100, 200))
var child = Transform2D(Vector2(0.5, 0), Vector2(0, 0.5), Vector2(100, 100))

# Calculate the child's world space transform
# origin = (2, 0) * 100 + (0, 1) * 100 + (100, 200)
var origin = parent.x * child.origin.x + parent.y * child.origin.y + parent.origin
# basis_x = (2, 0) * 0.5 + (0, 1) * 0
var basis_x = parent.x * child.x.x + parent.y * child.x.y
# basis_y = (2, 0) * 0 + (0, 1) * 0.5
var basis_y = parent.x * child.y.x + parent.y * child.y.y

# Change the node's transform to what we calculated.
transform = Transform2D(basis_x, basis_y, origin)

在實際工程中, 我們可以通過 * 運算子將一個變換應用到另一個變換中, 從而找到孩子的世界變換:

# Set up transforms like in the image, except make positions be 100 times bigger.
var parent = Transform2D(Vector2(2, 0), Vector2(0, 1), Vector2(100, 200))
var child = Transform2D(Vector2(0.5, 0), Vector2(0, 0.5), Vector2(100, 100))

# Change the node's transform to what would be the child's world transform.
transform = parent * child

備註

當矩陣相乘時, 順序很重要!別把它們弄混了.

最後, 應用身份變換始終不起任何作用.

如果您想瞭解更多資訊,可以查看 3Blue1Brown 關於矩陣組成的精彩影片:http://www.bilibili.com/video/BV1ys411472E?p=5

求逆變換矩陣

affine_inverse 函式返回一個 "撤銷" 前一個轉換的轉換. 這在某些情況下可能很有用, 但只提供幾個範例會更容易.

將反變換乘以法線變換將撤銷所有變換:

var ti = transform.affine_inverse()
var t = ti * transform
# The transform is the identity transform.

通過轉換轉換位置及其反轉會導致相同的位置(與 "xform_inv" 相同):

var ti = transform.affine_inverse()
position = transform * position
position = ti * position
# The position is the same as before.

這一切是如何在 3D 模式下工作的?

變換矩陣的一個偉大之處在於, 它們在2D和3D變換之間的工作方式非常相似. 上面用於2D的所有程式碼和公式在3D中的工作方式都相同, 只有3個不同之處: 增加了第三個軸, 每個軸的型別為 Vector3, 並且Godot將 BasisCLASS_Transform 分開儲存, 因為數學運算可能會很複雜, 因此將其分開是有意義的.

與二維相比, 有關平移, 旋轉, 縮放和剪切在三維中的工作方式的所有概念都是相同的. 要縮放, 我們取每個分量並將其相乘;要旋轉, 我們更改每個基向量指向的位置;要平移, 我們操縱原點;要剪切, 我們將基向量更改為不垂直.

../../_images/3d-identity.png

如果您願意, 最好嘗試一下轉換, 以瞭解它們是如何工作的. Godot 允許您直接從屬性面板編輯 3D 變換矩陣. 您可以下載此專案, 其中包含彩色線條和立方體, 以幫助在 2D 和 3D 中視覺化 Basis 向量和原點: https://github.com/godotengine/godot-demo-projects/tree/master/misc/matrix_transform

備註

不能在Godot 3.2的屬性面板中直接編輯Node2D的變換矩陣. 在Godot的未來版本中, 這一點可能會有所改變.

如果你想要更多的解釋,你可以查看 3Blue1Brown 關於 3D 線性變換的精彩影片:http://www.bilibili.com/video/BV1ys411472E?p=6

表示 3D 中的旋轉(高級)

2D 和 3D 變換矩陣之間最大的區別在於您如何在沒有基向量的情況下自行表示旋轉。

對於2D, 我們有一個在變換矩陣和角度之間切換的簡單方法(Atan2). 在3D中, 我們不能簡單地將旋轉表示為一個數字. 有一種叫做歐拉角的東西, 它可以將旋轉表示為一組3個數位, 但它們是有限的, 除了微不足道的情況外, 它們並不是很有用.

在 3D 中,我們通常不使用角度,我們要麼使用變換的基(在 Godot 中幾乎到處都使用),要麼使用四元數。Godot 可以使用 class_Quat 結構表示四元數。我給你的建議是完全忽略它們是如何在幕後工作的,因為它們非常複雜和不直觀。

然而, 如果你真的想知道它是如何工作的, 這裡有一些很棒的參考資料, 你可以按順序跟隨它們:

https://www.youtube.com/watch?v=mvmuCPvRoWQ

https://www.youtube.com/watch?v=d4EgbgTm0Bg

https://eater.net/quaternions