矩阵和变换

介绍

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

本教程将介绍 变换 并且会涉及一些矩阵(但不是深入的)。

转换大部分时间都是作为转换、旋转和缩放应用的,因此在这里它们将被视为优先级。

定向坐标系

想象一下我们在太空某处有一艘宇宙飞船。在Godot,这很容易,只要把船移动到某个地方并旋转它:

../../_images/tutomat1.png

在二维中,这个看起来很简单,一个位置和一个旋转的角度。但是请记住,我们在这里长大了,不使用角度(另外,在3D中工作时角度甚至没有那么有用)。

我们应该意识到,在某个时刻,有人 设计 这艘宇宙飞船。无论是绘图(如paint.net、gimp、photoshop等)中的二维,还是通过3D DCC工具(如blender、max、maya等)中的三维。

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

../../_images/tutomat2.png

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

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

../../_images/tutomat3.png

它是怎么到那里的?是什么把它从它设计的地方移到了现在的位置?答案是…一 转型 ,船是 转化 从原来的位置到新的位置。这样可以显示船的位置。

但是转换这个术语太通用了,无法描述这个过程。为了解决这个难题,我们将在当前位置叠加船舶的原始设计位置:

../../_images/tutomat4.png

因此,我们可以看到“设计空间”也发生了变化。我们如何才能最好地表现这种转变?让我们用3个向量来表示这个(在2d中),一个指向x正的单位向量,一个指向y正的单位向量和一个平移。

../../_images/tutomat5.png

让我们把这三个向量称为“x”、“y”和“origin”,并把它们叠加在船上,这样更有意义:

../../_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和origin)是 定向坐标系 . X和Y是 基础起源 是偏移量。

基础

我们知道什么是起源。设计坐标系的0,0(原点)在转换到新位置后结束。这就是为什么它被称为 起源 但实际上,这只是对新职位的一种补偿。

基础更有趣。基础是OCS中X和Y从新的转换位置开始的方向。它告诉我们在二维或三维中发生了什么变化。原点(偏移)和基准(方向)传达“嘿,你设计的原始x和y轴是 就在这里 ,指向 这些指示 ."

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

../../_images/tutomat10.png

向量在矩阵的上面,水平。下一个问题是……这个矩阵是什么?好吧,我们假设你从未听说过矩阵。

Godot的转变

本教程将不深入解释矩阵数学(及其运算),只解释其实际用途。有很多这样的材料,在完成本教程之后应该更容易理解。我们将解释如何使用转换。

变压器2D

变压器2D 是3x2矩阵。它有3个vector2元素,用于2d。“x”轴是元素0,“y”轴是元素1,“原点”是元素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”点右:矢量2(1,0)

  • “Y”点向上(或向下像素):矢量2(0,1)

  • “原点”是原点向量2(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))

操作

旋转

使用“旋转”功能完成旋转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

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

如果相反,需要翻译 地方的 矩阵的坐标(朝向 基础 是定向的),有 Transform2D.translated() 方法:

# 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);

但更好的是,正如您在下一节中所读到的那样,这里还有帮助函数。

局部到全局坐标,反之亦然

有一些辅助方法可以在局部坐标和全局坐标之间进行转换。

Node2D.to_local()Node2D.to_global() 对于二维以及 Spatial.to_local()Spatial.to_global() 对于3D。

规模

矩阵也可以缩放。缩放将基础向量乘以向量(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

矩阵的用法类似于乌龟。这只乌龟很可能体内有一个母体(而且你很可能学习了很多年 之后 发现圣诞老人不是真的)。

变换

变换是在坐标系之间切换的行为。要将位置(二维或三维)从“设计器”坐标系转换为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);

仿射逆

仿射逆矩阵是一个对另一个矩阵进行逆运算的矩阵,不管矩阵是否有尺度或轴向量是否正交。仿射逆运算采用仿射_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);

因为在矩阵数学中, 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,我们处理3 Vector3 旋转矩阵的向量,以及原点的额外向量。

基础

godot有一个3x3矩阵的特殊类型,名为 Basis . 它可以用来表示三维旋转和缩放。子向量可以访问为:

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))

三维旋转

三维旋转比二维旋转更复杂(平移和缩放相同),因为旋转是一种隐式的二维操作。要在三维中旋转,请执行以下操作: axis ,必须选择。然后,围绕这个轴旋转。

旋转轴必须是 法向量 . 和中一样,可以指向任何方向但长度必须为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

  • 起源 (属于类型) Vector3

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

一个例子:

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)