在读第五章第三节Unity内置文件和变量之前,建议细读第四章。嗯,重修过之前的线代和图形学基础后,这一章能看得稍微轻松一些。
一、知识点汇总
1.正交基和标准正交基
该知识点出现在4.2.2节。
3个坐标轴互相垂,且长度为1,这样的基矢量被称为标准正交基,但这并不是必须的。在一些坐标系中坐标轴之间互相垂直,但长度不为1,这样的基矢量被称为正交基。正交,可以理解为互相垂直的意思。
2.正交矩阵
在图形学笔记二 正交矩阵、转置矩阵和旋转中已经了解过正交矩阵的重要性质:正交矩阵是指其转置等于逆的矩阵。在《入门精要》的4.4节有详细介绍:
在三维变换中,我们经常会使用逆矩阵来求解反向的变换。但逆矩阵的求解往往计算量很大,而如果我们可以确定这个矩阵是正交矩阵的话,就可以直接通过转置矩阵得到逆矩阵。
那么如何判断的一个矩阵是否是正交矩阵呢,当然可以通过公式M右乘M的转置矩阵是否为单位矩阵来判断,但这仍然需要一定的计算量,这些计算量可能和直接求解逆矩阵无异。而且,如果我们判断出来这不是一个正交矩阵,那么这些花在验证是否是正交矩阵的计算就浪费了。因此,我们更想不需要计算,而仅仅根据一个矩阵的构造过程来判断这个矩阵是否是正交矩阵。为此,我们需要了解正交矩阵的几何意义。
image.png
这样,我们就有了9个等式:
image.png
可以得到如下结论:
- 矩阵的每一行(即c1,c2,c3)都是单位矢量,因为它们与自己的点积为1
- 矩阵的每一行(即c1,c2,c3)都互相垂直,因为它们互相的点积为0(参考点积的公式|a||b|cosθ)
- 上述的两条结论对每一列也同样适用。因为M是正交矩阵的话,MT也是正交矩阵。
也就说如果一个矩阵满足上面的条件,那么它就是一个正交矩阵。读者可以注意到, 一组标准正交基(定义详见4.2.2 节〉可以精确地满足上述条件。在4.6.2 节中,我们会使用坐标空间的基矢量来构建用于空间变换的矩阵。因此,如果这些基矢量是一组标准正交基的话(例如只存在旋转变换),那么我们就可以直接使用转置矩阵来求得该变换的逆变换。
读者: 我被标准正交、正交这些概念搞混了,可以再说明一下是什么意思吗?
我们:读者应该已经知道, 一个坐标空间需要指定一组基矢量,也就是我们理解的坐标轴。如果这些基矢量之间是互相垂直的,那么我们就把它们称为是一组正交基( orthogonal basis ) .但是,它们的长度并不要求一定是1 。如果它们的长度的确是1 的话,我们就说它们是一组标准正交基( orthonormal basis )。
因此,一个正交矩阵的行和列之间分别构成了一组标准正交基。但是, 如果我们使用一组正交基来构建一个矩阵的话,这个矩阵可能就不是一个正交矩阵,因为这些基矢量的长度可能不为1 ,也就是说它们不是标准正交基。
3.行矩阵还是列矩阵
假设有一个矢量v=(x,y,z),我们可以把它转换成行矩阵(x,y,z)或列矩阵(x,y,z)T。在Unity 中,常规做法是把矢量放在矩阵的右侧, 即把矢量转换成列矩阵来进行运算。因此,在本书后面的内容中, 如无特殊情况, 我们都将使用列矩阵。这意味着, 我们的矩阵乘法通常都是右乘,例如:CBAv = (C(B(Av)))
使用列向量的结果是,我们的阅读顺序是从右到左,即先对v使用A进行变换,再使用B进行变换,最后使用C进行变换。
4.平移矩阵不是一个正交矩阵
image.png5.缩放矩阵一般不是正交矩阵
如果缩放系数kx=ky=kz,我们把这样的缩放称为统一缩放( uniform scale ), 否则称为非统一 缩放( nonuniform scale )。
从外观上看,统一缩放是扩大整个模型,而非统一缩放会拉伸或挤压模 型。更重要的是,统一缩放不会改变角度和比例信息,而非统一缩放会改变与模型相关的角度 和比例。例如在对法线进行变换时,如果存在非统一缩放,直接使用用于变换顶点的变换矩阵的话,就会得到错误的结果。正确的变换方法可参见4 .7 节。
缩放矩阵的逆矩阵是使用原缩放系数的倒数来对点或方向矢量进行缩放,即
image.png
缩放矩阵一般不是正交矩阵。
上面的矩阵只适用于沿坐标轴方向进行缩放。如果我们希望在任意方向上进行缩放,就需要使用一个复合变换。其中一种方法的主要思想就是,先将缩放轴变换成标准坐标轴,然后进行沿坐标轴的缩放,再使用逆变换得到原来的缩放轴朝向。
6.旋转矩阵是正交矩阵
旋转矩阵的逆矩阵是旋转相反角度得到的变换矩阵。旋转矩阵是正交矩阵,而且多个旋转矩阵之间的串联同样是正交的。
7.复合变换顺序是先缩放,再旋转,最后平移
在绝大多数情况下,我们约定变换的顺序就是先缩放,再旋转,最后平移。
读者:为什么要约定这样的顺序,而不是其他顺序呢?
我们:因为这样的变换顺序是我们需要的。想象我们对奶牛妞妞进行一个复合变换。如果我们按先平移、再缩放的顺序进行变换,假设初始情况下妞妞位于原点,我们先按(0, 0, 5)平移它,现在它距离原点5 个单位。然后再将它放大2 倍,这样所有的坐标都变成了原来的2倍,而这意味着妞妞现在的位置是(0, 0, 10),这不是我们希望的。正确的做法是,先缩放再平移。也就是说,我们先在原点对妞妞进行2 倍的缩放,再进行平移,这样妞妞的大小正确了,位置也正确了。
8.旋转顺序
如果我们需要同时绕着3 个轴进行旋转,是先绕x 轴、再绕y 轴最后绕z 轴旋转还是按其他的旋转顺序呢?
当我们直接给出( θx, θy, θz)这样的旋转角度时,需要定义一个旋转顺序。在Unity 中,这个旋转顺序是zxy,这在旋转相关的API 文档中都有说明。这意味着,当给定(θx, θy, θz)这样的旋转角度时,得到的组合旋转变换矩阵是:
image.png
一些读者会有疑问:上面的公式书写顺序是不是反了?不是说列矩阵要从右往左读吗?这样 一来顺序不就颠倒了吗?实际上,有一个非常重要的东西我们没有说明白,那就是旋转时使用的 坐标系。给定一个旋转顺序(例如这里的zxy ),以及它们对应的旋转角度(θx, θy, θz),有两种坐标 系可以选择。
- 绕坐标系E下的z 轴旋转θz,绕坐标系E 下的y 轴旋转θy,绕坐标系E 下的x 轴旋转θx,即进行一次旋转时不一起旋转当前坐标系。
- 绕坐标系E下的z 轴旋转θz,在坐标系E 下在绕z 轴旋转θz 后的新坐标系E’下的y 轴旋转θy , 在坐标系E’下再绕y 轴旋转θy 后的新坐标系E”下的x 轴旋转θx,即在旋转时,把坐标系一起转动。
很容易知道,这两种选择的结果是不一样的。但如果把它们的旋转顺序颠倒一下,它们得到的结果就会是一样的!说得明白点,在第一种情况下,按zxy 顺序旋转和在第二种情况下,按yxz顺序旋转是一样的。而Unity 文档中说明的旋转顺序指的是在第一种情况下的顺序。
和上面不同类型的变换顺序导致的问题类似,不同的旋转顺序得到的结果也可能是不一样的。我们同样可以通过对比不同旋转顺序得到的变换矩阵来理解为什么会出现这样的不同。而这个验证过程留给读者作为练习。
这里也可以参考图形学笔记三 复数 四元数
9.坐标空间的变换
作者在4.6节举了一个例子,其实用基变换的思路更快,例子需求如下:
现在,我们已知坐标空间C的三个坐标轴在父坐标空间P下的表示Xc,Yc,Zc,以及原来的原点Oc。当给定一个子坐标空间中的一点Ac=(a,b,c),我们同样可以依照上面4个步骤来确定其在父坐标空间下的位置Ap
应用基变换,就是直接把子空间的Xc,Yc,Zc直接往父空间进行变换,直接就到这里了:
image.png
然后转化为齐次坐标:
image.png
一旦求出来Mc->p,Mp->c就可以通过求逆矩阵的方式求出来,因为从坐标空间C变换到坐标空间P与从坐标空间P变换到坐标空间C是互逆的两个过程。
可以看出来,变换矩阵Mc->p实际上可以通过坐标空间C在坐标空间P中的原点和坐标轴的矢量表示构建出来:把3个坐标轴依次放入矩阵的前三列,把原点矢量放到最后一列,再用0和1填充最后一行即可。
需要注意的是,这里我们并没有要求3个坐标轴Xc、Yc和Zc是单位矢量,事实上,如果存在缩放的话,这三个矢量值很可能不是单位矢量。
更加令人振奋的是,我们可以利用反向思维,从这个变换矩阵反推来获取子坐标空间的原点和坐标轴方向。例如,当我们已知从模型空间到世界空间的一个4×4的变换矩阵,可以提取它的第一列再进行归一化后(为了消除缩放的影响)来得到模型空间的x轴在世界空间下的单位矢量表示。同样的方法也可以提取y轴和z轴。我们可以从另一个角度来理解这个提取过程。因为矩阵Mc->p可以把一个方向矢量从坐标空间C变换到坐标空间P中,那么,我们只需要用它来变换坐标空间C中的x轴(1,0,0,0),即使用矩阵乘法M->p[1 0 0 0]T,得到的结果正是Mc->p的第一列。
另一个有趣的情况是,对方向矢量的坐标空间变换。我们知道,矢量是没有位置的,因此坐标空间的原点变换是可以忽略的。也就是说,我们仅仅平移坐标系的原点是不会对矢量造成任何影响的。那么,对矢量的坐标空间变换就可以使用3×3矩阵来表示,因为我们不需要平移变换。那么变换矩阵就是:
image.png
在shader中,我们常常会看到截取变换矩阵的前3行前3列来对法线方向、光照方向来进行空间变换,这正是原因所在。
现在,我们再来关注Mp->c。我们前面讲到,可以通过求Mc->p的逆矩阵方式求解出来反向变换Mp->c。但有一种情况我们不需要求解逆矩阵就可以得到Mp->c,这种情况就是Mc->p是一个正交矩阵。
如果它是一个正交矩阵的话,Mc->p的逆矩阵就等于它的转置矩阵。这意味着我们不需要进行复杂的求逆操作就可以得到反向变换。也就是说,如果我们知道坐标空间变换矩阵Ma->b是一个正交矩阵,那么我们可以提取它的第一列来得到坐标空间A的x轴在坐标空间B下的表示,还可以提取它的第一行来得到坐标空间B的x轴在坐标空间A下的表示。反过来,如果我们知道坐标空间B的x轴、y轴和z轴(必须是单位矢量,否则构建出来的就不是单位矩阵了)在坐标空间A下的表示,就可以把它们依次放在矩阵的每一行就可以得到A到B的变换矩阵了。
6.让人发晕的例子
当你不知道把坐标轴的表示是按行放还是按列放的时候,不妨先选择一种摆放方式来得到变换矩阵。例如,现在我们想把一个矢量从坐标空间A 变换到坐标空间B,而且我们已经知道坐标空间B 的x 轴、y 轴、z 轴在空间A 下的表示,即X B 、Y B 和Z B 。那么想要得到从A 到B 的变换矩阵M A→B ,我们是把它们按列放呢还是按行放呢?如果读者实在想不起来正确答案,我们不妨先随便选择一种方式,例如按列摆放。那么
image.png
这个才是正确的
这里冯乐乐的解释,我是真的看晕了。但是我仍然可以用基变换的思路来解释:
我想得到A到B的变换矩阵,只需要把A的基坐标变到B就行了,但问题就是,已知条件是反的,是知道B的基坐标在A中的表示。所以我改改思路,先求B到A的变换矩阵,再求逆即可。
B到A的基坐标变换,根据上面说的,在Unity 中,常规做法是把矢量放在矩阵的右侧, 即把矢量转换成列矩阵来进行运算。也就是:
image.png
然后对这个矩阵变换求逆,因为是正交矩阵,直接转置即可得到最终答案:
image.png
二、MVP实例
在图形学笔记四 MVP中,已经了解了MVP基本流程。原书举了一个例子,讲得很细,就是有个奶牛叫妞妞,她有自己的坐标空间即模型空间,在这个空间里,她的鼻子坐标是(0,2,4),最后如何显示在屏幕上呢?
1.模型变换(model transform)
首先,转化为齐次坐标(0,2,4,1)。
顶点变换的第一步就是将顶点坐标从模型空间变换到世界空间,这个变换通常叫做模型变换(model transform)。根据Transform的信息,妞妞进行了(2,2,2)的缩放,(0,150,0)的旋转以及(5,0,25)的平移。根据之前的知识,要先缩放再旋转再平移:
2.观察变换(view transform)
观察空间(view space)也被称为摄像机空间(camera space)。观察空间可以认为是模型空间的一个特例——在所有的模型中有一个非常特殊的模型,即摄像机(虽然通常来说摄像机本身是不可见的),它的模型空间值得我们单独拿出来讨论,也就是观察空间。
摄像机决定了我们渲染游戏所使用的视角。在观察空间中,摄像机位于原点,同样其坐标轴的选择可以是任意的,但由于我们是以unity为主,而unity中观察空间的坐标轴选择是:+x指向右方,+y指向上方,而正z轴指向摄像机的后方。在这里,读者可能会觉得奇怪,我们之前讨论的模型空间和世界空间中+z轴指的都是物体的前方,为什么这里不一样了呢?这是因为Unity在模型空间和世界空间选用的都是左手坐标系,而观察空间中使用的是右手坐标系。这是复合OpenGL的传统的,在这样的观察空间中,摄像机的正前方指向的是-z轴方向。
这种左右手坐标系之间改变很少会对我们在unity中的编程产生影响,因为unity为我们做了许多渲染底层的工作。但是如果读者需要调用类似Camera.cameraToWorldMatrix、Camera.WorldToCameraMatrix等接口自行计算某模型在观察空间中的位置上,就要小心这样的差异。
最后提醒读者的一点是,观察空间和屏幕空间是不同的。观察空间是一个三维空间,而屏幕空间是一个二维空间。从观察空间到屏幕空间需要一个操作,那就是投影(projection)。我们后面会讲到。
顶点变换的第二步,就是将顶点坐标从世界空间变换到观察空间中。这个变换叫观察变换(view transform)
回到我们的农场游戏。现在我们需要把妞妞的鼻子从世界空间变换到观察空间中。为此我们需要知道世界坐标系下摄像机的变换信息。这同样可以通过摄像机面板中的Transform组件得到:(1,1,1)的缩放,(30,0,0)的旋转,(0,10,-10)的平移。
在图形学笔记四 MVP中有提到:
上面的截图说的是,考虑到运动的相对性,如果相机和物体一起移动,那么拍出来的照片是相同的。沿着这种思路,把相机放在世界坐标的原点,并让坐标轴与世界空间重合,然后再让物体移动,就能达到同样的效果。
这里也介绍一下正常的思路,根据基变换的思路。要得到世界坐标的物体在相机空间的坐标,可以把世界坐标的基转换到相机空间。以Unity举例,我们更容易获得的是摄像机在世界空间中的坐标,所以需要对这个变换进行求逆,才能得到我们的目标矩阵。
这两种思路,最终选择的是把相机移到原点的思路。视频中说到这样做的好处:会让操作得到简化
为了把摄像机重新移回到初始状态(这里指摄像机原点位于世界坐标原点、坐标轴与世界空间中的坐标轴重合),我们需要进行逆向变换,即先按(0,-10,10)进行平移,以便摄像机回到原点,再按(-30,0,0)进行旋转,以便让坐标轴重合。因此变换矩阵就是:
注意,这里绕X轴旋转的公式很容易写出,而如果绕任意轴,这个矩阵会复杂很多。具体参考图形学笔记二 正交矩阵、转置矩阵和旋转
但是,由于观察空间使用的是右手坐标系,因此需要对z分量进行取反操作。我们可以通过乘以另一个特殊矩阵来得到最终的观察变换矩阵:
image.png
3.裁剪空间
顶点接下来要从观察空间转换到裁剪空间(clip space,也被称为齐次裁剪空间)中,这个用于变换的矩阵叫做裁剪矩阵(clip matrix),也被称为投影矩阵(projection matrix)。
裁剪矩阵的目标是能够方便的对渲染图元进行裁剪:完全位于这块空间内部的图元会被保留,完全位于这块空间外部的图元将会被剔除,而与这块空间相交的图元将会被裁剪。那么这块空间是如何决定的呢?答案是由视椎体(view frustum)来决定。
视椎体是指空间中的一片区域,这块区域决定了摄像机可以看到的空间。视椎体由6个平面包围而成,这些平面也被称为裁剪平面(clip planes)。视椎体有两种类型,这涉及到两种投影类型:一种是正交投影(orthographic projection),一种是透视投影(perspective projection)。
在视椎体的6块裁剪平面中,有两块裁剪平面比较特殊,它们分别被称为近裁剪平面(near clip plane)和远裁剪平面(far clip plane)。它们决定了摄像机可以看到的深度的范围。正交投影和透视投影的视椎体如下图所示。
image.png
从上图可以看出,透视投影的视椎体是一个金字塔形,侧面的4个裁剪平面会在摄像机处相交。它更符合视椎体这个词语。正交投影的视椎体是一个长方体。前面讲到,我们希望根据视椎体围成的区域对图元进行裁剪,但是如果直接使用视椎体定义的空间来进行裁剪,那么不同的视椎体就需要不同的处理过程,而且对于透视投影的视椎体来说,想要判断一个顶点是否处于一个金字塔内部是比较麻烦的。因此我们想要一种更加通用、方便和整洁的方式来进行裁剪的工作,这种方式就是通过一个投影矩阵把顶点转换到一个裁剪空间中。
投影矩阵有两个目的:
(1)首先为投影做准备。这是个迷惑点,虽然投影矩阵的名称包含了投影2字,但它并没有进行真正的投影工作,而是在为投影做准备。真正的投影发生在后面的齐次除法(homogeneous division)过程中。而经历过投影矩阵的变换后,顶点w的分量会具有特殊的意义。
读者:投影到底是什么意思呢?
我们:可以理解成是一个空间的降维,例如从四维空间投影到三维空间中。而投影矩阵实际上并不会真的进行这个步骤,它会为真正的投影做准备工作。真正的投影会在屏幕映射时发生,通过齐次除法来得到二维坐标。
(2)其次是对x、y、z分量进行缩放。我们上面讲过直接使用视椎体的6个裁剪平面进行裁剪会比较麻烦。而经过投影矩阵的缩放后,我们可以直接使用w分量作为一个范围值,如果x、y、z分量都位于这个范围内,就说明该顶点位于裁剪空间内。
在裁剪空间之前,虽然我们使用了齐次坐标来表示点和矢量,但它们的第四个分量都是固定的:点的w分量是1,方向矢量的w分量是0。经过投影矩阵变换后,我们会赋予齐次坐标的第四个坐标更加丰富的含义。下面,我们来看一下两种投影类型使用的投影矩阵具体是什么。
4.透视投影
视椎体的意义在于定义了场景中的一块三维空间。所有位于这块空间内的物体都会被渲染,否则就会被剔除或裁减。我们已经知道这块区域由6个裁剪平面定义,那么这6个裁剪平面又是怎么决定的呢?在Unity中,它们由Camera组件中的参数和Game视图的横纵比共同决定,如图所示。
image.png
由图可以看出,我们可以通过Camera组件的Field of View(简称FOV)属性来改变视椎体竖直方向的张开角度,而Clipping Planes中的Near和Far参数可以控制视椎体的近裁剪平面和远裁剪平面距离摄像机的远近。这样我们可以求出视椎体近裁剪平面和远裁剪平面的高度,也就是:
image.png
现在我们还缺乏横向信息。这可以通过摄像机的横纵比得到。在Unity中,一个摄像机的横纵比由Game视图的横纵比和Viewport Rect中的W和H属性共同决定(实际上,Unity允许我们在脚本里通过Camera.aspect进行更改,但这里不做讨论)。假设,当前摄像机的横纵比为Aspect,我们定义:
image.png
现在,我们可以根据已知的Near、Far、FOV和Aspect的值来决定透视投影的投影矩阵。如下:
image.png
上面公式的推导部分可以参见本章的扩展阅读部分。需要注意的是,这里的投影矩阵是建立在Unity对坐标系的假定上面,也就是说,我们针对的是观察空间为右手坐标系,使用列矩阵在矩阵右侧进行相乘,且变换z分量范围将在[-w,w]之间的情况。而在类似DirectX这样的图形接口中,它们希望变换后z分量范围将在[0,w]之间,因此就需要对上面的透视矩阵进行更改。这不在本书的讨论范围内。
而一个顶点和上述矩阵相乘后,可以由观察空间变换到裁剪空间中,结果如下:
image.png
从结果可以看出,这个投影矩阵本质就是对x、y和z分量进行了不同程度的缩放(当然,z分量还做了一个平移),缩放的目的是为了方便裁剪。我们可以注意到,此时顶点的w分量不再是1,而是原先z分量的取反结果。现在,我们就可以按如下不等式来判断一个变换后的顶点会否位于视锥体内,如果一个顶点在视锥体内,那么它变换后的坐标必须满足:
image.png
任何不满足上述条件的图元都需要被剔除或裁减。下图显示了经过上述投影矩阵后,视椎体的变化:
image.png
从上图还可以注意到,裁剪矩阵会改变空间的旋向性:空间从右手坐标系变换到了左手坐标系,这意味着离摄像机越远,z值将越大。
注:这一部分我看得有点崩,在图形学笔记四 MVP中,闫令琪是先讲了正交投影,然后基于正交投影又用了不少时间推出了透视投影,而且没有给出最终的透视投影公式。那么上面冯乐乐给出的,嗯,看不懂就先记下来吧
5.正交投影
这个在图形学笔记四 MVP讲得是很清楚的,简单很多,先平移,再缩放:
在Unity中,引入了Aspect参数,比起闫令琪讲的稍有变化,但原理是共通的。
image.png
由图可以看出,我们可以通过Camera组件的Size属性来改变视椎体竖直方向高度的一半,而Clipping Planes中的Near和Far参数可以控制视椎体的近裁剪平面和远裁剪平面距离摄像机的远近。这样,我们可以求出视椎体近裁剪平面和远裁剪平面的高度,也就是:
image.png
现在我们还缺乏横向的信息。同样我们可以通过摄像机的纵横比得到。假设,当前摄像机的纵横比为Aspect,那么:
image.png
现在,我们可以根据已知的Near、Far、Size和Aspect的值来确定正交投影的裁剪矩阵。如下:
image.png
同样,这里的投影矩阵是建立在Unity对坐标系的假定上面的。一个顶点和上述投影矩阵相乘后的结果如下:
image.png
注意到,和透视投影不同的是,使用正交投影的投影矩阵对顶点进行变换后,其w分量仍然为1。本质是因为投影矩阵的最后一行不同,透视投影的投影矩阵的最后一行是[0,0,-1,0],而正交投影的投影矩阵的最后一行是[0,0,0,1]。这样的选择是有原因的,是为了齐次除法做准备,在后面我们会讲到。
判断一个变换后的顶点是否位于视椎体内使用的等式和透视投影中一样,这种通用性也是为什么要使用投影矩阵的原因之一。下图显示了经过上述投影矩阵后,正交投影的视椎体变化。
同样,裁剪矩阵改变了空间的旋向性。可以注意到,经过正交投影变换后的顶点实际已经位于一个立方体内了。
6.继续来看我们的农场游戏
在上面,我们已经帮妞妞确定了它的鼻子在观察空间中的位置——(9,8。84,-27.31)。现在,我们要计算它在裁剪空间中的位置。
首先,我们需要知道农场游戏中使用的摄像机类型。由于农场游戏是一个3D游戏,因此这里我们使用了透视摄像机。摄像机参数和Game视图的纵横比如图所示:
image.png
据此,我们可以知道透视投影的参数:FOV为60度,Near为5,Far为40,Aspect为4/3=1.33。那么对应的投影矩阵是:
image.png
然后,我们用这个投影矩阵来把妞妞的鼻子从观察空间转换到裁剪空间中。如下
image.png
接下来Unity会判断妞妞的鼻子是否需要裁剪。通过比较得到,妞妞的鼻子满足下面的不等式:
image.png
由此,我们可以判断,妞妞的鼻子位于视椎体内,不需要被裁减。
7.屏幕空间
经过投影矩阵的变换后,我们可以进行裁剪操作。当完成了所有的裁剪操作后,就需要进行真正的投影了,也就是说我们需要把视椎体投影到屏幕空间(screen space)中。经过这一步变换,我们会得到真正的像素位置,而不是虚拟的三维坐标。
屏幕空间是一个二维空间,因此我们必须把顶点从裁剪空间投影到屏幕空间中,来生成对应的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这样的裁剪空间,如下图所示:
image.png
而对于正交投影来说,它的裁剪空间实际上已经是一个立方体了,而且由于经过正交投影矩阵变换后的顶点的w分量是1,因此齐次除法并不会对顶点的x,y,z坐标产生影响,如下图所示:
image.png
经过齐次除法后,透视投影和正交投影的视椎体都变换到相同的立方体内,现在我们可以根据变换后的x和y坐标来映射输出窗口的对应像素坐标。
在Unity中,屏幕空间左下角的像素坐标是(0,0),右上角的像素坐标是(pixelWidth,pixelHeight),由于当前x和y坐标都是[-1,1],因此,这个映射的过程就是一个缩放的过程。
齐次除法和屏幕映射的过程可以使用下面的公式来总结:
image.png
上面的式子对x和y分量都进行了处理,那么z分量呢?通常,z分量会被用于深度缓冲。一个传统的方式是把clipz/clipx的值直接存进深度缓存中,但这并不是必须的。通常驱动产商会根据硬件来选择最好的存储格式。此时clipw也并不会被抛弃,虽然它完成了它的主要工作——在齐次除法中作为分母来得到NDC,但它仍然会在后续的一些工作中起到重要作用,例如进行透校正插值。
在Unity中,从裁剪空间到屏幕空间的转换是由底层帮我们完成的,我们的顶点着色器只需要把顶点转换到裁剪空间即可。
在上一步中,我们知道了裁剪空间中妞妞鼻子的位置——(11.691,15.311,23.692,27.31)。现在我们终于可以确定妞妞鼻子在屏幕上像素的位置。假设,当前屏幕的宽度为400,高度为300。首先我们要进行齐次除法,把裁剪的坐标投影到NDC中,然后再映射到屏幕空间中。这个过程如下:
image.png
由此,我们知道了妞妞鼻子在屏幕上的位置——(285.617,234.096)
8.总结
以上就是一个顶点如何从模型空间变换到屏幕坐标的过程,下图总结了这些空间和用于变换的矩阵:
image.png
顶点着色器最基本的任务就是把顶点坐标从模型空间转换到裁剪空间中。这对应了图中前三个顶点变换过程。而在片元着色器中,我们通常也可以得到该片元在屏幕空间的像素位置。我们会在以后的讲解中看到如何得到这些像素的位置。
在Unity中,坐标系的旋向性也随着变换发生了改变。下图总结了Unity各个空间使用的坐标系旋向性。
image.png
从图中可以发现,只有在观察空间中Unity使用了右手坐标系。
需要注意的是,这里给出的仅仅是一些重要的坐标空间。还有一些空间在实际开发中也会遇到,例如切线空间(tangent space)。切线空间通常用于法线映射,在后面我们会说到。
注:4.8节的内置变量,放在第5章一起学习。
网友评论