第五章 渲染管线

第五章 渲染管线

本章的主要主题是渲染管线。 给定一个三维场景和定位的虚拟摄像机的几何描述,渲染管线是指基于虚拟摄像机看到的(图5.1)生成二维图像所需的所有步骤序列。本章主要是理论性的 - 下一章将我们学习使Direct3D进行绘制的理论付诸实践。在我们开始渲染流程之前,我们有两个简短的步骤:首先,我们讨论3D幻影的一些元素(即我们通过平面2D监视器屏幕来看待3D世界的错觉)。其次,我们解释颜色如何在数学和Direct3D代码中表现和处理。

目标:
1.发现用于传达二维图像中真实的体积和空间深度感的几个关键信号。
2.了解我们如何在Direct3D中表示3D对象。
3.了解我们如何建模虚拟相机。
4.了解渲染流水线 - 对3D场景进行几何描述并从中生成2D图像的过程。

这里写图片描述
图5.1 左图显示了3D世界中设置的一些物体的侧视图,其中一个摄像机被定位并瞄准; 中间的图像显示相同的场景,但从上到下的视图。视锥体指定观众可以看到的空间的体积; 视锥体以外的对象(和对象的一部分)是不可见的。右侧的图像显示了基于相机“看到”的2D图像。

5.1三维图像

在我们开始3D计算机图形之旅之前,还要研究一个问题:如何在平面的2D监视器屏幕上显示深度和体积的3D世界?很幸运,这个问题已经得到了很好研究,因为300年中艺术家一直在2D画布上绘制场景。本节中,我们概述了使图像看起来像3D的几个关键技术,即使它实际上绘制在2D平面上。

假设有两条不弯曲的铁轨,沿着直线延伸置无限远。铁轨一直保持相互平行,但是如果你站在铁路上,沿着它的路径观察,你会发现两条铁路轨道随着距离的增加而越来越近,最终它们在无限远处汇合。这是人类观察系统的特征之一:平行的视线汇聚到一个消失点; 见图5.2。

5-2
图5.2 平行的视线汇聚到一个消失点。 艺术家有时称之为线性透视。

这里写图片描述
图5.3 在此,所有的圆柱都是相同的大小,但由于深度现象观察者观察到的尺寸在减小

人类观察事物的另一个简单的现象是,物体的大小似乎随着深度增加而减小;也就是说,靠近我们的物体看起来比远处的物体要大。例如,远在山上的房子看起来很小,而我们附近的一棵树相比看起来非常大。图5.3显示了一个简单的场景,在这个场景中,平行的圆柱一个接一个排列。这些圆柱实际上大小相同,但随着观看深度的增加,它们看起来越来越小。还要注意柱子是如何收敛到地平线上的消失点的。

我们都经历过对象重叠(图5.4),这是指不透明的对象遮挡了它们后面的对象的部分(或全部)。这是一个重要的线索,因为它传达了场景中物体的深度顺序关系。我们已经讨论了(第4章)Direct3D如何使用深度缓冲区来确定哪些像素被遮挡,因此不应该绘制。

5-4
图5.4 由于一个在另一个之前而彼此部分混淆的一组对象(它们重叠)

5-5
图5.5 (a)看起来不明显的2D球体。 (b)看似3D的亮点球体。

考虑图5.5。 左边,我们有一个没有照明的球体,右边,我们有一个带照明的球体。 如你所见,左边的球体看起来相当平坦 - 也许它甚至不是一个球体,而只是一个纹理化的2D圈! 因此,灯光和阴影在描绘三维物体的立体形态和体积方面起着非常重要的作用。

最后,图5.6显示了一个宇宙飞船及其影子。影子有两个关键的目的。首先,它告诉我们现场光源的起源。其次,它为我们提供了飞船在地面上有多高的概念。

毫无疑问,刚刚讨论的观察结果从我们的日常经验中显而易见。尽管如此,在我们学习和研究3D计算机图形时,明确说明我们所知道的内容并记住这些观察是有帮助的。

5-6
图5.6 宇宙飞船及其阴影。阴影意味着光源在场景中的位置,也给出飞船在地面上有多高

5.2模型表示

一个三维对象用三角形网格近似表示,因此三角形构成了我们建模的对象的基本构建块。如图5.7所示,我们可以用三角形网格逼近任何现实世界的三维物体。一般来说,用来逼近对象的三角形越多,近似值就越好,因为您可以对更精细的细节进行建模。 当然,我们使用的三角形越多,需要的处理能力就越多,所以必须根据应用程序目标受众的硬件能力来进行平衡。除了三角形之外,绘制线条或点有时也很有用。 例如,一条曲线可以用像素较粗的一系列短线段的图形绘制。

图5.7中使用的大量三角形清楚地表明:手动列出三维模型的三角形是非常麻烦的。对于除最简单的模型以外的所有模型,称为3D建模器的特殊3D应用程序用于生成和操作3D对象。这些建模器允许用户在具有丰富工具集的可视和交互式环境中构建复杂逼真的网格,从而使整个建模过程变得更加简单。用于游戏开发的流行建模器的示例是3D Studio Max(http://usa.autodesk.com/3ds-max/),LightWave 3D(http://www.newtek.com/lightwave/),Maya(http: Softimage | XSI(http://www.softimage.com)和Blender(http://www.blender.org/)。 (对于爱好者来说,Blender的优势在于开源和免费)。然而,对于本书的第一部分,我们将手工生成我们的3D模型,或者通过一个数学公式(例如圆柱体和球体的三角形列表),可以很容易地用参数公式生成)。在本书的第三部分,我们展示了如何加载和显示从3D建模程序导出的3D模型。

5-7
图5.7 (左)用三角网格近似的一辆汽车。 (右)由三角网格近似的头骨

5.3基本计算机颜色

计算机显示器通过每个像素发出红,绿和蓝光的混合物。当光混合物进入眼睛并撞击到视网膜的一个区域时,锥形受体细胞受到刺激,并且神经冲动沿着视神经向脑部发送。大脑解释信号并产生一种颜色。随着光混合物的变化,细胞被不同地刺激,从而在头脑中产生不同的颜色。图5.8显示了混合红,绿和蓝以获得不同颜色的一些示例; 它也表现出不同的红色强度。通过对每个颜色分量使用不同的强度并将它们混合在一起,我们可以描述显示逼真图像所需的所有颜色。

通过RGB(红,绿,蓝)值来描述颜色的最佳方式是使用Adobe Photoshop等绘图程序,甚至是Win32的ChooseColor对话框(图5.9),并尝试使用不同的RGB组合来查看 他们生产的颜色。

显示器具有可发出的最大强度的红,绿和蓝光。为了描述光的强度,使用从0到1的归一化范围。零表示没有强度,1表示全部强度。中间值表示中等强度。例如,值(0.25,0.67,1.0)表示光混合物由25%的红光强度,67%的绿光强度和100%的蓝光强度组成。正如前面提到的例子所暗示的那样,我们可以用一个三维颜色向量(r,g,b)表示一个颜色,其中0≤r,g,b≤1,每个颜色分量描述红,绿和蓝在混合中的强度。

5-8
图5.8 (上)纯红色,绿色和蓝色的混合,以获得新的颜色。 (下)通过控制红光的强度发现不同的红色亮度。

5-9
图5.9 ChooseColor对话框。

5.3.1颜色操作

一些矢量操作也适用于颜色矢量。 例如,我们可以添加颜色矢量来获得新的颜色:

(0.0, 0.5, 0) + (0, 0.0, 0.25) = (0.0, 0.5, 0.25)

通过将中等强度的绿色与低强度的蓝色相结合,我们得到深绿色。

颜色也可以减去获得新的颜色:

(1, 1, 1) – (1, 1, 0) = (0, 0, 1)

也就是说,我们从白色开始,减去红色和绿色部分,最后以蓝色结束。

标量乘法也是有意义的。 考虑以下:

0.5 (1, 1, 1) = (0.5, 0.5, 0.5)

也就是说,我们从白色开始,乘以0.5,最后得到中等灰度。同理,操作2(0.25,0,0)=(0.5,0,0)使红色分量的强度加倍。

很明显,点积和叉积等表达式对于颜色向量没有意义。然而,颜色矢量确实得到了他们自己的称为调制或分量乘法的特殊颜色操作。它被定义为:

(c r, c g, c b) ⊗ (k r, k g, k b) = (c rk r, c gk g, c bk b)

此操作主要用于照明方程。例如,假设我们有一束入射的光线(r,g,b),它会照射一个反射50%红光,75%绿光和25%蓝光的表面,并吸收剩余的光线。那么反射光线的颜色由下式给出:

(r, g, b) ⊗ (0.5, 0.75, 0.25) = (0.5r, 0.75g, 0.25b)

所以我们可以看到,当光线撞击表面时,光线会失去一些强度,因为表面会吸收一些光线。

在进行颜色操作时,颜色成分可能会超出[0,1]的间隔; 考虑例如(1,0.1,0.6)+(0,0.3,0.5)=(1,0.4,1.1)的等式。因为1.0表示颜色分量的最大强度,所以不能比它更强烈。因此,1.1和1.0一样强烈。所以我们所做的就是钳制1.1→1.0。同样,监视器不能发出负光,因此任何负色分量(可能由减法操作产生)应该被钳位到0.0。

5.3.2 128位颜色

合并一个额外的颜色组件,通常被称为alpha组件。alpha分量通常用来表示颜色的不透明度,这在混合中很有用(第9章混合)。(因为我们还没有使用混合,现在只需要将alpha分量设置为1)。

包括alpha分量意味着我们可以用一个4D颜色向量(r,g,b,a)表示一个颜色,其中0≤r,g,b,a≤1。为了用128位表示颜色,每个分量使用浮点型。因为在数学上,颜色只是一个四维矢量,所以我们可以使用XMVECTOR类型来代表代码中的一个颜色,每当我们使用XNA Math矢量函数来进行颜色操作时(例如,颜色加法,减法 ,标量乘法)。对于分量乘法,XNA Math库提供了以下功能:

XMVECTOR XMColorModulate(// Returns (cr, cg, cb, ca) ⊗ (kr, kg, kb, ka)
FXMVECTOR C1, // (cr, cg, cb, ca)
FXMVECTOR C2); // (kr, kg, kb, ka)

5.3.3 32位颜色

为了用32位来表示一个颜色,每个组件都有一个字节。由于每种颜色都是8位字节,因此我们可以为每个颜色分量表示256种不同的色调,0表示无强度,255表示全强度,中间值表示中等强度。每个颜色分量的字节可能看起来很小,但是当我们查看所有的组合(256×256×256 = 16,777,216)时,我们可以看到数以百万计的不同颜色。XNA Math库提供了以下用于存储32位颜色的结构:

// ARGB Color; 8-8-8-8 bit unsigned normalized integer components
// packed into a 32 bit integer. The normalized color is packed into
// 32 bits using 8 bit unsigned, normalized integers for the alpha,
// red, green, and blue components.
// The alpha component is stored in the most significant bits and the
// blue component in the least significant bits (A8R8G8B8):
// [32] aaaaaaaa rrrrrrrr gggggggg bbbbbbbb [0]
typedef struct _XMCOLOR
{
    union
    {
        struct
        {
            UINT b : 8; // Blue: 0/255 to 255/255
            UINT g : 8; // Green: 0/255 to 255/255
            UINT r : 8; // Red: 0/255 to 255/255
            UINT a : 8; // Alpha: 0/255 to 255/255
        };
        UINT c;
    };
#ifdef __cplusplus
    _XMCOLOR() {};
    _XMCOLOR(UINT Color) : c(Color) {};
    _XMCOLOR(FLOAT _r, FLOAT _g, FLOAT _b, FLOAT _a);
    _XMCOLOR(CONST FLOAT *pArray);
        operator UINT () { return c; }
    _XMCOLOR& operator= (CONST _XMCOLOR& Color);
    _XMCOLOR& operator= (CONST UINT Color);
#endif // __cplusplus
} XMCOLOR;

通过将整数范围[0,255]映射到实值区间[0,1]上,可以将32位颜色转换为128位颜色。 这是通过除以255来完成的。也就是说,如果0≤n≤255是整数,则0≤(n/255)≤1给出从0到1的归一化范围中的强度。例如,32位颜色(80,140,200, 255)变成

80,140,200255(80255,140255,200255,255255)(0.31,0.55,0.78,1.0) ( 80 , 140 , 200 , 255 ) → ( 80 255 , 140 255 , 200 255 , 255 255 ) ≈ ( 0.31 , 0.55 , 0.78 , 1.0 )

另一方面,通过将每个分量乘以255并将其舍入到最接近的整数,可以将128位颜色转换为32位颜色。 例如:

(0.3, 0.6, 0.9, 1.0) → (0.3 · 255, 0.6 · 255, 0.9 · 255, 1.0 · 255) = (77, 153, 230, 255)

在将32位颜色转换为128位颜色时通常必须执行额外的位操作,反之,因为8位颜色分量通常被打包成32位整数值(例如,unsigned int),因为它是 在XMCOLOR中。 XNA Math库定义了以下函数,它使用XMCOLOR并从中返回一个XMVECTOR

5-10
图5.10 32位颜色,其中为每个颜色分量(alpha,红,绿和蓝)分配一个字节。

XMVECTOR XMLoadColor(CONST XMCOLOR* pSource);

图5.10显示了8位颜色分量如何打包到UINT中。 请注意,这只是打包颜色组件的一种方法。 另一种格式可能是ABGR或RGBA,而不是ARGB; 但是,XMCOLOR类使用ARGB布局。XNA Math库还提供了将XMVECTOR颜色转换为XMCOLOR的功能:

VOID XMStoreColor(XMCOLOR* pDestination, FXMVECTOR V);

通常,在许多颜色处理的地方(例如,在像素着色器中)使用128位颜色值。这样,我们有很多的计算精度,所以算术错误不会累积太多。然而,最终的像素颜色通常存储在后台缓冲区中的32位颜色值中; 目前的物理显示设备不能利用更高分辨率的颜色[Verth04]。

5.4 OVERVIEW OF THE RENDERING PIPELINE

给定具有定位和定向的虚拟相机的3D场景的几何描述,渲染流水线是指基于虚拟相机所看到的生成2D图像所必需的整个步骤序列。图5.11展示了组成渲染流水线的阶段以及GPU内存资源。从资源内存池到阶段的箭头意味着阶段可以访问资源作为输入;例如,像素着色器阶段可能需要从存储在存储器中的纹理资源中读取数据以便完成其工作。从一个阶段到内存的箭头意味着舞台写入GPU资源;例如,输出合并阶段将数据写入诸如后台缓冲区和深度/模板缓冲区的纹理。观察输出合并阶段的箭头是双向的(它读取和写入GPU资源)。正如我们所看到的,大多数阶段不写入GPU资源。相反,他们的产出只是输入到管道的下一个阶段;例如,顶点着色器阶段从输入组装器阶段输入数据,完成自己的工作,然后将其结果输出到几何着色器阶段。接下来的部分将概述渲染管道的每个阶段。

5-11
图5.11 渲染管线的阶段

5.5 输入汇集器的阶段

input assembler(IA)阶段从存储器读取几何数据(顶点和索引),并使用它来组装几何图元(例如,三角形,线)。 (在后面的小节中将会介绍索引,但是简单地说,它们定义了顶点应该如何组合起来形成原语。)

5.5.1顶点

在数学上,三角形的顶点是两条边相交的地方; 线的顶点是端点; 对于单点而言,点本身就是顶点。 图5.12图解地说明了顶点。

从图5.12看来,顶点似乎只是几何图元中的一个特殊点。 但是,在Direct3D中,顶点比这个更普遍。本质上,Direct3D中的一个顶点可以包含除空间位置之外的其他数据,这使得我们可以执行更复杂的渲染效果。例如,在第7章中,我们将法向量添加到顶点来实现光照,在第8章中,我们将纹理坐标添加到顶点以实现纹理化。Direct3D使我们能够灵活地定义我们自己的顶点格式(也就是说,它允许我们定义顶点的组成部分),我们将在下一章看到用来做这个的代码。在本书中,我们将根据我们正在做的渲染效果来定义几种不同的顶点格式。

5.5.2原始拓扑

顶点被绑定到渲染管线中的一个称为顶点缓冲区的特殊Direct3D数据结构中。 顶点缓冲区只存储连续内存中的顶点列表。 但是,并没有说明这些顶点应该如何组合起来形成几何图元。例如,顶点缓冲区中的每两个顶点应该被解释为一个线,还是顶点缓冲区中的每三个顶点应该被解释为一个三角形? 我们通过指定原始拓扑来告诉Direct3D如何从顶点数据形成几何图元:

5-12
图5.12 由三个顶点v0,v1,v2定义的三角形; 由两个顶点p0,p1定义的一条直线; 由顶点Q定义的点

void ID3D11DeviceContext::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;

所有后续的绘图调用将使用当前设置的原始拓扑,直到拓扑发生变化。 以下代码说明:

md3dImmediateContext->IASetPrimitiveTopology(
    D3D11_PRIMITIVE_TOPOLOGY_LINELIST);
/* ...draw objects using line list... */

md3dImmediateContext->IASetPrimitiveTopology(
    D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
/* ...draw objects using triangle list... */

md3dImmediateContext->IASetPrimitiveTopology(
    D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
/* ...draw objects using triangle strip... */

以下小节详细介绍了不同的原始拓扑结构。 在本书中,除了少数例外,我们主要使用三角列表。

5.5.2.1点列表

点列表由D3D11_PRIMITIVE_TOPOLOGY_POINTLIST指定。 使用点列表,绘图调用中的每个顶点都被绘制为一个单独的点,如图5.13a所示。

5.5.2.2线条

线条由D3D11_PRIMITIVE_TOPOLOGY_LINESTRIP指定。 在线条中,绘图调用中的顶点连接成线(见图5.13b); 所以n + 1个顶点引起n行。

5.5.2.3线路清单

线列表由D3D11_PRIMITIVE_TOPOLOGY_LINELIST指定。 在线列表中,绘图调用中的每两个顶点形成一条单独的线(参见图5.13c); 所以2n个顶点诱导n行。 线条列表与条纹之间的区别在于线条列表中的线条可能会断开,而线条会自动假定它们已连接。 通过假定连通性,可以使用较少的顶点,因为每个内部顶点由两条线共享。

5-13
图5.13 (a)点列表; (b)线条; (c)行列表; (d)三角形条

5.5.2.4三角形条

三角形条由D3D11_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP指定。 对于三角形条,假定三角形连接如图5.13d所示,以形成条。 通过假设连通性,我们看到顶点在相邻三角形之间共享,并且n个顶点引起n - 2个三角形。

NOTE:观察到三角形带中的三角形甚至三角形的缠绕顺序与奇数三角形的缠绕顺序不同,从而导致剔除问题(见§5.10.2)。 为了解决这个问题,GPU内部交换了偶数三角形的前两个顶点的顺序,以便像奇数三角形一样排列。

5.5.2.5三角列表

三角形列表由D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST指定。 用三角形列表,绘图调用中的每三个顶点形成一个单独的三角形(见图5.14a); 所以3n个顶点诱导n个三角形。 三角形列表和条带之间的区别在于三角形列表中的三角形可能被断开,而三角形条带假定它们已经连接。

5-14
图5.14 (a)三角形列表 (b)一个带有相邻三角形的列表 - 观察每个三角形需要6个顶点来描述它和它的相邻三角形。这样6n个顶点引起n个具有相邻信息的三角形

5.5.2.6 具有邻接的基元

一个具有邻接的三角形列表是每个三角形包括三个称为邻接三角形相邻的三角形; 参见图5.14b,观察这些三角形是如何定义的。这用于几何着色器,其中某些几何着色算法需要访问相邻的三角形。为了让几何着色器得到那些相邻的三角形,需要将相邻的三角形连同三角形本身一起提交到顶点/索引缓冲区中的管线,并且必须指定D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ拓扑,以便管道知道如何构造三角形以及从顶点缓冲区的获取相邻三角形。请注意,相邻图元的顶点仅用作几何着色器的输入 - 它们不会被绘制。如果没有几何着色器,则相邻的基元仍然没有绘制。

也可能有一个带有邻接的线列表,带有邻接的线带,带有带状邻接原语的三角形, 有关详细信息,请参阅SDK文档。

5.5.2.7控制点补丁列表

D3D11_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST拓扑类型指示顶点数据应该被解释为具有N个控制点的补丁列表。这些用在渲染管道的(可选)镶嵌阶段,因此,我们将推迟讨论,直到第13章“镶嵌阶段”。

5.5.3指数

如前所述,三角形是实体三维物体的基本构建块。 以下代码显示了使用三角形列表构造四边形和八边形的顶点数组(即,每三个顶点形成一个三角形)。

Vertex quad[6] = {
    v0, v1, v2, // Triangle 0
    v0, v2, v3, // Triangle 1
};
Vertex octagon[24] = {
    v0, v1, v2, // Triangle 0
    v0, v2, v3, // Triangle 1
    v0, v3, v4, // Triangle 2
    v0, v4, v5, // Triangle 3
    v0, v5, v6, // Triangle 4
    v0, v6, v7, // Triangle 5
    v0, v7, v8, // Triangle 6
    v0, v8, v1  // Triangle 7
};

NOTE:您指定三角形顶点的顺序很重要,称为缠绕顺序; 详情请参阅§5.10.2。

5-15
图5.15 (a)由两个三角形构成的四边形 (b)由八个三角形构成的八角形

如图5.15所示,形成3D对象的三角形共享许多相同的顶点。 更具体地说,图5.15a中四边形的每个三角形共享顶点v0和v2。 虽然复制两个顶点并不算太差,但是在八边形的例子中(图5.15b),复制更糟,因为每个三角形都复制了中心顶点v0,并且八边形的周边上的每个顶点都被两个三角形共享。 通常,随着模型的细节和复杂度的增加,重复顶点的数量会增加。

有两个原因我们不想复制顶点:
1.增加了内存需求。 (为什么不一次存储相同的顶点数据?)
2.增加了图形硬件的处理。 (为什么不一次地处理相同的顶点数据?)

只要几何形状可以按条状方式组织,三角形条就可以在某些情况下帮助重复顶点问题。 但是,三角形列表更加灵活(三角形不需要连接),因此值得设计一种删除三角形列表的重复顶点的方法。 解决方案是使用索引。 它是这样工作的:我们创建一个顶点列表和一个索引列表。 顶点列表由所有的唯一顶点组成,索引列表包含索引到顶点列表中的值,以定义如何将顶点放在一起形成三角形。 回到图5.16中的形状,四边形的顶点列表将被构造如下:

Vertex v[4] = {v0, v1, v2, v3};

然后,索引列表需要定义顶点列表中的顶点如何放在一起形成两个三角形。

UINT indexList[6] = {0, 1, 2, // Triangle 0
0, 2, 3}; // Triangle 1

在索引列表中,每三个元素定义一个三角形。 因此,前面的索引列表说:“通过使用顶点v [0]v [1]v [2]来形成三角形0,并且通过使用顶点v [0]v [2]v[3]“。

同样,圆的顶点列表将被构造如下:

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  // Triangle 7
};

在处理顶点列表中的唯一顶点后,图形卡可以使用索引列表将顶点放在一起形成三角形。 注意到我们把“重复”移到了索引列表中,但是这并不是因为以下原因而导致的:
1.索引只是整数,并不像完整的顶点结构那样占用太多的内存(当我们增加更多的组件时顶点结构会变大)。
2.良好的顶点高速缓存排序,图形硬件不必处理重复的顶点(太频繁)。

5.6顶点着色器阶段

在基元被组装之后,顶点被馈送到顶点着色器阶段。 顶点着色器可被认为是输入顶点并输出顶点的函数。每个绘制的顶点将通过顶点着色器被抽取; 其实可以大概想象硬件上下面代码的运行情况:

for(UINT i = 0; i < numVertices; ++i)
    outputVertex[i] = VertexShader (inputVertex[i]);

顶点着色器函数是我们实现的东西,但它是由GPU为每个顶点执行的,因此速度非常快。

在顶点着色器中可以完成许多特殊效果,如变换,光照和位移贴图。 请记住,我们不仅可以访问输入顶点数据,还可以访问存储在GPU内存中的纹理和其他数据,如变换矩阵和场景灯。

我们将看到很多不同顶点着色器的例子。 所以最后你应该清楚自己可以做些什么。 但是,对于我们的第一个代码示例,我们将只使用顶点着色器来转换顶点。以下小节解释了通常需要完成的转换类型。

5.6.1当前空间和世界空间

假设制作一部电影,而你的团队必须为一些特效镜头构建一个火车场景的微型版本。 假设你负责建造一座小桥。为了不会搞乱场景中其他东西,你不会选择在场景中建造这座桥,因为那样工作起来很困难。相反,你会在远离现场的工作台上工作。 然后,当这一切都完成后,你将桥梁摆放在场景中正确的位置和角度。

3D艺术家在构建3D对象时做了类似的事情。在构建3D对象时不是建立一个相对于3D艺术家的对象几何坐标做类似的事情。它们不是用相对于全局场景坐标系统(世界空间)的坐标建立一个对象的几何体,而是相对于局部坐标系统(局部空间)来指定它们。局部坐标系通常是位于物体附近且与物体轴对齐的一些便利的坐标系。一旦3D模型的顶点已经在局部空间中定义,它将被放置在全局场景中。为了做到这一点,我们必须确定当地空间和世界空间是如何相关的;这是通过指定我们想要本地空间坐标系的原点和轴相对于全局场景坐标系的位置,并执行坐标变换(见图5.16并回想§3.4)来完成的。将坐标系相对于局部坐标系转换为全局坐标系的过程称为世界变换,相应的矩阵称为世界矩阵。场景中的每个对象都有自己的世界矩阵。在每个对象从其本地空间转换到世界空间之后,所有对象的所有坐标都与相同的坐标系(世界空间)相关。如果你想直接在世界空间定义一个对象,那么你可以提供一个世界坐标的身份。

相对于自己的本地坐标系定义每个模型具有几个优点:
1.更容易。 例如,通常在局部空间中,物体将以原点为中心,相对于其中一个主轴是对称的。 作为另一个例子,如果我们选择以原点为中心在立方体并且轴与立方体面正交的局部坐标系,则立方体的顶点更容易指定; 见图5.17。

5-16
图5.16 (a)每个对象的顶点都用相对于它们自己的本地坐标系统的坐标来定义。 另外,根据我们想要物体在场景中的位置来定义每个局部坐标系相对于世界空间坐标系的位置和方向。 然后我们执行一个坐标转换的变化,使所有相对于世界空间系统的坐标。 (b)在世界变换之后,物体的顶点的坐标都与相同的世界有关。

2.对象可以在多个场景中重用,在这种情况下,相对于特定的场景对对象的坐标进行硬编码是没有意义的。相反,最好将其坐标相对于局部坐标系统进行存储,然后通过坐标矩阵的变化来定义局部坐标系和世界坐标系对每个场景的相关性。
3.最后,有时我们不止一次地在一个场景中绘制同一个对象,但是在不同的位置,方向和尺度上(例如,一个树木对象可能被重复使用几次来建立一个森林)。为每个实例复制对象的顶点和索引数据是浪费的。相反,我们存储几何体(即,顶点和索引列表)相对于其本地空间的单个副本。然后我们多次绘制对象,但每次都用不同的世界矩阵来指定实例在世界空间中的位置,方向和规模。这就是所谓的实例。
5-17
图5.17 当立方体以原点为中心并与坐标系轴对齐时,立方体的顶点很容易指定。 当立方体处于相对于坐标系统的任意位置和方向时指定坐标并不那么容易。 因此,当我们构造一个物体的几何形状时,我们通常总是在物体附近选择一个方便的坐标系,并与物体对齐,从而构建物体。

如§3.4.3所示,一个对象的世界矩阵是通过用相对于世界空间的坐标来描述其局部空间并将这些坐标放置在矩阵的行中来给出的。 如果Q w =(Q x,Q y,Q z,1),则u w =(u x,u y,u z,0),v w =(v x,v y,v z,0),w w =(w x,w y,w z,0) ,分别为具有相对于世界空间的均匀坐标的局部空间的原点,x,y和z轴,则从§3.4.3知道坐标矩阵从局部空间到世界空间的变化是:
W=uxvxwxQxuyvywyQyuzvzwzQz0001 W = [ u x u y u z 0 v x v y v z 0 w x w y w z 0 Q x Q y Q z 1 ]

我们看到要构建一个世界矩阵,我们必须直接找出局部空间原点和坐标轴相对于世界空间的坐标。这有时并不那么容易或直观。更常见的方法是将W定义为一系列的变换,例如W = SRT,缩放矩阵S将物体缩放到世界中,接着是旋转矩阵R,以定义相对于世界空间的转动,然后是平移矩阵T来定义相对于世界空间的本地空间的位移。从§3.5我们知道这个变换序列可以被解释为坐标变换,并且W = SRT的行向量存储了相对于世界空间坐标的x轴,y轴,z轴和原点的相对坐标的齐次坐标。

Example
假设我们有一个相对于一些局部空间的最小和最大点(-0.5,0,-0.5)和(0.5,0,0.5)定义的单位正方形。找到世界矩阵,使得世界空间中的正方形长度为2,正方形在世界空间的xz平面中顺时针旋转45°,并且正方形位于世界空间(10,0,10)处。 我们构造S,R,T和W如下:

S=2000010000200001R=2/202/2001002/202/200001T=100100100001100001 S = [ 2 0 0 0 0 1 0 0 0 0 2 0 0 0 0 1 ] R = [ 2 / 2 0 − 2 / 2 0 0 1 0 0 2 / 2 0 2 / 2 0 0 0 0 1 ] T = [ 1 0 0 0 0 1 0 0 0 0 1 0 10 0 10 1 ]

W=SRT=202100100202100001 W = S R T = [ 2 0 2 0 0 1 0 0 2 0 2 0 10 0 10 1 ]

§3.5W;uw=(2,0,2,0),vw=(0,1,0,0),ww=(2,0,2,0)Qw=10,0,10,1W5.18 现 在 从 § 3.5 中 , W 中 的 行 描 述 了 相 对 于 世 界 空 间 的 局 部 坐 标 系 ; 即 u w = ( 2 , 0 , − 2 , 0 ) , v w = ( 0 , 1 , 0 , 0 ) , w w = ( 2 , 0 , 2 , 0 ) 和 Q w = ( 10 , 0 , 10 , 1 ) 。 当 我 们 用 W 来 改 变 从 本 地 空 间 到 世 界 空 间 的 坐 标 时 , 这 个 正 方 形 就 在 世 界 空 间 的 理 想 位 置 结 束 ( 见 图 5.18 ) 。

[0.5,0,0.5,1]W=[102,0,0,1][0.5,0,+0.5,1]W=[0,0,10+2,1][+0.5,0,+0.5,1]W=[10+2,0,0,1][+0.5,0,0.5,1]W=[0,0,102,1] [ − 0.5 , 0 , − 0.5 , 1 ] W = [ 10 − 2 , 0 , 0 , 1 ] [ − 0.5 , 0 , + 0.5 , 1 ] W = [ 0 , 0 , 10 + 2 , 1 ] [ + 0.5 , 0 , + 0.5 , 1 ] W = [ 10 + 2 , 0 , 0 , 1 ] [ + 0.5 , 0 , − 0.5 , 1 ] W = [ 0 , 0 , 10 − 2 , 1 ]

5-18
图5.18 世界矩阵的行矢量用相对于世界坐标系的坐标来描述局部坐标系

这个例子的要点是,我们不是直接计算Q w,u w,v w和w w来形成世界矩阵,而是通过合成一系列简单的变换来构造世界矩阵。 这通常比直接计算Q w,u w,v w和w w要容易得多,因为我们只需要问:世界空间中我们想要的对象大小,我们希望在世界空间中的对象在什么方向,以及我们希望在世界空间中的对象的位置。

另一种考虑世界变换的方法是仅取局部空间坐标,并将它们视为世界空间坐标(这相当于使用单位矩阵作为世界变换)。因此,如果对象被建模在其本地空间的中心,那么该对象就在世界空间的中心。一般来说,世界的中心可能不是我们想要定位我们所有对象的地方。所以,对于每个对象,只需应用一系列变换即可在世界空间中缩放,旋转和定位所需的对象。在数学上,这与从局部空间到世界空间的坐标矩阵的变化效果相同。

5.6.2查看空间

为了形成场景的二维图像,我们必须在场景中放置一个虚拟相机。 相机指定了观看者可以看到的世界空间的范围及深度。让我们将一个局部坐标系统(称为视野空间,眼睛空间或相机空间)附加到相机上,如图5.19所示; 也就是说,摄像机坐落在正视z轴的原点处,x轴指向摄像机的右侧,y轴指向摄像机的上方。而不是在世界空间范围内描述我们的场景,这方便渲染管道后期阶段对于相机坐标来描述他们,从世界空间到视图空间的变化称为视图变换,相应的矩阵被称为视图矩阵。

5-19
图5.19 从世界空间的坐标到相机空间坐标的转换

Qw=QxQyQz1uw=uxuyuz0vw=vxvyvz0ww=wxwywz0xyz§3.4.3 如 果 Q w = ( Q x , Q y , Q z , 1 ) , 则 u w = ( u x , u y , u z , 0 ) , v w = ( v x , v y , v z , 0 ) , w w = ( w x , w y , w z , 0 ) 分 别 表 示 具 有 相 对 于 世 界 空 间 的 均 匀 坐 标 的 视 图 空 间 的 原 点 , x , y 和 z 轴 , 那 么 从 § 3.4.3 知 道 坐 标 矩 阵 从 视 图 空 间 到 世 界 空 间 的 变 化 是 :

W=uxvxwxQxuyvywyQyuzvzwzQz0001 W = [ u x u y u z 0 v x v y v z 0 w x w y w z 0 Q x Q y Q z 1 ]

但是,这不是我们想要的转变。我们需要从世界空间到观看空间的逆变换。但从§3.4.5中回想一下逆变换就是变换的逆处理。所以。W -1从世界空间转换到观看空间。

世界坐标系统和视图坐标系统一般只有位置和方向的不同,所以直观地说,W = RT(即世界矩阵可以分解为旋转,然后是平移)。这种形式使得反演更容易计算:

V=W1=(RT)1=T1R1=T1RT=100Qx010Qy001Qz0001uxuyuz0vxvyvz0wxwywz00001=uxuyuzQuvxvyvzQvwxwywzQw0001 V = W – 1 = ( R T ) – 1 = T – 1 R – 1 = T – 1 R T = [ 1 0 0 0 0 1 0 0 0 0 1 0 − Q x − Q y − Q z 1 ] [ u x v x w x 0 u y v y w y 0 u z v z w z 0 0 0 0 1 ] = [ u x v x w x 0 u y v y w y 0 u z v z w z 0 − Q · u − Q · v − Q · w 1 ]

所以视图矩阵的形式是:
V=uxuyuzQuvxvyvzQvwxwywzQw0001 V = [ u x v x w x 0 u y v y w y 0 u z v z w z 0 − Q · u − Q · v − Q · w 1 ]

我们现在展示一个直观的方法来构建视图矩阵所需的向量。设Q为摄像机的位置,并让T为摄像机瞄准的目标点。此外,令j是描述世界空间的“向上”方向的单位向量。(在此,我们使用世界xz平面作为我们的世界“地面”,世界y轴描述了“向上”的方向;因此,j =(0,1,0),但这只是一个惯例,有些应用可能会选择xy面作为地平面,z轴作为“up”方向)。参考图5.20,摄像机正在寻找的方向 是(谁)给的:
W=TQ||TQ|| W = T − Q | | T − Q | |

5-20
图5.20 根据相机位置,目标点和世界“向上”向量构建相机坐标系

这个向量描述摄像机的局部z轴。 针对w的“右”的单位向量由下式给出:
u=j×w||j×w|| u = j × w | | j × w | |

这个矢量描述摄像机的局部x轴。最后,描述相机局部y轴的矢量由下式给出:
v=w×u v = w × u

由于w和u是正交单位矢量,因此w×u必然是一个单位矢量,所以不需要归一化。

因此,给定相机的位置,目标点以及世界“向上”的方向,我们可以导出摄像机的局部坐标系,这可以用来形成视图矩阵。

XNA Math库提供了以下功能来基于刚刚描述的过程来计算视图矩阵:

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);

XMMATRIX V = XMMatrixLookAtLH(pos, target, up);

5.6.3 视图空间的透视与平行投影

到目前为止,我们已经描述了相机在世界上的位置和方向,但是相机的另一个组成部分是相机看到的空间的体积。这个量是用平截头体来描述的(图5.21)。

我们的下一个任务是将截锥体内的三维几何体投影到二维投影窗口上。投影必须以平行线收敛到消失点的方式完成,随着物体的3D深度增加,投影的大小减小;透视投影可以做到这一点,如图5.22所示。我们把从顶点到眼睛的线称为顶点的投影线。然后我们将透视投影变换定义为将三维顶点v变换到投影线与二维投影平面相交点v’的变换; 我们说v’是v的投影。3D对象的投影是指组成对象的所有顶点的投影。

5-21
图5.21 平截头体定义相机“看到”的空间体积

5-22
图5.22 3D空间中的两个圆柱体大小相同,但放置在不同的深度。 靠近眼睛的圆柱体的投影大于较远的圆柱体的投影。 将截锥内的几何投影到投影窗口上, 平截头体外的几何体被投影到投影平面上,但位于投影窗口之外

5.6.3.1定义一个平截头体

我们可以在视图空间中定义一个视锥体,投影的中心位于原点处,向下为z轴正方向,由以下四个量组成:近平面n,远平面f,垂直视场角α和宽高比r,在视图空间中,近平面和远平面平行于xy平面;因此我们简单地指定它们沿着z轴的原点的距离。宽高比由r = w / h定义,其中w是投影窗口的宽度,h是投影窗口的高度(视图空间中的单位)。投影窗口实质上是视图空间中的场景的2D图像。这里的图像最终将被映射到后台缓冲区;因此,我们喜欢投影窗口尺寸的比例与后缓冲区尺寸的比例相同。所以后台缓冲区的比例通常被指定为长宽比(这是一个没有单位的比值)。例如,如果后台缓冲区尺寸是800×600,那么我们指定r=800/600≈1.3333。如果投影窗口与后台缓冲区的宽高比不相同,则需要非均匀缩放来将投影窗口映射到后台缓冲区,这将导致失真(例如,投影窗口上的圆圈可能被拉伸,当映射到后台缓冲区时变成一个椭圆)。

5-23
图5.23 在给定垂直视场角α和纵横比r的情况下导出水平视场角β。

r=wh=w2w=2r r = w h = w 2 ⇒ w = 2 r

为了具有指定的垂直视场a,投影窗口必须放置在距离原点的距离d处:
tanα2=1dd=cotα2 t a n ⟮ α 2 ⟯ = 1 d ⇒ d = c o t ⟮ α 2 ⟯

现在我们已经确定投影窗口沿z轴的距离d,当投影窗口的高度为2时,垂直视场为a。现在我们可以求解β。看一下图5.23中的xz平面,我们现在看到:
tanβ2=rd=rcotα2=rtanα2 t a n ⟮ β 2 ⟯ = r d = r c o t α 2 = r · t a n α 2

因此,考虑到垂直视场角α和纵横比r,我们总能得到水平视场角β:
β=2tan1rtanα2 β = 2 t a n − 1 ⟮ r · t a n α 2 ⟯

5.6.3.2投影顶点

参考图5.24。 给定一个点(x,y,z),我们希望在投影平面z = d上找到它的投影(x’,y’,d)。通过分别考虑x坐标和y坐标并使用相似的三角形,我们得出:

xd=xzx=xdz=xcot(α/2)z=xztan(α/2) x ′ d = x z ⇒ x ′ = x d z = x c o t ( α / 2 ) z = x z t a n ( α / 2 )

yd=yzy=ydz=ycot(α/2)z=yztan(α/2) y ′ d = y z ⇒ y ′ = y d z = y c o t ( α / 2 ) z = y z t a n ( α / 2 )

5-24
图5.24 相似三角形。

注意到一个点当且仅当(x,y,z)在平截头内时有:
rxr1y1nzf − r ≤ x ′ ≤ r − 1 ≤ y ′ ≤ 1 n ≤ z ≤ f

5.6.3.3标准化设备坐标(NDC)

上一节中投影点的坐标是在视图空间中计算的。 在视图空间中,投影窗口的高度为2,宽度为2r,其中r是宽高比。问题在于尺寸取决于宽高比。这意味着我们需要告诉硬件高宽比,因为硬件以后需要做一些涉及投影窗口尺寸的操作(比如映射它到后台缓冲区)。如果我们能够消除这种对宽高比的依赖,那将会更加方便。解决的办法是将投影的x坐标从区间[-r,r]缩放到[-1,1],如下所示:

rxr1x/r1 − r ≤ x ′ ≤ r − 1 ≤ x ′ / r ≤ 1

在这个映射之后,x坐标和y坐标被称为标准化的设备坐标(NDC)(z坐标尚未被标准化),并且点(x,y,z)在平截头体内,如果
1x/r11y1nzf − 1 ≤ x ′ / r ≤ 1 − 1 ≤ y ′ ≤ 1 n ≤ z ≤ f

从视图空间到NDC空间的转换可视为单位转换。 我们有一个关系,即一个NDC单位在x轴上等于视图空间中的r个单位(即1 ndc = r vs)。 所以给定x个视图空间单位,我们可以使用这个关系来转换单位:
xvs1ndcrvs=xrndc x v s · 1 n d c r v s = x r n d c

我们可以修改我们的投影公式,直接在NDC坐标中给我们投影的x坐标和y坐标:
x=xrztan(α/2)y=yztan(α/2)(eq.5.1) (eq.5.1) x ′ = x r z t a n ( α / 2 ) y ′ = y z t a n ( α / 2 )

请注意,在NDC坐标中,投影窗口的高度为2,宽度为2.因此,现在尺寸是固定的,硬件无需知道纵横比,但我们有责任始终在NDC中提供投影坐标 空间(图形硬件假设我们会)。

5.6.3.4用矩阵写投影方程

为了均匀性,我们想用矩阵表示投影变换。 然而,方程5.1是非线性的,所以它没有矩阵表示。 “诀窍”是把它分成两部分:线性部分和非线性部分。 非线性部分是除以z。 正如下一节将要讨论的那样,我们要规范z坐标。 这意味着我们将不会有分割的原始z坐标。 因此,我们必须在转换之前保存输入的z坐标。 要做到这一点,我们利用齐次坐标,并将输入的z坐标复制到输出的w坐标。 就矩阵乘法而言,这通过设置条目[2] [3] = 1和条目[3] [3] = 0(基于零的索引)来完成。 我们的投影矩阵看起来像这样:

P=1rtan(α/2)00001tan(α/2)0000AB0010 P = [ 1 r t a n ( α / 2 ) 0 0 0 0 1 t a n ( α / 2 ) 0 0 0 0 A 1 0 0 B 0 ]

请注意,我们已将常数(将在下一节中确定)A和B放入矩阵; 这些常量将用于将输入的z坐标转换为标准化的范围乘以该矩阵的任意点(x,y,z,1)给出:
[x,y,z,1]1rtan(α/2)00001tan(α/2)0000AB0010=[xrtan(α/2),ytan(α/2),Az+B,z](eq.5.2) (eq.5.2) [ x , y , z , 1 ] [ 1 r t a n ( α / 2 ) 0 0 0 0 1 t a n ( α / 2 ) 0 0 0 0 A 1 0 0 B 0 ] = [ x r t a n ( α / 2 ) , y t a n ( α / 2 ) , A z + B , z ]

乘以投影矩阵(线性部分)后,我们通过将每个坐标除以w = z(非线性部分)来完成变换:
[xrtan(α/2),ytan(α/2),Az+B,z]dividebyw[xrztan(α/2),yztan(α/2),A+Bz,1](eq.5.3) (eq.5.3) [ x r t a n ( α / 2 ) , y t a n ( α / 2 ) , A z + B , z ] → d i v i d e b y w [ x r z t a n ( α / 2 ) , y z t a n ( α / 2 ) , A + B z , 1 ]

顺便提一句,你可能想知道一个可能的零除数; 然而,近平面应该大于零,所以这样一个点将被裁剪(§5.9)。 w除以有时被称为视角分裂或均匀分裂。 我们看到投影的x坐标和y坐标与方程5.1一致。

5.6.3.5标准化深度值

看起来好像在投影之后,我们可以丢弃原始的三维坐标,因为所有的投影点现在放置在二维投影窗口上,这形成了人眼看到的二维图像。 但是,我们仍然需要深度缓冲算法的3D深度信息。 就像Direct3D需要投影的x坐标和y坐标在标准化范围内,Direct3D需要深度坐标在标准化范围[0,1]。 因此,我们必须构造一个将区间[n,f]映射到[0,1]上的保序函数g(z)。因为函数是保序的,所以如果 z1z2[nf] z 1 , z 2 ∈ [ n , f ] z1<z2 z 1 < z 2 ,那么 gz1<gz2 g ( z 1 ) < g ( z 2 ) ; 所以即使深度值已被转换,相对深度关系仍然保持完整,所以我们仍然可以正确地比较标准化区间中的深度,这是深度缓冲算法所需要的。

将[n,f]映射到[0,1]可以通过缩放和平移来完成。 但是,这种方法不会融入我们目前的投影策略。 我们从方程5.3看到,z坐标经历了转换:

g(z)=A+Bz g ( z ) = A + B z

我们现在需要选择A和B受约束条件:
   条件1:g(n)= A + B / n = 0(近平面被映射为零)
   条件2:g(f)= A + B / f = 1(远平面映射到1)对B求解条件1产生:B = -An。
把这个代入条件2并解答A给出:
A+Anf=1AfAnf=1AfAn=fA=ffn A + − A n f = 1 A f − A n f = 1 A f − A n = f A = f f − n

所以有
g(z)=ffnnf(fn)z g ( z ) = f f − n − n f ( f − n ) z

g(图5.25)的图表显示它严格增加(保序)和非线性。这也表明,大部分的范围是靠近近平面的深度值“用完”的。因此,大多数深度值被映射到范围的一个小子集。这可能导致深度缓冲区精度问题(由于有限的数字表示,计算机不能再区分稍微不同的变换深度值)。通常的建议是使近平面和远平面尽可能接近,以使深度精度问题最小化。

现在我们已经解决了A和B,我们可以陈述完整的透视投影矩阵:

P=1rtan(α/2)00001tan(α/2)0000ffnnffn0010 P = [ 1 r t a n ( α / 2 ) 0 0 0 0 1 t a n ( α / 2 ) 0 0 0 0 f f − n 1 0 0 − n f f − n 0 ]

5-25
图5.25 不同近平面的g(z)图

在乘以投影矩阵之后,在视角分割之前,几何被认为是在均匀的剪辑空间或投影空间中。 在视角划分之后,几何被认为是在归一化的设备坐标(NDC)中。

5.6.3.6 XMMatrixPerspectiveFovLH

透视投影矩阵可以用下面的XNA Math函数来构建:

XMMATRIX XMMatrixPerspectiveFovLH( // 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

以下代码片段说明了如何使用D3DXMatrixPerspectiveFovLH。 在这里,我们指定一个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;
}

5.7镶嵌阶段

镶嵌细分是指细分网格的三角形以添加新的三角形。 这些新的三角形然后可以偏移到新的位置来创建更精细的网格细节(见图5.26)。
镶嵌有许多好处:
1.我们可以实现一个细节层次(LOD)机制,在相机附近的三角形被镶嵌以增加更多的细节,并且远离相机的三角形不被细分。 这样,我们只使用更多的三角形,其中额外的细节将被注意到。
2.我们在内存中保留了一个更简单的低多边形网格(低多边形意味着低三角形数),并且动态添加额外的三角形,从而节省内存。
3.我们在一个更简单的低多边形网格上进行动画和物理等操作,并只使用曲面细分的高分辨率网格进行渲染。

镶嵌阶段是Direct3D 11的新增功能,它们提供了在GPU上细分几何图形的方法。在Direct3D 11之前,如果您想实现曲面细分的形式,必须在CPU上完成,然后新的曲面细分几何将不得不上传回GPU进行渲染。然而,从CPU内存上传新的几何图形到GPU内存速度很慢,同时也会加重CPU的计算量。出于这个原因,在Direct3D 11之前,镶嵌方法在实时图形方面还不是很流行。Direct3D 11提供了一个API,可以在Direct3D 11兼容的视频卡的硬件中完全镶嵌棋盘格。这使镶嵌技术成为一个更有吸引力的技术。曲面细分阶段是可选的(如果您想镶嵌细分,您只需要使用它)。我们推迟到第13章对曲面细分进行讲解。

5-26
图5.26 左图显示了原始网格。 右图显示镶嵌后的网格

5.8几何渲染阶段

几何着色器的阶段是可选的,直到第11章我们才使用它,所以我们将在这里简要介绍。 几何着色器输入整个图元。 例如,如果我们绘制三角形列表,那么几何着色器的输入将是定义三角形的三个顶点。 (请注意,三个顶点已经穿过了顶点着色器。)几何着色器的主要优点是可以创建或销毁几何。 例如,输入基元可以扩展为一个或多个其他基元,或者几何着色器可以根据某些条件选择不输出基元。 这与不能创建顶点的顶点着色器相反:它输入一个顶点并输出一个顶点。 几何着色器的一个常见示例是将点扩展为四边形或将线扩展为四边形。

我们也注意到图5.11中的“流出”箭头。 也就是说,几何着色器可以将顶点数据输出到内存中的缓冲区,稍后可以绘制。 这是一种先进的技术,将在后面的章节中讨论。

NOTE : 离开几何着色器的顶点位置必须转换为同质的剪辑空间。

5.9剪切

完全在观察截头体之外的几何体需要被丢弃,并且与截头体的边界相交的几何体必须被裁剪,使得只有内部部分保留; 参见图5.27中的2D思想。

5-27
图5.27 (a)裁剪之前 (b)裁剪后

5-28
图5.28。 (a)用一平面裁剪一个三角形。 (b)剪裁后的三角形。 请注意,剪切后的三角形不是三角形,而是四边形。 因此,硬件将需要对产生的四边形进行三角测量,这对于凸多边形来说很直接

我们可以把这个平面视为由六个平面所界定的区域:顶部,底部,左边,右边,近边和远边的平面。 要将一个多边形与截锥体相夹,我们将它们依次夹在每个平截体面上。 当将一个多边形夹在一个平面上时(图5.28),平面的正半空间中的部分被保留,而负半空间中的部分被丢弃。 在平面上剪切凸多边形总是会产生一个凸多边形。 由于硬件为我们完成剪裁,所以我们不会在这里介绍细节;相反,我们将读者引用到流行的Sutherland-Hodgeman剪裁算法[Sutherland74]中。 它基本上相当于找到平面和多边形边之间的交点,然后排序顶点以形成新的剪切多边形。

[Blinn78]描述了如何在4D均匀空间中进行裁剪(图5.29)。 在视角分割之后,视锥内部的点 (xw,yw,zw,1) ( x w , y w , z w , 1 ) 处于归一化的设备坐标中,并且界定如下:

1x/w11y/w10z/w1 − 1 ≤ x / w ≤ 1 − 1 ≤ y / w ≤ 1 0 ≤ z / w ≤ 1

因此,在均匀片段空间中,在划分之前,平截头体内的四维点(x,y,z,w)有界限如下:
wxwwyw0zw − w ≤ x ≤ w − w ≤ y ≤ w 0 ≤ z ≤ w

5-29
图5.29 均匀裁剪空间中的xw平面中的平截头体边界。

也就是说,这些点是由简单的四维平面界定的:
Left:w=xRight:w=xBottom:w=yTop:w=yNear:z=0Far:z=w L e f t : w = – x R i g h t : w = x B o t t o m : w = – y T o p : w = y N e a r : z = 0 F a r : z = w

一旦我们知道齐次空间中的平截面方程组,我们就可以应用一个裁剪算法(如Sutherland-Hodgeman)。 请注意,段/平面相交测试的数学概括为R 4,所以我们可以在齐次剪辑空间中使用4D点和4D平面进行测试。

5.10 光栅化阶段

光栅化阶段的主要工作是计算投影的三角形的像素颜色。

5.10.1视口转换

裁剪之后,硬件可以进行透视分割,从同质裁剪空间转换到标准化设备坐标(NDC)。 一旦顶点处于NDC空间中,形成2D图像的2D x和y坐标就会转换为后端缓冲区上称为视口的矩形(回忆§4.2.8)。 在这个转换之后,x坐标和y坐标以像素为单位。 通常,视口转换不会修改z坐标,因为它用于深度缓冲,但它可以通过修改D3D11_VIEWPORT结构的MinDepthMaxDepth值。MinDepthMaxDepth值必须介于0和1之间。

5.10.2背面剔除

三角形有两面。 为了区分双方我们使用以下惯例。 如果三角形的顶点是有序的 v0v1v2 v 0 , v 1 , v 2 ,那么我们就像这样计算三角形的法线n

e0=v1v0e1=v2v0n=e0×e1||e0×e1|| e 0 = v 1 − v 0 e 1 = v 2 − v 0 n = e 0 × e 1 | | e 0 × e 1 | |

法线矢量发出的一面是正面,另一面是背面。 图5.30说明了这一点。

我们说,如果观众看到三角形的正面,那么三角形是面向前方的,而如果观看者看到三角形的背面,我们说三角形是向后。从图5.30的角度来看,左三角形是朝前的,而右三角形是朝后的。此外,从我们的角度来看,左三角形顺时针排列,而右三角形逆时针排列。这不是巧合:按照我们选择的惯例(即我们计算三角法线的方式),顺时针顺序的三角形面对观众)是面向前方的,(相对于观看者)逆时针排列的三角形是向后的。

现在,3D世界中的大多数对象都是封闭的实体对象。假设我们同意为每个对象构建三角形,使得法线始终是针对外部的。然后,相机看不到固体物体的背面三角形,因为正面三角形遮挡了背面三角形; 图5.31以2D和5.32来说明3D。因为正面的三角形遮住了背面的三角形,所以绘制它们是没有意义的。背面剔除是指从管线丢弃背面三角形的过程。 这可能会减少需要处理一半的三角形数量。

5-30
图5.30。 从我们的观点来看,左三角面向前,而从我们的观点来看,正三角面朝后。

5-31
图5.31 (a)具有正面和背面三角形的固体物体。 (b)剔除背面三角形后的场景。请注意,背面剔除不会影响最终图像,因为背面三角形被前面的三角形遮挡。

5-32
图5.32 (左)我们绘制立方体的透明度,以便您可以看到所有六面。(右)我们将立方体画成实心块。请注意,我们没有看到三面朝后的侧面,因为三面朝前的面遮住了它们,因此背面三角形实际上可以被丢弃,以免进一步处理,也没有人会注意到。

默认情况下,Direct3D将三角形以相对于观看者的顺时针方向(相对于观看者)视为正面,将三角形以逆时针方向(相对于观看者)视为背面。 但是,这种约定可以通过Direct3D渲染状态设置来反转。

5.10.3顶点属性插值

回想一下,我们通过指定它的顶点来定义一个三角形。除了位置之外,我们还可以将属性附加到顶点,如颜色,法向量和纹理坐标。在视口变换之后,这些属性需要针对覆盖三角形的每个像素进行插值。除了顶点属性之外,还需要对顶点深度值进行插值,以便每个像素具有深度缓冲算法的深度值。 顶点属性在屏幕空间中进行插值,使属性在三维空间中以三角形线性插值(图5.33)。这需要所谓的透视正确插值。基本上,插值允许我们使用顶点值来计算内部像素的值。

透视正确属性插值的数学细节不是我们所需要担心的,因为硬件会处理; 感兴趣的读者可以在[Eberly01]中找到数学推导。图5.34给出了该过程的基本概念。

5-33
图5.33 通过在三角形的顶点处的属性值之间进行线性内插可以获得三角形上的属性值p(s,t)。

5-34
图5.34。 正在投影到投影窗口的3D线(投影是屏幕空间中的2D线)。 我们看到沿着3D线采取统一的步长对应于在2D屏幕空间中采取不均匀的步长。 因此,要在3D空间中进行线性插值,需要在屏幕空间进行非线性插值。

5.11像素着色阶段

像素着色器是我们编写的在GPU上执行的程序。对每个像素片段执行像素着色器,并使用插入的顶点属性作为输入来计算颜色。像素着色器可以像返回一个常量的颜色一样简单,可以执行更复杂的事情,如每个像素的光照,反射和阴影效果。

5.12输出合并阶段

在像素着色器生成像素片段之后,它们移动到渲染管线的输出合并(OM)阶段。 在这个阶段,一些像素片段可能被拒绝(例如,来自深度或模板缓冲区测试)。 未被拒绝的像素片段被写入后台缓冲区。 混合也是在这个阶段完成的,一个像素可以与当前在后台缓冲区中的像素混合,而不是完全覆盖它。 透明等一些特殊效果是通过混合来实现的; 第9章致力于混合。

5.13 总结

1.我们可以根据我们在现实生活中看到的东西,采用多种技术来在2D图像上模拟3D场景。我们观察到平行线汇聚到灭点,物体随着深度而减小,物体模糊了它们后面的物体,光照和阴影描绘了3D物体的固体形状和体积,阴影暗示着光源相对于场景中的其他对象的位置。
2.我们用三角网格近似物体。我们可以通过指定三个顶点来定义每个三角形。在许多网格中,顶点由三角形共享;索引列表可以用来避免顶点重复。
3.通过指定红,绿和蓝的强度来描述颜色。这三种颜色以不同强度的加和混合使我们能够描述数百万种颜色。为了描述红色,绿色和蓝色的强度,使用从0到1的归一化范围是有用的。零表示没有强度,1表示全部强度,并且中间值表示中间强度。还有一个额外的颜色组件,被称为alpha组件。alpha分量通常用于表示颜色的不透明度,这在混合中很有用。包含alpha分量意味着我们可以用4D颜色向量(r,g,b,a)来表示颜色,其中0≤r,g,b,a≤1。因为表示颜色所需的数据是4D向量,我们可以使用XMVECTOR类型来表示代码中的颜色,每当我们使用XNA Math矢量函数来执行颜色操作时,我们就可以从SIMD操作中获益。为了用32位表示一个颜色,给每个组件一个字节; XNA Math库提供了用于存储32位颜色的XMCOLOR结构。除了我们必须将组件的[0,1]范围(或32位颜色的[0,255])之外,颜色矢量被添加,减去和缩放,就像常规矢量一样。其他向量操作(如点积和交叉积)对于颜色向量没有意义。符号⊗表示分量乘法,例如: c1c2c3c4k1k2k3k4=c1k1c2k2c3k3c4k4 ( c 1 , c 2 , c 3 , c 4 ) ⊗ ( k 1 , k 2 , k 3 , k 4 ) = ( c 1 k 1 , c 2 k 2 , c 3 k 3 , c 4 k 4 )
4.给定3D场景以及该场景中的定位和目标虚拟相机的几何描述,渲染流水线是指其基于虚拟相机看到生成可以在监视器屏幕上显示的2D图像所需的全部步骤序列。
5.渲染管线可分为以下几个主要阶段。 输入组件(IA)阶段; 顶点着色器(VS)阶段; 镶嵌阶段; 几何着色器(GS)阶段; 裁剪阶段; 光栅化阶段(RS); 像素着色器(PS)阶段; 和产出合并(OM)阶段。

5.14 练习

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值