《Practical Rendering & Computation with Direct3D11》读书总结 Chapter-3-Direct3D 11 The Rendering Pipeline

本章是这本书最长的一章,也比较枯燥= = 这一章详细介绍了流水线各个阶段的一些细节。对于流水线的每一个环节,需要注意它的输入、配置、处理过程、输出
下面进行逐个的总结:

Input Assembler

Input Assembler(输入装配阶段)是流水线的第一个阶段,在一个流水线中,被处理的对象都是一系列的顶点,因此在这个阶段就进行将顶点的数据以流水线中规定的形式输入到流水线中。
Input Assembler Pipeline Input
装配顶点数据首先需要有顶点的输入,对应的资源为Vertex Buffer,其次还有可能需要顶点索引,对应的资源为Index Buffer,这两个Buffer的创建在前一章已经详细介绍了。下面分别来看这两个Buffer的用法。
Vertex Buffer的装配方法:
对于一个顶点来说,它蕴含的数据有多种,例如位置、法线、纹理坐标等等,但并不总是需要获取它所有的数据,比如说在单纯地以线框模式渲染时可能只需要它的位置就够了,所以对顶点数据的组织方法是有多种的,要看实际的需求。Vertex Buffer是存储顶点数据的Buffer,在前一章介绍了,它的存储方式是以数组的形式,也就是说所有的顶点数据以数组的方式存储,但是要注意,并不是说顶点的数据只能用一个Vertex Buffer来组织,有的时候可以将顶点的位置数据存在一个Buffer中,然后再将发现数据存在一个Buffer中。Input Assembler是有多个INPUT SLOT的,每一个SLOT都可以填充Vertex Buffer Resource,确切的说,一共有16个INPUT SLOT供Vertex Buffer来装配。

pContext->IASetVertexBuffers(StartSlot,NumBuffers, aBuffers, aStrides,aOffsets);

具体的装配由IASetVertexBuffer函数完成,它的参数分别为 起始用的SLOT、BUFFER的数量、指向BUFFER数组的指针、指向Stride数组的指针、指向Offset数组的指针。其中Stride和Offset都是UINT类型,它们分别代表当前Buffer资源中的单个数据元素的大小 和 起始数据距离Buffer资源起始位置的偏移量。
Index Buffer的装配方法:
Index Buffer中的数据就比较简单了,一般都是UINT类型,代表顶点的索引,也只需要一个Buffer就够了,因为它并没有复杂的类型,Index Buffer的装配由IASetIndexBuffer完成:

m_pContext->IASetIndexBuffer( pBuffer, DXGI_FORMAT_R32_UINT, offset );

三个参数分别是IndexBuffer、IndexBuffer的数据格式、首元素的偏移量。数据格式取16bit和32bit都可以。

Input Assembler State Configuration
在做顶点装配的时候,Input Assembler还有两个独特的配置需要注意。一个是Input Layout,另一个是Primitive Topology。
Input Layout
顶点数据被装配以后,它该如何在Shader中被解析被使用?这就需要Input Layout来解决,先来看一段Input Layout创建的实例:

    HRESULT result;
    ID3D11InputLayout *input;
    D3D11_INPUT_ELEMENT_DESC desc[3];
    desc[0].SemanticName = "POSITION";
    desc[0].SemanticIndex = 0;
    desc[0].InputSlot = 0;
    desc[0].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    desc[0].AlignedByteOffset = 0;
    desc[0].InstanceDataStepRate = 0;
    desc[0].Format = DXGI_FORMAT_R32G32B32_FLOAT;

    desc[1].SemanticName = "TEXCOORD";
    desc[1].SemanticIndex = 0;
    desc[1].InputSlot = 0;
    desc[1].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    desc[1].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
    desc[1].InstanceDataStepRate = 0;
    desc[1].Format = DXGI_FORMAT_R32G32_FLOAT;

    desc[2].SemanticName = "NORMAL";
    desc[2].SemanticIndex = 0;
    desc[2].InputSlot = 0;
    desc[2].InputSlotClass = D3D11_INPUT_PER_VERTEX_DATA;
    desc[2].AlignedByteOffset = D3D11_APPEND_ALIGNED_ELEMENT;
    desc[2].InstanceDataStepRate = 0;
    desc[2].Format = DXGI_FORMAT_R32G32B32_FLOAT;

    result = device->CreateInputLayout(desc, 3, vertexShaderBuffer->GetBufferPointer(), vertexShaderBuffer->GetBufferSize(), &input);
    HR(result);

它所对应的Shader中的顶点数据类型的声明如下:

struct VertexInputType
{
    float4 position : POSITION;
    float2 tex : TEXCOORD0; 
    float3 normal : NORMAL;
};

创建一个InputLayout需要提供多个InputLayoutDesc,装配的顶点结构中有几种数据成员就需要有多少个Desc,对于每个Desc需要填充七个参数,SemanticName对应它在HLSL中声明时的标志,这个标志是有可能重复的,对于重复的需要在后边加一个编号,即SemanticIndex。InputSlot代表着该数据成员对应的Buffer在哪一个SLOT中,Format代表它的格式,AlignedByteOffset代表它的首元素在当前Buffer中的偏移量。需要着重注意的是剩余的两个参数:InputSlotClass、InstanceDataStepRate。这里涉及到一项技术:Instance Rendering,在实时渲染中,很多时候需要将一个物体模型重复在多个位置渲染多次,这个时候顶点的数据都是不变的,只是整体的位置有所变化而已,那么这个时候可以将不同的位置作为Vertex Buffer装配,这里不同的位置信息就是相对于每个Instance实例的数据,而不是每个顶点的数据。Input Slot Class就表示当前的这个数据究竟是相对于每个Instance的数据,还是对于每个顶点的数据,而InstanceDataStepRate则表示Instance的数据的变化频率是多少,即渲染多少个Instance会改变这个数据的值,和顶点数据不同,顶点数据是每变化一次顶点数据就要进行改变的。

Primitive Topology
决定顶点所表示的几何类型,有的时候输入的顶点就是三角网格的数据,而有的时候输入的顶点是一系列线段的数据,区分这些几何类型就需要Primitive Topology,它是一个枚举类型。Primitive Topology的所有取值
它的取值可以看这个链接。

Input Assembler Stage Processing
接下来介绍Input Assembler在配置完一些信息后具体做了些什么事情。
Vertex Streams
Input Assembler将输入的各种Vertex Buffer的数据进行逐顶点的整合,最终生成一个Vertex Stream,这个Stream有两种可能的顺序,一种是顶点原本的顺序,另一种是由Index Buffer决定的顺序,决定要用哪种顺序的是 Draw 函数的调用方式,这个之后详述。
Primitive Streams
Vertex Stream又进一步地被提炼为一个Primitive Stream,供后续的某些环节使用,这个Primitive Stream的构建方式取决于配置的Primitive Topology和 Draw函数的调用方式。
Draw函数的影响
Draw函数是流水线的启动函数,只影响Input Assembler的工作方式,它一共有七种形式:

Draw(...)
DrawAuto(...)
DrawIndexed(...)
DrawIndexedInstanced(...)
DrawInstanced(...)
DrawIndexedInstancedIndirect(...)
DrawInstancedIndirect(...)

通常的Draw函数是前两种,调用这样的Draw函数将使Vertex Stream的顺序为顶点输入的顺序,Primitive Stream的构造由Primitive Topology决定,并且构造的顺序也是按照顶点输入的顺序。
Indexed Draw类型的Draw函数包括:DrawIndexed(…)、DrawIndexedInstanced(…)、DrawIndexedInstancedIndirect(…),调用这样的Draw函数会让Vertex Stream按照Index Buffer的挑选顺序来构造,Primitive Stream的构造由Primitive Topology决定,并且构造的顺序按照Index Buffer挑选的顶点顺序。
Instanced Draw类型的Draw函数包括:DrawInstanced(…)、DrawInstancedIndirect(…)、DrawIndexedInstancedIndirect(…)、DrawIndexedInstanced(…),这种Draw函数包含Indexed的和非Indexed的,它对于Vertex和Primitive Stream的影响和前面两种介绍的相同,只是它会让Vertex和Primitive Stream的数量乘上Instance的数量。

Input Assembler Pipelilne Output
Input Assembler的输出体现在Vertex Shader的输入中,在Vertex Shader中可以直接用类似于

struct VertexInputType
{
    float4 position : POSITION;
    float2 tex : TEXCOORD0; 
    float3 normal : NORMAL;
};

代表输入的顶点,其中冒号后边的是用户自定义的成员的一个语义信息,但是要注意,有一些语义是系统预留的,有特殊的含义,这种标志都具有SV_的前缀,Input Assembler可以创建三种这样的语义:SV_VertexID、SV_PrimitiveID、SV_InstanceID,从字面意思上来看它们是区分不同的Vertex、Primitive、Instance的一个标志,其中VertexID和InstanceID在Vertex Shader中就可以使用,而PrimitiveID要等到之后的Hull Shader才可以使用,因为在Vertex Shader中是不考虑Primitive信息的。

Vertex Shader

Vertex Shader是第一个可编程的Shader,它实际上就是提供了一个函数,并对每个顶点都应用这样的一个函数。我们知道Vertex Shader的处理是独立于顶点的,各个顶点之间是互不干扰的,它的输出也是和每个顶点一一对应的。Vertex Shader位于Input Assembler和Hull Shader之间。

Vertex Shader Pipeline Input
Vertex Shader位于Input Assembler之后,显然它的输入肯定与Input Assembler的输出有关,这也就是为什么在创建Input Layout的时候,总是需要提供一个已经编译好的Vertex Shader的Byte Code来确保装配的顶点提供的数据符合Vertex Shader的需求。如前文所述,Input Assembler的输出就体现在HLSL的顶点定义结构体中。这也就是Vertex Shader的输入。

Vertex Shader State Configuration
Vertex Shader需要做的配置有四类:
一是Shader Program本身,需要将Vertex Shader编译,然后利用VSSetShader函数指定,编译的方法在后续章节会介绍。
二是Constant Buffers,在VS中经常会用到变换矩阵,变换矩阵就可以作为Constant Buffer配置。
三是Shader Resource Views,提供一些只读的资源。
四是Samplers,即ID3D11SamplerState,主要蕴含采样的一些配置。

Vertex Shader State Processing
在Vertex Shader中,其实还是可以做很多事情的。通常会做的有几何处理,例如顶点的空间变换,所有需要变换空间的成员都需要通过Matrix Constant Buffer来变换。还可以来做基于顶点的光照,即着色方法采用高洛德着色法的光照。还可以来做顶点贮藏,例如对于某些重复使用的顶点,将其数据存储起来,待到重复使用的时候直接取出,省略掉重复的计算。

Vertex Shader Pipeline Output
VertexShader的输出取决于Vertex Shader之后一些可选的Shader有没有被使用。在流水线中,Vertex Shader之后是跟曲面细分有关的三个阶段,然后是Geometry Shader,然后就是Rasterizer,而中间的曲面细分、Geometry Shader都是可选的,如果不使用的话,Vertex Shader就直接与Rasterizer相连,这个时候需要注意一些事情。
Vertex Shader的输出也是一个结构体,它与输入的顶点结构体是一一对应的,如果Vertex Shader直接与Rasterizer相连,那么这个输出的结构体就必须包含SV_POSITION语义,即顶点变换到裁剪空间下的坐标。而如果中间有可选的Shader时,这个语义就不是必须的,它可以在之后的Shader中完成。而如果曲面细分阶段被使用的话,Vertex Shader的输出自然就需要包含曲面细分阶段所需要的一些数据。
除此以外,Vertex Shader的输出还可以包含一些系统语义:SV_ClipDistance和SV_CullDistance。这两个表示点到平面的有向距离,用来作裁剪和顶点剔除的。对于SV_ClipDistance,如果输出包含这个语义,在之后的流水线中(光栅化之前或者之后),会将那些包含负的ClipDistance值的顶点做裁剪处理。而对于SV_CullDistance,如果一个Primitive的所有的顶点都是负值CullDistance,那么这个Primitive会直接被流水线剔除掉,之后不再处理。这两个语义都提供4个单元,也就是说可以提供4个裁剪平面和4个剔除平面。

Hull Shader

接下来的三个阶段都是跟曲面细分有关的,第一个是Hull Shader。Hull Shader需要完成两个工作,第一个是Hull Shader Program,由输入Control Patch生成输出的Control Patch,所谓的Control Patch可以理解为一个细分的结构,例如如果要细分一个三角形,那么这个三角形的三个控制点就可以看做是一个三角形Control Patch,Hull Shader Program要对每个控制点都运行一次。第二个是Patch Constant Function,它只对每个Control Patch运行一次,用来定义一些细分的参数,它告诉之后的Tessellator该具体如何进行细分。

Hull Shader Pipeline Input
Hull Shader的输入是Control Patch,它在Vertex Shader之后,那么它的输入不应该是叫做顶点吗?其实控制点和顶点没什么大的区别,他们蕴含的信息都是差不多的,只是它们的用途不同,控制点将被用来进一步生成更多的顶点,而顶点就直接被之后的环节进行渲染了,不会用来做其他的操作。如果说Hull Shader被禁用的话,就可以说Vertex Shader是在处理顶点,而如果Hull Shader是活跃的话,那么Vertex Shader其实就是在处理控制点。

HS_CONTROL_POINT_OUTPUT HS(InputPatch<VS_OUTPUT_HS_INPUT, 3> inputPatch, uint uCPID : SV_OutputControlPointID )
{
    HS_CONTROL_POINT_OUTPUT output = (HS_CONTROL_POINT_OUTPUT)0;

    // Copy inputs to outputs
    output.vWorldPos =          inputPatch[uCPID].vWorldPos.xyz;
    output.vNormal =            inputPatch[uCPID].vNormal;
    output.texCoord =           inputPatch[uCPID].texCoord;
    output.vLightTS =           inputPatch[uCPID].vLightTS;
#if ADD_SPECULAR==1
    output.vViewTS =            inputPatch[uCPID].vViewTS;
#endif

    return output;
}

上面是摘自DirectX SDK Sample的一个例子,可以看见,Hull Shader的输入是InputPatch < VS_OUTPUT_HS_INPUT, 3>,其中这个数字3表示Patch中包含控制点的个数。
uint uCPID : SV_OutputControlPointID是必须要有的一个参数,有了它才能确认具体到某个控制点。此外还可以选择使用 SV_PrimitiveID。

Hull Shader State Configuration
作为一个可编程Shader,它与Vertex Shader一样可以配置它的Shader Program、Constant Buffer、Shader Resource View、Sampler States,这些都不赘述。
此外它还需要有一个重要的参数配置,这个是在HLSL中完成的,看下面的例子:

[domain("tri")]
[partitioning("fractional_odd")]
[outputtopology("triangle_cw")]
[outputcontrolpoints(3)]
[patchconstantfunc("ConstantsHS")]
[maxtessfactor(9.0)]

domain:表示进行细分的Patch的Primitive Topology
partitioning:表示如何切割细分的Patch,这将在第四章详细讨论
outputtopology:表示在细分之后得到的Primitive Topology
outputcontrolpoints:表示Hull Shader生成的Patch中控制点的个数
patchconstantfunc:表示Hull Shader第二个工作要用的那个函数名
maxtessfactor:表示细分允许接受的最大系数,它是可选的

Hull Shader Pipeline Processing
前文提到了,Hull Shader需要完成两个工作:
Hull Shader Program:对输入的Control Patch进行处理,得到输出的Control Patch,在这个过过程中,可以对Patch中的控制点做增添、删减、修改。
Tessellation Factor Calculations:这一步主要是计算两个系统语义:SV_TessFactor、SV_InsideTessFactor,这两个语义决定了之后的Tessellation阶段的细分该具体如何处理。这两个参数受配置的参数 domain、partitioning、outputtopology影响。

Hull Shader Pipeline Output
Hull Shader的输出分别对应两个任务完成的结果,一个是输出的Control Patch,另一个是输出的Patch的两个参数SV_TessFactor、SV_InsideTessFactor。

Tessellator

Tessellator是一个固定管线,只能改变它的一些参数。它根据输入的Domain来生成一系列的点,这些点指明了新创建的Vertex的位置。

Tessellator Stage Pipeline Input
它所接受的输入是Hull Shader中第二项工作得到的Tessellator Factor,这些Factor都是简单的浮点数类型,指明了该如何在一个Domain的各个局部进行细分。注意Tessellator是在Domain中作细分,如果配置的Domain是三角形,那么无论输入的Control Patch是什么,它最终输出的结果都是在一个三角形中进行细分后的一系列的点。也就是说它是与输入的Control Patch无关的,它只与Tessellator Factor有关。

Tessellator Stage State Configuration
Tessellator的配置在Hull Shader中实际上已经完成了,与之有关的四个配置为Domain、Partitioning、Output Topology、Max Tessellation Factor。

Tessellator Stage Processing
Tessellator完成两项工作:
Sample Locations:会根据SV_TessFactor、SV_InsideTessFactor的值来决定具体是在Domain的边界附近还是中心附近进行采样,然后生成这些所有采样点的坐标。因此Tessellator Stage实际上并不是在做对数据的处理,而是在做数据的生成。
Primitive Generation:这里可能会有一个疑问,就是Primitive不是在Input Assembler的时候就确定了吗?事实上如果Vertex Shader直接与Rasterizer相连,那么最终输出的结果就是最初指定的Primitive。而如果中间有曲面细分的几个阶段,那么Vertex Shader实际上是在生成控制点,Input Assembler确定的Primitive也只是控制点的Primitive,它并不是最终渲染的Primitive,而最终渲染的Primitive是由这一步生成的。Tessellator生成的Primitive由Hull Shader输入的配置决定。

Tessellator Stage Pipeline Output
Tessellator的任务是生成一系列的点的位置信息,由SV_DomainLocation语义指定。因此它的输出就是SV_DomainLocation。

Domain Shader

Domain Shader也是一个可编程Shader,它的任务是要生成最终能够被Rasterizer渲染的顶点,因此它也可以被认为是整个曲面细分的核心。

Domain Shader Pipeline Input
Domain Shader接受Hull Shader和Tesselator所有的Output,即Control Patch、TesselationFactor、SV_DomainLocation

DS_OUTPUT DS( HS_CONSTANT_DATA_OUTPUT input, 
float3 BarycentricCoordinates : SV_DomainLocation, 
const OutputPatch<HS_CONTROL_POINT_OUTPUT, 3> TrianglePatch )

Domain Shader State Configuration
和Vertex Shader一样,可以配置Shader Program、Constant Buffer、Shader Resource View、SamplerState。

Domain Shader Stage Proccesing
根据SV_DomainLocation提供的重心坐标值,求出生成点的坐标以及法线等信息。这一步可以用来实现Bezier曲线等。

Domain Shader Pipeline Output
Domain Shader的输出原则上是可以直接被Rasterizer处理的顶点,因此必须包含SV_Position语义。

Geometry Shader

Geometry Shader是最后一个可以操控几何信息的流水线阶段,它是可编程的,并且有其他阶段不具备的一些功能:可以增添或删减几何信息、可以通过Stream Output Stage将几何信息传递到Vertex Buffer中、可以创建与输入不同的Primitive。

Geomertry Shader Pipeline Input
还是看一个Geometry Shader输入的例子:

[instance(4)]
[maxvertexcount(3)]
void GSScene( triangleadj GSSceneln input[6],
inout TriangleStream<PSSceneIn> OutputStream )
{
    PSSceneln output = (PSSceneIn)0;
    for ( uint i = 0; i < 6; i += 2 )
    {
        output.Pos = input[i].Pos;
        output.Norm = input[i].Norm;
        output.Tex = input[i].Tex;
        OutputStream.Append( output );
    }
    OutputStream.RestartStrip();
}

首先注意到这个函数与Vertex Shader、Hull Shader之类的区别,它的类型void,即没有返回值。再看它的参数,第一个参数由三部分构成 :Primitive Type、Structure、ID,这个Primitive Type的取值有5种,分别为point、line、triangle、lineadj、triangleadj,后两种是邻接类型的Primitive,即相邻Primitive重叠的顶点会重复使用。第二个参数有显然就是处理后的结果,类似于C++中的引用类型,注意这里的inout修饰符,它表示这个参数既是输入也是输出,在Geometry Shader中,这个参数有三种选择:PointStream、LineStream、TriangleStream,T是传递到Stream中的结构体。
有一点要记住,当曲面细分阶段是活跃状态的时候,Geometry Shader是不可以接受lineadj、triangleadj和point的,因为曲面细分阶段只能产生line和triangle。
Geometry Shader的处理频率为逐Primitive处理,每个Primitive调用一次Geometry Shader。

Geomertry Shader State Configuration
和Vertex Shader一样,可以装配它的Shader Program、Constant Buffer、Shader Resource View和Sampler State,其中Shader Program需要注意,如果不使用Stream Output,应该使用ID3D11Device::CreateGeometryShader函数来创建,如果使用Stream Output的话就应该使用ID3D11Device::CreateGeometryShaderWithStreamOutput函数来创建。
还需要注意到在前面的这段HLSL代码中有两个参数的指定:instance和maxvertexcount,maxvertexcount表示最多可以填入OutputStream中的顶点个数,以确保Geometry Shader不会产生错误数量的顶点;instance用于Geometry Shader Instancing,传递到Geometry Shader的Primitive会被复制多次,次数就是instance的值,用来简化将Geometry传递到多个Output的需求。

Geomertry Shader Stage Processing
Geometry Shader Process Flow
前面的HLSL代码的处理过程中,用到了Append()函数和RestartStrip()函数,这两个函数就是Geometry Shader处理Stream的主要函数。
Append()表示将当前结构体推入Stream中,并自动与之前的结构体合并为Primitive,例如现在Stream中有5个结构体,采用的输出结构是三角形,那么前三个结构体就自动成为一个三角形的三个顶点,而后两个将等待下一个结构体的推入。而如果想要舍弃当前不足以构成Primitive的结构体,可以使用RestartStrip()函数来另起一个Primitive。

Multiple Stream Objects
在GeometryShader中OutputStream并不被局限为1个,可以是多个,参考如下代码:

void MyGS( InVertexverts[2],inout PointStream<OutVertexl> myStream1,
inout PointStream<0utVertex2> myStream2 )
{
    OutVertex1 myVert1 = TransformVertex1( verts[0] );
    OutVertex2 myVert2 = TransformVertex2( verts[1] );
    myStream1.Append(myVert1);
    myStream2.Append(myVert2);
}

如果只使用单个OutputStream的话,这个OutputStream会被送入Rasterizer Stage中,被进一步切成Fragment。而如果使用多个OutputStream的话,只有其中的一个会被送入Rasterizer Stage中,而其余的OutputStream将会被绑定到Stream Output Stage中。
关于多个OutputStream的使用需要考虑到:如果要用到多个OutputStream,那么它们的TopologyType只能是Point,这就意味着不能把Line和Triangle信息传递出去,还有那个被传递到Rasterizer的OutputStream最终被渲染出来的也是单个顶点,因此用到多个OutputStream的时候,一般是不考虑将其中某个传递到Rasterizer阶段的。有的时候可以采用单个OutputStream,将它既传递到Rasterizer,也绑定到Stream Output Stage,这样可以在Rasterizer中保留Primitive信息。

Geomertry Shader Pipeline Output
Geometry Shader的输出也是结构体,它必须包含SV_POSITION语义。除此之外,它还可以有2个SV语义:
SV_RenderTargetIndex:当绑定到Output Merger Stage的Render Target是一个Texture Array的时候,它可以用来指定具体是哪个SLICE被渲染。
SV_ViewportArrayIndex:指定在Rasterizer Stage中具体哪一个Viewport被使用。

Stream Output

Geometry Shader之后有两个分支,一个是直接到Rasterizer,另一个是到Stream Output中,Stream Output的作用就是将GeometryShader得到的结果存储到绑定到Stream Output的CPU的资源中,以实现GPU和CPU的交互。
Stream Output是不可编程的。

Stream Output Pipeline Input

Stream Output的输入只能是在Geometry Shader Program内声明的那些Output Stream,并且至多4个。

Stream Output State Configuration
首先要将拷贝到CPU中的那些资源绑定到Stream Output中

    ID3D11Buffer* pBuffers[1] = { pBuffer1 };
    UINT aOffsets[1] = { 0 };
    pContext->SOSetTargets( 1, pBuffers, offset );

注意这些Buffer的Bind Flag必须是D3D11_BIND_STREAM_OUTPUT
另外一个需要配置的是D3D11_SO_DECLARATION_ENTRY结构体,

struct D3D11_S0_DECLARATI0N_ENTRY {
    UINT Stream;
    LPCSTR SemanticName;
    UINT Semanticlndex;
    BYTE StartComponent;
    BYTE ComponentCount;
    BYTE OutputSlot;
}

第一个参数表示是哪一个Stream,第二个第三个表示结构体成员的语义,第四个第五个是对向量形式的成员的一个局部限定,例如某个成员是一个四维向量(x,y,z,w),如果StartComponent设定为1,Count设定为2,那么最后获得的就是(y,z),第六个参数决定哪一个Stream Output Stage Buffer会获得这个成员数据。填充好这个结构体后,就可以使用CreateGeometryShaderWithStreamOutput函数:

HRESULT Create6eometryShaderWithStream0utput(
    const void *pShaderBytecode,
    SIZE_T BytecodeLength,
    const D3D11_SO_DECLARATION_ENTRY *pSODeclaration,
    UINT NumEntrieS;
    const UINT *pBufferStrideS;
    UINT NumStrides,
    UINT RasterizedStream,
    ID3D11ClassLinkage *pClassLinkage,
    ID3D11GeometryShader **ppGeometryShader
);

其中唯一一个需要注意的是RasterizedStream,它表示被送入Rasterizer的那个OutputStream的编号,如果没有送入Rasterizer的,就置为
D3D11_SO_NO_RASTERIZED_STREAM

Stream Output Stage Processing
被写入Buffer中的Output Stream的数据有两种用途,一种是用来作Automated Drawing,另一种是用来Debug。
Automated Drawing
如果要将Output Stream的结果作为Input Assembler的输入进行渲染,那么可以采用DrawAuto()函数,它会将OutputStream的结果自动的送入Input Assembler,并不与CPU再作任何的交互。可能会有疑问,为什么不让这个Output Stream的结果直接送到Rasterizer中,而是还要让作为输入来继续走一遍这个流水线呢?需要考虑到在Input Assembler到Geometry Shader之前的这几个阶段,其实是非常耗费性能的,计算开销非常大,而有的时候并不需要在每次渲染的时候都对顶点做相应的类似空间变换的操作,于是可以将这些结果保存下来,在之后的渲染中跳过这之间不需要的阶段,从而提升性能,通过这个方式,也可以用来实现多Pass渲染。

Stream Output As a Debugging Tool
即让Output Stream的Buffer被CPU分析从而Debug,要做到这一点需要用到两个Buffer,第一个是Usage为Default的Buffer,它用来接受Output Stream的结果,而Default Usage的Buffer是不能被CPU读取的,那么就需要另一个Buffer,将Usage设为Staging,然后将前一个Buffer的内容拷贝到这个Staging Buffer中。

Stream Output Pipeline Output
Stream Output之后就没有其他的阶段了,它的输出就是那些绑定在它内部的那些Buffer。

Rasterizer

Rasterize即光栅化,它将Geometry转化为可以直接被应用到Buffer中的栅格化数据。这一阶段还要完成三项操作:剔除、裁剪、Scissor Test

Rasterizer Pipeline Input
Rasterizer接受独立的Primitive,Primitive是由顶点来塑造其形状的,每个顶点至少应该包含SV_Position的语义,它是一个四位的齐次坐标。关于齐次坐标、齐次裁剪空间的内容如果不太熟悉的话应该去阅读图形学基础教程。
除了SV_Position,它还可以接受顶点中的一些其他语义。例如SV_ClipDistance和SV_CullDistance,分别代表到裁剪平面和剔除平面的有向距离;SV_ViewportArrayIndex代表在光栅化时将要使用哪一个Viewport;SV_RenderTargetArrayIndex代表最终的结果将要写入到哪一个Render Target Array中的Slice。

Rasterizer Stage State Configuration
Rasterizer有三个东西需要配置:Rasterizer State、Viewport State、Scissor Rectangle State
下面分别来介绍:

typedef struct D3D11_RASTERIZER_DESC {
      D3D11_FILL_MODE FillMode;
      D3D11_CULL_MODE CullMode;
      BOOL            FrontCounterClockwise;
      INT             DepthBias;
      FLOAT           DepthBiasClamp;
      FLOAT           SlopeScaledDepthBias;
      BOOL            DepthClipEnable;
      BOOL            ScissorEnable;
      BOOL            MultisampleEnable;
      BOOL            AntialiasedLineEnable;
};

FillMode:填充模式,包括SOLID FILL和WireFrame,前者是填充颜色,后者是线框模式
CullMode:剔除模式,包括正面剔除和背面剔除
FrontCounterClockWise:表示正面是否为顶点逆时针排列
DepthBias、SlopeScaledDepthBias、DepthBiasClamp:与实现一些特定的算法有关,例如Shadow Mapping,通常全部取0就行了。
DepthClipEnable:是否做深度剔除
ScissorEnable:是否做Scissor Test
MultisampleEnable:是否支持MSAA
AntialiasedLineEnable:当Primitivi Topology为Line时,是否反走样

typedef struct D3D11_VIEWPORT {
      FLOAT TopLeftX;
      FLOAT TopLeftY;
      FLOAT Width;
      FLOAT Height;
      FLOAT MinDepth;
      FLOAT MaxDepth;
};

VIEWPORT的成员都很好理解,就不多说了。
Viewport指的是最终在屏幕上显示的那个区域,它是不改变最终呈现的内容的范围的。

Scissor Rectangle是D3D11_RECT的类型,而D3D11_RECT只是RECT的一个typedef,可以直接看RECT的结构:

typedef struct _RECT {
  LONG left;
  LONG top;
  LONG right;
  LONG bottom;
} RECT, *PRECT;

它所改变的是最终呈现的图像的范围。
举个例子,现在有一个Render Target填充了一副图,那么如果这个时候将Viewport的高度减小到原来的二分之一,那么最终呈现的是将这副图拉窄二分之一的结果;而如果将RECT的高度减小到原来的二分之一,那么最终呈现的是这幅图的一半,而图本身没有做拉伸。

Rasterizer Stage Processing
光栅化的处理过程不是几句话可以总结的了的,想要真正理解这个过程应该试着去写一遍软光栅渲染器,所以这里就不做介绍了。

Rasterizer Stage Pipeline Output
Rasterizer阶段的输出是一系列的Fragment,所谓的Fragment可以就理解为像素,它包含SV_Position语义,在Rasterizer的输入中,SV_Position表示的是顶点在齐次空间下的坐标,而Rasterizer的输出中的SV_Position则表示Fragment的位置。

Pixel Shader

Pixel Shader是最后一个可编程Shader,它主要用来计算每个像素的颜色的,因此它是逐Fragment处理的。每个Fragment的处理互不干扰,是各自独立的。在D3D11中,Pixel Shader又有一些新的能力,包括写入Unordered Access View等,在此之前,Pixel Shader只能写入深度信息和颜色到传递到它的Pixel中,这是一项巨大的突破,用这个特性可以实现很多算法。

Pixel Shader Pipeline Input
Pixel Shader的输入是一个自定义的结构体,其中至少包含SV_POSITION信息,当然也有其他可用的语义,以及自定义的语义,下面来看这些系统语义:
SV_SampleIndex:包含了这个语义就表明Pixel Shader将要以更高的逐-SubSample来处理,会比逐Pixel运行更多次。
SV_Coverage:一个无符号整数,它的每一个比特位分别对应当前这个像素的各个SubSample,表示对应的SubSample是否被rasterizer所覆盖,就是是否被判定在Primitive内部或边界上。
SV_Position:它是经过Rasterizer之后的SV_Position,尽管它也是一个四维的向量,但是它最有用的就是它的X、Y分量,代表这个Fragment在Render Target中的位置。
SV_Depth:表示在NDC空间中的值,范围为[0,1]
SV_RenderTargetArrayIndex:当RenderTarget是一个Array形式的资源的时候,需要用这个来指明具体的Render Target在数组中的索引。
SV_IsFrontFace:表示构建当前Fragment的Primitive是不是Front Face。

Pixel Shader State Configuration
通常Shader的配置就不说了。
Pixel Shader可以写入Unordered Access View,因此它还需要绑定UAV。绑定UAV是通过DeviceContext,但是确切的来说,将UAV绑定到Pixel Shader实际上是把它绑定到了Output Merger阶段,这样做就把Pipeline所有可能的输出都整合在了一个阶段内。

Pixel Shader Stage Processing
在Pixel Shader的处理中,通常都是采用2x2的方式,即并行地同时运行4个PixelShader。
Pixel Shader也可以使用Multiple Render Targets技术,Pixel Shader的结果都是要写入到Render Target中的,而Render Target并没有被限定在1个,它最多可以有8个,而由于是Pixel Shader产生了8个对应的结果,Pixel Shader之前的昂贵的Vertex Shader、Tesselation、Geometry Shader,都是完全相同的,最终这8个结果有可能会被处理合并为1个结果,这就省略掉了重复的Vertex Shader之类的操作,提升了效率。

Pixel Shader Pipeline Output
Pixel Shader的结果是要送到流水线的最后一个阶段Output Merger中的,而Output Merger只能处理颜色和深度信息,额外的信息它是接受不了的。因此Pixel Shader的输出语义被限定为SV_Target[n] (颜色)、SV_Depth(深度)

Output Merger

Output Merger是流水线的最后一个阶段,是不可编程的。这一阶段是合并所有的Fragment,利用颜色和深度信息填充一个Render Target,而在这一过程中,会做三个操作:深度测试、模板测试、混合。

Output Merger Pipeline Input
Output Merger只可以接受颜色和深度信息,颜色信息可以是多个,因为有可能用到了Multiple Render Target技术。

Output Merger State Configuration
为了完成归并Fragment的三个操作,需要配置两个东西:DepthStencilState和BlendState。

typedef struct D3D11_DEPTH_STENCIL_DESC {
      BOOL                       DepthEnable;
      D3D11_DEPTH_WRITE_MASK     DepthWriteMask;
      D3D11_COMPARISON_FUNC      DepthFunc;
      BOOL                       StencilEnable;
      UINT8                      StencilReadMask;
      UINT8                      StencilWriteMask;
      D3D11_DEPTH_STENCILOP_DESC FrontFace;
      D3D11_DEPTH_STENCILOP_DESC BackFace;
};

这个DESC是用来设定模板测试和深度测试的一些参数的。
可以参考如下的创建样例:

    D3D11_DEPTH_STENCIL_DESC desc;

    desc.StencilEnable = true;
    desc.StencilReadMask = 0xFF;
    desc.StencilWriteMask = 0xFF;

    desc.DepthEnable = true;
    desc.DepthFunc = D3D11_COMPARISON_LESS_EQUAL;
    desc.DepthWriteMask = D3D11_DEPTH_WRITE_MASK_ALL;

    desc.FrontFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
    desc.FrontFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
    desc.FrontFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
    desc.FrontFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;

    desc.BackFace.StencilFunc = D3D11_COMPARISON_ALWAYS;
    desc.BackFace.StencilFailOp = D3D11_STENCIL_OP_KEEP;
    desc.BackFace.StencilPassOp = D3D11_STENCIL_OP_REPLACE;
    desc.BackFace.StencilDepthFailOp = D3D11_STENCIL_OP_KEEP;

    result = m_Device->CreateDepthStencilState(&desc, &m_DepthStencilState);
    HR(result);

    //指定 State 到 OutputMergerStage
    m_DeviceContext->OMSetDepthStencilState(m_DepthStencilState, 1);
typedef struct D3D11_BLEND_DESC {
  BOOL                           AlphaToCoverageEnable;
  BOOL                           IndependentBlendEnable;
  D3D11_RENDER_TARGET_BLEND_DESC RenderTarget[8];
};
typedef struct D3D11_RENDER_TARGET_BLEND_DESC {
      BOOL           BlendEnable;
      D3D11_BLEND    SrcBlend;
      D3D11_BLEND    DestBlend;
      D3D11_BLEND_OP BlendOp;
      D3D11_BLEND    SrcBlendAlpha;
      D3D11_BLEND    DestBlendAlpha;
      D3D11_BLEND_OP BlendOpAlpha;
      UINT8          RenderTargetWriteMask;
};

注意除了这两个,还需要绑定好Render Target,不过这个应该在做D3D初始化的时候就应该绑定好了。

Output Merger Stage Processing
深度测试和模板测试统称为可见性测试。一般Depth Stencil View的Format都设定为DXGI_FORMAT_D24_UNORM_S8_UINT,其中24个比特用来做深度测试,其余的8个比特用来做模板测试。可以看见对于深度测试和模板测试的配置中,都指定了比较函数以及未通过和通过时的操作,深度测试无非就是比较当前值与缓冲中的值的大小,而模板测试是具体比较什么的呢?对于模板测试,它有下面这个比较式:
(StencilRef & StencilMask) CompFunc (StencilBufferValue & StencilMask)
这样的一个式子的结果要么为true要么为false,就对应着通过与不通过的情况。
而混合操作,就是根据缓冲中的颜色与当前的颜色做一个怎样的取舍或者混合。

Output Merger Pipeline Output
作为最后一个阶段,它的输出就体现在绑定在它上的Depth Stencil Buffer和Render Target Buffer得到了填充,以及绑定在Pixel Shader中的UAV也得到了修改。如果用到了MSAA的话,最终也需要将MSAA的包含SubSample的Render Target转化至非MSAA的,这样才能在屏幕中显示,这一步要调用ID3D11DeviceContext::ResolveSubresource()。

好了流水线的所有阶段就介绍完了,每个阶段说的都比较简略,其实书中是包含了很多的细节的,最好还是参看这本书的原文。

猜你喜欢

转载自blog.csdn.net/yjr3426619/article/details/81266312