Freigeben über


Effizientes Zeichnen mehrerer Instanzen von Geometrie (Direct3D 9)

Bei einer Szene, die viele Objekte enthält, die dieselbe Geometrie verwenden, können Sie viele Instanzen dieser Geometrie mit unterschiedlichen Ausrichtungen, Größen, Farben usw. zeichnen und so weiter eine deutlich bessere Leistung erzielen, indem Sie die Menge an Daten reduzieren, die Sie für den Renderer bereitstellen müssen.

Dies kann durch die Verwendung von zwei Techniken erreicht werden: die erste zum Zeichnen indizierter Geometrie und die zweite für nicht indizierte Geometrie. Beide Techniken verwenden zwei Vertexpuffer: einen zum Bereitstellen von Geometriedaten und einen zum Bereitstellen von Objekt-instance Daten. Die instance Daten können eine Vielzahl von Informationen wie eine Transformation, Farbdaten oder Lichtdaten sein – im Grunde alles, was Sie in einer Vertexdeklaration beschreiben können. Das Zeichnen vieler Instanzen von Geometrie mit diesen Techniken kann die Menge an Daten, die an den Renderer gesendet werden, erheblich reduzieren.

Zeichnen indizierte Geometrie

Der Vertexpuffer enthält Vertexdaten pro Vertex, die durch eine Vertexdeklaration definiert werden. Angenommen, ein Teil jedes Scheitelpunkts enthält Geometriedaten und ein Teil jedes Scheitelpunkts enthält daten pro Objekt instance, wie im folgenden Diagramm dargestellt.

Diagramm eines Scheitelpunktpuffers für indizierte Geometrie

Für diese Technik ist ein Gerät erforderlich, das das Vertex-Shadermodell 3_0 unterstützt. Dieses Verfahren funktioniert mit jedem programmierbaren Shader, aber nicht mit der festen Funktionspipeline.

Für die oben gezeigten Vertexpuffer sind die entsprechenden Vertexpufferdeklarationen aufgeführt:

const D3DVERTEXELEMENT9 g_VBDecl_Geometry[] =
{
{0,  0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL,   0},
{0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TANGENT,  0},
{0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BINORMAL, 0},
{0, 48, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};

const D3DVERTEXELEMENT9 g_VBDecl_InstanceData[] =
{
{1, 0,  D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1},
{1, 16, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 2},
{1, 32, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 3},
{1, 48, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 4},
{1, 64, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR,    0},
D3DDECL_END()
};

Diese Deklarationen definieren zwei Vertexpuffer. Die erste Deklaration (für Stream 0, die durch die Nullen in Spalte 1 angegeben wird) definiert die Geometriedaten, die aus folgenden Koordinaten bestehen: Positions-, Normal-, Tangenten-, Binormal- und Texturkoordinatendaten.

Die zweite Deklaration (für Stream 1, die durch die in Spalte 1 angegeben wird) definiert die Objekt-instance Daten. Jede instance wird durch vier Gleitkommazahlen mit vier Komponenten und eine Vierkomponentenfarbe definiert. Die ersten vier Werte können verwendet werden, um eine 4x4-Matrix zu initialisieren, was bedeutet, dass diese Daten jede instance der Geometrie eindeutig vergrößern, positionieren und rotieren. Die ersten vier Komponenten verwenden eine Texturkoordinatensemantik, was in diesem Fall bedeutet: "Dies ist eine allgemeine Vier-Komponenten-Zahl". Wenn Sie beliebige Daten in einer Vertexdeklaration verwenden, verwenden Sie eine Texturkoordinatensemantik, um sie zu markieren. Das letzte Element im Stream wird für Farbdaten verwendet. Dies kann im Vertex-Shader angewendet werden, um jedem instance eine eindeutige Farbe zu verleihen.

Vor dem Rendern müssen Sie SetStreamSourceFreq aufrufen, um die Vertexpufferdatenströme an das Gerät zu binden. Hier sehen Sie ein Beispiel, das beide Vertexpuffer bindet:

// Set up the geometry data stream
pd3dDevice->SetStreamSourceFreq(0,
    (D3DSTREAMSOURCE_INDEXEDDATA | g_numInstancesToDraw));
pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
    D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

// Set up the instance data stream
pd3dDevice->SetStreamSourceFreq(1,
    (D3DSTREAMSOURCE_INSTANCEDATA | 1));
pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
    D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

SetStreamSourceFreq verwendet D3DSTREAMSOURCE_INDEXEDDATA , um die indizierten Geometriedaten zu identifizieren. In diesem Fall enthält Stream 0 die indizierten Daten, die die Objektgeometrie beschreiben. Dieser Wert wird logisch mit der Anzahl der zu zeichnenden Instanzen der Geometrie kombiniert.

Beachten Sie, dass D3DSTREAMSOURCE_INDEXEDDATA und die Anzahl der zu zeichnenden Instanzen immer in Stream Null festgelegt werden müssen.

Im zweiten Aufruf verwendet SetStreamSourceFreqD3DSTREAMSOURCE_INSTANCEDATA, um den Stream zu identifizieren, der die instance Daten enthält. Dieser Wert wird logisch mit 1 kombiniert, da jeder Scheitelpunkt einen Satz instance Daten enthält.

Die letzten beiden Aufrufe von SetStreamSource binden die Vertexpufferzeiger an das Gerät.

Wenn Sie mit dem Rendern der instance Daten fertig sind, stellen Sie sicher, dass Sie die Vertexstreamhäufigkeit wieder auf den Standardzustand zurücksetzen (wobei keine Instancing verwendet wird). Da in diesem Beispiel zwei Streams verwendet wurden, legen Sie beide Streams wie unten gezeigt fest:

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(1,1);

Leistungsvergleich für indizierte Geometrie

Es ist zwar nicht möglich, eine einzige Schlussfolgerung darüber zu treffen, wie sehr diese Technik die Renderzeit in jeder Anwendung reduzieren könnte, berücksichtigen Sie jedoch den Unterschied in der Menge an Daten, die in die Laufzeit gestreamt werden, und die Anzahl der Zustandsänderungen, die reduziert werden, wenn Sie die Instancing-Technik verwenden. Diese Rendersequenz nutzt das Zeichnen mehrerer Instanzen derselben Geometrie:

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    // Set up the geometry data stream
    pd3dDevice->SetStreamSourceFreq(0,
                (D3DSTREAMSOURCE_INDEXEDDATA | g_numInstancesToDraw));
    pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

    // Set up the instance data stream
    pd3dDevice->SetStreamSourceFreq(1,
                (D3DSTREAMSOURCE_INSTANCEDATA | 1));
    pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
                D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

    pd3dDevice->SetVertexDeclaration( ... );
    pd3dDevice->SetVertexShader( ... );
    pd3dDevice->SetIndices( ... );

    pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    
    pd3dDevice->EndScene();
}

Beachten Sie, dass die Renderschleife einmal aufgerufen wird, die Geometriedaten einmal gestreamt und n Instanzen einmal gestreamt werden. Diese nächste Rendersequenz ist in der Funktionalität identisch, nutzt jedoch nicht die Vorteile der Instancing:

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    for(int i=0; i < g_numObjects; i++)
    {
        pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));


        pd3dDevice->SetVertexDeclaration( ... );
        pd3dDevice->SetVertexShader( ... );
        pd3dDevice->SetIndices( ... );

        pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    }                             
    
    pd3dDevice->EndScene();
}

Beachten Sie, dass die gesamte Renderschleife von einer zweiten Schleife umschlossen wird, um jedes Objekt zu zeichnen. Jetzt werden die Geometriedaten n mal (statt einmal) in den Renderer gestreamt, und alle Pipelinezustände können auch redundant für jedes gezeichnete Objekt festgelegt werden. Diese Rendersequenz ist sehr wahrscheinlich deutlich langsamer. Beachten Sie auch, dass sich die Parameter in DrawIndexedPrimitive zwischen den beiden Renderschleifen nicht geändert haben.

Zeichnen von nicht indizierten Geometrien

In Zeichnung indizierte Geometrie wurden Vertexpuffer konfiguriert, um mehrere Instanzen der indizierten Geometrie mit größerer Effizienz zu zeichnen. Sie können auch SetStreamSourceFreq verwenden, um nicht indizierte Geometrie zu zeichnen. Dies erfordert ein anderes Vertexpufferlayout und weist unterschiedliche Einschränkungen auf. Um nicht indizierte Geometrie zu zeichnen, bereiten Sie Ihre Vertexpuffer wie im folgenden Diagramm vor.

Diagramm eines Vertexpuffers für nicht indizierte Geometrie

Dieses Verfahren wird von der Hardwarebeschleunigung auf keinem Gerät unterstützt. Sie wird nur von der Softwarevertexverarbeitung unterstützt und funktioniert nur mit vs_3_0 Shadern.

Da dieses Verfahren mit nicht indizierten Geometrien funktioniert, gibt es keinen Indexpuffer. Wie das Diagramm zeigt, enthält der Vertexpuffer, der geometrie enthält, n Kopien der Geometriedaten. Für jede gezeichnete instance werden die Geometriedaten aus dem ersten Vertexpuffer gelesen und die instance Daten aus dem zweiten Vertexpuffer gelesen.

Hier sind die entsprechenden Vertexpufferdeklarationen:

const D3DVERTEXELEMENT9 g_VBDecl_Geometry[] =
{
{0,  0, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_POSITION, 0},
{0, 12, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_NORMAL,   0},
{0, 24, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TANGENT,  0},
{0, 36, D3DDECLTYPE_FLOAT3, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_BINORMAL, 0},
{0, 48, D3DDECLTYPE_FLOAT2, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 0},
D3DDECL_END()
};

const D3DVERTEXELEMENT9 g_VBDecl_InstanceData[] =
{
{1, 0,  D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 1},
{1, 16, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 2},
{1, 32, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 3},
{1, 48, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_TEXCOORD, 4},
{1, 64, D3DDECLTYPE_FLOAT4, D3DDECLMETHOD_DEFAULT, D3DDECLUSAGE_COLOR,    0},
D3DDECL_END()
};

Diese Deklarationen sind mit den Deklarationen identisch, die im Beispiel für die indizierte Geometrie vorgenommen werden. Erneut definiert die erste Deklaration (für Stream 0) die Geometriedaten und die zweite Deklaration (für Stream 1) die Objekt-instance Daten. Wenn Sie den ersten Scheitelpunktpuffer erstellen, müssen Sie ihn mit der Anzahl der Instanzen der Geometriedaten laden, die Sie zeichnen.

Vor dem Rendern müssen Sie den Dividator einrichten, der der Runtime mitteilt, wie der erste Vertexpuffer in n Instanzen unterteilt werden soll. Legen Sie dann den Teiler mithilfe von SetStreamSourceFreq wie folgt fest:

// Set the divider
pd3dDevice->SetStreamSourceFreq(0, 1);
// Bind the stream to the vertex buffer
pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
        D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

// Set up the instance data stream
pd3dDevice->SetStreamSourceFreq(1, verticesPerInstance);
pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
        D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

Der erste Aufruf von SetStreamSourceFreq besagt, dass Stream 0 n Instanzen von m-Scheitelpunkten enthält. SetStreamSource bindet dann Stream 0 an den Geometrievertexpuffer.

Im zweiten Aufruf identifiziert SetStreamSourceFreq Stream 1 als Quelle der instance Daten. Der zweite Parameter ist die Anzahl der Scheitelpunkte in jedem Objekt (m). Denken Sie daran, dass der instance Datenstrom immer als zweiter Datenstrom deklariert werden muss. SetStreamSource bindet dann Stream 1 an den Vertexpuffer, der die instance Daten enthält.

Wenn Sie das Rendern der instance Daten abgeschlossen haben, stellen Sie sicher, dass Sie die Vertexstreamfrequenz wieder auf den Standardzustand zurücksetzen. Da in diesem Beispiel zwei Streams verwendet wurden, legen Sie beide Streams wie unten gezeigt fest:

pd3dDevice->SetStreamSourceFreq(0,1);
pd3dDevice->SetStreamSourceFreq(1,1);

Leistungsvergleich für nicht indizierte Geometrie

Der Hauptvorteil dieses Instancingstils besteht darin, dass er für nicht indizierte Geometrien verwendet werden kann. Obwohl es nicht möglich ist, eine einzige Schlussfolgerung darüber zu treffen, wie sehr diese Technik die Renderzeit in jeder Anwendung reduzieren könnte, berücksichtigen Sie den Unterschied in der Menge an Daten, die in die Runtime gestreamt werden, und die Anzahl der Zustandsänderungen, die für die folgende Rendersequenz reduziert werden:

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    // Set the divider
    pd3dDevice->SetStreamSourceFreq(0, 1);
    pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

    // Set up the instance data stream
    pd3dDevice->SetStreamSourceFreq(1, verticesPerInstance));
    pd3dDevice->SetStreamSource(1, g_VB_InstanceData, 0, 
                D3DXGetDeclVertexSize( g_VBDecl_InstanceData, 1 ));

    pd3dDevice->SetVertexDeclaration( ... );
    pd3dDevice->SetVertexShader( ... );
    pd3dDevice->SetIndices( ... );

    pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    
    pd3dDevice->EndScene();
}

Beachten Sie, dass die Renderschleife einmal aufgerufen wird. Die Geometriedaten werden einmal gestreamt, obwohl es n Instanzen der Geometrie gibt, die gestreamt werden. Die Daten aus dem instance Vertexpuffers werden einmal gestreamt. Diese nächste Rendersequenz ist in der Funktionalität identisch, nutzt jedoch nicht die Vorteile der Instancing:

if( SUCCEEDED( pd3dDevice->BeginScene() ) )
{
    for(int i=0; i < g_numObjects; i++)
    {
        pd3dDevice->SetStreamSource(0, g_VB_Geometry, 0,
                D3DXGetDeclVertexSize( g_VBDecl_Geometry, 0 ));

        pd3dDevice->SetVertexDeclaration( ... );
        pd3dDevice->SetVertexShader( ... );
        pd3dDevice->SetIndices( ... );

        pd3dDevice->DrawIndexedPrimitive( D3DPT_TRIANGLELIST, 0, 0, 
                g_dwNumVertices, 0, g_dwNumIndices/3 );
    }
    
    pd3dDevice->EndScene();
}

Ohne Instancing muss die Renderschleife von einer zweiten Schleife umschlossen werden, um jedes Objekt zu zeichnen. Wenn Sie die zweite Renderschleife entfernen, sollten Sie eine bessere Leistung erwarten, da weniger Änderungen am Renderzustand vorgenommen werden, die innerhalb der Schleife aufgerufen werden.

Insgesamt ist es vernünftig zu erwarten, dass die indizierte Technik (Zeichnen indizierte Geometrie) eine bessere Leistung als die nicht indizierte Technik (Zeichnen von nicht indizierten Geometrien) aufweist, da die indizierte Technik nur eine Kopie der Geometriedaten streamt. Beachten Sie, dass sich die Parameter in DrawIndexedPrimitive für keine der Rendersequenzen geändert haben.

Weiterführende Themen

Instancing-Beispiel