我也是一个学习者,有问题请指出🙏 Reference: 《Unity Shader 入门精要》 《Games101》课程 《Real-Time Rendering 4th editon》
众所周知,OpenGL与DirectX使用了不同的屏幕空间坐标系
如果要将右侧坐标系变为左侧那种,我们只需要做一些旋转操作,将右侧坐标系顺时针旋转180度,再将整个坐标系水平翻转即可。我们可以通过一定的旋转操作将两个坐标系重合,那么我们就称它们具有相同的旋向性(handedness)。
但对于三维坐标系来说,有时单靠旋转不能将两个坐标系重合,比如左手系与右手系。
除了坐标轴的朝向不同外,它们对于正向旋转的定义也不同,分别由左手法则与右手法则定义。一般来说,左手系的旋转正方向是顺时针,而右手系是逆时针。
左右手系之间可以进行相互转换,只需要让任意一轴反转,其他轴保持不变即可。
对于开发者来说,使用左手系和右手系都是一样的,不会影响底层的数学运算,只会在视觉上有一些差别。左右手坐标系在z轴上的移动以及旋转方向是不同的,如果要从一种坐标系转移到另一种坐标系,并保持视觉上的不变,则需要进行一些转换。
Unity中,模型空间和世界空间使用左手系;对于观察空间,则是右手系;对于观察空间,我们目视屏幕的方向一定是z轴,我们的右手边是x轴正方向;右手系则代表着z轴正方向是从屏幕指向了我们,z值越小代表着深度越大,离屏幕越远。
向量的点积运算
向量的点积运算可以用来判断两个向量的方向
向量的叉积运算
注意运算结果的方向与坐标系的类型有关系,如果是左手系需要用左手定则判断结果向量的方向,右手系则要用右手定则。
叉积一个很常见的应用则是判断一个点是否在三角形内部:
P1(x1, y1, z1)
, P2(x2, y2, z2)
, P3(x3, y3, z3)
。V1 = P2 - P1
V2 = P3 - P2
V3 = P1 - P3
3. 计算点 P(x, y, z) 到三角形三条边的叉积向量:
N1 = (P - P1) × V1
N2 = (P - P2) × V2
N3 = (P - P3) × V3
4. 如果三个叉积向量的方向全部相同,则点 `P` 在三角形内部。
5. 如果三个叉积向量的方向有任何不同,则点 `P` 在三角形外部。
一个矩阵是逆矩阵(inverse matrix)的前提是它是一个方阵(Square matrix)。
对于一个方阵\mathbf{M} ,它的逆矩阵\mathbf{M}^{-1} 有如下的性质:
\mathbf{MM^{-1}} = \mathbf{M^{-1}M} = \mathbf{I}
也不是所有的方阵都有逆矩阵,比如全部元素为0的方阵。
如果一个矩阵的行列式不为0,则它就是可逆的,有逆矩阵。
逆矩阵的几何意义就是,一个矩阵可以用来表示一个变换,而逆矩阵就可以还原这个变换。这很容易证明:
对于一个向量\mathbf{v} : \mathbf{M^{-1}Mv} = \mathbf{Iv} = \mathbf{v}
如果一个方阵\mathbf{M} 与它的转置矩阵的乘积是一个单位矩阵,那么这个矩阵就是正交(orthogonal matrix),反过来也是成立。
\mathbf{MM^{T} = M^{T}M = I}
我们发现这个性质与逆矩阵很相似,所以我们能得出,如果一个矩阵正交,则:
\mathbf{M^{T} = M^{-1}}
这个式子非常有用,在三维变换中我们经常需要求解一个变换的逆矩阵来还原这个变换,而逆矩阵的计算量往往很大,而转置矩阵矩阵很容易求解。
如何知道一个方阵是否是正交矩阵?如果要判断\mathbf{MM^{T} = I} 从成立显然需要一定的计算量,可能和直接求解逆矩阵无异。因此我们更想不需要计算,而是仅仅从一个矩阵的构造过程来判断这个矩阵是否是正交矩阵,那么我们需要了解正交矩阵的几何意义。
以3×3 的矩阵正交矩阵为例,根据正交矩阵的定义:
\begin{array}{l}\boldsymbol{M}^{\mathrm{T}} \boldsymbol{M}=\left[\begin{array}{ccc}- & \mathbf{c}_{1} & - \\- & \mathbf{c}_{2} & - \\- & \mathbf{c}_{3} & -\end{array}\right]\left[\begin{array}{lll}\mid & & \mid \\\mathbf{c}_{1} & \mathbf{c}_{2} & \mathbf{c}_{3} \\\mid & \mid & \mid\end{array}\right] \\=\left[\begin{array}{lll}\mathbf{c}_{1} \cdot \mathbf{c}_{1} & \mathbf{c}_{1} \cdot \mathbf{c}_{2} & \mathbf{c}_{1} \cdot \mathbf{c}_{3} \\\mathbf{c}_{2} \cdot \mathbf{c}_{1} & \mathbf{c}_{2} \cdot \mathbf{c}_{2} & \mathbf{c}_{2} \cdot \mathbf{c}_{3} \\\mathbf{c}_{3} \cdot \mathbf{c}_{1} & \mathbf{c}_{3} \cdot \mathbf{c}_{2} & \mathbf{c}_{3} \cdot \mathbf{c}_{3}\end{array}\right] \\= \left[\begin{array}{lll}1 & 0 & 0 \\0 & 1 & 0 \\0 & 0 & 1\end{array}\right]=\boldsymbol{I}\end{array}
这样我们就有9个等式
\begin{array}{lll}\mathrm{c}_{1} \cdot \mathrm{c}_{1}=1, & \mathrm{c}_{1} \cdot \mathrm{c}_{2}=0, & \mathrm{c}_{1} \cdot \mathrm{c}_{3}=0 \\\mathrm{c}_{2} \cdot \mathrm{c}_{1}=0, & c_{2} \cdot \mathrm{c}_{2}=1, & c_{2} \cdot \mathrm{c}_{3}=0 \\\mathrm{c}_{3} \cdot \mathrm{c}_{1}=0, & c_{3} \cdot \mathrm{c}_{2}=0, & c_{3} \cdot c_{3}=1\end{array}
那么:
因为正交矩阵的性质,这个结果对于每一列也是适用的。如果矩阵满足这样的条件,那么它就是一个正交矩阵,而一组标准正交基满足这个条件。
正交基与标准正交基:
正交基是指在一个向量空间中,一组相互正交的基向量。这意味着基向量之间的点积为零。正交基向量不一定是单位向量,即它们的长度可以不是1。很常见的就是坐标轴。
标准正交基是一组不仅正交而且归一化的基向量。在标准正交基中,每个基向量不仅相互正交,而且都是单位向量(长度为1)。
比如(\sin\theta, -\cos\theta) 和(\cos\theta, \sin\theta) 就是一个二维空间中的标准正交基
也就是说如果我们使用标准正交基构造矩阵,例如\begin{bmatrix} \sin\theta & -\cos\theta \\ \cos\theta & \sin\theta \end{bmatrix} 那么这个矩阵一定就是正交矩阵。
当一个向量要用来和一个矩阵相乘时,我们就需要考虑将向量转换为行矩阵还是列矩阵。
要注意的是,因为矩阵相乘对两个矩阵的形式是有要求的,并且矩阵的计算顺序也会对结果有影响。
在图形学计算中,一般将矢量转化为列矩阵放在矩阵的右侧进行矩阵相乘。
变换(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 + (n_1, n_2, n_3) ,线性变换只满足向量乘不满足向量加,因此三维空间中的变换,仅用一个3×3 的矩阵是不够的。
为了能够解决使用一个矩阵表示全部变换的问题,仿射变换(affine transform)出现了,它合并了线性变换和平移变换,先进行一次线性变换,再进行一次平移变换。
仿射变换可以用一个4×4 的矩阵来表示,扩展到了四维空间:齐次坐标空间(homogeneous space)下。
变换矩阵扩展到4 ×4后,为了实现变换(矩阵乘法),向量也需要扩展到四维向量,也就是齐次坐标(homogeneous coordinate)。
对于一个点,扩展的方式是将其的w分量(x, y, z, w)设置为1,对于方向向量来说,则是将其w分量设置为0。这样的设计有很多原因与好处,最直接的是,对一个点进行齐次坐标的变换时,平移、旋转、缩放都会应用到这个点;而对于方向向量,平移不会应用。
我们将纯位移、纯旋转和纯缩放的变换矩阵叫做基础变换矩阵,而能够表示全部变换的齐次坐标下的4 ×4矩阵则可以这样分解:
\begin{bmatrix} \mathbf{M_{3×3}} &\mathbf{t_{3×1}} \ \mathbf{0_{1×3}} &1 \end{bmatrix}
M_{3×3}用于表示旋转和缩放,\mathbf{t_{3×1}}用于表示平移,\mathbf{0_{1×3}}是零矩阵
对一个点进行平移变换:
\begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix}\begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} x + t_x \\ y + t_y\\ z + t_z \\ 1 \end{bmatrix}
如果对一个方向向量进行平移操作则不会生效。
平移矩阵的逆矩阵,其实就是反向位移
\begin{bmatrix} 1 & 0 & 0 & -t_x \\ 0 & 1 & 0 & -t_y \\ 0 & 0 & 1 & -t_z \\ 0 & 0 & 0 & 1 \end{bmatrix}
很明显,平移矩阵不是正交矩阵。
对一个模型沿着x、y和z轴进行缩放:
\begin{bmatrix} k_x& 0 & 0 & 0 \\ 0 & k_y & 0 & 0 \\ 0 & 0 & k_z & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix} \begin{bmatrix} x \\ y \\ z \\ 1 \end{bmatrix} = \begin{bmatrix} k_xx \\ k_yy\\ k_zz \\ 1 \end{bmatrix}
缩放变换对于方向向量同样生效。
如果k_x = k_y = k_z,那么这个缩放就是一个统一缩放(uniform scale),否则就是非统一缩放(nonuniform scale)。从视觉上看,统一缩放就是按模型原有的比例去缩放模型,而非统一缩放会压缩或者拉伸模型,更重要的是,统一缩放不会改变一些信息,例如对法线进行变换时,如果使用非统一缩放,就会得到错误的结果。
如果缩放因子中有一个或者三个为负的分量,那么我们就获得了一个反射矩阵(reflection matrix)或者说镜像矩阵(mirror matrix);如果有两个为负的分量,那么这个矩阵会让物体旋转180度(中心对称)。
通常在检测到一个镜像变换的时候,都会进行一些特殊处理,例如一个顶点顺序为逆时针的三角形经过镜像变换之后变为顺时针,顶点顺序的改变会导致错误的光照效果和背面剔除。如果缩放矩阵的行列式的值为负数,说明这是一个反射矩阵。
缩放矩阵的逆矩阵:
\begin{bmatrix} \frac{1}{k_x} & 0 & 0 & 0 \\ 0 & \frac{1}{k_y} & 0 & 0 \\ 0 & 0 & \frac{1}{k_z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
缩放矩阵(一般情况下)也不是正交矩阵。
注意,上面的矩阵用于沿着坐标轴缩放,如果要沿着任意方向进行缩放,则需要先进行一个变换改变朝向,使得缩放轴与坐标轴一致,之后进行缩放,最后使用一个逆变换将朝向变回来。
暂时无法在飞书文档外展示此内容
具体来说,如果给定一个方向\mathbf{f},首先将其分解为3个标准正交向量,然后构建旋转变换矩阵
\mathbf{F} = \begin{bmatrix} \mathbf{f^x} & \mathbf{f^y} & \mathbf{f^z} & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
先变换会标准坐标系,也就是先乘以\mathbf{F^T},再进行缩放操作,之后再乘以\mathbf{F}变换会原来的坐标系
注意下面提到3个矩阵的旋转
将点绕x轴旋转\theta度:
\mathbf{R_x(\theta)} = \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos\theta & -\sin\theta & 0 \\ 0 & \sin\theta & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
绕y轴旋转:
\mathbf{R_y(\theta)} = \begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
绕z轴旋转:
\mathbf{R_z(\theta)} = \begin{bmatrix} \cos\theta & -\sin\theta & 0 & 0 \\ \sin\theta & \cos\theta & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}
旋转矩阵的逆矩阵,只需要旋转相反的角度就可以得到。
旋转矩阵是正交矩阵(注意,这里说的是非齐次坐标下的矩阵,也就是只有M_{3×3}的部分),而且多个旋转矩阵之间的串联同样是正交的。
如果我们想让物体以某个点为中心,绕三个轴旋转,那么我们可以先向物体平移,使得旋转点与原点重合,再进行旋转。
我们可以将平移、缩放、旋转组合起来,而这个变换过程可以用下面的公式表示:
P' = M_{translation}M_{rotation}M_{scale}P
而根据矩阵乘法的结合律,TRS这三个矩阵可以提前计算合成一个矩阵P' = \mathbf{M}P,假如有几百万个点都需要应用同样的平移、缩放、旋转矩阵,用提前合成的一个矩阵要比分别使用三个矩阵计算要快得多。
之前提到了我们会将向量转换为列向量,所以上面公式的计算顺序实际上是从右向左;并且矩阵乘法时,矩阵的计算顺序会影响计算结果,也就是我们需要确定好变换的顺序,在绝大多数情况下,我们约定的变换顺序是先缩放,再旋转,最后平移。
除了需要注意不同类型的变换顺序外,我们有时还需要小心旋转的变换顺序。当给出(\theta_x, \theta_y, \theta_z)的旋转角度时,我们需要定义旋转顺序。在Unity中,这个旋转顺序是zxy,这在旋转相关的API文档中都有说明,
但得到的分解的旋转变换矩阵是:
M_{rotateZ}M_{rotateX}M_{rotateY}
这个矩阵与上面说的计算顺序从右向左冲突了,这是因为有两种不同的旋转方式(即两种不同的坐标系选择):
第一种方式:
对于3次旋转,每次旋转都相对于原始固定坐标系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));
的情况,每一次调用都会改变物体的局部坐标系,正是因为每次旋转都改变旋转坐标系,所以倒序得到的就和一次调动的结果相同,这就是分解后旋转矩阵是倒序的原因
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。