Compartir a través de


Dibujar eficazmente varias instancias de geometría (Direct3D 9)

Dada una escena que contiene muchos objetos que usan la misma geometría, puede dibujar muchas instancias de esa geometría en diferentes orientaciones, tamaños, colores, etc. con un rendimiento considerablemente mejor reduciendo la cantidad de datos que necesita proporcionar al representador.

Esto se puede lograr mediante el uso de dos técnicas: la primera para dibujar geometría indizada y la segunda para la geometría no indizada. Ambas técnicas usan dos búferes de vértices: uno para proporcionar datos de geometría y otro para proporcionar datos de instancia por objeto. Los datos de instancia pueden ser una amplia variedad de información, como una transformación, datos de color o datos de iluminación, básicamente todo lo que se puede describir en una declaración de vértice. Dibujar muchas instancias de geometría con estas técnicas puede reducir drásticamente la cantidad de datos enviados al representador.

Dibujo de geometría indizada

El búfer de vértices contiene datos por vértice definidos por una declaración de vértice. Supongamos que parte de cada vértice contiene datos de geometría y parte de cada vértice contiene datos de instancia por objeto, como se muestra en el diagrama siguiente.

diagrama de un búfer de vértices para geometría indizada

Esta técnica requiere un dispositivo que admita el modelo de sombreador de vértices 3_0. Esta técnica funciona con cualquier sombreador programable, pero no con la canalización de función fija.

Para los búferes de vértices mostrados anteriormente, estas son las declaraciones de búfer de vértices correspondientes:

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

Estas declaraciones definen dos búferes de vértices. La primera declaración (para la secuencia 0, indicada por los ceros de la columna 1) define los datos de geometría que constan de: datos de coordenadas de posición, normal, tangente, binormal y textura.

La segunda declaración (para la secuencia 1, indicada por las de la columna 1) define los datos de instancia por objeto. Cada instancia se define mediante cuatro números de punto flotante de cuatro componentes y un color de cuatro componentes. Los cuatro primeros valores se pueden usar para inicializar una matriz 4x4, lo que significa que estos datos tendrán un tamaño único, posición y rotarán cada instancia de la geometría. Los cuatro primeros componentes usan una semántica de coordenadas de textura que, en este caso, significa "se trata de un número general de cuatro componentes". Cuando se usan datos arbitrarios en una declaración de vértices, use una semántica de coordenadas de textura para marcarlos. El último elemento de la secuencia se usa para los datos de color. Esto se puede aplicar en el sombreador de vértices para proporcionar a cada instancia un color único.

Antes de la representación, debe llamar a SetStreamSourceFreq para enlazar las secuencias del búfer de vértices al dispositivo. Este es un ejemplo que enlaza ambos búferes de vértices:

// 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 usa D3DSTREAMSOURCE_INDEXEDDATA para identificar los datos de geometría indizada. En este caso, la secuencia 0 contiene los datos indizado que describen la geometría del objeto. Este valor se combina lógicamente con el número de instancias de la geometría que se va a dibujar.

Tenga en cuenta que D3DSTREAMSOURCE_INDEXEDDATA y el número de instancias que se van a dibujar siempre deben establecerse en la secuencia cero.

En la segunda llamada, SetStreamSourceFreq usa D3DSTREAMSOURCE_INSTANCEDATA para identificar la secuencia que contiene los datos de la instancia. Este valor se combina lógicamente con 1, ya que cada vértice contiene un conjunto de datos de instancia.

Las dos últimas llamadas a SetStreamSource enlazan los punteros del búfer de vértices al dispositivo.

Cuando haya terminado de representar los datos de instancia, asegúrese de restablecer la frecuencia de flujo de vértices a su estado predeterminado (que no usa la creación de instancias). Dado que en este ejemplo se usan dos secuencias, establezca ambas secuencias como se muestra a continuación:

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

Comparación de rendimiento de geometría indizada

Aunque no es posible realizar una única conclusión sobre la cantidad de esta técnica podría reducir el tiempo de representación en cada aplicación, considere la diferencia en la cantidad de datos transmitidos al tiempo de ejecución y el número de cambios de estado que se reducirán si usa la técnica de creación de instancias. Esta secuencia de representación aprovecha el dibujo de varias instancias de la misma geometría:

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

Observe que el bucle render se llama una vez, los datos de geometría se transmiten una vez y n instancias se transmiten una vez. Esta siguiente secuencia de representación es idéntica en la funcionalidad, pero no aprovecha las ventajas de la creación de instancias:

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

Observe que el bucle de representación completo se ajusta mediante un segundo bucle para dibujar cada objeto. Ahora los datos de geometría se transmiten al representador n veces (en lugar de una vez) y los estados de canalización también se pueden establecer redundantemente para cada objeto dibujado. Es muy probable que esta secuencia de representación sea significativamente más lenta. Observe también que los parámetros de DrawIndexedPrimitive no han cambiado entre los dos bucles de representación.

Dibujo de geometría no indizada

En Dibujo de geometría indizada, los búferes de vértices se configuraron para dibujar varias instancias de geometría indizada con mayor eficacia. También puede usar SetStreamSourceFreq para dibujar geometría no indizada. Esto requiere un diseño de búfer de vértices diferente y tiene restricciones diferentes. Para dibujar geometría no indizada, prepare los búferes de vértices como el diagrama siguiente.

diagrama de un búfer de vértices para geometría no indexada

Esta técnica no es compatible con la aceleración de hardware en ningún dispositivo. Solo es compatible con el procesamiento de vértices de software y solo funcionará con sombreadores de vs_3_0 .

Dado que esta técnica funciona con geometría no indizada, no hay ningún búfer de índices. Como se muestra en el diagrama, el búfer de vértices que contiene geometría contiene n copias de los datos de geometría. Para cada instancia dibujada, los datos de geometría se leen desde el primer búfer de vértices y los datos de instancia se leen desde el segundo búfer de vértices.

Estas son las declaraciones de búfer de vértices correspondientes:

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

Estas declaraciones son idénticas a las declaraciones realizadas en el ejemplo de geometría indizada. Una vez más, la primera declaración (para la secuencia 0) define los datos de geometría y la segunda declaración (para la secuencia 1) define los datos de instancia por objeto. Al crear el primer búfer de vértices, asegúrese de cargarlo con el número de instancias de los datos de geometría que va a dibujar.

Antes de la representación, debe configurar el divisor que indica al tiempo de ejecución cómo dividir el primer búfer de vértices en n instancias. A continuación, establezca el divisor con SetStreamSourceFreq de la siguiente manera:

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

La primera llamada a SetStreamSourceFreq indica que la secuencia 0 contiene n instancias de m vértices. A continuación, SetStreamSource enlaza la secuencia 0 al búfer de vértices de geometría.

En la segunda llamada, SetStreamSourceFreq identifica la secuencia 1 como origen de los datos de la instancia. El segundo parámetro es el número de vértices de cada objeto (m). Recuerde que el flujo de datos de la instancia siempre debe declararse como segundo flujo. A continuación, SetStreamSource enlaza la secuencia 1 al búfer de vértices que contiene los datos de instancia.

Cuando haya terminado de representar los datos de la instancia, asegúrese de restablecer la frecuencia de flujo de vértices a su estado predeterminado. Dado que en este ejemplo se usan dos secuencias, establezca ambas secuencias como se muestra a continuación:

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

Comparación de rendimiento de geometría no indizada

La principal ventaja de este estilo de creación de instancias es que se puede usar en geometría no indizada. Aunque no es posible realizar una única conclusión sobre la cantidad de esta técnica podría reducir el tiempo de representación en cada aplicación, tenga en cuenta la diferencia en la cantidad de datos transmitidos en tiempo de ejecución y el número de cambios de estado que se reducirán para la siguiente secuencia de representación:

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

Observe que el bucle de representación se llama una vez. Los datos de geometría se transmiten una vez, aunque hay n instancias de la geometría que se transmiten. Los datos del búfer de vértices de instancia se transmiten una vez. Esta siguiente secuencia de representación es idéntica en la funcionalidad, pero no aprovecha las ventajas de la creación de instancias:

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

Sin necesidad de crear instancias, el bucle de representación debe encapsularse mediante un segundo bucle para dibujar cada objeto. Al eliminar el segundo bucle de representación, debe esperar un mejor rendimiento debido a menos cambios de estado de representación a los que se llama dentro del bucle.

En general, es razonable esperar que la técnica indizada (Dibujo geometría indexada) funcione mejor que la técnica no indizada (Dibujo geometría no indexada) porque la técnica indizada solo transmite una copia de los datos de geometría. Observe que los parámetros de DrawIndexedPrimitive no han cambiado para ninguna de las secuencias de representación.

Temas avanzados

Ejemplo de creación de instancias