流输出阶段入门

本节介绍如何将几何着色器与流输出阶段结合使用。

编译几何着色器

此几何着色器 (GS) 会计算每个三角形的面法线,并输出位置、法线和纹理坐标数据。

struct GSPS_INPUT
{
    float4 Pos : SV_POSITION;
    float3 Norm : TEXCOORD0;
    float2 Tex : TEXCOORD1;
};

[maxvertexcount(12)]
void GS( triangle GSPS_INPUT input[3], inout TriangleStream<GSPS_INPUT> TriStream )
{
    GSPS_INPUT output;
    
    //
    // Calculate the face normal
    //
    float3 faceEdgeA = input[1].Pos - input[0].Pos;
    float3 faceEdgeB = input[2].Pos - input[0].Pos;
    float3 faceNormal = normalize( cross(faceEdgeA, faceEdgeB) );
    float3 ExplodeAmt = faceNormal*Explode;
    
    //
    // Calculate the face center
    //
    float3 centerPos = (input[0].Pos.xyz + input[1].Pos.xyz + input[2].Pos.xyz)/3.0;
    float2 centerTex = (input[0].Tex + input[1].Tex + input[2].Tex)/3.0;
    centerPos += faceNormal*Explode;
    
    //
    // Output the pyramid
    //
    for( int i=0; i<3; i++ )
    {
        output.Pos = input[i].Pos + float4(ExplodeAmt,0);
        output.Pos = mul( output.Pos, View );
        output.Pos = mul( output.Pos, Projection );
        output.Norm = input[i].Norm;
        output.Tex = input[i].Tex;
        TriStream.Append( output );
        
        int iNext = (i+1)%3;
        output.Pos = input[iNext].Pos + float4(ExplodeAmt,0);
        output.Pos = mul( output.Pos, View );
        output.Pos = mul( output.Pos, Projection );
        output.Norm = input[iNext].Norm;
        output.Tex = input[iNext].Tex;
        TriStream.Append( output );
        
        output.Pos = float4(centerPos,1) + float4(ExplodeAmt,0);
        output.Pos = mul( output.Pos, View );
        output.Pos = mul( output.Pos, Projection );
        output.Norm = faceNormal;
        output.Tex = centerTex;
        TriStream.Append( output );
        
        TriStream.RestartStrip();
    }
    
    for( int i=2; i>=0; i-- )
    {
        output.Pos = input[i].Pos + float4(ExplodeAmt,0);
        output.Pos = mul( output.Pos, View );
        output.Pos = mul( output.Pos, Projection );
        output.Norm = -input[i].Norm;
        output.Tex = input[i].Tex;
        TriStream.Append( output );
    }
    TriStream.RestartStrip();
}

记住该代码,几何着色器看起来很像顶点或像素着色器,但有以下例外:函数返回的类型、输入参数声明和内部函数。

说明
函数返回类型
函数返回类型只执行一项操作,即声明着色器可以输出的最大顶点数。 在本例中,
maxvertexcount(12)

将输出定义为最多 12 个顶点。

输入参数声明

此函数采用两个输入参数:

triangle GSPS_INPUT input[3] , inout TriangleStream<GSPS_INPUT> TriStream

第一个参数是由 GSPS_INPUT 结构定义的顶点数组(本例中为 3 个)(它将每个顶点数据定义为位置、法线和纹理坐标)。 第一个参数还使用关键字 triangle,这意味着输入汇编程序阶段必须将数据作为三角形基元类型(三角形列表或三角形条带)之一输出到几何着色器。

第二个参数是由类型 TriangleStream<GSPS_INPUT> 定义的三角形流。 这意味着参数是一个三角形数组,每个三角形由三个顶点(包含来自 GSPS_INPUT 成员的数据)组成。

使用 triangle 和trianglestream 关键字来标识 GS 中的单个三角形或三角形流。

内部函数

着色器函数中的代码行使用 common-shader-core HLSL 内部函数,但最后两行除外,它们调用 Append 和 RestartStrip。 这些函数仅适用于几何着色器。 Append 会通知几何着色器将输出附加到当前条带;RestartStrip 将创建一个新的原始条带。 在 GS 阶段的每次调用中都会隐式创建一个新条带。

着色器的其余部分看起来与顶点或像素着色器非常相似。 几何着色器使用结构体来声明输入参数,并使用 SV_POSITION 语义标记位置成员,以指示硬件这是位置数据。 输入结构将其他两个输入参数标识为纹理坐标(即使其中一个包含面法线)。 如果愿意,可以对面部法线使用你自己的自定义语义。

设计好几何着色器后,调用 D3DCompile 进行编译,如以下代码示例所示。

DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
ID3DBlob** ppShader;

D3DCompile( pSrcData, sizeof( pSrcData ), 
  "Tutorial13.fx", NULL, NULL, "GS", "gs_4_0", 
  dwShaderFlags, 0, &ppShader, NULL );

与顶点和像素着色器一样,你需要一个着色器标志来告诉编译程序你希望如何编译着色器(用于调试、优化速度等)、入口点函数以及要验证的着色器模型。 此示例使用 GS 函数创建从Tutorial13.fx 文件构建的几何着色器。 该着色器是针对着色器模型 4.0 编译的。

使用流输出创建几何着色器对象

一旦你知道将从几何体流式传输数据,并且已成功编译着色器,下一步就是调用 ID3D11Device::CreateGeometryShaderWithStreamOutput 来创建几何着色器对象。

但首先,你需要声明流输出 (SO) 阶段输入签名。 该签名会在创建对象时匹配或验证 GS 输出和 SO 输入。 以下代码是 SO 声明的示例。

D3D11_SO_DECLARATION_ENTRY pDecl[] =
{
    // semantic name, semantic index, start component, component count, output slot
    { "SV_POSITION", 0, 0, 4, 0 },   // output all components of position
    { "TEXCOORD0", 0, 0, 3, 0 },     // output the first 3 of the normal
    { "TEXCOORD1", 0, 0, 2, 0 },     // output the first 2 texture coordinates
};

D3D11Device->CreateGeometryShaderWithStreamOut( pShaderBytecode, ShaderBytecodesize, pDecl, 
    sizeof(pDecl), NULL, 0, 0, NULL, &pStreamOutGS );

该函数需要几个参数,包括:

  • 指向已编译几何着色器的指针(如果不存在几何着色器,并且数据将直接从顶点着色器流出,则为顶点着色器)。 有关如何获取此指针的信息,请参阅获取指向已编译着色器的指针
  • 指向描述流输出阶段的输入数据的声明数组的指针。 (请参阅 D3D11_SO_DECLARATION_ENTRY。)最多可以提供 64 个声明,每个声明对应于要从 SO 阶段输出的每种不同类型的元素。 声明条目数组描述了数据布局,无论是仅为流输出绑定单个缓冲区还是绑定多个缓冲区。
  • SO 阶段写出的元素数量。
  • 指向所创建的几何着色器对象的指针(请参阅 ID3D11GeometryShader)。

在这种情况下,缓冲区步长为 NULL,要发送到光栅化器的流的索引为 0,并且类链接接口为 NULL。

流输出声明定义了将数据写入缓冲区资源的方式。 可以在输出声明中添加任意数量的组件。 使用 SO 阶段写入单个缓冲区资源或多个缓冲区资源。 对于单个缓冲区,SO 阶段可以为每个顶点写入许多不同的元素。 对于多个缓冲区,SO 阶段只能将每个顶点数据的单个元素写入每个缓冲区。

若要使用 SO 阶段而不使用几何着色器,请调用 ID3D11Device::CreateGeometryShaderWithStreamOutput 并将指向顶点着色器的指针传递给 pShaderBytecode 参数。

设置输出目标

最后一步是设置 SO 阶段缓冲区。 数据可以流入内存中的一个或多个缓冲区,以供以后使用。 以下代码演示了如何创建可用于顶点数据以及用于将数据流式传输到的 SO 阶段的单个缓冲区。

ID3D11Buffer *m_pBuffer;
int m_nBufferSize = 1000000;

D3D11_BUFFER_DESC bufferDesc =
{
    m_nBufferSize,
    D3D11_USAGE_DEFAULT,
    D3D11_BIND_STREAM_OUTPUT,
    0,
    0,
    0
};
D3D11Device->CreateBuffer( &bufferDesc, NULL, &m_pBuffer );

通过调用 ID3D11Device::CreateBuffer 创建缓冲区。 此示例说明了默认使用情况,这对于预计由 CPU 相当频繁更新的缓冲区资源来说是典型的。 绑定标志标识资源可以绑定到的管道阶段。 SO 阶段使用的任何资源也必须使用绑定标志 D3D10_BIND_STREAM_OUTPUT 创建。

成功创建缓冲区后,通过调用 ID3D11DeviceContext::SOSetTargets 将其设置为当前设备:

UINT offset[1] = 0;
D3D11Device->SOSetTargets( 1, &m_pBuffer, offset );

此调用需要缓冲区的数量、指向缓冲区的指针和一组偏移量(每个缓冲区中的一个偏移量指示从何处开始写入数据)。 当调用绘制函数时,数据将被写入这些流输出缓冲区。 内部变量跟踪开始将数据写入流输出缓冲区的位置,并且该变量将继续递增,直到再次调用 SOSetTargets 并指定新的偏移值。

写出到目标缓冲区的所有数据都将是 32 位值。

流输出阶段