Erste Schritte mit der Streamausgabestufe

In diesem Abschnitt wird beschrieben, wie Sie einen Geometrie-Shader mit der Streamausgabestufe verwenden.

Kompilieren eines Geometrie-Shaders

Dieser Geometrie-Shader (GS) berechnet für jedes Dreieck eine Normale und gibt Positions-, Normal- und Texturkoordinatendaten aus.

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

Unter Berücksichtigung dieses Codes sieht ein Geometrie-Shader ähnlich wie ein Vertex- oder Pixelshader aus, aber mit den folgenden Ausnahmen: Der von der Funktion zurückgegebene Datentyp, die Eingabeparameterdeklarationen und die systeminterne Funktion.

Element Beschreibung
Rückgabetyp der Funktion
Der Rückgabetyp der Funktion macht eine Sache, er deklariert nämlich die maximale Anzahl von Scheitelpunkten, die vom Shader ausgegeben werden können. In diesem Fall definiert
maxvertexcount(12)

die Ausgabe auf maximal 12 Scheitelpunkte.

Eingabeparameterdeklarationen

Diese Funktion verwendet zwei Eingabeparameter:

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

Der erste Parameter ist ein Array von Scheitelpunkten (3 in diesem Fall), das durch eine GSPS_INPUT Struktur definiert wird (das die Daten je Vertex als Position, Normal- und Texturkoordinate definiert). Der erste Parameter verwendet auch das Dreieck-Schlüsselwort, was bedeutet, dass die Eingabeassemblerstufe Daten als eines der Dreiecksgrundtypen (Dreiecksliste oder Dreiecksstreifen) an den Geometrie-Shader ausgeben muss.

Der zweite Parameter ist ein Dreiecksstrom, der vom Typ TriangleStream<GSPS_INPUT> definiert wird. Dies bedeutet, dass der Parameter ein Array von Dreiecken ist, von denen jedes aus drei Scheitelpunkten besteht (welche die Daten aus den Elementen von GSPS_INPUT enthalten).

Verwenden Sie die Schlüsselwörter Dreieck und Dreiecksstrom, um einzelne Dreiecke oder einen Dreiecksstrom in einem GS zu identifizieren.

Systeminterne Funktion

Die Codezeilen in der Shader-Funktion verwenden systeminterne HLSL-Funktionen mit allgemeinem Shaderkern außer den letzten beiden Zeilen, die Append und RestartStrip aufrufen. Diese Funktionen sind nur für einen Geometrie-Shader verfügbar. Append informiert den Geometrie-Shader, die Ausgabe an den aktuellen Strip anzufügen; RestartStrip erstellt einen neuen Primitivstreifen. In jedem Aufruf der GS-Stufe wird implizit ein neuer Streifen erstellt.

Der Rest des Shaders ähnelt einem Vertex- oder Pixel-Shader. Der Geometrie-Shader verwendet eine Struktur zum Deklarieren von Eingabeparametern und kennzeichnet das Positionselement mit der SV_POSITION-Semantik, um der Hardware mitzuteilen, dass es sich um Positionsdaten handelt. Die Eingabestruktur identifiziert die anderen beiden Eingabeparameter als Texturkoordinaten (auch wenn einer davon eine Normale enthält). Sie können, wenn Sie es vorziehen, Ihre eigene benutzerdefinierte Semantik für die Normale verwenden.

Rufen Sie nach dem Entwerfen des Geometrie-Shaders D3DCompile auf, um zu kompilieren, wie im folgenden Codebeispiel gezeigt.

DWORD dwShaderFlags = D3DCOMPILE_ENABLE_STRICTNESS;
ID3DBlob** ppShader;

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

Genau wie Vertex- und Pixelshader benötigen Sie eine Shader-Kennzeichnung, um dem Compiler mitzuteilen, wie der Shader kompiliert werden soll (zum Debuggen, optimiert für Geschwindigkeit usw.), die Einstiegspunktfunktion und das Shader-Modell, mit dem überprüft werden soll. In diesem Beispiel wird ein Geometrie-Shader erstellt, der aus der Datei Tutorial13.fx mithilfe der GS-Funktion erstellt wurde. Der Shader wird für Shader-Modell 4.0 kompiliert.

Erstellen eines Geometry-Shader-Objekts mit Streamausgabe

Sobald Sie wissen, dass Sie die Daten aus der Geometrie streamen und den Shader erfolgreich kompiliert haben, besteht der nächste Schritt darin, ID3D11Device::CreateGeometryShaderWithStreamOutput aufzurufen, um das Geometrie-Shaderobjekt zu erstellen.

Zuerst müssen Sie jedoch die Eingabesignatur der Datenstromausgabe (SO) deklarieren. Diese Signatur gleicht die GS-Ausgaben und die SO-Eingaben zum Zeitpunkt der Objekterstellung ab oder überprüft sie. Der folgende Code ist ein Beispiel für die SO-Deklaration.

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

Diese Funktion verwendet mehrere Parameter, unter anderem:

  • Einen Zeiger auf den kompilierten Geometrie-Shader (oder Vertex-Shader, wenn kein Geometrie-Shader vorhanden ist und Daten direkt vom Vertex-Shader gestreamt werden). Informationen zum Abrufen dieses Zeigers finden Sie unter Abrufen eines Zeigers zu einem kompilierten Shader.
  • Ein Zeiger auf ein Array von Deklarationen, das die Eingabedaten für die Datenstromausgabestufe beschreiben. (See D3D11_SO_DECLARATION_ENTRY.) Sie können bis zu 64 Deklarationen bereitstellen; eine für jeden anderen Elementtyp, der aus der SO-Phase ausgegeben werden soll. Das Array von Deklarationseinträgen beschreibt das Datenlayout, unabhängig davon, ob nur ein einzelner Puffer oder mehrere Puffer für die Datenstromausgabe gebunden werden sollen.
  • Die Anzahl der Elemente, die von der SO-Phase geschrieben werden.
  • Ein Zeiger auf das Geometrie-Shader-Objekt, das erstellt wird (siehe ID3D11GeometryShader).

In diesem Fall ist die Pufferstride NULL, der Index des Datenstroms, der an den Rasterizer gesendet werden soll, 0, und die Klassenverbindungsschnittstelle ist NULL.

Die Datenstromausgabedeklaration definiert die Art und Weise, wie Daten in eine Pufferressource geschrieben werden. Sie können der Ausgabedeklaration beliebig viele Komponenten hinzufügen. Verwenden Sie die SO-Phase, um in eine einzelne Pufferressource oder viele Pufferressourcen zu schreiben. Bei einem einzelnen Puffer kann die SO-Phase viele verschiedene Elemente pro Scheitelpunkt schreiben. Bei mehreren Puffern kann die SO-Phase nur ein einzelnes Element pro Scheitelpunktdaten in jeden Puffer schreiben.

Um die SO-Phase ohne Verwendung eines Geometrie-Shaders zu verwenden, rufen Sie ID3D11Device::CreateGeometryShaderWithStreamOutput auf und übergeben Sie einen Zeiger an einen Vertex-Shader an den pShaderBytecode-Parameter.

Festlegen der Ausgabeziele

Der letzte Schritt besteht darin, die SO-Phasenpuffer festzulegen. Daten können später in einen oder mehrere Puffer im Arbeitsspeicher gestreamt werden. Der folgende Code zeigt, wie Sie einen einzelnen Puffer erstellen, der für Scheitelpunktdaten sowie für die SO-Phase zum Streamen von Daten verwendet werden kann.

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

Erstellen Sie einen Puffer durch Aufrufen von ID3D11Device::CreateBuffer. In diesem Beispiel wird die Standardverwendung veranschaulicht, die typisch für eine Pufferressource ist, die von der CPU relativ häufig aktualisiert wird. Das Bindungskennzeichnung identifiziert die Pipelinephase, an welche die Ressource gebunden werden kann. Alle Ressourcen, die von der SO-Phase verwendet werden, müssen auch mit dem Bindungskennzeichen D3D10_BIND_STREAM_OUTPUT erstellt werden.

Nachdem der Puffer erfolgreich erstellt wurde, legen Sie ihn auf das aktuelle Gerät fest, indem Sie ID3D11DeviceContext::SOSetTargets aufrufen:

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

Dieser Aufruf verwendet die Anzahl der Puffer, einen Zeiger auf die Puffer und ein Array von Offsets (ein Offset in jeden der Puffer, der angibt, wo mit dem Schreiben von Daten begonnen werden soll). Daten werden in diese Streamingausgabepuffer geschrieben, wenn eine Draw-Funktion aufgerufen wird. Eine interne Variable verfolgt die Position, an der mit dem Schreiben von Daten in die Streamingausgabepuffer begonnen werden soll, und diese Variablen werden weiter erhöht, bis SOSetTargets erneut aufgerufen wird und ein neuer Offsetwert angegeben wird.

Alle Daten, die in die Zielpuffer geschrieben wurden, sind 32-Bit-Werte.

Streamausgabestufe