Dessin efficace de plusieurs instances de Geometry (Direct3D 9)

Dans une scène qui contient de nombreux objets qui utilisent la même géométrie, vous pouvez dessiner de nombreuses instances de cette géométrie à différentes orientations, tailles, couleurs, etc. avec de meilleures performances en réduisant la quantité de données que vous devez fournir au convertisseur.

Pour ce faire, vous pouvez utiliser deux techniques : la première pour dessiner une géométrie indexée et la seconde pour la géométrie non indexée. Les deux techniques utilisent deux tampons de vertex : l’un pour fournir des données geometry et l’autre pour fournir des données instance par objet. Les données instance peuvent être un large éventail d’informations, telles qu’une transformation, des données de couleur ou des données d’éclairage, essentiellement tout ce que vous pouvez décrire dans une déclaration de vertex. Dessiner de nombreuses instances de géométrie avec ces techniques peut réduire considérablement la quantité de données envoyées au convertisseur.

Dessin de géométrie indexée

La mémoire tampon de vertex contient des données par sommet qui sont définies par une déclaration de vertex. Supposons qu’une partie de chaque sommet contienne des données géométriques et qu’une partie de chaque sommet contienne des données instance par objet, comme illustré dans le diagramme suivant.

diagramme d’une mémoire tampon de vertex pour la géométrie indexée

Cette technique nécessite un appareil qui prend en charge le modèle de nuanceur de vertex 3_0. Cette technique fonctionne avec n’importe quel nuanceur programmable, mais pas avec le pipeline de fonction fixe.

Pour les mémoires tampons de vertex indiquées ci-dessus, voici les déclarations de tampon de vertex correspondantes :

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

Ces déclarations définissent deux tampons de vertex. La première déclaration (pour le flux 0, indiqué par les zéros de la colonne 1) définit les données géométriques qui se composent des données de coordonnées de position, normales, tangentes, binormales et de texture.

La deuxième déclaration (pour le flux 1, indiqué par ceux de la colonne 1) définit les données instance par objet. Chaque instance est défini par quatre nombres à virgule flottante à quatre composants et une couleur à quatre composants. Les quatre premières valeurs peuvent être utilisées pour initialiser une matrice 4x4, ce qui signifie que ces données vont dimensionner, positionner et faire pivoter de façon unique chaque instance de la géométrie. Les quatre premiers composants utilisent une sémantique de coordonnées de texture qui, dans ce cas, signifie « il s’agit d’un nombre général à quatre composants ». Lorsque vous utilisez des données arbitraires dans une déclaration de vertex, utilisez une sémantique de coordonnées de texture pour les marquer. Le dernier élément du flux est utilisé pour les données de couleur. Cela peut être appliqué dans le nuanceur de vertex pour donner à chaque instance une couleur unique.

Avant le rendu, vous devez appeler SetStreamSourceFreq pour lier les flux de mémoire tampon de vertex à l’appareil. Voici un exemple qui lie les deux tampons de vertex :

// 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 utilise D3DSTREAMSOURCE_INDEXEDDATA pour identifier les données géométriques indexées. Dans ce cas, le flux 0 contient les données indexées qui décrivent la géométrie de l’objet. Cette valeur est logiquement combinée au nombre d’instances de la géométrie à dessiner.

Notez que D3DSTREAMSOURCE_INDEXEDDATA et le nombre d’instances à dessiner doivent toujours être définis dans le flux zéro.

Dans le deuxième appel, SetStreamSourceFreq utilise D3DSTREAMSOURCE_INSTANCEDATA pour identifier le flux contenant les données instance. Cette valeur est logiquement combinée avec 1, car chaque vertex contient un ensemble de données instance.

Les deux derniers appels à SetStreamSource lient les pointeurs de mémoire tampon de vertex à l’appareil.

Lorsque vous avez terminé de restituer les données instance, veillez à rétablir la fréquence du flux de vertex à son état par défaut (qui n’utilise pas l’instanciation). Étant donné que cet exemple utilisait deux flux, définissez les deux flux comme indiqué ci-dessous :

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

Comparaison des performances de la géométrie indexée

Bien qu’il ne soit pas possible de tirer une seule conclusion sur la façon dont cette technique peut réduire le temps de rendu dans chaque application, tenez compte de la différence entre la quantité de données diffusées dans le runtime et le nombre de changements d’état qui seront réduits si vous utilisez la technique d’instanciation. Cette séquence de rendu tire parti du dessin de plusieurs instances de la même géométrie :

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

Notez que la boucle de rendu est appelée une seule fois, que les données geometry sont diffusées une fois et que n instances sont diffusées une seule fois. Cette séquence de rendu suivante est identique dans les fonctionnalités, mais ne tire pas parti de l’instanciation :

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

Notez que la boucle de rendu entière est encapsulée par une deuxième boucle pour dessiner chaque objet. À présent, les données géométriques sont diffusées en continu dans le convertisseur n fois (au lieu d’une seule fois) et tous les états de pipeline peuvent également être définis de manière redondante pour chaque objet dessiné. Cette séquence de rendu est très probablement beaucoup plus lente. Notez également que les paramètres de DrawIndexedPrimitive n’ont pas changé entre les deux boucles de rendu.

Dessin d’une géométrie non indexée

Dans Drawing Indexed Geometry, les tampons de vertex ont été configurés pour dessiner plusieurs instances de géométrie indexée avec une plus grande efficacité. Vous pouvez également utiliser SetStreamSourceFreq pour dessiner une géométrie non indexée. Cela nécessite une disposition de la mémoire tampon de vertex différente et a des contraintes différentes. Pour dessiner une géométrie non indexée, préparez vos mémoires tampons de vertex comme dans le diagramme suivant.

diagramme d’une mémoire tampon de vertex pour la géométrie non indexée

Cette technique n’est prise en charge par l’accélération matérielle sur aucun appareil. Il est uniquement pris en charge par le traitement du vertex logiciel et fonctionne uniquement avec vs_3_0 nuanceurs.

Étant donné que cette technique fonctionne avec la géométrie non indexée, il n’existe aucune mémoire tampon d’index. Comme le montre le diagramme, la mémoire tampon de vertex qui contient geometry contient n copies des données geometry. Pour chaque instance dessinée, les données geometry sont lues à partir de la première mémoire tampon de vertex et les données instance sont lues à partir de la deuxième mémoire tampon de vertex.

Voici les déclarations de tampon de vertex correspondantes :

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

Ces déclarations sont identiques à celles effectuées dans l’exemple de géométrie indexée. Une fois de plus, la première déclaration (pour le flux 0) définit les données geometry et la deuxième déclaration (pour le flux 1) définit les données instance par objet. Lorsque vous créez la première mémoire tampon de vertex, veillez à la charger avec le nombre d’instances des données geometry que vous allez dessiner.

Avant le rendu, vous devez configurer le séparateur qui indique au runtime comment diviser la première mémoire tampon de vertex en n instances. Définissez ensuite le séparateur à l’aide de SetStreamSourceFreq comme suit :

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

Le premier appel à SetStreamSourceFreq indique que le flux 0 contient n instances de sommets m. SetStreamSource lie ensuite le flux 0 à la mémoire tampon de vertex géométrique.

Dans le deuxième appel, SetStreamSourceFreq identifie le flux 1 comme source des données instance. Le deuxième paramètre est le nombre de sommets dans chaque objet (m). N’oubliez pas que le flux de données instance doit toujours être déclaré comme deuxième flux. SetStreamSource lie ensuite le flux 1 à la mémoire tampon de vertex qui contient les données instance.

Lorsque vous avez terminé de restituer les données instance, veillez à rétablir la fréquence du flux de vertex à son état par défaut. Étant donné que cet exemple utilisait deux flux, définissez les deux flux comme indiqué ci-dessous :

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

Comparaison des performances de géométrie non indexées

Le principal avantage de ce style d’instanciation est qu’il peut être utilisé sur une géométrie non indexée. Bien qu’il ne soit pas possible de tirer une seule conclusion sur la façon dont cette technique peut réduire le temps de rendu dans chaque application, tenez compte de la différence dans la quantité de données diffusées dans le runtime et du nombre de modifications d’état qui seront réduites pour la séquence de rendu suivante :

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

Notez que la boucle de rendu est appelée une seule fois. Les données géométriques sont diffusées une seule fois, bien qu’il existe n instances de la géométrie en cours de diffusion. Les données de la mémoire tampon de vertex instance sont diffusées une seule fois. Cette séquence de rendu suivante est identique dans les fonctionnalités, mais ne tire pas parti de l’instanciation :

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

Sans instanciation, la boucle de rendu doit être encapsulée par une deuxième boucle pour dessiner chaque objet. En éliminant la deuxième boucle de rendu, vous devez vous attendre à de meilleures performances en raison de moins de modifications d’état de rendu appelées à l’intérieur de la boucle.

Dans l’ensemble, il est raisonnable de s’attendre à ce que la technique indexée (Drawing Indexed Geometry) fonctionne mieux que la technique non indexée (Drawing Non-Indexed Geometry) car la technique indexée diffuse une seule copie des données geometry. Notez que les paramètres de DrawIndexedPrimitive n’ont pas changé pour les séquences de rendu.

Rubriques avancées

Exemple d’instanciation