规范化深度值
你可能认为在投影之后可以丢弃原始的3D z坐标,因为所有的投影点已经摆放在2D投影窗口上,形成了我们最终看到的2D图像,不会再使用3D z坐标了。其实不然,我们仍然需要为深度缓存算法提供3D深度信息。就如同Direct3D希望我们把x、y投影坐标映射到一个规范化区间一样,Direct3D也希望我们将深度坐标映射到一个规范化区间[0,1]中。所以,我们需要创建一个保序函数(order preserving function)g(x)把[n,f]区间映射到[0,1]区间。由于该函数是保序的,所以当z1,z2∈[n,f]且z1
XMMATRIX XMMatrixPerspective FovLH(// returns projection matrix
FLOAT FovAngleY, // vertical field of view angle in radians
FLOAT AspectRatio, // aspect ratio = width / height
FLOAT NearZ, // distance to near plane
FLOAT FarZ); // distance to far plane
下面的代码片段示范了XMMatrixPerspectiveFovLH函数的使用方法。这里,我们将垂直视域角设为45°,近平面z设为1,远平面z设为1000(这些长度是在观察空间中的)。
XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*MathX::Pi,
AspectRatio(),1.0f,1000.0f);
横纵比要匹配窗口的横纵比:
float D3Dapp::AspectRatio() const
{
return static_cast<float>(mClientWidth)/mClientHeight;
}
曲面细分阶段
曲面细分(Tessellation)是指通过添加三角形的方式对一个网格的三角形进行细分,这些新添加的三角形可以偏移到一个新的位置,让网格的细节更加丰富。(见图5.26)。
图5.26 左图是原始网格,右图是经过曲面细分处理后的网格下面是曲面细分的一些优点:
1.我们可以通过曲面细分实现细节层次(level -of-detai l,LOD),使靠近相机的三角形通过细分产生更多细节,而那些远离相机的三角形则保持不变。通过这种方式,我们只需在需要细节的地方使用更多的三角形就可以了。
2.我们可以在内存中保存一个低细节(低细节意味着三角形数量少)的网格,但可以实时地添加额外的三角形,这样可以节省内存。
3.我们可以在一个低细节的网格上处理动画和物理效果,而只在渲染时才使用细分过的高细节网格。
曲面细分阶段是Direct3D 11中新添加的,这样我们就可以在GPU上对几何体进行细分了。而在Direct3D 11之前,如果你想要实现曲面细分,则必须在CPU上完成,经过细分的几何体还要发送到GPU用于渲染。然而,将新的几何体从CPU内存发送到显存是很慢的,而且还会增加CPU的负担。因此,在Direct3D 11出现之前,曲面细分的方法在实时图形中并不流行。Direct3D 11提供了一个可以完全在硬件上实现的曲面细分API。这样曲面细分就成为了一个非常有吸引力的技术了。曲面细分阶段是可选的(即在需要的时候才使用它)。我们要在第13章才会详细介绍曲面细分。
几何着色器阶段
几何着色器阶段(geometry shader stage)是可选的,我们在第11章之前不会用到它,所以这里只做一个简短的概述。几何着色器以完整的图元作为输入数据。例如,当我们绘制三角形列表时,输入到几何着色器的数据是构成三角形的三个点。(注意,这三个点是从顶点着色器传递过来的。)几何着色器的主要优势是它可以创建或销毁几何体。例如,输入图元可以被扩展为一个或多个其他图元,或者几何着色器可以根据某些条件拒绝输出某些图元。这一点与顶点着色器有明显的不同:顶点着色器无法创建顶点,只要输入一个顶点,那么就必须输出一个顶点。几何着色器通常用于将一个点扩展为一个四边形,或者将一条线扩展为一个四边形。
我们可以在图5.11中看到一个“流输出(stream output)”箭头。也就是,几何着色器可以将顶点数据流输出到内存中的一个顶点缓冲区内,这些顶点可以在管线的随后阶段中渲染出来。这是一项高级技术,我们会在后面的章节中对它进行讨论。
注意:顶点位置在离开几何着色器之前,必须被变换到齐次裁剪空间。
裁剪阶段
我们必须完全丢弃在平截头体之外的几何体,裁剪与平截头体边界相交的几何体,只留下平截头体内的部分;图 5.27以2D形式说明了一概念。
图5.27 (a)裁剪之前。(b)裁剪之后。
我们可以将平截头体视为由6个平面界定的空间范围:顶、底、左、右、近、远平面。要裁剪与平截头体方向相反的多边形,其实就是逐个裁剪与每个平截头体平面方向相反的多边形,当裁剪一个与平面方向相反的多边形时(参见图5.28),我们将保留平面正半空间中的部分,而丢弃平面负半空间中的部分。对一个与平面方向相反的凸多边形进行裁剪,得到的结果仍然会是一个凸多边形。由于硬件会自动完成所有的裁剪工作,所以我们不在这里讲解具体的实现细节;有兴趣的读者可以参阅[Sutherland74],了解一下目前流行的Sutherland-Hodgeman裁剪算法。它基本思路是:求出平面与多边形边之间的交点,然后对顶点进行排序,形成新的裁剪后的多边形。
图 5.28 (a)裁剪一个与平面方向相反的三角形。(b)裁剪后的三角形。注意,裁剪后的三角形已经不再是一个三角形了,它是一个四边形。所以,硬件必须将个四边形重新划分为三角形,对于凸多边形来说这是一个非常简单的处理过程。
[Blinn78]描述了如何在4D齐次空间中实现裁剪算法(图5.29)。在透视除法之后,平截头体内的点(x/w, y/w, z/w ,1)将位于规范化设备空间,它的边界如下:
−1 ≤ x/w ≤ 1
−1 ≤ y/w ≤ 1
0 ≤ z/w ≤ 1
那么在透视除法之前,平截头体内的4D点(x , y , z , w)在齐次裁剪空间中的边界为:
−w ≤ x ≤ w
−w ≤ y ≤ w
0 ≤ z ≤ w
也就是,顶点被限定在以下4D平面构成的空间范围内:
左:w = −x
右:w = x
底:w = −y
顶:w = y
近:z = 0
远:z = w
只要我们知道齐次剪裁空间中的平截头体平面方程,我们就能使用任何一种裁剪算法(比如Sutherland-Hodgeman)。注意,由线段/平面相交测试的数学推论可知,这个测试在ℝ4也能使用,所以我们可以在齐次裁剪空间中进行4D点和4D平面的相交测试。
光栅化阶段
光栅化(rasterization)阶段的主要任务是为投影后的3D三角形计算像素颜色。
视口变换
在裁剪之后,硬件会自动执行透视除法,将顶点从齐次裁剪空间变换到规范化设备空间(NDC)。一旦顶点进入NDC空间,构成2D图像的2D x、y坐标就会被变换到后台缓冲区中的一个称为视口的矩形区域内(回顾4.2.8节)。在该变换之后,x、y坐标将以像素为单位。通常,视口变换不修改z坐标,因为z坐标还要由深度缓存使用,但是我们可以通过D3D11_VIEWPORT结构体的MinDepth和MaxDepth值修改z坐标的取值范围。MinDepth和MaxDepth的值必须在0和1之间。
背面消隐
一个三角形有两个面。我们使用如下约定来区分这两个面。假设三角形的顶点按照v0、v1、v2的顺序排列,我们这样来计算三角形的法线n:
e0 = v1 - v0
e1 = v2 – v1
n=e0×e1∥e0×e1∥n=e0×e1∥e0×e1∥
带有法线向量的面为正面,而另一个面为背面。图5.30说明了这一概念。
图5.30 左边的三角形正对我们的观察点,而右边的三角形背对我们的观察点。
当观察者看到三角形的正面时,我们说三角形是朝前的;当观察者看到三角形的背面时, 我们说三角形是朝后的。如图5.30所示,左边的三角形是朝前的,而右边的三角形是朝后的。而且,按照我们的观察角度,左边的三角形会按顺时针方向环绕,而右边的三角形会按逆时针方向环绕。这不是巧合:因为按照我们选择的约定(即,我们计算三角形法线的方式),按顺时针方向环绕的三角形(相对于观察者)是朝前的,而按逆时针方向环绕的三角形(相对于观察者)是朝后的。
现在,3D空间中的大部分物体都是封闭实心物体。当我们按照这一方式将每个三角形的法线指向物体外侧时,摄像机就不会看到实心物体朝后的三角形,因为朝前的三角形挡住了朝后的三角形;图5.31和图5.32分别以2D和3D形式说明了一概念。由于朝前的三角形挡住了朝后的三角形,所以绘制它们是毫无意义的。背面消隐(backface culling)是指让管线放弃对朝后的三角形的处理。这可以将所要处理的三角形的数量降低到原数量的一半。
图5.31 (a)一个带有朝前和朝后三角形的实心物体。(b)在剔除了朝后的三角形之后的场景。注意,背面消隐不会影响最终的图像,因为朝后的三角形会被朝前的三角形阻挡。
图5.32 (左图)当以透明方式绘制立方体时,我们可以看到所有的6个面。(右图)当以实心方式绘制立方体时,我们无法看到朝后的3个面,因为朝前的3个面挡住了它们——所以朝后的三角形可以被直接丢弃,不再接受后续处理,没人能看到些朝后的三角形。
默认情况下,Direct3D将(相对于观察者)顺时针方向环绕的三角形视为朝前的三角形,将(相对于观察者)逆时针方向环绕的三角形视为朝后的三角形。不过,这一约定可以通过修改Direct3D渲染状态颠倒过来。
顶点属性插值
如前所述,我们通过指定三角形的3个顶点来定义一个三角形。除位置外,顶点还可以包含其他属性,比如颜色、法线向量和纹理坐标。在视口变换之后,这些属性必须为三角形表面上的每个像素进行插值。顶点深度值也必须进行插值,以使每个像素都有一个可用于深度缓存算法的深度值。对屏幕空间中的顶点属性进行插值,其实就是对3D空间中的三角形表面进行线性插值(如图5.33所示);这一工作需要借助所谓的透视矫正插值(perspective correct interpolation)来实现。本质上,三角形表面内部的像素颜色都是通过顶点插值得到的。
图5.33:通过对三角形顶点之间的属性值进行线性插值,可以得到三角形表面上的任一属性值p(s,t)。
我们不必关心透视精确插值的数学细节,因为硬件会自动完成这一工作;不过,有兴趣的读者可以在[Eberly01]中查阅相关的数学推导过程。图5.34介绍了一点基本思路:
图5.34 一条3D线被投影到投影窗口上(在屏幕空间中投影是一条2D线)。我们看到,在3D线上取等距离的点,在2D屏幕空间上的投影点却不是等距离的。所以,我们在3D空间中执行线性插值,在屏幕空间需要执行非线性插值。
像素着色器阶段
像素着色器(Pixel shader)是由我们编写的在GPU上执行的程序。像素着色器会处理每个像素片段(pixel fragment),它的输入是插值后的顶点属性,由此计算出一个颜色。像素着色器可以非常简单地输出一个颜色,也可以很复杂,例如实现逐像素光照、反射和阴影等效果。
输出合并阶段
当像素片段由像素着色器生成之后,它们会被传送到渲染管线的输出合并(output merger,简称OM)阶段。在该阶段中,某些像素片段会被丢弃(例如,未能通过深度测试或模板测试)。未丢弃的像素片段会被写入后台缓冲区。混合(blending)工作是在该阶段中完成的,一个像素可以与后台缓冲区中的当前像素进行混合,并以混合后的值作为该像素的最终颜色。某些特殊效果,比如透明度,就是通过混合来实现的;我们会在第9章专门讲解混合。
小结
1.我们可以根据人眼的视觉特性,在2D图像上模拟3D场景。我们发现平行线会汇集为一个零点(或称消失点),物体的尺寸会随着深度的增加而减小,一个物体会挡住它后面的其他物体,灯光和阴影可以表现物体的立体感和体积感,阴影可以体现光源的位置并烘托物体之间的层次关系。
2.我们使用三角形网络来模拟物体。我们可以通过指定三角形的3个顶点来定义一个三角形。在许多网格中,顶点是由多个三角形共享的;索引列表可以用于避免顶点重复。
3.颜色可以通过红、绿、蓝的强度来描述。通过将这三种颜色以不同的强度进行混合,可以得到上千万种不同的颜色。我们通常使用规范化区间[0,1]描述红、绿、蓝的强度。0表示没有强度,1表示最高强度,中间值表示中等强度。通常在颜色中还会包含一个称为alpha分量的附加分量,它用于表示颜色的不透明度。当使用混合时,alpha分量非常有用。我们可以使用4D向量(r ,g ,b ,a )来表示带有alpha分量的颜色,其中0 ≤r ,g ,b ,a ≤ 1。在Direct3D中,颜色由XMVECTOR类来表示,使用XNA数学库进行颜色操作可以发挥SIMD的优势。如果用32位表示颜色,则每个分量占一个字节,XNA数学库提供了XMCOLOR结构用于存储32位颜色。颜色向量可以像普通向量那样进行加法、减法和标量乘法运算,只是每个分量的取值范围必须限定在[0, 1]区间内(或32位颜色的[0,255]区间内)。其他的向量运算(比如点积和叉积)对颜色向量来说没有意义。符号⨂表示分量乘法,它的含义为:(c1 ,c2 ,c3 ,c4 )⨂(k1 ,k2 ,k3 ,k4 ) = (c1k1 , c2k2 , c3k3 , c4k4 )。
4.渲染管线是指:在给定一个3D场景的几何描述及一架已确定位置和方向的虚拟摄像机时,根据虚拟摄像机的视角生成2D图像的一系列步骤。
5.渲染管线可以被分解为以下主要阶段:输入装配(IA)阶段;顶点着色器(VS)阶段;曲面细分阶段;几何着色器(GS)阶段;裁剪阶段、光栅化(RS)阶段、像素着色器(PS)阶段和输出合并器(OM)阶段。