DX11龙书学习笔记---渲染管线
1、输入装配阶段
输入装配(Input Assembler, 简称IA)阶段从内存读取几个数据(顶点和索引)并将这些数据组合为几何图元(例如三角形、直线)。
- 顶点
- 上图说明顶点只是几何图元中的一个特殊的点。不过,在Direct3D中,顶点具有更多含义。本质上,Direct3D中的顶点由空间位置和各种附加属性组成;例如,在后面,我们会在顶点中添加法线向量实现光照;在顶点中添加纹理坐标实现纹理。Direct3D可以让我们灵活地建立属于我们自己的顶点格式(例如,允许我们定义顶点的分量)。
- 图元拓扑
- 顶点是以一个叫做顶点缓冲区的Direct3D数据结构的形式绑定到图形管线的。顶点缓冲区只是在连续的内存中存储了一个顶点列表。它并没有说明以何种方式组织顶点,形成几何图元。例如,是应该把顶点缓冲区中的每两个顶点解释为一条直线,还是应该把顶点缓冲区中的每三个顶点解释为一个三角形?我们通过指定图元拓扑来告诉Direct3D以何种方式组成几何图元:
id ID3D11Device::IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY Topology);
typedef enum D3D11_PRIMITIVE_TOPOLOGY
{
D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED = 0,
D3D11_PRIMITIVE_TOPOLOGY_POINTLIST = 1,
D3D11_PRIMITIVE_TOPOLOGY_LINELIST = 2,
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP = 3,
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4,
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5,
D3D11_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10,
D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11,
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ = 12,
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ= 13,
D3D11_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST = 33,
D3D11_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST = 34,
.
.
.
D3D11_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64,
} D3D11_PRIMITIVE_TOPOLOGY; - 所有的绘图操作以当前设置的图元拓扑方式为准。在没有改变拓扑方式之前,当前设置的拓扑方式会一直有效。下面的代码说明了一点:
md3dDevice->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
/* …draw objects using line list… /
md3dDevice->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
/ …draw objects using triangle list… /
md3dDevice->IASetPrimitiveTopology(
D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
/ …draw objects using triangle strip… */
下面会详细描述各种不同的图元拓扑方式。在本书中,我们主要使用三角形列表。
- 点列表
- 点列表(point list)由D3D11_PRIMITIVE_TOPOLOGY_POINTLIST标志值表示。当使用点列表时,每个顶点都会被绘制为一个独立的点,如下图所示。
(a)点列表。(b)线带。©线列表。(d)三角形带。
- 线带
- 线带(line strip)由D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP标志值表示。当使用线带时,前后相邻的两个顶点会形成一条直线(参见图b);这样,n+1个顶点可以形成n条直线。
- 线列表
- 线列表(line list)由D3D11_PRIMITIVE_TOPOLOGY_LINELIST标志值表示。当使用线列表时,每两个顶点会形成一条独立的直线(参见图c);这样,2n个顶点可以形成n条直线。线列表和线带之间的区别是线列表中的直线可以断开,而线带中的直线会自动连在一起;因为内部的每个顶点由两条直线共享,所以线带使用的顶点数量更少。
- 三角形带
- 三角形带(triangle strip)由D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP标志值表示。当使用三角形带时,顶点会按照图d所示的带状方式形成连续的三角形。我们可以看到顶点由相邻的三角形共享,n个顶点可以形成n-2个三角形。
注意:偶数三角形和奇数三角形的顶点环绕顺序不同,由此会产生背面消隐问题。为了解决一问题,GPU会在内部交换偶数三角形的前两个顶点的顺序,以使它们的环绕顺序与奇数三角形相同。
- 三角形列表
- 三角形列表(triangle list)由D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST标志值表示。当使用三角形列表时,每三个顶点会形成一个独立的三角形(参见图a);这样,3n个顶点可以形成n个三角形。三角形列表与三角形带之间的区别是:三角形列表中的三角形可以断开,而三角形带中的三角形会自动连在一起。
(a)三角形列表。(b)邻接三角形列表——可以看到每个三角形需要6个顶点来描述它和它的邻接三角形。所以,6n个顶点可以形成n个带有邻接信息的三角形。
- 带有邻接信息的图元
- 在包含邻接信息的三角形列表中,每个三角形都有与之相邻的3个邻接三角形;图5b说明了这些三角形的定义方式。它们主要用于几何着色器,因为某些几何着色算法需要访问邻接三角形。为了实现这些算法,邻接三角形必须与原三角形一起通过顶点/索引缓冲区提交给管线。通过指定D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ拓扑标志值可以使管线知道如何从顶点缓冲区中构建三角形以及它的邻接三角形。注意,邻接图元顶点只能作为几何着色器的输入数据——它们不会被绘制出来。如果没有几何着色器,那么邻接图元也不会被绘制出来。线列表、线带和三角形带也可以包含邻接图元;详情请参见SDK文档
- 控制点面片列表
- D3D11_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST拓扑标志表示将顶点数据作为N控制点的面片列表,这些点用于(可选)图形管线的曲面细分阶段(tessellation stage)。
- 索引
-
如前所述,三角形是构成3D物体的基本单位。下面的代码示范了使用三角形列表来构建四边形和八边形的顶点数组(即,每三个顶点构成一个三角形)。
Vertex quad[6] ={
v0, v1, v2, // Triangle0
v0, v2, v3, // Triangle1
};
Vertex octagon[24] ={
v0, v1, v2, // Triangle0
v0, v2, v3, // Triangle1
v0, v3, v4, // Triangle2
v0, v4, v5, // Triangle3
v0, v5, v6, // Triangle4
v0, v6, v7, // Triangle5
v0, v7, v8, // Triangle6
v0, v8, v1 // Triangle 7
};
注意:三角形的顶点顺序非常重要,我们将该顺序称为环绕顺序(winding order); -
如下图所示,构成3D物体的三角形会共享许多相同的顶点。更确切地说,在图a中,四边形的每个三角形都会共享顶点v0和v2。当复制两个顶点时问题并不明显,但是在八边形的例子中问题就比较明显了(图b),八边形的每个三角形都会复制中间的顶点v0,而且边缘上的每个顶点都由相邻的两个三角形共享。通常,复制顶点的数量会随着模型细节和复杂性的提高而骤然上升。
(a)由2个三角形构成的四边形。(b)由8个三角形构成的八边形。
我们不希望对顶点进行复制,主要有两个原因:
增加内存需求量。(为什么要多次存储相同的顶点数据?)
增加图形硬件的处理负担。(为什么要多次处理相同的顶点数据?)
三角形带在一定程度上可以解决复制顶点问题,但是几何体必须按照带状方式组织,实现起来难度较大。相比之下,三角形列表具有更好的灵活性(三角形不必彼此相连),如果能找到一种方法,即移除复制顶点,又保留三角形列表的灵活性,那么会是一件非常有价值的事情。索引(index)可以解决一问题。它的工作原理是:我们创建一个顶点列表和一个索引列表。顶点列表包含所有唯一的顶点,而索引列表包含指向顶点列表的索引值,这些索引定义了顶点以何种方式组成三角形。回顾图中的图形,四边形的顶点列表可以这样创建:
Vertex v[4] = {v0, v1, v2, v3};
而索引列表需要定义如何将顶点列表中的顶点放在一起,构成两个三角形。
UINT indexList[6] = {0, 1, 2, // Triangle0
0, 2, 3}; // Triangle 1
在索引列表中,每3个元素表示一个三角形。所以上面的索引列表的含义为:“使用顶点v[0]、v[1]、v[2]构成三角形0,使用顶点v[0]、v[2]、v[3]构成三角形1”。
与之类似,八边形的顶点列表可以这样创建:
Vertex v[9] = {v0, v1, v2, v3, v4, v5, v6, v7, v8};
索引列表为:
UINT indexList[24] = {
0, 1, 2, // Triangle 0
0, 2, 3, // Triangle 1
0, 3, 4, // Triangle 2
0, 4, 5, // Triangle 3
0, 5, 6, // Triangle 4
0, 6, 7, // Triangle 5
0, 7, 8, // Triangle 6
0, 8, 1 // Triangle7
};
当顶点列表中的唯一顶点得到处理之后,显卡可以使用索引列表把顶点放在一起构成三角形。我们将“复制问题”转嫁给了索引列表,但是这种复制是可以让人接受的。因为:
索引是简单的整数,不像顶点结构体那样占用很多内存(顶点结构体包含的分量越多,占用的内存就越多)。通过适当的顶点缓存排序,图形硬件不必重复处理顶点(在绝大多数的情况下)。
2、顶点着色器阶段
- 在完成图元装配后,顶点将被送往顶点着色器(vertex shader)阶段。顶点着色器可以被看成是一个以顶点作为输入输出数据的函数。每个将要绘制的顶点都会通过顶点着色器推送至硬件;实际上,我们可以概念性地认为在硬件上执行了如下代码:
for(UINT i = 0; i < numVertices; ++i)
outputVertex[i] = VertexShader(inputVertex[i]);
顶点着色器函数由我们自己编写,但是它会在GPU上运行,所以执行速度非常快。
许多效果,比如变换(transformation)、光照(lighting)和置换贴图映射(displacement mapping)都是由顶点着色器来实现的。记住,在顶点着色器中,我们不仅可以访问输入的顶点数据,也可以访问在内存中的纹理和其他数据,比如变换矩阵和场景灯光。
-局部空间和世界空间
(a)物体的每个顶点都是相对于它们自己的局部坐标系来定义的。我们根据物体在场景中的位置和方向来定义每个局部坐标系相对于世界坐标系的位置和方向。然后我们执行坐标转换变换,将所有坐标转换到世界坐标系中。(b)在世界变换后,所有物体的顶点都会位于同一个世界坐标系中。
- 世界矩阵描述的是一个物体的局部空间相对于世界空间的原点位置和坐标轴方向,这些坐标可以存放在一个行矩阵中。设Qw = (Qx,Qy ,Qz ,1)、uw= (ux ,uy ,uz ,0)、vw= (vx ,vy ,vz ,0)、ww = (wx ,wy ,wz ,0)分别表示局部空间相对于世界空间的原点、x轴、y轴、z轴的齐次坐标,由3.4.3节可知,从局部空间到世界空间的坐标转换矩阵为:
-观察空间
- 为了生成场景的2D图像,我们必须在场景中放置一架虚拟摄像机。虚拟摄像机指定了观察者可以看到的场景范围,或者说是我们所要生成的2D图像所显示的场景范围。我们把一个局部坐标系(称为观察空间、视觉空间或摄像机空间)附加在摄像机上,如图5.19所示;该坐标系以摄像机的位置为原点,以摄像机的观察方向为z轴正方向,以摄像机的右侧为x轴,以摄像机的上方为y轴。在渲染管线的随后阶段中,使用观察空间来描述顶点比使用世界空间来描述顶点要方便的多。从世界空间到观察空间的坐标转换称为观察变换(view transform),相应的矩阵称为观察矩阵(view matrix)。
设Qw= (Qx ,Qy ,Qz ,1)、uw= (ux ,uy ,uz ,0)、vw= (vx ,vy ,vz ,0)、ww = (wx ,wy ,wz ,0)分别表示观察空间相对于世界空间的原点、x轴、y轴、z轴的齐次坐标,我们由3.4.3节可知,从观察空间到世界空间的坐标转换矩阵为:
不过,这不是我们想要的结果。我们希望得到的是从世界空间到观察空间的反向变换。由上面可知,反向变换可由逆运算取得。也就是,W-1为世界空间到观察空间的变换矩阵。
世界坐标系和观察坐标系通常具有不同的位置和方向,所以凭直觉就可以知道W = RT的含义(即,世界矩阵可以被分解为一个旋转矩阵和一个平移矩阵)。这种方式可以使逆矩阵的计算过程更简单一些:
V = W-1 = (RT)-1 = T-1R-1 = T-1RT
我们现在介绍一种更直观的方法来创建构成观察矩阵的向量。设Q为摄像机的位置,T为摄像机瞄准的目标点,j为描述世界空间“向上”方向的单位向量。参考下图,摄像机的观察方向为:
向量w描述的是摄像机坐标系的z轴。指向w“右边”的单位向量
为:向量u描述的是摄像机坐标系的x轴。最后,描述摄像机坐标系y轴的向量为:
v=w×u
由于w和u是相互垂直的单位向量,所以w×u必定为单位向量,不需要对它做规范化处理。
这样,给出摄像机的位置、目标点和世界“向上”向量,我们就能够得到摄像机的局部坐标系,该坐标系可以用于创建观察矩阵。
XNA库提供了如下函数,根据刚才描述的过程计算观察矩阵:
XMMATRIX XMMatrixLookAtLH( // Outputs resulting view matrix V
FXMVECTOR EyePosition, // Input camera position Q
FXMVECTOR FocusPosition, // Input target point T
FXMVECTOR UpDirection); // Input world up vector j
通常,世界坐标系的y轴就是“向上”方向,所以“向上”向量j通常设为(0, 1,0)。举一个例子,假设摄像机相对于世界空间的位置为(5, 3, −10),目标点为世界原点(0, 0,0)。我们可以使用如下代码创建观察矩阵:
XMVECTOR pos = XMVectorSet(5,3,-10,1.0f);
XMVECTOR target = XMVectorZero();
XMVECTOR up = XMVectorSet(0.0f,1.0f,0.0f,0.0f);
XMMATRIXV = XMMatrixLookAtLH(pos,target,up);
-投影与齐次裁剪空间
平截头体描述了摄像机可以“看到”的空间范围
我们的下一个任务是把平截头体内的3D物体投影到2D投影窗口上。投影(projection)必须按照平行线汇集为零点的方式来实现,随着一个物体的3D深度增加,它的投影尺寸会越来越小;图5.22说明了透视投影的实现过程。我们将“从顶点连向观察点的直线”称为顶点的投影线。然后我们可以定义透视投影变换,将3D顶点v变换到它的投影线与2D投影平面相交的点vʹ上;我们将vʹ称为v的投影。对一个3D物体的投影就是对组成该物体的所有顶点的投影。
在3D空间中,大小相同、深度不同的两个圆柱体。与观察点距离较近的圆柱体生成的投影较大。在平截头体内的物体可以被映射到投影窗口上;在平截头体外的物体可以映射到投影平面上,但是不会映射到投影窗口上。
-定义平截头体
- 我们可以在观察空间中使用如下4个参数来定义以原点为投影中心、以z轴正方向为观察方向的平截头体:近平面n、远平面f、垂直视域角α和横纵比r。注意,在观察空间中,近平面和远平面都平行于xy平面;所以,我们只需要简单地指定它们沿z轴方向到原点之间的距离即可表示这两个平面。横纵比由r = w/ℎ定义,其中w表示投影窗口的宽度,ℎ表示投影窗口的高度(单位由观察空间决定)。投影窗口本质上是指场景在观察空间中的2D图像。该图像最终会被映射到后台缓冲区中;所以,我们希望投影窗口的尺寸比例与后台缓冲区的尺寸比例保持相同。在大多数情况下,横纵比就是指后台缓冲区的尺寸比例(它是一个比例值,所以没有单位)。例如,当后台缓冲区的尺寸为800×600时,横纵比r = 800/600 ≈ 1.333。如果投影窗口的横纵比与后台缓冲区的横纵比不同,那么当投影窗口映射到后台缓冲区时,必然会出现比例失衡,导致图像变形(例如,投影窗口中的一个正圆会被拉伸为后台缓冲区中的一个椭圆)。
将水平视域角设为β,它是由垂直视域角α和横纵比r决定的。考虑下图,分析一下如何通过α、r来求解β。注意,投影窗口的实际尺寸并不重要,重要的只是横纵比。所以,我们将高度设定为2,则对应的宽度为:
为了获得指定的垂直视域角α,投影窗口必须放在与原点距离为d的位置上:
观察可知
所以,只要给出垂直视域角α和横纵比r,我们就能求出水平视域角β。
-对顶点进行投影
如下图,给出一个点 (x, y, z),求它在投影平面z = d上的投影点 (xʹ, yʹ, d)。通过分析x、y坐标以及使用相似三角形,我们可以求出
当且仅当以下条件成立时,点(x, y , z)在平截头体内。
−r ≤ xʹ ≤ r
−1 ≤ yʹ ≤ 1
n ≤ z ≤ f
-规范化设备坐标(NDC)
- 在观察空间中,投影窗口的高度为2,宽度为2r,其中r表示横纵比。这里存在的一个问题是:尺寸依赖于横纵比。这意味着我们必须为硬件指定横纵比,否则硬件将无法执行那些与投影窗口尺寸相关的运算(比如,将投影窗口映射到后台缓冲区)。如果我们能去除对横纵比的依赖性,那么会使相关的运算变得更加简单。为了解决一问题,我们将的投影x坐标从[−r , r] 区间缩放到[−1,1]区间:
−r ≤ xʹ ≤ r
−1 ≤ xʹ ⁄r ≤ 1
在映射之后,x、y坐标称为规范化设备坐标(normalized device coordinates,简称NDC)(z坐标还没有被规范化)。当且仅当以下条件成立时,点(x ,y, z)在平截头体内。
−1 ≤ xʹ⁄r ≤ 1
−1 ≤ yʹ ≤ 1
n ≤ z ≤ f
从观察空间到NDC空间的变换可以看成是一个单位转换。我们有这样一个关系式:在 x轴上的一个NDC单位等于观察空间中的r个单位(即,1 ndc = r vs)。所以给出x观察空间单位,我们可以使用这个关系式来转换单位:
我们可以修改之前的投影公式,直接使用NDC空间中的x、y投影坐标:
注意:在NDC空间中,投影窗口的高度和宽度都为2。也就是说,现在的尺寸是固定的,硬件不需要知道横纵比,但是我们必须自己来完成投影坐标从观察空间到NDC空间的转换工作(图形硬件假定我们会完成一工作) 。
- 用矩阵来描述投影方程
- 为了保持一致,我们将用一个矩阵来描述投影变换。不过,上面的方程是非线性的,无法用矩阵描述。所以我们要使用一种“技巧”将它分为两部分来实现:一个线性部分和一个非线性部分。非线性部分要除以z。我们会在下一节讨论“如何规范化z坐标”时讲解这一问题;现在读者只需要知道,我们会因为个除法操作而失去原始的z坐标。所以,我们必须在变换之前保存输入的z坐标;我们可以利用齐次坐标来解决一问题,将输入的z坐标复制给输出的w坐标。在矩阵乘法中,我们要将元素[2][3]设为1、元素[3][3]设为0(从0开始的索引)。我们的投影矩阵大致如下:
注意矩阵中的常量A和B(它们将在下一节讨论);这些常量用于把输入的z坐标变换到规范化区间。将一个任意点(x , y, z,1)与该矩阵相乘,可以得到:
在与投影矩阵(线性部分)相乘之后,我们要将每个坐标除以w = z(非线性部分),得到最终的变换结果:
有时,与w相除的过程也称为透视除法(perspective divide)或齐次除法(homogeneous divide)。
-规范化深度
你可能认为在投影之后可以丢弃原始的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<z2时,必有g(z1)<g(z2)。这样,即使深度值已经被变换过了,相对的深度关系还是会被完好无损地保留下来,我们依然可以在规范化区间中得到正确的深度测试结果,这就是我们要为深度缓存算法做的全部工作。
通过缩放和平移可以实现从[n ,f]到[0,1]的映射。但是,这种方式无法与我们当前的投影方程整合。我们可以从上面方程中看到经过变换的z坐标为:
我们现在需要让A和B满足以下条件:
条件1:g(n) = A + B/n = 0(近平面映射为0)
条件2:g(f) = A +B/f = 1(远平面映射为1)
由条件1得到B的结果为:B= −An。把它代入条件2,得到A的结果为:
从g(z)的曲线图中可以看出,它会限制增长的幅度(保序)而且是非线性的。从图中我们还可以看到,区间中的大部分取值落在近平面附近。因此,大多数深度值被映射到了一个很窄的取值范围内。这会导致深度缓冲区出现精度问题(由于所能表示的数值范围有限,计算机将无法识别变换后的深度值之间的微小差异)。通常的建议是让近平面和远平面尽可能接近,把深度的精度性问题减小到最低程度。
现在我们已经解出了A和B,我们可以确定出完整的透视投影矩阵:
在与投影矩阵相乘之后,进行透视除法之前,几何体所处的空间称为齐次裁剪空间(homogeneous clip space)或投影空间(projection space)。在透视除法之后,几何体所处的空间称为规范化设备空间(normalized device coordinates,简称NDC)。
3、曲面细分阶段
- 曲面细分(Tessellation)是指通过添加三角形的方式对一个网格的三角形进行细分,这些新添加的三角形可以偏移到一个新的位置,让网格的细节更加丰富。
左图是原始网格,右图是经过曲面细分处理后的网格
下面是曲面细分的一些优点:
1.我们可以通过曲面细分实现细节层次(level -of-detai l,LOD),使靠近相机的三角形通过细分产生更多细节,而那些远离相机的三角形则保持不变。通过这种方式,我们只需在需要细节的地方使用更多的三角形就可以了。
2.我们可以在内存中保存一个低细节(低细节意味着三角形数量少)的网格,但可以实时地添加额外的三角形,这样可以节省内存。
3.我们可以在一个低细节的网格上处理动画和物理效果,而只在渲染时才使用细分过的高细节网格。
曲面细分阶段是Direct3D 11中新添加的,这样我们就可以在GPU上对几何体进行细分了。而在Direct3D 11之前,如果你想要实现曲面细分,则必须在CPU上完成,经过细分的几何体还要发送到GPU用于渲染。然而,将新的几何体从CPU内存发送到显存是很慢的,而且还会增加CPU的负担。因此,在Direct3D 11出现之前,曲面细分的方法在实时图形中并不流行。Direct3D 11提供了一个可以完全在硬件上实现的曲面细分API。这样曲面细分就成为了一个非常有吸引力的技术了。曲面细分阶段是可选的(即在需要的时候才使用它)。我们要在后面才会详细介绍曲面细分。
4、几何着色器阶段
- 几何着色器阶段(geometry shader stage)是可选的,我们在第11章之前不会用到它,所以这里只做一个简短的概述。几何着色器以完整的图元作为输入数据。例如,当我们绘制三角形列表时,输入到几何着色器的数据是构成三角形的三个点。(注意,这三个点是从顶点着色器传递过来的。)几何着色器的主要优势是它可以创建或销毁几何体。例如,输入图元可以被扩展为一个或多个其他图元,或者几何着色器可以根据某些条件拒绝输出某些图元。这一点与顶点着色器有明显的不同:顶点着色器无法创建顶点,只要输入一个顶点,那么就必须输出一个顶点。几何着色器通常用于将一个点扩展为一个四边形,或者将一条线扩展为一个四边形。
我们可以在渲染管线图中看到一个“流输出(stream output)”箭头。也就是,几何着色器可以将顶点数据流输出到内存中的一个顶点缓冲区内,这些顶点可以在管线的随后阶段中渲染出来。这是一项高级技术,我们会在后面的章节中对它进行讨论。
注意:顶点位置在离开几何着色器之前,必须被变换到齐次裁剪空间。
5、裁剪阶段
- 我们必须完全丢弃在平截头体之外的几何体,裁剪与平截头体边界相交的几何体,只留下平截头体内的部分;下图以2D形式说明了一概念。
(a)裁剪之前。(b)裁剪之后。
6、光栅化阶段
光栅化(rasterization)阶段的主要任务是为投影后的3D三角形计算像素颜色。
-视口变换
在裁剪之后,硬件会自动执行透视除法,将顶点从齐次裁剪空间变换到规范化设备空间(NDC)。一旦顶点进入NDC空间,构成2D图像的2D x、y坐标就会被变换到后台缓冲区中的一个称为视口的矩形区域内。在该变换之后,x、y坐标将以像素为单位。通常,视口变换不修改z坐标,因为z坐标还要由深度缓存使用,但是我们可以通过D3D11_VIEWPORT结构体的MinDepth和MaxDepth值修改z坐标的取值范围。MinDepth和MaxDepth的值必须在0和1之间。
-背面消隐
一个三角形有两个面。我们使用如下约定来区分这两个面。假设三角形的顶点按照v0、v1、v2的顺序排列,我们这样来计算三角形的法线n:
带有法线向量的面为正面,而另一个面为背面。下图说明了这一概念。
左边的三角形正对我们的观察点,而右边的三角形背对我们的观察点。
当观察者看到三角形的正面时,我们说三角形是朝前的;当观察者看到三角形的背面时, 我们说三角形是朝后的。如上图所示,左边的三角形是朝前的,而右边的三角形是朝后的。而且,按照我们的观察角度,左边的三角形会按顺时针方向环绕,而右边的三角形会按逆时针方向环绕。这不是巧合:因为按照我们选择的约定(即,我们计算三角形法线的方式),按顺时针方向环绕的三角形(相对于观察者)是朝前的,而按逆时针方向环绕的三角形(相对于观察者)是朝后的。
现在,3D空间中的大部分物体都是封闭实心物体。当我们按照这一方式将每个三角形的法线指向物体外侧时,摄像机就不会看到实心物体朝后的三角形,因为朝前的三角形挡住了朝后的三角形;图5.31和图5.32分别以2D和3D形式说明了一概念。由于朝前的三角形挡住了朝后的三角形,所以绘制它们是毫无意义的。背面消隐(backface culling)是指让管线放弃对朝后的三角形的处理。这可以将所要处理的三角形的数量降低到原数量的一半。
(a)一个带有朝前和朝后三角形的实心物体。(b)在剔除了朝后的三角形之后的场景。注意,背面消隐不会影响最终的图像,因为朝后的三角形会被朝前的三角形阻挡。
(左图)当以透明方式绘制立方体时,我们可以看到所有的6个面。(右图)当以实心方式绘制立方体时,我们无法看到朝后的3个面,因为朝前的3个面挡住了它们——所以朝后的三角形可以被直接丢弃,不再接受后续处理,没人能看到些朝后的三角形。
默认情况下,Direct3D将(相对于观察者)顺时针方向环绕的三角形视为朝前的三角形,将(相对于观察者)逆时针方向环绕的三角形视为朝后的三角形。不过,这一约定可以通过修改Direct3D渲染状态颠倒过来。
-顶点属性插值
如前所述,我们通过指定三角形的3个顶点来定义一个三角形。除位置外,顶点还可以包含其他属性,比如颜色、法线向量和纹理坐标。在视口变换之后,这些属性必须为三角形表面上的每个像素进行插值。顶点深度值也必须进行插值,以使每个像素都有一个可用于深度缓存算法的深度值。对屏幕空间中的顶点属性进行插值,其实就是对3D空间中的三角形表面进行线性插值(如图5.33所示);这一工作需要借助所谓的透视矫正插值(perspective correct interpolation)来实现。本质上,三角形表面内部的像素颜色都是通过顶点插值得到的。
通过对三角形顶点之间的属性值进行线性插值,可以得到三角形表面上的任一属性值p(s,t)。
我们不必关心透视精确插值的数学细节,因为硬件会自动完成这一工作;不过,有兴趣的读者可以在[Eberly01]中查阅相关的数学推导过程。下图介绍了一点基本思路:
一条3D线被投影到投影窗口上(在屏幕空间中投影是一条2D线)。我们看到,在3D线上取等距离的点,在2D屏幕空间上的投影点却不是等距离的。所以,我们在3D空间中执行线性插值,在屏幕空间需要执行非线性插值。
7、像素着色器阶段
- 像素着色器(Pixel shader)是由我们编写的在GPU上执行的程序。像素着色器会处理每个像素片段(pixel fragment),它的输入是插值后的顶点属性,由此计算出一个颜色。像素着色器可以非常简单地输出一个颜色,也可以很复杂,例如实现逐像素光照、反射和阴影等效果。
8、输出合并阶段
- 当像素片段由像素着色器生成之后,它们会被传送到渲染管线的输出合并(output
merger,简称OM)阶段。在该阶段中,某些像素片段会被丢弃(例如,未能通过深度测试或模板测试)。未丢弃的像素片段会被写入后台缓冲区。混合(blending)工作是在该阶段中完成的,一个像素可以与后台缓冲区中的当前像素进行混合,并以混合后的值作为该像素的最终颜色。某些特殊效果,比如透明度,就是通过混合来实现的;我们会在后边专门讲解混合。