目录
5.4 OVERVIEW OF THE RENDERING PIPELINE
5.6.1 Local Space and World Space
5.6.3 Projection and Homogeneous Clip Space
5.10.3 Vertex Attribute Interpolation
这一章节主要讲渲染管线,并大多都是理论知识,在下一章会把这些理论在学习用Direct3D绘制的练习中应用。
5.1 THE 3D ILLUSION
我们是如何在平面2D的显示器上显示具有深度和体积的3D世界的?
近大远小:在我们人类看来(以眼睛为原点的透视坐标系),一个物体离我们越远(深度越大),它的尺寸会越小。
遮挡:这表明了不透明的物体把在它后面的物体遮挡了。
光照:在描绘三维物体的实体形态和体积方面起着非常重要的作用。比如左边的球看起来非常平面,让人感觉就是一个2D的圆。
阴影:告诉了光源在场景中的位置,也让我们大致了解宇宙飞船离地面有多高。
5.2 MODEL REPRESENTATION
一个实体的三维对象由一个三角形网格近似来表示,因此,三角形构成了我们建模对象的基础建构。
5.3 BASIC COMPUTER COLOR
计算机显示器从每个像素发出红色、绿色和蓝色的混合光。当这种混合光进入眼睛并照射到视网膜的某个区域时,锥状受体细胞受到刺激,神经冲动沿视神经向下传递到大脑,大脑解读信号并产生颜色。当光的混合物发生变化时,细胞受到不同的刺激,从而在大脑中产生不同的颜色。下图显示了混合红色、绿色和蓝色以获得不同颜色的一些示例;它也显示出不同的红色强度。通过为每个颜色组件使用不同的强度并将它们混合在一起,我们可以描述需要显示真实图像的所有颜色。
5.3.1 Color Operations
和向量一样,颜色也可以用加法减法和乘法,而点积和叉积对颜色来说没有任何意义。然而颜色有自己特殊的颜色运算,称为调制或分量乘法,定义为:
5.3.2 128-Bit Color
通常会合并一个额外的颜色通道,称为alpha通道。添加了alpha分量,意味着我们可以用四维颜色向量来表示一种颜色,为了表示一个128位的颜色,我们为每个通道使用一个浮点值。因为从数学上讲,颜色只是一个四维向量,所以我们可以使用XMVECTOR类型在代码中表示颜色,并且每当我们使用DirectXMath向量函数进行颜色操作时,我们都可以获得SIMD操作的好处。对于分量乘法,DirectX数学库提供以下函数:
XMVECTOR XM_CALLCONV XMColorModulate( // Returns c1 ⊗ c2
FXMVECTOR C1,
FXMVECTOR C2);
5.3.3 32-Bit Color
为了表示32位的颜色,每个通道都是一个字节。DirectX::PackedVector名称空间中提供了以下结构,用于存储32位颜色::
namespace DirectX
{
namespace PackedVector
{
// 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]
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; }
};
} // end PackedVector namespace
} // end DirectX namespace
x86是小端,XMCOLOR的格式是ARGB。
32位颜色和128位颜色可以互相转换。在将32位颜色转换为128位颜色时,通常必须执行额外的位操作,因为8位颜色组件通常打包成32位整数值(例如无符号整型),就像在XMCOLOR中一样。DirectXMath库定义了以下函数,它接受一个XMCOLOR并从中返回一个XMVECTOR:
XMVECTOR XM_CALLCONV PackedVector::XMLoadColor(const XMCOLOR* pSource);
DirectX数学库还提供了一个将XMVECTOR颜色转换为XMCOLOR的函数:
void XM_CALLCONV PackedVector::XMStoreColor(
XMCOLOR* pDestination,
FXMVECTOR V);
通常,128位颜色值用于需要高精度颜色操作的地方(例如,在像素着色器中);这样,我们的计算就有很多精度,所以算术误差不会积累太多。然而,最后的像素颜色通常存储在32位的颜色值的back buffer中;当前的物理显示设备无法利用更高分辨率的颜色(现在back buffer也都用HDR模式了,也有部分显示器支持HDR)。
5.4 OVERVIEW OF THE RENDERING PIPELINE
渲染管道指的是根据虚拟摄像机看到的内容生成2D图像所需的整个步骤序列:
5.5 THE INPUT ASSEMBLER STAGE
input assembler(IA)阶段从内存中读取几何数据(顶点和索引)并使用它来组装几何图元(如三角形、直线)。(索引将在后面的小节中讨论,但简单地说,它们定义了顶点如何组合在一起形成图元。组装图元在后续也会有补充)
5.5.1 Vertices
基本上,除了空间位置之外,Direct3D中的一个顶点还可以包含其他数据,这使得我们可以执行更复杂的渲染效果。例如第8章中,我们将向顶点添加法向量来实现光照,第9章中我们将向顶点添加纹理坐标来实现纹理。Direct3D为我们提供了定义自己的顶点格式的灵活性(它允许我们定义顶点的组件),我们将在下一章中看到用于定义顶点格式的代码。在本书中,我们将根据渲染效果定义几种不同的顶点格式。
5.5.2 Primitive Topology
顶点通过一个特殊的Direct3D数据结构中被绑定到渲染管道,这个数据结构称为vertex buffer。vertex buffer是在连续内存中存储的顶点列表。然而,它并没有说这些顶点应该如何放在一起组成几何图元。例如顶点缓冲区中的每两个顶点都应该插值成为一条直线,还是顶点缓冲区中的每三个顶点都应该插值成为一个三角形?我们告诉Direct3D如何通过指定图元拓扑来从顶点数据中形成几何图元:
void ID3D12GraphicsCommandList::IASetPrimitiveTopology( D3D_PRIMITIVE_TOPOLOGY Topology);
typedef enum D3D_PRIMITIVE_TOPOLOGY
{
D3D_PRIMITIVE_TOPOLOGY_UNDEFINED = 0,
D3D_PRIMITIVE_TOPOLOGY_POINTLIST = 1,
D3D_PRIMITIVE_TOPOLOGY_LINELIST = 2,
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP = 3,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST = 4,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP = 5,
D3D_PRIMITIVE_TOPOLOGY_LINELIST_ADJ = 10,
D3D_PRIMITIVE_TOPOLOGY_LINESTRIP_ADJ = 11,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ = 12,
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP_ADJ = 13,
D3D_PRIMITIVE_TOPOLOGY_1_CONTROL_POINT_PATCHLIST = 33,
D3D_PRIMITIVE_TOPOLOGY_2_CONTROL_POINT_PATCHLIST = 34,
.
.
.
D3D_PRIMITIVE_TOPOLOGY_32_CONTROL_POINT_PATCHLIST = 64,
} D3D_PRIMITIVE_TOPOLOGY;
所有后续的绘图调用都将使用当前设置的图元拓扑,直到通过命令列表更改拓扑为止。以下代码说明:
mCommandList->IASetPrimitiveTopology(
D3D_PRIMITIVE_TOPOLOGY_LINELIST);
/* …draw objects using line list… */
mCommandList->IASetPrimitiveTopology(
D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
/* …draw objects using triangle list… */
mCommandList->IASetPrimitiveTopology(
D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP);
/* …draw objects using triangle strip… */
下面的小节将详细介绍不同的基本拓扑。在本书中,我们主要使用三角形列表,很少有例外。
5.5.2.1 Point List
一个点列表由D3D_PRIMITIVE_TOPOLOGY_POINTLIST指定。使用点列表,绘制调用中的每个顶点都被绘制为单独的点(见下图a)。
5.5.2.2 Line Strip
线带由D3D_PRIMITIVE_TOPOLOGY_LINESTRIP指定。用线带将绘制调用中的顶点连接成线带,n + 1个顶点有n条直线(见下图b)。
5.5.2.3 Line List
线列表由D3D_PRIMITIVE_TOPOLOGY_LINELIST指定。在一个线列表中,绘制调用中的每两个顶点形成一个单独的线(见下图c);2n个顶点,有n条直线。线列表和线带之间的区别在于线列表中的线可能是断开的,而线带则自动假定它们是连接的;通过假设连接,可以使用较少的顶点,因为每个内部顶点由两条线共享。
5.5.2.4 Triangle Strip
三角形带由D3D_PRIMITIVE_TOPOLOGY_TRIANGLESTRIP指定。以三角形带为例,假设各三角形连接如下图d所示,形成一条带。通过下面的假设连接,我们看到顶点在相邻三角形之间共享,n个顶点产生n - 2个三角形。
注意,三角形带中的偶数三角形的缠绕顺序与奇数三角形不同,因此会导致剔除问题(参考5.10.2)。为了解决这个问题,GPU内部交换偶数三角形的前两个顶点的顺序,使它们的顺序与奇数三角形一致。
5.5.2.5 Triangle List
三角形列表由D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST指定。使用三角形列表,绘制调用中的每三个顶点形成一个单独的三角形(见下图a)。所以3n个顶点会产生n个三角形。三角形列表和带之间的区别是,三角形列表中的三角形可能是断开连接的,而三角形带假设它们是连接的。
b是带邻接的三角形列表——注意每个三角形需要6个顶点来描述它和它的邻接三角形。这样6n个顶点引出n个具有邻接信息的三角形。
5.5.2.6 Primitives with Adjacency
包含邻接的三角形列表,对每个三角形还包括它的三个相邻三角形,称为邻接三角形。参见上图b观察这些三角形是如何定义的。几何着色器中某些某些算法需要访问相邻三角形。为了让几何着色器获得那些相邻三角形,相邻三角形需要连同三角形本身一起提交给顶点/索引缓冲区中的管道,并且必须指定D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST_ADJ拓扑,以便管道知道顶点缓冲区里的三角形及其相邻三角形是什么样的构造。请注意,相邻图元的顶点仅用作几何着色的输入—它们不被绘制。如果没有几何着色器,相邻的图元仍然没有绘制。
相似的,也可以有带邻接的线列表、带邻接的线带、带邻接图元的三角形;有关详细信息,请参阅SDK文档。
5.5.2.7 Control Point Patch List
D3D_PRIMITIVE_TOPOLOGY_N_CONTROL_POINT_PATCHLIST拓扑类型表示顶点数据应该解释为一个包含N个控制点的补丁列表。这些是在渲染管道的(可选)tessellation阶段使用的,我们在第14章将讨论它们。
5.5.3 Indices
如前所述,三角形是3D物体的基本建构。下面的代码显示了使用三角形列表构造四边形和八边形的顶点数组(每三个顶点形成一个三角形)。
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
};
指定三角形顶点的顺序很重要,称为winding order。详见5.10.2。
形成3D物体的三角形共享许多相同的顶点。更具体地说,下图a中四边形的每个三角形共享顶点v0和v2。虽然复制两个顶点不是很糟糕,但是在八边形的例子中复制情况更糟(下图b),因为每个三角形都复制了中心顶点v0,并且八边形周长上的每个顶点都由两个三角形共享。通常,重复顶点的数量随着模型的细节和复杂性的增加而增加。
我们不想复制顶点有两个原因:
- 增加了内存需求。
- 增加了图形硬件的处理。
在某些情况下,三角形带可以帮助解决重复顶点问题,前提是几何图形可以像条带那样组织起来。然而,三角形列表更灵活(三角形不需要被连接),解决方法是使用索引,顶点列表由所有唯一的顶点组成,而索引列表包含索引到顶点列表的值,来定义如何将顶点放在一起形成三角形。上图四方面的顶点列表将构建如下:
Vertex v[4] = {v0, v1, v2, v3};
然后索引列表需要定义如何将顶点列表中的顶点放在一起以形成两个三角形。
UINT indexList[6] = {
0, 1, 2, // Triangle 0
0, 2, 3 // Triangle 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 // Triangle 7
};
5.6 THE VERTEX SHADER STAGE
图元组装完成后,顶点被送入顶点着色器阶段。顶点着色器可以被认为是一个输入和输出一个顶点的函数。每一个顶点绘制将泵通过顶点着色器,事实上我们可以从概念上想象一下硬件上发生的以下事情:
for(UINT i = 0; i < numVertices; ++i)
outputVertex[i] = VertexShader( inputVertex[i] );
顶点着色器函数是我们实现的,但是它是由GPU为每个顶点执行的,所以它非常快。
许多特殊效果可以在顶点着色器中完成,比如转换、光照和位移映射。记住,我们不仅可以访问输入的顶点数据,还可以访问纹理和其他存储在GPU内存中的数据,如变换矩阵和场景灯光。
我们将看到许多不同的顶点着色器在这本书的例子,到最后你应该清楚如何利用它们。然而对于我们的第一个代码示例,我们将只使用顶点着色器来转换顶点。下面的小节解释了通常需要进行的转换类型。
5.6.1 Local Space and World Space
5.6.2 View Space
矩阵相关就不介绍了,就直接跳到和DirectX 12 API相关的地方。
- 得到w基向量,描述了摄像机的Z轴:
- 得到u基向量,描述了摄像机的X轴:
- 获得v基向量,描述了摄像机的y轴:
DirectXMath库提供了以下功能,用于基于上述过程计算视图矩阵:
XMMATRIX XM_CALLCONV XMMatrixLookAtLH( // Outputs view matrix V
FXMVECTOR EyePosition, // Input camera position Q
FXMVECTOR FocusPosition, // Input target point T
FXMVECTOR UpDirection); // Input world up direction 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 Projection and Homogeneous Clip Space
5.6.3.1 Defining a Frustum
5.6.3.2 Projecting Vertices
5.6.3.3 Normalized Device Coordinates (NDC)
5.6.3.4 Writing the Projection Equations with a Matrix
5.6.3.5 Normalized Depth Value
5.6.3.6 XMMatrixPerspectiveFovLH
利用DirectX的数学函数可以建立一个透视投影矩阵:
// Returns the projection matrix
XMMATRIX XM_CALLCONV XMMatrixPerspectiveFovLH(
float FovAngleY, // vertical field of view angle in radians
float Aspect, // aspect ratio = width / height
float NearZ, // distance to near plane
float FarZ); // distance to far plane
下面的代码片段说明了如何使用XMMatrixPerspectiveFovLH。这里,我们指定了一个45°的fov,一个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;
}
Primitive Assembly
虽然前面提到组装图元是在IA阶段进行的,但在《A trip through the Graphics Pipeline》中写到会有一个Primitive Assembly阶段在Vert Shader之后进行图元的组装。可以参考https://fgiesen.wordpress.com/2011/07/09/a-trip-through-the-graphics-pipeline-2011-index/,它对GPU的渲染管线有更为详细的描述,而龙书很多内容都不完全,有些为了简单甚至是错误的,这系列笔记就不指出龙书的其它问题了,可以参考其它文章。
5.7 THE TESSELLATION STAGES
细分是指细分网格的三角形,以添加新的三角形。这些新的三角形可以被偏移到新的位置,以创建更精细的网格细节(见下图):
tessellation有很多好处:
- 我们可以实现一个level-of-detail(LOD)机制,其中靠近相机的三角形被细分以添加更多的细节,而远离相机的三角形则不被细分。在这种情况下,我们只在会注意到更多的细节时使用更多的三角形。
- 我们在内存中保持一个简单的低多边形网格(低多边形意味着低三角形数),并动态添加额外的三角形,从而节省内存。
- 我们在一个更简单的低多边形网格上进行动画和物理等操作,并且只使用网格化的高多边形网格进行渲染。
我们将在第14章对tessellation详细讨论。
5.8 THE GEOMETRY SHADER STAGE
几何着色器的主要优点是它可以创建或破坏几何图形。例如,可以将输入图元扩展为一个或多个其他图元,或者几何着色器可以根据某些条件选择不输出图元。这与顶点着色器不同,顶点着色器不能创建顶点:它输入一个顶点,输出一个顶点。一个常见的例子,几何着色器把一个点扩大成四边形或把一个线扩大成四边形。我们将在第12章对几何着色器详细讨论。
我们还注意到渲染管线整个流程图中的箭头:几何着色器可以将顶点数据流到内存中的缓冲区中,以便以后绘制。这是一种高级技术,将在后面的章节中讨论。
离开几何着色器的顶点位置必须转换为齐次裁剪空间。
5.9 CLIPPING
完全在视锥截体之外的几何形状需要丢弃,与视锥截体边界相交的几何形状必须修剪,只保留内部部分,参见下图:
因为硬件为我们做裁剪,这里不会说明细节,可以参考Sutherland-Hodgeman裁切算法,基本上相当于找到平面和多边形边之间的交点,然后对顶点进行排序形成新的裁剪多边形。
5.10 THE RASTERIZATION STAGE
光栅化阶段的主要工作是根据投影的3D三角形计算像素颜色。
5.10.1 Viewport Transform
裁剪之后,硬件做透视除法从齐次裁剪空间转换为归一化的器件坐标(NDC)。一旦顶点进入NDC空间中,2d的xy坐标形成二维图像转化为在back buffer的一个矩形,称为视窗(4.3.9)。在这个变换中,x和y坐标以像素为单位。通常,viewport转换不会修改z坐标,因为它用于深度缓冲,但是可以通过修改D3D12_VIEWPORT结构的MinDepth和MaxDepth值来修改z坐标。MinDepth和MaxDepth值必须在0和1之间。
5.10.2 Backface Culling
三角形有两条边。为了区分这两个方面,我们使用以下约定。如果三角形顶点的顺序是v0 v1 v2,那么我们计算三角形的法向量n就像这样:
法向量的方向是前面,另一边是后面。
此外在我们看来,左三角形是顺时针的,而右三角形是逆时针的。 我们选择的惯例是(计算三角形法向量的方法)顺时针排列的三角形(相对于观察者)是正面,逆时针排列的三角形(相对于观察者)是反面。
叉积是右手定则,但因为我们现在在像素坐标空间,而在D3D像素空间y向下增加,而不是向上,所以方向是相反的。我测试了下,确实对于不在像素空间的三角形,逆时针才是正面。
默认情况下,Direct3D将顺时针缠绕顺序(相对于观察者)的三角形视为正面,而将逆时针缠绕顺序(相对于观察者)的三角形视为背面。然而,这种约定可以通过Direct3D渲染状态设置逆转。
5.10.3 Vertex Attribute Interpolation
我们通过指定顶点来定义三角形。除了位置之外,我们还可以将属性附加到顶点上,比如颜色、法向量和纹理坐标。在viewport转换之后,需要为覆盖三角形的每个像素插入这些属性。除了顶点属性外,顶点深度值也需要插值,以便每个像素都有一个深度值用于深度缓冲算法。在屏幕空间内对顶点属性进行插值,使其在3D空间内对三角形进行线性插值(下图)。这就需要所谓的perspective correct interpolation透视正确插值。本质上,插值就是允许我们使用顶点的值来计算内部像素的值。
我们不需要担心透视正确属性插值的数学细节,因为这是硬件做的。感兴趣的读者可以在[Eberly01]中找到数学推导。下图给出了它的基本思路。
5.11 THE PIXEL SHADER STAGE
像素着色器是我们写的在GPU上执行的程序。对每个像素片段执行像素着色器,并使用插值顶点属性作为输入来计算颜色。像素着色器可以很简单地返回一个恒定的颜色,也可以做更复杂的事情,比如每个像素的照明,反射和阴影效果。
5.12 THE OUTPUT MERGER STAGE
像素着色器生成像素片元后,它们进入渲染管线的输出合并(OM)阶段。在这个阶段,一些像素片段可能会被拒绝(如在深度测试或模板缓冲测试)。未被拒绝的像素片段被写入back buffer。混合也在这个阶段完成,在这个阶段,一个像素可以与当前在back buffer中的像素混合,而不是完全覆盖它。一些特殊的效果如透明度就是通过混合来实现的。我们在第10章讨论混合。