左手系与右手系
众所周知,OpenGL与DirectX使用了不同的屏幕空间坐标系

如果要将右侧坐标系变为左侧那种,我们只需要做一些旋转操作,将右侧坐标系顺时针旋转180度,再将整个坐标系水平翻转即可。我们可以通过一定的旋转操作将两个坐标系重合,那么我们就称它们具有相同的旋向性(handedness)。
但对于三维坐标系来说,有时单靠旋转不能将两个坐标系重合,比如左手系与右手系。

除了坐标轴的朝向不同外,它们对于正向旋转的定义也不同,分别由左手法则与右手法则定义。一般来说,左手系的旋转正方向是顺时针,而右手系是逆时针。
左右手系之间可以进行相互转换,只需要让任意一轴反转,其他轴保持不变即可。
对于开发者来说,使用左手系和右手系都是一样的,不会影响底层的数学运算,只会在视觉上有一些差别。左右手坐标系在z轴上的移动以及旋转方向是不同的,如果要从一种坐标系转移到另一种坐标系,并保持视觉上的不变,则需要进行一些转换。
Unity中,模型空间和世界空间使用左手系;对于观察空间,则是右手系;对于观察空间,我们目视屏幕的方向一定是z轴,我们的右手边是x轴正方向;右手系则代表着z轴正方向是从屏幕指向了我们,z值越小代表着深度越大,离屏幕越远。
向量
向量的点积运算
向量的点积运算可以用来判断两个向量的方向
- 如果两个向量的点积大于0,则它们的夹角小于90度,即它们的方向趋于一致。
- 如果两个向量的点积小于0,则它们的夹角大于90度,即它们的方向趋于相反。
- 如果两个向量的点积等于0,则它们的夹角等于90度,即它们是正交(垂直)的。

向量的叉积运算
注意运算结果的方向与坐标系的类型有关系,如果是左手系需要用左手定则判断结果向量的方向,右手系则要用右手定则。
叉积一个很常见的应用则是判断一个点是否在三角形内部:
-
确定三角形的三个顶点坐标,分别记为 P1(x1, y1, z1), P2(x2, y2, z2), P3(x3, y3, z3)。
-
计算三角形的三条边向量:
- 边向量1:
V1 = P2 - P1
- 边向量2:
V2 = P3 - P2
- 边向量3:
V3 = P1 - P3
-
计算点 P(x, y, z) 到三角形三条边的叉积向量:
N1 = (P - P1) × V1
N2 = (P - P2) × V2
N3 = (P - P3) × V3
-
- 如果三个叉积向量的方向全部相同,则点
P 在三角形内部。
- 如果三个叉积向量的方向有任何不同,则点
P 在三角形外部。
矩阵
逆矩阵
一个矩阵是逆矩阵(inverse matrix)的前提是它是一个方阵(Square matrix)。
对于一个方阵M,它的逆矩阵M−1有如下的性质:
MM−1 = M−1M = I
也不是所有的方阵都有逆矩阵,比如全部元素为0的方阵。
如果一个矩阵的行列式不为0,则它就是可逆的,有逆矩阵。
逆矩阵的几何意义就是,一个矩阵可以用来表示一个变换,而逆矩阵就可以还原这个变换。这很容易证明:
对于一个向量v: M−1Mv = Iv = v
正交矩阵
如果一个方阵M与它的转置矩阵的乘积是一个单位矩阵,那么这个矩阵就是正交(orthogonal matrix),反过来也是成立。
MMT = MTM = I
我们发现这个性质与逆矩阵很相似,所以我们能得出,如果一个矩阵正交,则:
MT=M−1
这个式子非常有用,在三维变换中我们经常需要求解一个变换的逆矩阵来还原这个变换,而逆矩阵的计算量往往很大,而转置矩阵矩阵很容易求解。
如何知道一个方阵是否是正交矩阵?如果要判断MMT = I成立显然需要一定的计算量,可能和直接求解逆矩阵无异。因此我们更想不需要计算,而是仅仅从一个矩阵的构造过程来判断这个矩阵是否是正交矩阵,那么我们需要了解正交矩阵的几何意义。
以3×3的矩阵正交矩阵为例,根据正交矩阵的定义:
MTM=−−−c1c2c3−−−∣c1∣c2∣∣c3∣=c1⋅c1c2⋅c1c3⋅c1c1⋅c2c2⋅c2c3⋅c2c1⋅c3c2⋅c3c3⋅c3=100010001=I
这样我们就有9个等式
c1⋅c1=1,c2⋅c1=0,c3⋅c1=0,c1⋅c2=0,c2⋅c2=1,c3⋅c2=0,c1⋅c3=0c2⋅c3=0c3⋅c3=1
那么:
- 矩阵的每一行是一个单位矢量
- 矩阵的每一行的矢量相互垂直
因为正交矩阵的性质,这个结果对于每一列也是适用的。如果矩阵满足这样的条件,那么它就是一个正交矩阵,而一组标准正交基满足这个条件。
正交基与标准正交基:
正交基是指在一个向量空间中,一组相互正交的基向量。这意味着基向量之间的点积为零。正交基向量不一定是单位向量,即它们的长度可以不是1。很常见的就是坐标轴。
标准正交基是一组不仅正交而且归一化的基向量。在标准正交基中,每个基向量不仅相互正交,而且都是单位向量(长度为1)。
比如(sinθ, −cosθ)和(cosθ, sinθ)就是一个二维空间中的标准正交基
也就是说如果我们使用标准正交基构造矩阵,例如[sinθ cosθ −cosθ sinθ]那么这个矩阵一定就是正交矩阵。
行矩阵与列矩阵
当一个向量要用来和一个矩阵相乘时,我们就需要考虑将向量转换为行矩阵还是列矩阵。
要注意的是,因为矩阵相乘对两个矩阵的形式是有要求的,并且矩阵的计算顺序也会对结果有影响。
在图形学计算中,一般将矢量转化为列矩阵放在矩阵的右侧进行矩阵相乘。
变换
变换(transform)指的是把一些数据,如点、向量甚至是颜色通过某种方式转换的过程。
线性变换(linear transform)指的是只保留向量加和标量乘的变换。
f(x) + f(y) = f(x + y)
kf(x) = f(kx)
比如缩放变换是一个线性变换,例如放大两倍:f(x) = 2x;旋转也是一种线性变换。
对于三维空间中的线性变换,仅用一个3×3的矩阵就可以表示所有的线性变换。
线性变换还包括错切(shear)、镜像(mirroring或reflection)、正交投影
但线性变换并不能满足所有的变换,考虑平移变换f(x) = x + (n1, n2, n3),线性变换只满足向量乘不满足向量加,因此三维空间中的变换,仅用一个3×3的矩阵是不够的。
为了能够解决使用一个矩阵表示全部变换的问题,仿射变换(affine transform)出现了,它合并了线性变换和平移变换,先进行一次线性变换,再进行一次平移变换。
仿射变换可以用一个4×4的矩阵来表示,扩展到了四维空间:齐次坐标空间(homogeneous space)下。
齐次坐标
变换矩阵扩展到4 ×4后,为了实现变换(矩阵乘法),向量也需要扩展到四维向量,也就是齐次坐标(homogeneous coordinate)。
对于一个点,扩展的方式是将其的w分量([x, y, z, w])设置为1,对于方向向量来说,则是将其w分量设置为0。这样的设计有很多原因与好处,最直接的是,对一个点进行齐次坐标的变换时,平移、旋转、缩放都会应用到这个点;而对于方向向量,平移不会应用。
我们将纯位移、纯旋转和纯缩放的变换矩阵叫做基础变换矩阵,而能够表示全部变换的齐次坐标下的4 ×4矩阵则可以这样分解:
[M3×301×3t3×11]
M3×3用于表示旋转和缩放,t3×1用于表示平移,01×3是零矩阵
平移
对一个点进行平移变换:
10 0 0 0 1 0 0 0 0 1 0 tx ty tz 1 x y z 1 = x + tx y + tyz + tz 1

如果对一个方向向量进行平移操作则不会生效。
平移矩阵的逆矩阵,其实就是反向位移
10 0 0 0 1 0 0 0 0 1 0 −tx −ty −tz 1
很明显,平移矩阵不是正交矩阵。
缩放
对一个模型沿着x、y和z轴进行缩放:
kx0 0 0 0 ky 0 0 0 0 kz 0 0 0 0 1 x y z 1 = kxx kyykzz 1
缩放变换对于方向向量同样生效。
如果kx = ky = kz,那么这个缩放就是一个统一缩放(uniform scale),否则就是非统一缩放(nonuniform scale)。从视觉上看,统一缩放就是按模型原有的比例去缩放模型,而非统一缩放会压缩或者拉伸模型,更重要的是,统一缩放不会改变一些信息,例如对法线进行变换时,如果使用非统一缩放,就会得到错误的结果。
如果缩放因子中有一个或者三个为负的分量,那么我们就获得了一个反射矩阵(reflection matrix)或者说镜像矩阵(mirror matrix);如果有两个为负的分量,那么这个矩阵会让物体旋转180度(中心对称)。
通常在检测到一个镜像变换的时候,都会进行一些特殊处理,例如一个顶点顺序为逆时针的三角形经过镜像变换之后变为顺时针,顶点顺序的改变会导致错误的光照效果和背面剔除。如果缩放矩阵的行列式的值为负数,说明这是一个反射矩阵。
缩放矩阵的逆矩阵:
kx1 0 0 0 0 ky1 0 0 0 0 kz1 0 0 0 0 1
缩放矩阵(一般情况下)也不是正交矩阵。
注意,上面的矩阵用于沿着坐标轴缩放,如果要沿着任意方向进行缩放,则需要先进行一个变换改变朝向,使得缩放轴与坐标轴一致,之后进行缩放,最后使用一个逆变换将朝向变回来。
暂时无法在飞书文档外展示此内容
具体来说,如果给定一个方向f,首先将其分解为3个标准正交向量,然后构建旋转变换矩阵
F = [fx 0 fy 0 fz 0 0 1]
先变换会标准坐标系,也就是先乘以FT,再进行缩放操作,之后再乘以F变换会原来的坐标系
旋转
注意下面提到3个矩阵的旋转
将点绕x轴旋转θ度:
Rx(θ) = 1 0 0 0 0 cosθ sinθ 0 0 −sinθ cosθ 0 0 0 0 1
绕y轴旋转:
Ry(θ) = cosθ 0 −sinθ 0 0 1 0 0 sinθ 0 cosθ 0 0 0 0 1
绕z轴旋转:
Rz(θ) = cosθ sinθ 0 0 −sinθ cosθ 0 0 0 0 1 0 0 0 0 1
旋转矩阵的逆矩阵,只需要旋转相反的角度就可以得到。
旋转矩阵是正交矩阵(注意,这里说的是非齐次坐标下的矩阵,也就是只有M3×3的部分),而且多个旋转矩阵之间的串联同样是正交的。
如果我们想让物体以某个点为中心,绕三个轴旋转,那么我们可以先向物体平移,使得旋转点与原点重合,再进行旋转。

复合变换
我们可以将平移、缩放、旋转组合起来,而这个变换过程可以用下面的公式表示:
P′ = MtranslationMrotationMscaleP
而根据矩阵乘法的结合律,TRS这三个矩阵可以提前计算合成一个矩阵P′ = MP,假如有几百万个点都需要应用同样的平移、缩放、旋转矩阵,用提前合成的一个矩阵要比分别使用三个矩阵计算要快得多。
之前提到了我们会将向量转换为列向量,所以上面公式的计算顺序实际上是从右向左;并且矩阵乘法时,矩阵的计算顺序会影响计算结果,也就是我们需要确定好变换的顺序,在绝大多数情况下,我们约定的变换顺序是先缩放,再旋转,最后平移。
除了需要注意不同类型的变换顺序外,我们有时还需要小心旋转的变换顺序。当给出(θx, θy, θz)的旋转角度时,我们需要定义旋转顺序。在Unity中,这个旋转顺序是zxy,这在旋转相关的API文档中都有说明,
但得到的分解的旋转变换矩阵是:
MrotateZMrotateXMrotateY
这个矩阵与上面说的计算顺序从右向左冲突了,这是因为有两种不同的旋转方式(即两种不同的坐标系选择):
第一种方式:
每次旋转都相对于原始固定坐标系E进行
第二种方式:
每次旋转都相对于上一次旋转后的新坐标系进行。
简单举例来说,如果在Unity中调用transform.Rotate(30, 40, -50),使用的就是第一种旋转方式,以全局坐标系的顺序进行旋转的,即先旋转 Z 轴,再旋转 X 轴,最后旋转 Y 轴,所有的旋转都是基于物体初始的坐标系。
而如果分开调用 transform.Rotate(new Vector3(0, 90, 0));,transform.Rotate(new Vector3(30, 0, 0)); 和 transform.Rotate(new Vector3(0, 0, -40)); 的情况,每一次调用都会改变物体的局部坐标系,正是因为每次旋转都改变旋转坐标系,所以倒序得到的就和一次调动的结果相同,这就是分解后旋转矩阵是倒序的原因