矩阵与变换

简介

在阅读本教程之前,建议阅读关于 向量数学 的前一篇教程,因为这是一个直接的续作。

本教程将介绍 转换 并将介绍一些关于矩阵的内容(但不深入)。

转换大部分时间用于平移、旋转和缩放,因此这里将优先考虑它们。

定向坐标系(OCS)

想象我们有一艘宇宙飞船在太空的某个地方。在Godot中,这很简单,只要把船移动到某个地方,然后旋转它:

../../_images/tutomat1.png

在2D中,这个看起来很简单,旋转的位置和角度。但请记住,我们在这里成长,不使用角度(另外,在3D中,角度甚至没有那么有用)。

我们应该意识到,在某个时刻,有人设计了这个宇宙飞船。无论是用于绘图中的2D,如Paint.net、Gimp、Photoshop等,还是通过3DDCC工具(如Blender、Max、Maya等)进行3D处理。

在设计时,它没有旋转。它是在它自己的坐标系中设计的。

../../_images/tutomat2.png

这意味着船的顶端有一个坐标,鳍有另一个坐标,等等,可以是像素(2D)或顶点(3D)。

那么,让我们再次回忆一下这艘船在太空的某个地方:

../../_images/tutomat3.png

它是如何到达那里的?是什么把它从设计的位置移动和旋转到现在的位置?答案是…一个 变换,船从原来的位置 变换 到新的位置。这才使得船显示在那里。

但是变换是描述这个过程的术语过于泛化。为了解决这个难题,我们将将船只的原始设计位置叠加在当前位置:

../../_images/tutomat4.png

所以,我们可以看到 “设计空间” 也被改造了。我们怎样才能最好地表示这个变换?我们用3个向量(在2D中)一个指向X正的单位向量,一个指向Y正的单位向量和一个平移。

../../_images/tutomat5.png

我们称这三个向量为``X````Y``和``原点``,我们也把它们叠加在飞船上,这样更有意义:

../../_images/tutomat6.png

这个更好,但是仍然没有意义。X,Y和原点和飞船如何到达那里有什么关系?

好吧,让我们以船顶端的点作为参考:

../../_images/tutomat7.png

让我们对它应用下面的操作(对飞船上的所有点也是如此,但我们将跟踪顶端作为参考点):

var new_pos = pos - origin
var newPosition = pos - origin;

对选定点执行此操作将将其移回中心:

../../_images/tutomat8.png

这是意料之中的,但接下来让我们做一些更有趣的事情。用X和这个点的点积,再加上Y和这个点的点积:

var final_pos = Vector2(x.dot(new_pos), y.dot(new_pos))
var finalPosition = new Vector2(x.Dot(newPosition), y.Dot(newPosition));

那么我们所拥有的是…等一下,这是船的设计位置!

../../_images/tutomat9.png

这个黑魔法是怎么发生的?船在太空中迷失了方向,现在它又回到了家里!

这可能看起来很奇怪,但它确实有很多逻辑。记住,正如我们在 向量数学 中看到的,到X轴的距离和到Y轴的距离是计算出来的。计算方向或平面上的距离是点积的用途之一。这足以获得船上每个点的设计坐标。

到目前为止,我们一直在研究的(X, Y和原点)是一个 面向坐标系 。X和Y是 原点 是偏移量。

Basis

我们知道原点是什么。它是设计坐标系原点(0,0)在变换到一个新位置后的最终位置。这就是为什么它叫做原点,但是在实践中,它只是对新位置的偏移。

基础更有趣。 基础是来自新的转换位置的OCS中的X和Y的方向。 它以二维或三维方式告知已发生的变化。 原点(偏移)和基础(方向)通信``嘿,您的设计的原始X轴和Y轴正好 在这里 ,指向 这些方向 。``

所以,让我们改变基础的表示。 我们使用 矩阵 而不是2个向量。

../../_images/tutomat10.png

向量在矩阵中水平地向上。 现在的下一个问题是……这个矩阵是什么东西? 好吧,我们假设您从来没有听说过矩阵。

Godot中的变换

本教程不会深入解释矩阵数学(及其操作),只是实际应用。 网上有很多关于矩阵的资料,但是完成本教程后应该会加深对矩阵理解。 我们将解释如何使用变换。

Transform2D

class_Transform2D 是一个3x2矩阵。 它有3个Vector2元素,用于2D。 ``X``轴是第0个元素,``Y``轴是第1个元素,``Origin``是第2个元素.它不是为了方便而在基础/原点上划分,是因为这样简单。

var m = Transform2D()
var x = m[0] # 'X'
var y = m[1] # 'Y'
var o = m[2] # 'Origin'
var m = new Transform2D();
Vector2 x = m[0]; // 'X'
Vector2 y = m[1]; // 'Y'
Vector2 o = m[2]; // 'Origin'

大多数操作将使用此数据类型(Transform2D)进行说明,但相同的逻辑适用于3D。

单位矩阵

一个重要的变换是``单位``矩阵。 这意味着:

  • X’ 指向右侧:Vector2(1,0)
  • Y’指向上方(或以像素为单位时,表示指向下方):Vector2(0,1)
  • ‘Origin’是原点 Vector2(0,0)
../../_images/tutomat11.png

很容易猜到 单位 矩阵只是一个将变换对齐其父坐标系的矩阵。 它是尚未平移,旋转或缩放的 OCS

# The Transform2D constructor will default to Identity
var m = Transform2D()
print(m)
# prints: ((1, 0), (0, 1), (0, 0))
// Due to technical limitations on structs in C# the default
// constructor will contain zero values for all fields.
var defaultTransform = new Transform2D();
GD.Print(defaultTransform);
// prints: ((0, 0), (0, 0), (0, 0))

// Instead we can use the Identity property.
var identityTransform = Transform2D.Identity;
GD.Print(identityTransform);
// prints: ((1, 0), (0, 1), (0, 0))

操作

旋转

通过使用 “rotated” 函数完成旋转Transform2D:

var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
../../_images/tutomat12.png

平移

有两种方法可以平移Transform2D,第一种方法是移动原点:

# Move 2 units to the right
var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
m[2] += Vector2(2,0)
// Move 2 units to the right
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
m[2] += new Vector2(2, 0);
../../_images/tutomat13.png

这将始终在全局坐标系中工作。

If instead, translation is desired in local coordinates of the matrix (towards where the basis is oriented), there is the Transform2D.translated() method:

# Move 2 units towards where the basis is oriented
var m = Transform2D()
m = m.rotated(PI/2) # rotate 90°
m = m.translated( Vector2(2,0) )
// Move 2 units towards where the basis is oriented
var m = Transform2D.Identity;
m = m.Rotated(Mathf.Pi / 2); // rotate 90°
m = m.Translated(new Vector2(2, 0));
../../_images/tutomat14.png

您也可以手动将全局坐标系转换为局部坐标系:

var local_pos = m.xform_inv(point)
var localPosition = m.XformInv(point);

但更好的是用辅助函数,您可以在下一节中阅读。

从本地到全局坐标,反之亦然

有用于在局部坐标和全局坐标之间进行转换的辅助方法。

There are Node2D.to_local() and Node2D.to_global() for 2D as well as Spatial.to_local() and Spatial.to_global() for 3D.

Scale(大小)

矩阵也可以缩放。 缩放将基向量乘以向量(X向量乘标量的x分量,Y向量乘标量的y分量)。 它让原点不变:

# Make the basis twice its size.
var m = Transform2D()
m = m.scaled( Vector2(2,2) )
// Make the basis twice its size.
var m = Transform2D.Identity;
m = m.Scaled(new Vector2(2, 2));
../../_images/tutomat15.png

矩阵中的这些操作是累积的。 这意味着每个操作都相对于前一个开始的。 对于那些一直生活在这个星球上足够长的人来说,变换如何工作的一个很好的参考是:

../../_images/tutomat16.png

矩阵与乌龟类似。 乌龟可能内部有一个矩阵(您可能需要在发现圣诞老人是假的后,再过许多年才理解到这句话)。

变换

变换是在坐标系之间切换的行为。 要将位置(2D或3D)从``设计者``坐标系转换为OCS,请使用``xform``方法。

var new_pos = m.xform(pos)
var newPosition = m.Xform(position);

仅限基(无平移):

var new_pos = m.basis_xform(pos)
var newPosition = m.BasisXform(position);

逆变换

为了进行相反的操作(我们用火箭做了什么),使用了``xform_inv``方法:

var new_pos = m.xform_inv(pos)
var newPosition = m.XformInv(position);

仅适用于基:

var new_pos = m.basis_xform_inv(pos)
var newPosition = m.BasisXformInv(position);

正交矩阵

但是,如果矩阵已被缩放(向量不是单位长度),或者基向量不是正交的(90°),则逆变换将不起作用。

换句话说,逆变换仅在 正交 矩阵中有效。 为此,必须计算仿射逆的这些情况。

单位矩阵的变换或逆变换将返回未改变的位置:

# Does nothing, pos is unchanged
pos = Transform2D().xform(pos)
// Does nothing, position is unchanged
position = Transform2D.Identity.Xform(position);

仿射逆

仿射逆是一个矩阵,它执行另一个矩阵的逆运算,无论矩阵是否具有比例或者轴向量不是正交的。 使用affine_inverse()方法计算仿射逆:

var mi = m.affine_inverse()
pos = m.xform(pos)
pos = mi.xform(pos)
# pos is unchanged
var mi = m.AffineInverse();
position = m.Xform(position);
position = mi.Xform(position);
// position is unchanged

如果矩阵是正交的,那么:

# if m is orthonormal, then
pos = mi.xform(pos)
# is the same is
pos = m.xform_inv(pos)
// if m is orthonormal, then
position = mi.Xform(position);
// is the same is
position = m.XformInv(position);

矩阵乘法

矩阵可以做乘法。 两个矩阵的乘法 ``链``(连接)它们的变换。

但是,按照惯例,乘法可以交换顺序。

示例:

var m = more_transforms * some_transforms
var m = moreTransforms * someTransforms;

为了更清楚一点,这个:

pos = transform1.xform(pos)
pos = transform2.xform(pos)
position = transform1.Xform(position);
position = transform2.Xform(position);

与下面这个是相同的:

# note the inverse order
pos = (transform2 * transform1).xform(pos)
// note the inverse order
position = (transform2 * transform1).Xform(position);

实际上,这是不一样的:

# yields a different results
pos = (transform1 * transform2).xform(pos)
// yields a different results
position = (transform1 * transform2).Xform(position);

因为在矩阵数学中,A * B与B * A不同。

乘以逆矩阵

矩阵乘以逆,将得到单位矩阵:

# No matter what A is, B will be identity
var B = A.affine_inverse() * A
// No matter what A is, B will be identity
var B = A.AffineInverse() * A;

乘以单位矩阵

矩阵乘以单位矩阵将得到矩阵本身:

# B will be equal to A
B = A * Transform2D()
// B will be equal to A
var B = A * Transform2D.Identity;

矩阵提示

使用变换层次结构时,请记住矩阵乘法是相反的! 要获取层次结构的全局变换,请执行以下操作:

var global_xform = parent_matrix * child_matrix
var globalTransform = parentMatrix * childMatrix;

对3个级别的情况:

var global_xform = gradparent_matrix * parent_matrix * child_matrix
var globalTransform = grandparentMatrix * parentMatrix * childMatrix;

要制作相对于父级的矩阵,请使用仿射逆(或正交矩阵的常规逆)。

# transform B from a global matrix to one local to A
var B_local_to_A = A.affine_inverse() * B
// transform B from a global matrix to one local to A
var bLocalToA = A.AffineInverse() * B;

恢复它就像上面的示例:

# transform back local B to global B
B = A * B_local_to_A
// transform back local B to global B
B = A * bLocalToA;

好的,希望这应该足够了! 让我们转移到3D矩阵来完成本教程。

3D中的矩阵&变换

如前所述,对于3D,我们处理3个用于旋转矩阵的 Vector3 向量,以及用于原点的额外向量。

Basis

Godot has a special type for a 3x3 matrix, named Basis. It can be used to represent a 3D rotation and scale. Sub vectors can be accessed as:

var m = Basis()
var x = m[0] # Vector3
var y = m[1] # Vector3
var z = m[2] # Vector3
var m = new Basis();
Vector3 x = m[0];
Vector3 y = m[1];
Vector3 z = m[2];

或者,像这样:

var m = Basis()
var x = m.x # Vector3
var y = m.y # Vector3
var z = m.z # Vector3
var m = new Basis();
Vector3 x = m.x;
Vector3 y = m.y;
Vector3 z = m.z;

单位基具有以下值:

../../_images/tutomat17.png

并且可以像这样访问:

# The Basis constructor will default to Identity
var m = Basis()
print(m)
# prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))
// 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);;
// prints: ((1, 0, 0), (0, 1, 0), (0, 0, 1))

3D中的旋转

3D中的旋转比2D中的旋转更复杂(平移和缩放是相同复杂度),因为旋转是个隐式2D操作。 要在3D中旋转,必须选取 。 然后,围绕该轴旋转。

旋转轴必须是 法线向量 。 就像可以指向任何方向的向量,但长度必须为1(1.0)。

#rotate in Y axis
var m3 = Basis()
m3 = m3.rotated( Vector3(0,1,0), PI/2 )
// rotate in Y axis
var m3 = Basis.Identity;
m3 = m3.Rotated(new Vector3(0, 1, 0), Mathf.Pi / 2);

变换

为了将最终组件添加到混合中,Godot提供了 Transform 类型。 变换有两个成员:

  • basis (类型是 Basis)
  • origin (类型是 Vector3)

任何3D变换都可以用Transform表示,基础和原点的分离使得分别进行平移和旋转变得更容易。

一个示例:

var t = Transform()
pos = t.xform(pos) # transform 3D position
pos = t.basis.xform(pos) # (only rotate)
pos = t.origin + pos # (only translate)
var t = new Transform(Basis.Identity, Vector3.Zero);
position = t.Xform(position); // transform 3D position
position = t.basis.Xform(position); // (only rotate)
position = t.origin + position; // (only translate)