1.2 顶点处理阶段

3D渲染流水线的第一个阶段便是顶点处理阶段。本阶段将会读取描述三维场景内容的顶点信息并进行处理。在计算机图形学中,可以使用各种建模方案提供描述三维场景的顶点信息,常用和高效的方式是使用多边形网格(polygon mesh)去组织顶点。

不同3D渲染流水线的实作,所支持的组成网格的多边形种类有所不同,但都需要使用凸多边形凸多边形的定义和属性可参考相关资料。(convex polygon)。在实践中大都使用三角形网格,如图1-2所示。当使用三角形网格对表面进行近似模拟时,可以考虑使用多种细分(tessellation)处理方法。当三角形的细分程度越高时,网格就越接近原始表面,处理时间也会随之增加。因此,在实践中要根据当前硬件条件和开发需求选择一个折中的方案。

▲图1-2 利用三角形网格组织顶点

1.2.1 顶点的组织方式

描述三角形网格的常见方法有多种,其中一个是列举顶点,即顺序读取3个顶点构成一个三角形。存储顶点的内存区域即1.1节介绍的顶点缓冲区。如图1-3所示,缓冲区内的顶点可以定义3个三角形,这种表达方式称为三角形列表(triangles list)。三角形列表的方式比较直观,但显然缓冲区中的顶点信息存在冗余。非索引方式的三角形列表,一个网格如果包含n个三角形,则顶点缓冲区中有3n个顶点。在图1-3中,3个三角形同时拥有(1,1,0)处的顶点,且该顶点在缓冲区中重复出现了3次。

在三角形网格中,一个顶点经常被多个三角形共享。因此,可以给每个顶点都分配一个整数索引值,记录三角形的方式可以从直接记录顶点本身变成记录顶点索引,以减少数据冗余,让每一个顶点在顶点缓冲区中只需要存储一份。顶点索引则存储在索引缓冲区(index buffer)中,如图1-4所示。

▲图1-3 三角形列表

▲图1-4 索引方式的三角形列表,索引缓冲区负责存储三角形用到的顶点编号信息

图1-4中的顶点只包含了位置信息,乍一看,顶点缓冲区中节省出来的空间也得要用在索引缓冲区上。在实际应用中,除了位置信息外,顶点缓冲区中存储的信息还包含法线(normal)、纹理映射坐标(texture mapping coordinate);如果是用作动画模型的顶点,那么还有骨骼权重(bone weight)等。因此,当顶点数量很大时,如果使用索引方式,那么顶点缓冲区冗余数据的减少量是非常可观的。

1.2.2 坐标系统和顶点法线的确定方式

描述空间方位的坐标系有多种,如极坐标系、球面坐标系和笛卡儿坐标系等。在3D渲染流水线中,使用最广泛的是笛卡儿坐标系。笛卡儿坐标系可以分为左手坐标系和右手坐标系。当左手大拇指或右手大拇指指向坐标系z轴正方向时,其余四指指尖的环绕方向,就是坐标系x轴绕向y轴的方向,满足这一规则的笛卡儿坐标系即称为左手坐标系或右手坐标系,如图1-5所示。

▲图1-5 笛卡儿坐标系

在光照计算中需要使用法线。法线既可以是一个顶点的法线,也可以是一个多边形的法线。首先探讨一个三角形的法线。在右手坐标系中定义一个三角形的法线朝向,使用右手法则定义,即右手四指围拢,按照组成三角形3个顶点在缓冲区中先后排列顺序围拢四指,此时右手拇指的朝向就是三角形的法线方向;在左手坐标系中,则使用左手法则定义,方法和右手法则相同,左手拇指的朝向就是三角形的法线方向。

如图1-6所示,三角形处于右手坐标系下,其3个顶点p1p2p3在顶点缓冲区中以<p1,p2,p3>的顺序排列,那么采用右手.法则,使右手四指按顶点先后排列顺序的走向弯曲,得到图1-6所示的三角形法线朝向。

在给定了顶点排列顺序之后,用向量叉积运算可以计算出法向量的值。依然以图1-6为例,假设连接p1p2形成边向量v12,连接p1p3形成边向量v13。利用两个边向量的叉积,可以得到垂直于两个边向量的向量。用向量除以它自己的长度便得到单位化(normalized,又称为规格化)的法向量。

必须注意的是,向量的叉积运算是不满足交换律的。式(1-1)中v12v13如果交换叉积运算顺序,就相当于图1-6的顶点在缓冲区中的排列顺序变为< p1,p2,p3>,此时法向量的计算公式应该为

依据向量叉乘的性质,式(1-1)和式(1-2)得出的向量的方向是相反的。由此可知,给定三角形的法向量主要依赖于该三角形顶点的排列顺序。<p1,p2,p3>的排列顺序称为逆时针方向(counter clockwise,CCW),<p1,p2,p3>的排列顺序称为顺时针方向(clockwise,CW)。另外,在右手坐标系中以某种顶点排列顺序形成的三角形,不改变顶点排列顺序地放在左手坐标系中时,其法线也和在右手坐标系时相反。如果把法线朝向方定义为三角形外表面,法线朝向方相反方向定义为内表面,则当把顶点数据从左(右)手导入右(左)手坐标系时,会产生内外表面相反的情况。如图1-7所示,图1-7(a)是右手坐标系,三角形的顶点按< p1,p2,p3>的顺序排列,此时三角形的法线朝外;图1-7(b)是左手坐标系,如果保持按< p1, p2, p3>的顺序排列,此时三角形的法线则朝里。

▲图1-6 确定三角形法线朝向

因此,要解决图1-7揭示的三角形的内外表面反转的问题,需要重新调整顶点在缓冲区中的排列顺序。图1-7(a)中右手坐标系的顶点,在图1-7(b)的左手坐标系下排列顺序改为< p1,p2,p3>即可解决问题。

在实际开发中,顶点的法线就更为重要一些。大部分建模软件在编辑模型时就可以直接指定顶点法线。与三角形的法线就是该三角形所在平面垂直的向量不同,理论上一个顶点的法线可以是过该点的任意一条射线。一般情况下,某顶点的法线通常通过共享该顶点的三角形法线进行计算,如图1-8所示。法线n的计算公式如下。

▲图1-7 右手坐标系和左手坐标系

▲图1-8 顶点的法线n共享该顶点的三角形法线

除了后面将要介绍的观察空间使用右手坐标系外,Unity 3D中的其他空间均使用左手坐标系。

1.2.3 把顶点从模型空间变换到世界空间

1.2.2节提到的顶点数据的创建方式,用于创建包含顶点数据的多边形网格的坐标系称为模型坐标系(model coordinate),坐标系所对应的空间称为模型空间(model space有些书籍把model coordinate和model space译作局部坐标系与局部空间,本书统一译为模型坐标系和模型空间。)。在顶点处理阶段,顶点数据将会贯穿多个空间,直至到达裁剪空间(clip space),如图1-9所示。

▲图1-9 顶点处理的变换操作和对应的空间

1.仿射变换和齐次坐标

图1-9中的世界变换和观察变换由缩放变换(scale transform)、旋转变换(rotation transform)和平移变换(translation transform)这3种变换组合而成。其中,缩放变换和旋转变换称为线性变换(linear transform),线性变换和平移变换统称为仿射变换(affine transform)。投影变换所用到的变换则称为射影变换。

三维的缩放变换可以用一个3×3矩阵Mscale描述:

式中,scalex、scaley和scalez表示沿着xyz轴方向上的缩放系数。如果全部缩放系数都相等,那么该缩放操作称为均匀缩放操作;否则,称为非均匀缩放操作。

如果是均匀缩放操作,那么Unity 3D会定义一个名为UNITY_ASSUME_UNIFORM_SCALING的着色器多样体(shader variant,3.4节会详述此概念,目前可以将其视为一个“宏”)。着色器代码将会根据此多样体是否定义了执行不同的操作。如果UNITY_ASSUME_UNIFORM_ SCALING未被启用,当把顶点从模型空间变换到世界空间中,或者从世界空间变换到观察空间中时,需要对顶点的法线做一个操作,使得它能正确地变换。4.2.4节会详述此问题。

Unity 3D中使用列向量和列矩阵描述顶点信息,所以可以把顶点的坐标值右乘缩放矩阵实现缩放操作,如下:

式(1-4)表示把位置点[xyz]T(由于排版的原因,本书将会在正文中用“行向量的转置”的方法描述一个列向量)进行缩放操作,变换得到新坐标值

要定义一个三维旋转操作,需要定义对应的旋转轴。当某向量分别绕坐标系的xyz轴旋转θ角度时,分别有以下旋转矩阵MrxMryMrz

如果要对一个位置点[xyz]T进行平移操作,可以让位置点加上一个描述沿着每个坐标值移动多少距离的向量,如下所示。

与缩放变换和旋转变换不同,上述的平移变换是一个加法操作。事实上,用右乘一个三阶矩阵的方法去对一个三维向量进行平移变换是不可能实现的,因为平移变换不是一个线性操作。要解决这个问题,需要使用齐次坐标,把三维向量[x y z]T扩展成一个四维齐次坐标[x y z w]T,然后把平移向量扩展成以下4阶矩阵的形式。

旋转矩阵和缩放矩阵也可以通过增加第4列与第4行的方式把矩阵四阶化,这样也可以对齐次坐标进行变换,式(1-4)可以改写为

如果变换矩阵是仿射矩阵,则矩阵的第4行是[0 0 0 1];如果是射影变换,如后面将要介绍的投影变换,则矩阵的第4行不是[0 0 0 1]。对于顶点的齐次向量,第4项w依据不同的使用场合,有各种不同的取值。齐次向量[xyzw]Tw≠0时对应于笛卡儿坐标,且该齐次向量表示一个位置点;如果w=0,则表示方向。如果对一个方向向量进行平移,实际上是不会产生任何作用的,如在式(1-7)中,向量[xyzw]T代入w=0,乘以平移矩阵后得到的结果仍然是[xyz0]T

2.世界矩阵及其推导过程

当包含顶点数据的模型建模完成后,所有顶点隶属于模型空间且固定不动。某一模型的模型空间和其他模型的模型空间没有任何的关联关系。在渲染流水线中,首先是要把分属在不同模型中的所有顶点整合到单一空间中。该单一空间就是世界空间。

假设有一个球体模型和一个圆柱模型,把它们从自身的模型空间中变换到世界空间,如图1-10所示。

▲图1-10 将球体模型和圆柱模型从自身的模型空间中变换到世界空间

针对球体模型的世界变换仅为缩放操作,假设缩放系数为2,则球体模型的世界变换矩阵Msphere

在图1-10中,球体模型的北极点在其模型空间中的坐标是[0 1 0]T,将其齐次化为[0 1 0 1]T,利用世界变换矩阵Msphere将其变换到世界空间中的[0 2 0 1]T处,如下所示。

圆柱模型的世界变换操作包含绕x轴旋转-90°的旋转操作,以及旋转后沿着x轴平移5个单位的平移操作。

定义旋转角度的正负规则:让旋转轴朝向观察者,如果此时的旋转方向为逆时针方向,则表示旋转的角度为负值;若为顺时针方向,则为正值。

根据式(1-5)和式(1-7)可得旋转矩阵Mrx和平移矩阵Mt,如下所示。

经过旋转操作后,基于模型空间的圆柱模型顶面圆心的齐次化坐标[0 2 0 1]T变换到了[0 0-2 1]T,如下所示。

旋转后再做一个平移操作,可以得到最后的圆柱模型顶面圆心的齐次化坐标为[5 0-2 1]T,如下所示。

可见,如果把顶点的三维坐标齐次化成四维齐次坐标,那么针对此顶点所有的平移、旋转、缩放变换(仿射变换)可以通过矩阵连乘的方式变换。由于Unity 3D的顶点坐标是采用列向量的方式描述,因此对应的矩阵连乘方式是右乘,即坐标列向量写在公式的最右边,各变换矩阵按变换的先后顺序依次从右往左写。

3.表面法线的变换

上文提到了缩放矩阵有非均匀缩放操作和均匀缩放操作两种。如果某三角形网格的变换矩阵为M,即网格上的所有顶点也将使用M进行变换。当M是旋转变换矩阵、平移变换矩阵和均匀缩放矩阵中的一种或者它们的组合时,顶点的法线也可以直接通过乘以M从模型空间变换到世界空间;当M为旋转变换矩阵、平移变换矩阵和非均匀缩放矩阵中的一种或者它们的组合时,则要把顶点的法线从模型空间变换到世界空间,该变换矩阵就必须为M的逆转置矩阵,即(M-1)T

4.Unity 3D中的模型空间坐标系和世界空间坐标系

Unity 3D在各平台上,顶点的模型坐标系统一使用左手坐标系。因此,从3ds Max、Maya等建模工具导出顶点数据时,无论原始坐标系是什么,都要准确地将其转换到左手坐标系。在顶点缓冲区中,把顶点坐标定义为w分量为1的四维齐次坐标。因为w分量为1,所以等同于三维的笛卡儿坐标。令在模型空间的顶点坐标vInModelSpace为(x, y, z, 1),变换到世界空间中的坐标vInWorldSpace为(wx, wy, wz, 1)。通过使用unity_ObjectToWorld内置变量(参见4.1.1节),可以把顶点从模型空间变换到世界空间,代码如下:

float4vInWorld = mul(unity_ObjectToWorld,vInModel);

1.2.4 把顶点从世界空间变换到观察空间

对所有的顶点进行世界变换操作完毕后,可以在世界空间内定义摄像机(camera)。当给定摄像机的状态后,则观察空间有些书籍使用摄像机空间/相机空间(camera space)表示view space的概念,这两者是等价的。本书统一使用view space,译作观察空间。(view space)也得以确立,且世界空间中的顶点也随之变换到观察空间中。

1.观察空间

通常摄像机需要通过3个参数定义,即Eye、LookAt和Up。Eye指摄像机在世界空间中位置的坐标;LookAt指世界坐标中摄像机所观察位置的坐标;Up则指在世界空间中,近似于(注意,并不是等于)摄像机朝上的方向向量,通常定义为世界坐标系的y轴。构造观察空间的方法和步骤如图1-11所示。给定Eye、LookAt和Up后,即可定义观察空间。观察空间的原点位于Eye处,由3个向量{u,v,n}(对应于x、y、z坐标轴)构成。在观察空间中,摄像机位于原点处且指向-n,即摄像机的观察方向(也称朝前方向,forward)为-n

▲图1-11 构造观察空间的方法和步骤

2.观察矩阵及其推导过程

从图1-11可以看出,在定义u向量时,采用右手法则确定Up与n的叉乘,即u方向。最终向量uvn是相互垂直的。如果把这3个向量分别视为观察空间xyz轴,则观察空间的坐标系是一个右手坐标系,Unity 3D中定义的观察空间也是右手坐标系。假设图1-11中的LookAt点和Eye点在世界空间的坐标值是(0,2,10)与(0,3,20),那么在观察空间中,点LookAt则位于轴上,在轴、轴上的值为0,且LookAt点到Eye点的距离为。因此,在观察坐标系下,点LookAt的坐标值为

世界空间中的所有顶点,如果按点LookAt的方式重新定义在观察空间中,则该重定义操作便可称为观察变换(view transform)。考察观察变换用到的变换矩阵,可以把变换操作分解为平移和旋转两步。从理论上来说,把模型从世界空间变换到观察空间,应是在给定观察坐标系的前提下,保持观察坐标系不动;然后计算出模型的顶点相对于观察坐标系3个坐标轴平移了多少位移,旋转了多少度;最后套用式(1-5)和式(1-7)得出平移矩阵和旋转矩阵,最终组合成观察变换矩阵。但这样计算平移量比较简便,计算旋转度就会麻烦。因此,可以换一个思路,让模型和坐标系“锚定”,然后通过平移旋转观察坐标系,使之最终与世界坐标系重合。如图1-12所示,当观察坐标系平移旋转时,与之“锚定”的模型顶点也随之平移旋转。最终世界空间与观察空间重合时,模型顶点变换后得到的世界坐标值,实质上就是它在观察坐标系下的值。

▲图1-12 观察变换矩阵的推导和分解步骤

以圆柱顶面上的点LookAt为例,首先把点LookAt和Eye用一个刚性的连线“锚定”,然后把观察坐标系从Eye处移到世界坐标系原点O处。平移向量为O-Eye。写成平移矩阵Mt,如下所示。

如图1-12中的平移变换步骤,移动观察坐标系时,与观察坐标系“锚定”的点LookAt也随之平移。代入前面给定点Eye和LookAt在世界坐标系下的值,得到平移后的点值LookAttranslation为(0,-1,-10,1),如下所示。

完成平移后,需要在保持观察坐标系的3条坐标轴uvn始终相互垂直的前提下,旋转它们并使其朝向和世界坐标系的3条坐标轴xyz完全重合。世界坐标系的3条坐标轴的方向向量分别为 [1 0 0 0 ]T、 [0 1 0 0 ]T和 [0 0 1 0 ]T。也就是说,要构造一个矩阵,使得坐标轴uvn的方向向量值右乘矩阵Mr时,分别等于xyz的方向向量值。矩阵Mr

式(1-16)各列中的分量即为uvn轴的方向向量值。代入u轴的方向向量值[uxuyuz0]T,计算可得

式(1-17)中的结果向量 [uxuz+uyuy+uzuzvxux+vyuy+vzuznzux+nyuy+nzuz]T的3个分量值实质上就是向量u分别与向量u、n、v的点积值,因为u、n、v三向量相互垂直且为单位向量。向量点积公式为

式中,θab的夹角。

如图1-12中的旋转变换步骤,可得结果向量实质上就是[1 0 0 0]T。同理可得,MrvMrn的值分别为[0 1 0 0]T和[0 0 1 0]T。因此,到了这一步,变换矩阵MtempView

注意,到这一步变换还没有结束,因为采用的世界坐标系和观察坐标系都是按照Unity 3D的实现,分别是左手坐标系和右手坐标系,并且这两种坐标系的x轴和y轴是重合的,z轴则相反。因此,还必须让MtempView右乘一个矩阵Mz并对z轴取反,才能得到最终变换矩阵Mview,如下所示。

观察变换的矩阵推导和分解步骤,如图1-12所示。

顶点在世界坐标系下的位置坐标值右乘Mview,便可以变换到观察空间中。

3.Unity 3D中的观察空间坐标系

在各平台下,Unity 3D的观察坐标系统一使用右手坐标系。令在世界空间中顶点的坐标为vInWorldSpace=(wxwywz,1),观察空间中的顶点坐标vInViewSpace=(vxvyvz,1)。通过使用unity_MatrixV内置变量(参见4.1.1节),可以把顶点从基于左手坐标系的世界空间变换到基于右手坐标的观察空间,代码如下。

float4vInViewSpace = mul(unity_MatrixV,vInWorldSpace);

1.2.5 把顶点从观察空间变换到裁剪空间

通过观察变换可以将模型顶点从世界空间变换到观察空间。1.2.4节使用uvn表示观察坐标系的3条坐标轴。因为变换之后所有数据已经不需要在世界空间中进行考察,所以接下来将依照习惯使用xyz表示观察坐标系的3条坐标轴。

1.视截体

通常摄像机的取景范围(或者称为视野范围)是有限的。在渲染流水线中,通常使用视截体(view frustum有些书籍把view frustum称作视锥,本书统一称作视截体。)去框定这一取景范围。视截体是一个正棱台(regular prismoid),其两个底面平行且宽高比例相等。使用4个参数加以定义,即fovY、aspect、nf,如图1-13所示。fovY定义了沿垂直方向的视野区域(field of view,FOV)。aspect表示视截体底面的宽度与高度。如果把视截体正棱台的4个侧边向较小底面一端延伸,正棱台将延展成为正棱锥(regular pyramid)。正棱锥的顶点就是4个侧边的汇聚点,摄像机就位于此点。n表示近截面,即靠近摄像机位置点的视截体底面,显然在观察坐标系下近截面所处的平面是z=-nf表示远截面,即离摄像机位置点较远的视截体底面,在观察坐标系下远截面所处的平面是z=-f。在图1-13中,圆柱体和立方体都处于视截体之外,因而不可见。必须指出的是,视截体定义的近截面和远截面是不符合人类视觉原理的,就好比“近在眼前”的物体,虽然比近截面离摄像机的距离还要小,但摄像机不可能拍摄不到。之所以如此定义,主要是为了提高计算效率。

▲图1-13 视截体的定义

图1-13中的球体和圆柱体,这一类在视截体外的模型不会对最终渲染出来的图像效果产生任何贡献。因此,如果在把顶点数据投递给渲染流水线之前,把这些无贡献的模型顶点丢弃,则会大幅地提升性能。该操作称为视截体剔除操作(view frustum culling),通常由软件完成,成熟的3D渲染引擎都有实现。典型的视截体剔除操作的流程:在运行之前的预处理阶段,计算好多边形网格的包围体(bounding volumn),可以是包围盒(bounding box)或者包围球(bounding sphere),随后CPU(central processing unit,中央处理器)执行多边形网格的包围体与视截体的相交测试。如果多边形网格的包围体完全在视截体之外,则丢弃;完全或部分在视截体的,则投递进流水线。

图1-13中仅有立方体通过了视截体剔除操作被投递进流水线。但立方体并不是完全在视截体内,有一部分与远截面相交并且在其之后。因此,多边形应根据视截体的边界面进行裁剪处理,仅显示视截体之内的那一部分多边形。但必须要注意的是,裁剪操作并不是在观察空间,而是在光栅化阶段中的裁剪空间中,由硬件完成。因此,在顶点处理阶段,投影变换可视为最后一步操作。

即使不进行视截体剔除操作,把所有的模型顶点都投递到渲染流水线中,在光栅化处理阶段也还是会把不该显示的模型给裁剪掉。但因为光栅化阶段是在顶点处理阶段之后,如果不剔除,对最终渲染图像无贡献的模型也会经过顶点着色器执行处理,这种“劳而无功”的操作是很低效的方法。

2.投影矩阵及其推导过程

通过投影变换(projection transform)可以将正棱台状的视截体转换为一个轴对齐的(axis-aligned)立方体。该轴对齐立方体所框定的空间就是裁剪空间。更为准确地说,这种投影变换称为透视投影(perspective projection)。如图1-14所示,立方体的xy的取值范围都是[-1,1],z的取值范围是[-1,0]Direct3D中的轴对齐立方体的z的取值范围是[0,1],而OpenGL中z的取值范围是[-1,1]。本书推导投影矩阵所使用的z的取值范围是[-1,0],对说明原理没有影响。。“投影”一词容易让读者联想起投影机投射到银幕上的图像,但在渲染流水线中,透视投影变换并不生成二维图像,其只使场景中的三维物体发生变形。

▲图1-14 投影变换

图1-14(a)把一个视截体变为正方形,视截体内的物体也随之变形。图1-14(b)为视截体的横截面,视截体可以视为投影线的相交结果。投影线相交于摄像机原点处,通常把该原点称为投影中心点(center of projection)。假设投影平面位于视截体和投影中心点之间,投影线将构成投影平面内的场景图像。

在图1-14(b)中,在左边视截体中定义了两个线段l1l2。在三维空间中l1长于l2,但在投影平面内,这两线段的投影长度是相等的,这表现了透视投影的“近大远小”特性;在右边的轴对齐立方体中,投影变换使得投影线变为相互平行,这种相互平行的投影线称为通用投影线。从图1-14可见,线段l1l2经投影变换后,各自对应的线段l1l2的长度是相等的。

令视截体中有一个顶点p,其坐标是(xyz),p经投影变换转换为(x′y′z′)的p′。因为投影变换限定了x′y′的取值范围都是[-1,1],z′的取值范围是[-1,0],所以可计算得到x′ y′z′的值。

首先计算y′。图1-15显示了视截体的横切面。pp′分别表示(yz)和(y′z′),图1-15定义了一个投影平面,此平面的定义公式为

y′的取值限制在[-1,1]中,可以通过相似三角形计算获得,如图1-15所示。

▲图1-15 计算投影矩阵1

在图1-15中,△pAO和△p′A′ O′ 都是直角三角形且相似。根据相似三角形的性质公式:

可得

如图1-16所示,D是坐标系原点到投影平面的距离,即z′。同理计算x′x′的取值范围是[-1,1],令fovX为水平方向的视野角,有:

▲图1-16 计算投影矩阵2

在式(1-23)中,fovX未知,但可以根据fovY和aspect得到fovX,进而推导出x′,aspect为视截体的截面的宽高比。因为视截体是一个正棱台,所以也等于图1-16中的投影平面的WH的比值。同时,根据三角函数,有以下等式:

整理式(1-24),可得

结合式(1-23)和式(1-25),可以得到x′

到了这一步,代入x′y′的值,点p′的坐标值(x′y′z′)齐次化之后可以写成

式(1-27)中的z′依然未知。根据齐次坐标的性质,如果w≠0,则(xyz,1)等价于(wxwywzw)。现在把p′的坐标值乘以-z,得

观察式(1-28)中的坐标值,实质上p′的坐标向量值可以由p的坐标向量值[xyz1]T右乘一个矩阵Mprojection得到,即

如果把矩阵Mprojection第3行的4个数找到,即可找到投影矩阵,即要求解以下方程式中系数m1m2m3m4的值。

要解得这4个系数值,首先看图1-17。在视截体中,任意一个平行于投影平面的平面,其上面的任意一个顶点投影到投影平面上,其z都是相等的,即待投影点的投影z和其x、y无关。

前面提到视截体是正棱台,且投影平面平行于棱台的两底面,即远截面和近截面,那么从图1-17中可以看出,在视截体中任意选取一个平行于底面的平面,该平面上任意一点在投影平面上的投影点的z,与该点在平面上的xy是无关的。因此,式(1-30)中的m1m2应该为0才能使xy的具体取值不影响投影点的z′,即

又由于投影后z′的取值范围是[-1,0],显然远截面的z(即-f)投影后的z′为-1,近截面的z(即-n)投影后z′为0,因此代入式(1-31)可得方程组。

▲图1-17 视截体中平面的投影

解式(1-32),即可得到。代入矩阵MtempProjection的第3行,可得

图1-14中所定义的裁剪空间(轴对齐立方体)是基于右手坐标系的。在顶点处理阶段,投影变换可以视为最后一步操作,随后的顶点将进入硬件光栅化阶段。在光栅化阶段,裁剪空间采用左手坐标系,Unity 3D也遵循该规则,因此需要把右手坐标系的裁剪空间变换到左手坐标系。左手坐标系的裁剪空间的x 、y轴的朝向与右手坐标系相同,即z轴相反。因此,要把MtempProjection右乘一个倒转z轴的矩阵才能得到最终的投影矩阵Mprojection,如下:

再回头看式(1-31)。前面假定了轴对齐立方体的z的取值范围是[-1,0]。现在把该轴对齐立方体拉大,使其取值范围变成[-1,1]。这时远截面的z(即-f)投影后的z′为-1,近截面的z(即-n)投影后z′为1。因此,代入式(1-31)可得新方程组:

解式(1-35),可得

代入两值到矩阵MtempProjection的第3行后,再根据式(1-34),可得到最终的投影矩阵MProjection

实质上,式(1-34)和式(1-36)的Mprojection分别是Unity 3D在Direct3D平台和OpenGL平台上的投影矩阵值。根据式(1-31)中z′的不同取值范围,投影矩阵的第3行在不同平台上有不同的值。在Direct3D平台,z′的取值范围是[-1,0],而OpenGL的是[1,-1]。当把坐标系从右手坐标系变换到左手坐标系时,在Direct3D平台,z′的取值范围是[0,1],而OpenGL的是[-1,1]。

3.裁剪空间中未做透视除法的顶点坐标的z分量

必须注意的是,用顶点坐标右乘投影矩阵,从观察空间变换到裁剪空间时,得到的齐次坐标值的w分量不为1。又根据式(1-28),把右手坐标系转成左手坐标系时有p′=zz′可以得知:在Direct3D平台上,p′=zz′中的z′的取值范围是[0,1],p′=zz′中的z的取值范围是[nf],所得到未除以w分量的齐次坐标值z分量的取值范围是[0,1]。在OpenGL平台上,z′的取值范围是[-1,1],p′=zz′中的z′的取值范围是[nf],得到未除以w分量的齐次坐标值z分量的取值范围是[-nf]。把w分量不为1的裁剪空间坐标值除以w,使得w分量等于1,四维齐次坐标降维成三维笛卡儿坐标的操作,称为透视除法。透视除法是在光栅化阶段进行的,参见1.3.2节。

4.Unity 3D的裁剪空间坐标系

在各平台下,Unity 3D的裁剪空间坐标系统一使用左手坐标系,并且在未经透视除法之前,是一个不等价于三维笛卡儿坐标的四维齐次坐标系。令在基于右手坐标系的观察空间中顶点坐标为vInViewSpace=(vxvyvz,1),经投影变换后,变换到基于左手坐标系的裁剪空间的顶点坐标为vInClipSpace=(cxcyczcw)。由于投影变换不是仿射变换,因此顶点在裁剪空间中的齐次坐标vInClipSpace的w分量不为1。调用UnityViewToClipPos函数(参见4.2.4节),可以把顶点从观察空间变换到裁剪空间,代码如下:

float4vInClipSpace = UnityViewToClipPos(
float3(vInViewPos.x,vInViewPos.y,vInViewPos.z));

至此,顶点的变换处理过程已经完成,在现代渲染流水线实作中,这些变换操作通常在顶点着色器中完成。因此,Unity 3D引擎在其内置着色器中预先定义了很多变换用的矩阵。这些矩阵在运行时由引擎填充好,并通过CPU传递给GPU着色器,在第4章中会详细介绍这些矩阵。在阅读第4章时,若对代码背后蕴含的数学原理感到困惑,也可以回过头来查阅本章。