五、矩阵的几何意义:变换
(1)什么是变换
变换(transform),指的是我们把一些数据,如点、方向矢量甚至是颜色等,通过某种方式进行转换的过程。
线性变换(linear transform),指的是可以保留矢量加和标量乘的变换。数学公式表示为:
仿射变换(affine transform),指的是合并线性变换和平移变换的变换类型。用4X4矩阵来表示。
(2)齐次坐标
齐次坐标(homogeneous coordinate)是将一个原本是n维的向量用n+1位维向量来表示。
一个点,从三维坐标转换成齐次坐标是把其 W 分量设为1。这样设置会导致,当用一个 4X4 矩阵对一个点进行变换时,平移、旋转、缩放都会施加于该点。
方向矢量,需要把其 W 分量设为0。用于变换一个方向矢量,平移的效果就会被忽略。
(3)分解基础变换矩阵
一个 4X4 的矩阵来表示平移、旋转和缩放。
把表示纯平移、纯旋转和纯缩放的变换矩阵叫做基础变换矩阵。
这些矩阵具有一些共同点,可以把一个基础变换矩阵分解成 4 个组成部分:
(4)平移矩阵(不是正交矩阵)
使用矩阵乘法来表示对一个点进行平移变换:
使用矩阵乘法来表示对一个方向矢量进行平移变换;
矢量没有位置属性,它可以位于空间中的任意一点,因此平移不应该对方向矢量产生影响。
如何构建一个平移矩阵:基础变换矩阵中的 矢量对应了平移矢量,左上角的矩阵
为单位矩阵
。
平移矩阵的逆矩阵就是反向平移得到的矩阵,即
(5)缩放矩阵(一般不是正交矩阵)
使用矩阵乘法来表示一个缩放变换:
对方向矢量使用同样的矩阵进行缩放:
如果缩放系数把这样的缩放称为统一缩放(uniform scale),否则称为非统一缩放(nonuniform scale)。
统一缩放不会改变角度和比例信息,而非统一缩放会改变与模型相关的角度和比例。
缩放矩阵的逆矩阵是使用原缩放系数的倒数来对点或方向矢量进行缩放,即
上面的矩阵只适用于沿坐标轴方向进行缩放。
如果要在任意方向上进行缩放,就需要使用一个复合变换。主要思想是:先将缩放轴变换成标准坐标轴,然后进行沿坐标轴的缩放,再使用逆变换得到原来的缩放轴朝向。
(6)旋转矩阵(正交矩阵)
把点绕着 x 轴旋转 度:
绕 y 轴:
绕 z 轴:
旋转矩阵的逆矩阵是旋转相反角度得到的变换矩阵。旋转矩阵是正交矩阵,而且多个旋转矩阵之间的串联同样是正交的。
(7)复合变换
复合变换可以通过矩阵的串联来实现:
上面使用的是列矩阵,阅读顺序是从右到左,即先进行缩放变换,再进行旋转变换,最后进行平移变换。
变换的结果是依赖于变换顺序的,由于矩阵乘法不满足交换律,因此矩阵乘法的顺序很重要。
在绝大多数情况下,约定的变换顺序是先缩放,再旋转,最后平移。
只考虑 y 轴的旋转,按先缩放、再旋转、最后平移,得到的变换矩阵是:
+
只考虑 y 轴旋转,按先平移、再缩放、最后旋转,得到的变换矩阵是:
当给出 ()这样的旋转角度时,在Unity中,这个旋转顺序是 zxy ,得到的组合旋转变换矩阵是:
给定一个旋转顺序,以及它们对应的旋转角度,有两种坐标系可以选择:
- 绕坐标系 E 下的 z 轴旋转
,绕坐标系 E 下的 y 轴旋转
,绕坐标系 E 下的 x 轴旋转
,即进行一次旋转时不一起旋转当前坐标系。
- 绕坐标系 E 下的 z 轴旋转
,在旋转后的新坐标系
下的 y 轴旋转
,而后在新坐标系
下的 x 轴旋转
,即在旋转时,把坐标系一起转动。
然而,在第一种情况下按 zxy 顺序旋转和在第二种情况下按 yxz 顺序旋转得到的结果是一样的。
六、坐标空间
(1)坐标空间的变换
- 坐标空间会形成一个层次结构——每个坐标空间都是另一个坐标空间的子空间,反过来说,每个空间都有一个父坐标空间。
- 对坐标空间的变换实际上就是在父空间和子空间之间对点和矢量进行变换。
A. 假设,现有父坐标空间 P 以及一个子空间 C 。
- 把子坐标空间下表示的点或矢量
转换到父坐标空间下的表示
- 把父坐标空间下表示的点或矢量
转换到子坐标空间下的表示
B. 重要 如何求出 子坐标空间 变换到 父坐标空间 的 变换矩阵
已知子坐标空间 C 的 3 个坐标轴在父坐标空间 P 下的表示
、
、
,以及其原点位置
。给定一个子坐标空间中的一点
=(a,b,c)
1.从坐标空间的原点开始
已知子坐标空间的原点位置
。
2.向 x 轴方向移动 a 个单位
3.向 y 轴方向移动 b 个单位
4.向 z 轴方向移动 c 个单位
5.得到父坐标空间下的位置
6.把
用矩阵表示
上式扩展到齐次坐标空间中,得:
7.得到变换矩阵
(这里并没有要求 3 个坐标轴是单位矢量,如果存在缩放的话,这 3 个矢量很可能不是单位矢量)
C. 从变换矩阵反推子坐标空间的原点和坐标轴方向:
使用矩阵乘法,提取变换矩阵的第一列,再进行归一化,得到模型空间 x 轴在世界空间下的单位矢量表示,同理得 y 轴和 z 轴。
由于矢量是没有位置的,因此坐标空间的原点变换是可以忽略的,那么,对矢量的坐标空间变换就可以使用 3X3 的矩阵来表示,因为不需要表示平移。那么变换矩阵就是:
在Shader中,常会截取变换矩阵的前 3 行前 3 列来对法线方向、光照方向来进行空间变换的原因所在。
再来关注 Mp→c 。如果它是一个正交矩阵的话,那么的逆矩阵就等于他的转置矩阵,也就是说:
(2)顶点的坐标空间变换过程
A.模型空间
模型空间(model space),有时也被称为 对象空间(object space) 或 局部空间(local space)。每个模型都有自己独立的坐标空间,当它移动或者旋转的时候,模型空间也会跟随着它移动和旋转。
模型空间的原点和坐标轴通常是由美术人员在建模软件里确定好的,当导入到Unity中后,我们可以在顶点着色器中访问到模型的顶点信息,其中包含了每个顶点的坐标。这些坐标都是相对于模型空间中的原点定义的。
B.世界空间
世界空间(world space)是一个特殊的坐标系,因为它建立了我们所关心的最大的空间。
世界空间可以被用于描述绝对位置。在Unity中,世界空间同样使用了左手坐标系,三轴是固定不变的。
在Unity中,可以通过调整Transform组件中的Position属性来改变模型的位置 ,这里的位置指的是相对于这个Transform的父节点(parent)的模型坐标空间中的原点定义的。如果一个Transform没有任何父节点,那么这个位置就是在世界坐标系中的位置,如下图所示:
顶点变换的第一步,叫做模型变换(model transform),是将顶点坐标从模型空间变换到世界空间中。
例:
根据Transform组件上的信息,可以构建出模型变换的变换矩阵:
【疑问?】 在同一坐标系下, 复合变换矩阵,是用来计算模型移动后,得到的新位置属性。为什么在这里可以用为坐标空间转换?
【解答】妞妞此时在世界空间中的位置,相当于模型空间的坐标轴在世界空间的位置,利用坐标空间变换的方法,是可以构造变换矩阵的。
C.观察空间
观察空间(view space)也被称为摄像机空间(camera space)。在Unity中,观察空间使用的是右手坐标系:+z轴指向摄像机的后方。
如果需要调用类似 Camera.cameraToWorldMatrix 、Camera.worldToCameraMatrix 等接口自行计算某模型在观察空间中的位置,要小心左右手坐标系的差异。
从 三维观察空间 变换到 二维的屏幕空间 需要经过投影(projection)。
顶点变换的第二步,叫做观察变换(view transform),就是将顶点坐标从世界空间变换到观察空间中。
例:
方法一:计算观察空间的三个坐标轴在世界空间下的表示,构建出从观察空间到世界空间的变换矩阵,再求逆,得到所需变换矩阵。
方法二:想象平移整个观察空间,让摄像机原点位于世界坐标的原点,坐标轴于世界空间中的坐标轴重合即可。
两种方法得到的变换矩阵是一样的,只是思考方式不同。
这里使用方法二,由Transform组件可以知道,摄像机在世界空间中的变换时先按(30,0,0)进行旋转,然后按(0,10,-10)进行了平移。为了把摄像机重新移回到初始状态,需要进行逆向变换,让坐标轴重合,因此变换矩阵为:
由于观察空间使用的是右手坐标系,因此需要对 z 分量进行取反操作。
用它来对妞妞的鼻子进行顶点变换:
这样,我们就得到了观察空间中妞妞鼻子的位置(9,8.84,-27.31)。
D.裁剪空间
顶点接下来要从 观察空间 转换到 裁剪空间(clip space),也被称为齐次裁剪空间,用于变换的矩阵叫裁剪矩阵(clip matrix),也被称为投影矩阵(projection matrix)。
裁剪空间的目标是能够方便地对渲染图元进行裁剪。
视锥体(view frustum)指的是空间中的一块区域,这块区域决定了摄像机可以看到的空间。
视锥体由六个平面包围而成,这些平面也被称为裁剪平面(clip planes)。
两种投影类型:正交投影(orthographic projection)和 透视投影(perspective projection)。
近裁剪平面(near clip plane)和 远裁剪平面(far clip plane),决定了摄像机可以看到的深度范围。
投影矩阵有两个目的:
a.首先为投影做准备。投影矩阵并没有进行真正的投影(空间降维)工作,而是在为投影做准备。真正的投影发生在后面的齐次除法(homogeneous division)过程中。而经过投影矩阵的变换后,顶点的 w 分量将会具有特殊的意义。
b.其次是对 x,、y、z 分量进行缩放。直接用视锥体的 6 个裁剪平面来进行裁剪会比较麻烦,而经过投影矩阵的缩放后,直接使用 w 分量作为一个范围值,如果 x、y、z 分量都位于这个范围内,就说明该顶点位于裁剪空间内。
(1)透视投影
在Unity中,6个裁剪平面是由 Camera 组件中的参数和 Game 视图的纵横比共同决定。
通过 Camera 组件的 Field of View (简称 FOV)属性来改变视锥体竖直方向的张开角度,而 Clipping Planes 中的 Near 和 Far 参数可以控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近。这样,可以求出视锥体近裁剪平面和原裁剪平面的高度:
缺少的横向信息,可以通过摄像机的横纵比得到。在 Unity 中,一个摄像机的横纵比由 Game 视图的横纵比和 Viewport Rect 中的 W 和 H 属性共同决定 (Unity 允许在脚本里通过 Camera.aspect 进行更改)。
假设,当前摄像机的横纵比为 Aspect,我们定义:
根据已知的 Near、Far、FOV 和 Aspect 的值来确定透视投影的投影矩阵:
上面的投影矩阵,针对的是观察空间为右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换后 z 分量范围将在 [-w,w] 之间的情况。而类似 DirectX 这样的图形接口中,它们希望变换后 z 分量范围将在 [0,w] 之间,因此就需要对上面的透视矩阵进行一些更改。
一个顶点和上述投影矩阵相乘后,可由观察空间变换到裁剪空间中:
从结果看出,投影矩阵的本质就是对 x、y 和 z 分量进行了不同程度的缩放(z 分量还做了一个平移),缩放的目的是为了方便裁剪。此时顶点的 w 分量不再是 1 ,而是原先 z 分量的取反结果。
如果一个顶点在视锥体内,那么它变换后的坐标必须满足:
任何不满足上述条件的图元都需要被剔除或者裁剪。下图为经过上述投影矩阵后,视锥体的变换:
裁剪矩阵会改变空间的旋向性:空间从右手坐标系变换到了左手坐标系。
(2)正交投影
在Unity中,6 个裁剪平面是由Camera组件中的参数和Game视图横纵比共同决定的。
正交投影的视锥体是一个长方体,通过Camera 组件的 Size 属性来改变视锥体竖直方向上高度的一半,而Clipping Planes 中的 Near 和 Far 参数可以控制视锥体的近裁剪平面和远裁剪平面距离摄像机的远近。
可以求出视锥体的近裁剪平面和远裁剪平面:
缺乏的横向信息通过摄像机的横纵比得到。假设,当前摄像机的横纵比为 Aspect :
根据已知的 Near、Far、Size 和 Aspect 的值来确定正交投影的裁剪矩阵:
上面的投影矩阵,同样针对的是观察空间为右手坐标系。一个顶点和上述投影矩阵相乘后的结果如下:
和透视投影不同的是,使用正交投影的投影矩阵对顶点变换后,w 分量仍然为 1 。本质是因为投影矩阵的最后一行的不同,透视投影的最后一行是 [0 0 -1 0],而正交投影的最后一行是 [0 0 0 1]。这样的选择,是为了齐次除法做准备。
如果一个顶点在视锥体内,那么它变换后的坐标必须满足:
下图为经过上述投影矩阵后,视锥体的变换:
同样,裁剪矩阵改变了空间的旋向性。经过正交投影变换后的顶点实际已经位于一个立方体内了。
在上一小节最后,确定了妞妞鼻子在观察空间中的位置(9,9.84,-27.31)。现在计算其在裁剪空间中的位置。
首先,需要知道农场游戏中使用的摄像机类型,是透视摄像机,摄像机参数和 Game 视图的横纵比如下图:
可知,透视投影的参数:FOV 为 60°,Near 为 5,Far 为 40,Aspect 为 4/3=1.333。那么对应的投影矩阵为:
用这个投影矩阵来把妞妞的鼻子从观察空间转换到裁剪空间中。如下:
求得,妞妞的鼻子在裁剪空间中的位置(11.961,15.311,23.692,27.31)。Unity会判断是否需要裁剪。比较得到,妞妞的鼻子满足下面的不等式:
由此判断,妞妞的鼻子位于视锥体内,不需要被裁剪。
(3)屏幕空间
屏幕空间是二维空间,必须把顶点从裁剪空间投影到屏幕空间中,生成对应的2D坐标。
首先,进行标准齐次除法(homogeneous division),也称为透视除法(perspective division)。就是用齐次坐标系的 w 分量去除以 x、y、z 分量。在 OpenGL中,把这一步得到的坐标叫做归一化的设备坐标(Normalized Device Coordinates,NDC)。
经过这一步,可以把齐次裁剪坐标空间转换到NDC中。经过透视投影变换后的裁剪空间,再经过齐次除法后会变换到一个立方体内。在 OpenGL 中,这个立方体的 x、y、z 分量的范围都是 [-1,1] 。在 DirectX 的 API 中,z 分量的范围是 [0,1]。
Unity 使用的是 OpenGL 的齐次裁剪空间,如下图所示:
对于正交投影来说,它的裁剪空间已经是一个立方体了,经过正交投影矩阵变换后的顶点的 w 分量是 1 ,因此齐次除法并不会对顶点的 x、y、z 坐标产生影响。如下图所示:
经过齐次除法后,透视投影和正交投影的视锥体都变换到一个相同的立方体内。现在可以根据变换后的 x 和 y 坐标来映射输出窗口的对应像素坐标。
在 Unity 中,屏幕空间左下角的像素坐标是(0,0),右上角的像素坐标是(pixelWidth,pixelHeight)。由于当前 x 和 y 坐标都是[-1,1],因此这个映射就是一个缩放的过程。
齐次除法和屏幕映射的过程可以使用下面的公式来总结:
上面的式子对 x 和 y 分量都进行了处理,通常 z 分量会被用于深度缓冲中。一个传统方式是把 的值直接存进深度缓冲中,但这并不是必须的。此时
并不会被抛弃,在后续一些工作中也会起到重要作用,例如,进行透视校正插值。
在 Unity 中,从 裁剪空间 到屏幕空间 的转换是由底层完成的,顶点着色器只需要把 顶点 转换到 裁剪空间 即可。
上一步中,得到了妞妞鼻子的位置——(11.691,15.311,23.692,27.31)。现在确定妞妞的鼻子在屏幕上的像素位置。假设,当前屏幕的像素宽度为400,高度为300。首先,进行齐次除法,把裁剪空间的坐标投影到NDC中。然后,再映射到屏幕空间中。过程如下:
综上,得到妞妞鼻子在屏幕上的位置——(285.617,234.096)。
(4)总结
七、法线变换
法线(normal),也被称为法矢量(normal vector)。
一般来说,点和绝大部分方向矢量都可以使用4X4 或 3X3 的变换矩阵把其从坐标空间 A 变换到坐标空间 B 中。但在变换法线的时候,如果使用同一个变换矩阵,可能就无法确保维持法线的垂直性。
切线是由两个顶点之间的差值计算得到的,可直接使用变换顶点的变换矩阵来变换切线。假设,使用 3X3 变换矩阵来变换顶点:
其中和
分别表示在坐标空间 A 下和坐标空间 B 下的切线方向。如果直接使用该矩阵,得到的新法线方向就不会与表面垂直了。如下图:
已知同一个顶点的切线
和法线
必须满足垂直条件,即
。给定变换矩阵
已知
。假设,矩阵 G 用来变换法线
,使其变换后仍然与切线垂直。即:
对上式进行推导后,得:
由于:
假设:
,那么上式即可成立。
如果
,那么使用 原变换矩阵 的 逆转置矩阵 来变换法线就可以得到正确的结果。
如果变换只包括旋转变换,那么变换矩阵
是正交矩阵,那么
,因此
;
如果变换只包含旋转和统一缩放,不包含非统一缩放,那么
;
如果变换中包含的非统一变换,那就必须通过求解逆矩阵来得到变换法线的矩阵。