[C++]学习《DirectX12 3D 游戏开发实战》 第六天 渲染流水线

知识点整理

1. 3D 视觉

实体 3D 对象是借用三角形网格 (triangle mesh) 来近似表示。要以三角形作为 3D 物体建模的基石。并通过指定三角形的 3 个顶点来定义三角形。在许多网格中都存在着顶点被不同三角形所共用的现象,而索引列表则可以用于避免因重复使用顶点而复制顶点数据所带来的冗余信息。

2. 计算机色彩基础

通过指定红、绿、蓝三色光的强度来描述颜色。利用此三色光不同强度的相加混色 (additive mixing,加色法),可以表示出数以千万计的颜色。

通常用归一化范围 0~1 来描述三色的强度,0 表示没有强度,1 表示最高强度,两者之间的值表示相应的中间强度。

2.1 颜色运算

向量的部分运算规则在颜色向量上同样适用。点积和叉积就不适用。

分量式 (modulation 或 componentwise) 乘法:

(c_{r},c_{g},c_{b})\bigotimes (k_{r},k_{g},k_{b})=(c_{r}k_{r},c_{g}k_{g},c_{b}k_{b})

颜色分量有可能会超出 [0,1] 这个区间。由于 1.0 代表颜色分量的最大强度,所以任何光的强度不可能超过此值,因此只得将值钳制 (clamp) 为 1.0,同样地,显示器也不能发出强度为负值的光,所以亦应把负的颜色分量(由减法运算所得到的结果)钳制为 0.0。

2.2 128 位颜色

alpha 分量 (alpha component):通常用于颜色不透明度 (opacity。0.0 表示完全透明,1.0表示不透明)。

4D 颜色向量 (r,g,b,a) 来表示颜色,其中 0≤r,g,b,a≤1。代码中能用 XMVECTOR 类型来描述。

为了用 128位 (128 bit) 数据来表示一种颜色,每个分量都要使用浮点值

在通过 DirectXMath 向量函数来进行颜色运算(如颜色的加法运算、减法运算和标量乘法运算)的同时,也能借助 SIMD 技术加快数据的处理速度。

DirectXMath 库针对分量式乘法运算提供了下列函数:

XMVECTOR XM_CALLCONV XMColorModular( // 返回 c1⊗c2
    FXMVECTOR C1,
    FXMVECTOR C2);
2.3 32 位颜色

为了用 32位 (32bit) 数据表示一种颜色,每个分量仅能分配到 1 个字节。每个占用 8 位字节的颜色分量就可以分别描述 256 种不同的颜色,0 代表无强度,255 是最大强度。

DirectXMath 库 (#include <DirectXPackedVector.h>) 在DirectX::PackedVector命名空间中提供了下面的结构用于存储 32 位颜色:

namespace DirectX
{
namespace PackedVector
{
// ARGB颜色表示法;以8-8-8-8位的无符号归一化整数分量封装为一个32位的整数
// 将alpha、红、绿、蓝4种分量分别用8位无符号归一化整数表示,以此封装32位归一化颜色
// alpha分量存于最高8位有效位,而蓝色分量则存于最低8位有效位(A8R8G8B8)
// [32] aaaaaaaa rrrrrrrr gggggggg bbbbbbbb [0][7]
struct XMCOLOR
{
  union
  {
    struct
    {
      uint8_t b; // Blue: 0/255 to 255/255
      uint8_t g; // Green: 0/255 to 255/255
      uint8_t r; // Red: 0/255 to 255/255
      uint8_t a; // Alpha: 0/255 to 255/255
    };
    uint32_t c;
  };

  XMCOLOR() {}
  XMCOLOR(uint32_t Color) : c(Color) {}
  XMCOLOR(float _r, float _g, float _b, float _a);
  explicit XMCOLOR(_In_reads_(4) const float *pArray);

  operator uint32_t () const { return c; }
  XMCOLOR& operator= (const XMCOLOR& Color) { c = Color.c; return 
    *this; }
  XMCOLOR& operator= (const uint32_t Color) { c = Color; return *this; 
    }
};
} // PackedVector 命名空间结束
} // DirectX 命名空间结束

通过将整数范围 [0, 255] 映射到实数区间 [0, 1],就可以将 32 位颜色转换为 128 位颜色,具体做法是将每个分量分别除以 255。

由于在 XMCOLOR 中通常将 4 个 8 位颜色分量封装为一个 32 位整数值,因此在 32 位颜色与 128 位颜色互相转换的过程中常常需要进行一些额外的位运算(提取出每个分量)。

DirectXMath 库定义了一个获取 XMCOLOR 类型实例并返回其相应 XMVECTOR 类型值的函数:

XMVECTOR XM_CALLCONV PackedVector::XMLoadColor(
    const XMCOLOR* pSource);

DirectXMath 库提供了一个可将 XMVECTOR 转换至 XMCOLOR 的函数:

void XM_CALLCONV PackedVector::XMStoreColor(
    XMCOLOR* pDestination,
    FXMVECTOR V);

一般来说,128位颜色值常用于高精度的颜色运算(例如位于像素着色器中的各种运算)。在这种情况下,由于运算所用的精度较高,因此可有效降低计算过程中所产生的误差。但是,最终存储在后台缓冲区中的像素颜色数据,却往往都是以32位颜色值来表示。

3. 渲染流水线概述

渲染流水线 (rendering pipeline):给出某个 3D 场景的几何描述,并在此场景中设置一台具有特定位置与朝向的虚拟摄像机,那么渲染流水线就是根据该虚拟摄像机的视角,生成能呈现在显示器中对应 2D 图像的这一系列完整步骤。

渲染流水线可以划分为输入装配 (Input Assembly,IA)阶段、顶点着色器 (Vertex Shader,VS) 阶段、曲面细分 (tessellation) 阶段、几何着色器 (Geometry Shader,GS) 阶段、裁剪 (clip) 阶段、光栅化阶段 (Rasterization Shage,RS)、像素着色器 (Pixel Shader,PS) 阶段以及输出合并 (Output Merger,OM) 等重要阶段。

4. 输入装配器阶段

输入装配器 (Input Assembler,IA) 阶段会从显存中读取几何数据 (顶点和索引,vertex and index),再将它们装配为几何图元

几何图元 (geometric primitive):几何基元,如三角形和线条这种构成图形的基本元素。

4.1 顶点

除空间位置,Direct3D 中的顶点还可以包含其他信息。

4.2 图元拓扑 (primitive topology,或称基元拓扑)

在Direct3D中,我们要通过一种名为顶点缓冲区 (vertex buffer) 的特殊数据结构,将顶点数据与渲染流水线相绑定。顶点缓冲区利用连续的内存来存储一系列顶点。

在用户通过命令列表 (command list) 修改图元拓扑之前,所有的绘制调用都会沿用当前设置的图元拓扑方式。图元拓扑的具体配置方法:

mCommandList->IASetPrimitiveTopology(
  D3D_PRIMITIVE_TOPOLOGY_LINELIST);
/* …通过线列表来绘制对象… */

mCommandList->IASetPrimitiveTopology(
  D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
/* …通过三角形列表来绘制对象… */

mCommandList->IASetPrimitiveTopology(
  D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
/* …通过三角形带来绘制对象… */

除了少数情况外,大多数使用三角形列表。

4.2.1 点列表 (point list)

通过枚举项 D3D_PRIMITIVE_TOPOLOGY_POINTLIST 来指定点列表。当使用点列表拓扑时,所有的顶点都将在绘制调用的过程中被绘制为一个单独的点,如图 (a) 所示。

4.2.2 线条带 (line strip)

通过枚举项 D3D_PRIMITIVE_TOPOLOGY_LINESTRIP 来指定线条带。在使用线条带拓扑时,顶点将在绘制调用的过程中被连接为一系列的连续线段(如图 (b) 所示)。所以,在这种拓扑模式下,若有 n+1 个顶点就会生成 n 条线段。

4.2.3 线列表 (line list)

通过枚举项 D3D_PRIMITIVE_TOPOLOGY_LINELIST 来指定线列表。当使用线列表拓扑时,每对顶点在绘制调用的过程中都会组成单独的线段(如图 (c) 所示)。所以 2n 个顶点就会生成  n 条线段。线列表与线条带的区别是:线列表中的线段可以彼此分开,而线条带中的线段则是相连的。如果线段相连的话,绘制同样数量的线段便会占用更少的顶点,因为每个处于线条带中间位置的顶点都可以同时被两条线段所共用。

4.2.4 三角形带 (triangle strip)

通过枚举项 D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP 来指定三角形带。当使用三角形带拓扑时,所绘制的三角形将像图 (d) 所示的那样被连接成带状。可以看到,在这种三角形连接的结构中,处于中间位置的顶点将被相邻的三角形所共同使用。因此,利用 n 个顶点即可生成 n-2 个三角形。

经过观察可以发现,在三角形带中,次序为偶数的三角形与次序为奇数三角形的绕序 (winding order,即装配图元的顶点顺序为逆时针或顺时针方向) 是不同的,这就是剔除 (culling) 问题的由来。为了解决这个问题,GPU内部会对偶数三角形中前两个顶点的顺序进行调换,以此使它们与奇数三角形的绕序保持一致。

在 DirectX 12 中,图 (d) 里三角形带的实际环绕顺序为:012、132、234、354 等。按道理来讲,次序为偶数的三角形的顶点绕序也应遵守默认的顶点编号顺序(如第2、4个三角形的默认顶点编号顺序应为 123、345),但事实上并非如此。为什么?作者讲到:为了使绕序保持一致,都为顺时针。而绕序又与剔除技术有关。(这样看来,作者似乎把偶数三角形中要调换的顶点理解错了,在DirectX 中"置换"的是后两个顶点的顺序,而在 OpenGL 里"置换"的才是前两个顶点的顺序。)

4.2.5 三角形列表 (triangle list)

通过枚举项 D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST 来指定三角形列表。当使用三角形列表拓扑时,在绘制调用的过程中会将每3个顶点装配成独立的三角形(如图 (a) 所示);所以每 3n 个顶点会生成 n 三角形。三角形列表与三角形带的区别是:三角形列表中的三角形可以彼此分离,而三角形带中的三角形则是相连的

4.2.6 具有邻接数据的图元拓扑

对于存有邻接数据的三角形列表而言,每个三角形都有 3 个与之相邻的邻接三角形 (adjacent triangle)。上图 (b) 中展示的就是这种图元拓扑。在几何着色器中,往往需要访问这些邻接三角形来实现特定的几何着色算法。为了使几何着色器可以顺利地获得这些邻接三角形的信息,我们就需要借助顶点缓冲区与索引缓冲区 (index buffer) 将它们随主三角形一并提交至渲染流水线。另外,此时一定要将拓扑类型指定为 D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ,只有这样,渲染流水线才知道如何以顶点缓冲区中的顶点来构建主三角形及其邻接三角形。注意,邻接图元的顶点只能用作几何着色器的输入数据,却并不会被绘制出来。即便程序没有用到几何着色器,但依旧不会绘制邻接图元。通过观察可以发现,每个三角形共需要 6 个顶点来描述它及其邻接的三角形,因此,6n 个顶点可以生成 n 个三角形及其邻接数据线列表。

4.2.7 控制点面片列表

D3D_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST 拓扑类型表示:将顶点数据解释为具有 N 个控制点的面片列表 (patch list)。此图元常用于渲染流水线的曲面细分阶段,因此,我们将这种列表拓扑延至第14章中再进行讨论。

4.3 索引 (index)

绕序 (winding order):为三角形指定的顶点顺序。

一般来说,随着模型细节和复杂度的增加,复制顶点的数量会急剧上升。

我们不希望复制顶点数据的原因有两个:

1. 增加内存的需要——为什么要多次存储同一个顶点数据呢?

2. 增加图形硬件的处理负荷——为什么要多次处理同一个顶点数据呢?

借助三角形带可以在某些情况下改善顶点的复制问题,前提是这些几何体能够被组织为带状结构。但是,由于三角形列表更为灵活(该拓扑中的三角形都无需互相连接),所以值得花些心思研究一种利用三角形列表移除重复顶点的设计方案。在此,我们所采用的解决方案是使用索引 (index)。

具体工作流程是这样的:先创建一个顶点列表和一个索引列表。在顶点列表中收录一份所有独立的顶点,并在索引列表中存储顶点列表的索引值,这些索引定义了顶点列表中的顶点是如何组合在一起,从而构成三角形的。

待处理完顶点列表中那些独立的顶点之后,显卡就能通过索引列表把顶点组合成一系列三角形。我们已经将“复用的顶点数据”转化为索引列表,但这样做的效果比之前的方法更好,这是因为:

1. 索引皆是简单的整数,不会像使用整个顶点结构体那样占用更多的内存(而且,随着顶点结构体中分量的不断增多,将会使内存的需求变得更为急迫)。

2. 若辅以适当的顶点缓存排序,则图形硬件将不必再次处理重复使用的顶点,从缓存中直接取得即可(这种情况十分普遍)。

5. 顶点着色器阶段

图元装配完毕后,其顶点就会被送入顶点着色器阶段 (vertex shader stage)。我们可以把顶点着色器看作一种输入与输出数据皆为单个顶点的函数。每个要被绘制的顶点都须经过顶点着色器的处理再送往后续阶段。

顶点着色器函数 (VertexShader):这一阶段中对顶点的操作实际是由 GPU 来执行的,所以速度很快。

可以利用顶点着色器来实现许多特效,例如变换、光照和位移贴图 (displacement mapping,位移映射)。

请牢记:在顶点着色器中,不但可以访问输入的顶点数据,也可以访问纹理和其他存于显存中的数据(如变换矩阵与场景的光照信息)。

5.1 局部空间和世界空间

局部坐标系 (local space,局部空间):通常是一种以目标物体中心原点,并且坐标轴与该物体对齐的简易便用坐标系。

世界坐标系 (world space,世界空间/全局场景坐标系):物体相对于它来定方位。

只要在局部空间中定义了 3D 模型的各顶点,我们就能将它变换至全局场景之中。

根据物体的位置与朝向,指定其局部空间坐标系的原点和诸坐标轴相对于全局场景坐标系的坐标,再运用坐标变换即可将物体从局部空间变换至世界空间。

将局部坐标系内的坐标转换到全局场景坐标系中的过程叫作世界变换 (world transform),所使用的变换矩阵名为世界矩阵 (world matrix)。由于场景中每个物体的朝向和位置都可能各不相同,因此它们都有自己特定的世界矩阵。当每个物体都从各自的局部空间变换到世界空间后,它的坐标都将位于同一个坐标系(即世界坐标系)之中。如果希望直接在世界空间内定义一个物体,那么就可以使用单位世界矩阵 (identity world matrix)。

在每个 3D 模型各自的局部坐标系中来定义它们的优点:

1. 易于使用。

2. 物体应当可以跨越多个场景而重复使用,所以将物体坐标相对于某个特定场景进行硬编码并不是明智之举。

3. 最后一点,我们有时可能需要在场景中多次绘制同一个物体,但是它们的位置、方向和大小却各不相同。如若在每次创建物体实例时都要复制它的顶点和索引数据,将其消耗资源。因此,我们通常的做法是存储一份几何体相对于其局部空间的副本(该几何体的顶点列表和索引列表)。接着,按所需次数来绘制此物体,每次辅以不同的世界矩阵来指定物体在世界空间中的位置、方向和大小。这种方法称为实例化 (instancing)。

从局部空间至世界空间的坐标变换矩阵为:

W=\begin{bmatrix} 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 \end{bmatrix}

W = SRT

首先,缩放矩阵 S 将物体缩放到世界空间。

其次,旋转矩阵 R 用来定义局部空间相对于世界空间的朝向。

最后,平移矩阵 T 定义的是局部空间的原点相对于世界空间的位置。

5.2 观察空间 (view space)

也译为观察坐标系、视图空间、视觉空间 (eye space) 或摄像机空间 (camera space)

在此坐标系中,该虚拟摄像机位于原点并沿 z 轴的正方向观察,x 轴指向摄像机的右侧,y 轴则指向摄像机的上方。

由世界空间至观察空间的坐标变换称为取景变换 (view transform,也译作观察变换、视图变换等),此变换所用的矩阵则称为观察矩阵 (view matrix,亦译作视图矩阵)。

世界坐标系和观察坐标系通常只有位置和朝向这两点差异,所以由观察空间到世界空间的变换可以直接表示为 W = RT (世界矩阵可以分解为一个旋转矩阵与一个平移矩阵的乘积)。

从世界空间到观察空间的坐标变换矩阵为 W^{-1};逆变换易于计算:

V=W^{-1}=(RT)^{-1}=T^{-1}R^{-1}=T^{-1}R^T

因此,观察矩阵为:

V=\begin{bmatrix} u_{x} & v_{x}& w_{x}& 0\\ u_{y}& v_{y} & w_{y} & 0\\ u_{z}& v_{z}& w_{z} &0 \\ -Q\cdot u& -Q\cdot v & -Q\cdot w & 1 \end{bmatrix}

只要给定摄像机的位置、观察目标点以及世界空间中“向上”方向的向量(在本书中,我们用世界空间中的平面 xz 作为场景中的“地平面”,并以世界空间的 y 轴来指示场景内“向上”的方向),我们就能构建出对应的摄像机局部坐标系,并推导出相应的观察矩阵。

DirectXMath库针对计算观察矩阵的处理流程提供了以下函数:

XMMATRIX XM_CALLCONV XMMatrixLookAtLH(      // 输出观察矩阵V
  FXMVECTOR EyePosition,                    // 输入虚拟摄像机位置Q
  FXMVECTOR FocusPosition,                  // 输入观察目标点T
  FXMVECTOR UpDirection);                   // 输入世界空间中向上方向的向量j

一般来说,世界空间中的轴方向与虚拟摄像机“向上”向量的方向相同,所以,我们通常将“向上”向量定为 j = (0,1,0)

5.3 投影和齐次裁剪空间

摄像机在世界空间中的位置和朝向之外,还有另一个关键组成要素:即摄像机可观察到的空间体积 (volume of space)。此范围可用一个由四棱锥截取的平截头体 (frustum,即四棱台) 来表示。

下一个任务是:平截头体内的 3D 几何体投影到一个 2D 投影窗口 (projection window) 之中。根据前文所述的透视投影 (perspective projection) 的原理可知,投影必将沿众平行线汇聚于消失点上,而且随着物体 3D 深度的增加,其投影的尺寸也逐渐变小。我们将由顶点到观察点 (eye point,也译为视点) 的连线称为顶点的投影线 (vertex's line of projection)。继而就可以定义出:将 3D 顶点 v 变换至其投影线与 2D 投影平面交点 v' 的透视投影变换 (perspective projection transformation)。我们称点 v' 为点 v 的投影。3D 物体的投影即为构成该物体上所有顶点的投影。

5.3.1 定义平截头体 (frustum)

在观察空间中,我们可以通过近平面 (near plane,近裁剪面) n、远平面 (far plane,远裁剪面) f、垂直视场角 (vertical field of view angle) α 以及纵横比 (aspect ratio,宽高比) r 这 4 个参数来定义一个:以原点作为投影的中心,并沿 z 轴正方向进行观察的平截头体。值得注意的是,位于空间中的远、近平面皆平行于平面 xy,因此,我们就能方便地确定出它们分别沿 z 轴到原点的距离。纵横比的定义为 r = w/h,其中 w 为投影窗口宽度,h 为投影窗口高度(以观察空间的单位为准)。投影窗口实质上即为观察空间中场景的 2D 图像。由于该图像终将被映射到后台缓冲区中,因此,我们希望令投影窗口后台缓冲区两者的纵横比保持一致。为此,我们通常将投影窗口的纵横比指定为后台缓冲区的纵横比(比值并没有单位)。若投影窗口与后台缓冲区的纵横比不一致,那么映射过程中就需要对投影窗口在将投影窗口进行不等比缩放 (non-uniform scaling),继而导致图像出现拉伸变形的现象。

根据指定的垂直视场角 α 和纵横比 r 来推导水平视场角 β

5.3.2 投影顶点

求出给定点 (x,y,z) 在投影平面 z = d 中的投影 (x',y',z'):

利用相似三角形的性质来求取目标点在投影平面内的投影

5.3.3 规格化设备坐标 (Normalized Device Coordinates,NDC)

为了能去除投影窗口对纵横比的依赖,让处理过程更加简单。对此,我们的解决办法是将 x 坐标上的投影区间从 [-r,r] 缩放至归一化区间 [-1,1]。经此映射处理后,x 坐标和 y 坐标就成为了规格化设备坐标 (请注意,这里并没有对 z 坐标进行归一化处理)。此时,若点 (x,y,z) 位于平截头体之中,当且仅当:

-1 ≤ x'/r ≤ 1

-1 ≤ y' ≤ 1

n ≤ z ≤ f

我们可以把由观察空间到 NDC 空间的变换视为一种单位换算 (unit conversion)。观察两者的转换过程,可知存在如下关系:在 x 轴上,1 个 NDC 单位等于观察空间中的 r 个单位 (即 1 ndc = r vs) 所以,若给出 x 个观察空间单位,我们就可以根据上述关系将它转换为 NDC 单位(式中的 vs 即观察空间 view space 的缩写):

x\; vs\cdot \frac{1 ndc}{r\; vs }=\frac{x}{r}ndc

我们可以基于上式修改投影公式,从而直接求出以 NDC 坐标来表示的 x 轴和 y 轴上的投影坐标:

x'=\frac{x}{rz\; tan(a/2)}

y'=\frac{y}{z\; tan(a/2)}

(5.1)

注意,在 NDC 坐标中,投影窗口的高和宽都为 2,所以它的大小是固定的,硬件也就无须知道纵横比。但是,我们一定要确保将投影坐标映射到 NDC 空间内(图形硬件假设我们会完成这项工作)。

5.3.4 用矩阵来表示投影公式

为了保证变换的一致性,我们将用矩阵来表示投影变换。然而,由于式(5.1)的非线性特征,所以并不存在与之对应的矩阵表示。我们的“诀窍”是将其“一分为二”:即将其分为线性与非线性两个处理部分。非线性部分要进行除以 z 的计算过程。正如在下节中所讨论的,我们还将对 z 坐标进行归一化处理;这就意味着在执行非线性部分除以 z 的计算时,我们却无最初的 z 坐标可用。也就是说,我们一定要在此变换之前保存早先传入的初始 z 坐标。为了做到这一点,我们要利用齐次坐标将输入的 z 坐标复制到输出的 w 坐标。根据矩阵的乘法运算法则,需要令元素 [2][3] = 1 以及元素 [3][3] = 0 来加以实现(这里采用的是以 0 为基准的索引)。此投影矩阵形如:

P=\begin{bmatrix} \frac{1}{rtan(\alpha /2)} & 0 & 0 & 0\\ 0& \frac{1}{tan(\alpha /2)} & 0 & 0\\ 0& 0& A &1\\ 0& 0& B& 0 \end{bmatrix}

我们在矩阵当中设置了常量 A 和常量 B (在下一节中会推导它们的定义),利用它们即可把输入的 z 坐标变换到归一化范围。令任意点 (x,y,z,1) 与该矩阵相乘将会得到:

[x,y,z,1]\begin{bmatrix} \frac{1}{rtan(\alpha /2)} & 0 & 0 & 0\\ 0& \frac{1}{tan(\alpha /2)} & 0 & 0\\ 0& 0& A &1\\ 0& 0& B& 0 \end{bmatrix}=[\frac{x}{rtan(\frac{a}{2})} , \frac{y}{tan(\frac{a}{2})} , Az+B,z ]

在顶点与投影矩阵相乘之后(即线性部分),我们还要将每个坐标分别除以 w = z (即非线性部分)来完成整个变换过程:

[\frac{x}{rtan(\frac{\alpha }{2})} , \frac{y}{tan(\frac{\alpha }{2})} , Az+B , z ]\overset{\div w}{\rightarrow}[\frac{x}{rztan(\frac{\alpha }{2})} , \frac{y}{ztan(\frac{\alpha }{2})} , A+\frac{B}{z} , 1]

除以 w 的计算过程有时被称为透视除法 (perspective divide) 或齐次除法 (homogeneous divide)。

5.3.5 归一化深度值

Direct3D 希望将 x,y 坐标映射到归一化范围一样,深度坐标也要被映射到归一化区间 [0,1] 以内。因此,我们必须构建一个保序 (order preserving) 函数 g(z),用来把 z 坐标从区间 [n,f] 映射到区间 [0,1]。对深度值进行归一化处理后,深度关系保持不变。

A=\frac{f}{f-n}

B=-An

g(z)=\frac{f}{f-n}-\frac{nf}{(f-n)z}

完整的透视投影矩阵 (perspective projection matrix):

P=\begin{bmatrix} \frac{1}{rtan(\alpha /2)} & 0 & 0 & 0\\ 0& \frac{1}{tan(\alpha /2)} & 0 & 0\\ 0& 0& \frac{f}{f-n} &1\\ 0& 0& \frac{-nf}{f-n}& 0 \end{bmatrix}

在顶点乘以投影矩阵之后但还未进行透视除法之前,几何体会处于所谓的齐次裁剪空间投影空间 (projection space) 之中。待完成透视除法之后,便是用规格化设备坐标 (NDC) 来表示几何体了。

5.3.6 XMMatrixPerspectiveFovLH 函数

利用 DirectXMath 库内的 XMMatrixPerspectiveFovLH 函数来构建透视投影矩阵:

// 返回投影矩阵
XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH(
  float FovAngleY,    // 用弧度制表示的垂直视场角
  float Aspect,       // 纵横比 = 宽度 / 高度
  float NearZ,        // 到近平面的距离
  float FarZ);        // 到远平面的距离

下面的代码片段详细解释了 XMMatrixPerspectiveFovLH 函数的用法。在此例中,我们将垂直视场角指定为 45°,近平面位于 z = 1 处,远平面位于 z = 1000 处(这些长度皆以观察空间中的单位表示)。

XMMATRIX P = XMMatrixPerspectiveFovLH(0.25f*XM_PI, 
AspectRatio(), 1.0f, 1000.0f);

纵横比采用的是我们窗口的宽高比:

float D3DApp::AspectRatio()const
{
  return static_cast<float>(mClientWidth) / mClientHeight;
}
6. 曲面细分阶段

曲面细分阶段 (tessellation stages) 是利用镶嵌化处理技术对网格中的三角形进行细分 (subdivide),以此来增加物体表面上的三角形数量。再将这些新增的三角形偏移到适当的位置,使网格表现出更加细腻的细节。

使用曲面细分优点

1. 能借此实现一种细节层次 (level-of-detail,LOD) 机制,使离虚拟摄像机较近的三角形经镶嵌化处理得到更加丰富的细节,而对距摄像机较远的三角形不进行任何更改。通过这种方式,即可只针对用户关注度高的部分网格增添三角形,从而提升其细节效果。

2. 在内存中仅维护简单的低模 (low-poly,低精度模型) 网格 (低模网格是指三角形数量较少的网格,已逐渐形成一门独特画风的艺术制作手段),再根据需求为它动态地增添额外的三角形,以此节省内存资源。

3. 可以在处理动画和物理模拟之时采用简单的低模网格,而仅在渲染的过程中使用经镶嵌化处理的高模 (high-poly) 网格。

7. 几何着色器阶段

几何着色器 (geometry shader stage,GS) 是一个可选渲染阶段。几何着色器接受的输入应当是完整的图元。几何着色器的主要优点是可以创建或销毁几何体。顶点着色器与之相比,则不能创建顶点:它只能接受输入的单个顶点,经处理后再将该顶点输出。几何着色器的常见拿手好戏是将一个点或一条线扩展为一个四边形。

8. 裁剪

完全位于视锥体 (view frustum) 之外的几何体需要被丢弃,而处于平截头体交界以外的几何体部分也一定要接受被裁剪 (clip) 的操作。因此,只有在平截头体之内的物体对象才会最终保留下来。

我们可以把平截头体看作由顶、底、左、右、近、远这6个平面所围成的空间范围。为了裁剪一个与平截头体相交的多边形,我们需要对两者相交的每个平面都逐一进行裁剪操作。

由于裁剪操作是由硬件来负责的,所以不再描述具体的实现细节。

一种流行的裁剪方法:苏泽兰(萨瑟兰德)-霍奇曼裁剪算法 (Sutherland-Hodgman clipping algorithm)

9. 光栅化阶段

光栅化阶段 (rasterization stage,RS,亦有将rasterization 译作像素化或栅格化) 的主要任务是为投影至屏幕上的3D三角形计算出对应的像素颜色。

9.1 视口变换

当裁剪操作完成之后,硬件会通过透视除法将物体从齐次裁剪空间变换为规格化设备坐标 (NDC)。一旦物体的顶点位于 NDC 空间内,构成 2D 图像的 2D 顶点 x,y 坐标就会被变换到后台缓冲区中称为视口 (viewport) 的矩形里。待此变换完成后,这些 x,y 坐标都将以像素为单位表示。通常来讲,由于 z 坐标常在深度缓冲技术中用作深度值,因此视口变换是不会影响此值的。即便如此,我们还是可以通过修改 D3D12_VIEWPORT 结构体中的 MinDepthMaxDepth 值来做到这一点。届时,我们只需保证 MinDepth 和 MaxDepth 的取值为 0~1 即可。

9.2 背面剔除

三角形的顶点顺序为 v0、v1、v2,那么,我们通过下述方法来计算此三角形的法线 n:

e_{0}=v_{1}-v_{0}

e_{1}=v_{2}-v_{0}

n=\frac{e_{0}\times e_{1}}{\left \| e_{0}\times e_{1}\right \|}

法向量由正面 (front side) 射出,另一面则为背面 (back side)。

背面剔除 (backface culling) 就是用于将背面朝向的三角形从渲染流水线中除去的处理流程。这种操作能将待处理的三角形总量削减一半。

背面剔除技术并不会影响最终显示的图像。

9.3 顶点属性插值

顶点除了位置信息外,还可以给顶点附加颜色、法向量和纹理坐标等其他属性。经过视口变换后,我们需要为求取三角形内诸像素所附的属性而进行插值 (interpolate,内插) 运算。而且还需对顶点的深度值进行内插,继而得到每个像素参与实现深度缓冲算法的深度值。

为了得到屏幕空间 (screen space) 中各个顶点的插值属性,往往要通过一种名为透视校正插值 (perspective correct interpolation) 的方法,对3D空间中三角形的属性进行线性插值。从本质上来说,插值法即利用三角形顶点的属性值计算出其内部像素的属性值。

通过对图中三角形上3个顶点进行线性插值,即可求出该三角形内任意一点所具有的属性值p(s,t)

这里的插值是硬件处理的,总之一句话:在 3D 空间中执行的是线性插值,在屏幕空间里进行非线性插值

10. 像素着色器阶段

像素着色器 (pixel shader,PS) 是一种由 GPU 来执行的程序。它会针对每一个像素片段 (pixel fragment,亦有译为片元) 进行处理(即每处理一个像素就要执行一次像素着色器),并根据顶点的插值属性作为输入来计算出对应的像素颜色。像素着色器既可以直接返回单一的恒定颜色,也可以实现如逐像素光照 (per-pixel lighting)、反射 (reflection) 以及阴影 (shadow) 等效果。

11. 输出合并阶段

通过像素着色器生成的片元会被移送至渲染流水线的输出合并 (Output Merger,OM) 阶段。在此阶段中,一些像素片段可能会被丢弃(例如,那些未通过深度缓冲区测试或模板缓冲区测试的片元)。而后剩下的片元将会被写入后台缓冲区中。混合 (blend) 操作也是在此阶段实现的,此技术可令当前处理的像素与后台缓冲区中的对应像素相融合,而不仅是对后者进行完全的覆写。

  • 27
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值