\\ Home : Articoli : Stampa
Geometry Instancing
Di RobyDx (del 29/07/2007 @ 08:33:01, in DirectX9, linkato 1860 volte)

Nonostante la potenza dell'hardware aumenti di anno in anno sembra non sia mai sufficiente il numero di poligoni da renderizzare. Infatti anche se le moderne schede video promettono di gestire centinaia di milioni di poligoni al secondo, in realtà questo numero scende drasticamente senza oppurtune ottimizzazioni. Il problema si manifesta soprattutto quando occorre mandare a schermo tantissimi oggetti separatamente.

Una scena molto complessa viene di solito renderizzata con pochi passaggi cercando di raggruppare i poligoni in modo da fare meno rendering possibili. In genere il motore grafico raggruppa tutti gli elementi con le stesse caratteristiche, li fonde insieme ed effettua il draw di molti vertici contemporaneamente (tecnica di batching). Al contrario non è possibile renderizzare insieme poligoni dinamici perchè la posizione dei vertici cambia continuamente e di conseguenza o si modificano i vertici nel vertexbuffer per ogni frame o si fa il draw separatamente per ognuno. Entrambe le soluzioni uccidono il frame rate. Un simile problema era già stato riscontrato per i point sprite, i sistemi particellari. Occorreva renderizzare tante immagini, ad esempio delle scintille, e non si poteva fare il draw per centinaia o migliaia di sprite. DirectX introdusse quindi un sistema che permettesse di gestire tanti sprite semplicemente come un array di punti ottimizzando moltissimo le prestazioni.

Il geometry instancing è qualcosa di simile ma applicato a modelli interamente 3D. Attraverso il device è possibile caricare un modello 3D e moltiplicarlo sullo schermo dando ad ognuna di queste copie caratteristiche specifiche di posizione, colore o qualsiasi altra cosa vogliate; il tutto accellerato dall'hardware.

Unica nota dolente è che solo le schede video con tecnologia shader 3.0 implementano tale caratteristica e sarà quindi necessario aspettare ancora un pò prima di vedere tale tecnologia sfruttata appieno.

Spieghiamo o bene il funzionamento. Prendiamo come esempio un cubo che vogliamo moltiplicare sullo schermo 1000 volte. Occorrono 2 vertexbuffer: il primo sarà il vertexbuffer di questo cubo mentre il secondo sarà un nuovo vertexbuffer contenente 1000 vertici con questa struttura.

geometryInstance

Structure cubeData
    Dim posizione As Matrix
    Dim colore As Vector4
End Structure

Questa struttura è in grado di contenere una matrice (world x view x projection) ed un colore. DirectX non farà altro che fondere i 2 stream unendo tutti i vertici del modello con tutti i vertici di questo secondo buffer.

device.SetStreamSourceFrequency(0, (1 << 30) Or 1000)
device.SetStreamSourceFrequency(1, (2 << 30) Or 1)
device.SetStreamSource(0, cubo.VertexBuffer, 0, 32)
device.SetStreamSource(1, instanceBuffer, 80)
device.Indices = cubo.IndexBuffer

Le prime 2 istruzioni servono ad impostare la frequenza degli stream. Infatti il primo stream deve essere ripetuto 1000 volte (vogliamo 1000 cubi), mentre il secondo solo 1 volta (sono i mille vertici che contengono la matrice di posizione ed il colore). Dopo di che imposteremo i buffer nello stream. Nell'esempio la variabile cubo è una mesh. Occorrerà impostare la dimensione in byte dei buffer e l'indexbuffer della mesh. Ora semplicemente occorre renderizzare il tutto con un drawprimitive. I valori 1<<30 e 2<<30 sono 2 valori usati da directX. Stranamente non hanno introdotto delle costanti e non ne capisco il motivo.

device.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, cubo.NumberVertices, 0, cubo.NumberFaces)

Questo è il ciclo di rendering. Attenzione però. i dati non vengono letti automaticamente da directX. Occorre infatti adoperare obbligatoriamente uno shader per funzionare. Occorre quindi dichiarare per bene l'instance buffer

 instanceBuffer = New VertexBuffer(GetType(cubeData), 1000, device, Usage.None, VertexFormats.None, Pool.Managed)

Gli shader non accettano come valori delle matrici, quindi dobbiamo fargli arrivare i valori in modo che lui possa interpretarli. Per il formato del vertice va bene qualsiasi cosa e quindi lascio none.  Ora la cosa più importante, la vertex declaration.

Dim elementi() As VertexElement = { _
New VertexElement(0, 0, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Position, 0), _
New VertexElement(0, 12, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Normal, 0), _
New VertexElement(0, 24, DeclarationType.Float2, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 0), _
New VertexElement(1, 0, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 1), _
New VertexElement(1, 16, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 2), _
New VertexElement(1, 32, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 3), _
New VertexElement(1, 48, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 4), _
New VertexElement(1, 64, DeclarationType.Float4, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 5), _
VertexElement.VertexDeclarationEnd}
Return New VertexDeclaration(device, elementi)

Osservate, i primi 3 vertex element hanno stream uguale a 0 e sono la posizione, la normale e la coordinate texture 0. Questa è infatti la dichiarazione del nostro cubo che si trova nel primo indice dello stream. Le altre invece si trovano nel secondo stream (valore uguale a 1), ed interpretate come coordinate texture da 1 a 5. In questo modo lo shader interpreterà la matrice come coordinate texture da 1 a 4 (ricordate che sono 16 valori disposti 4 x 4 e quindi equivalenti a 4 float4). La coordinate texture 5 verrà usata come colore.

Ora non dovrete fare altro che usare la stessa struttura nello shader ed usare uno qualsiasi dei sistemi di shader di directX (assembler, HLSL, effect). Eccone un semplice esempio.

struct vertexInput {
    float3 position : POSITION;
    float3 normal : NORMAL;
    float2 texCoordDiffuse : TEXCOORD0;
    float4 row1 : TEXCOORD1;
    float4 row2 : TEXCOORD2;
    float4 row3 : TEXCOORD3;
    float4 row4 : TEXCOORD4;
    float4 color : TEXCOORD5;
};
struct vertexOutput {
    float4 hPosition : POSITION;
    float4 color : COLOR0;
};
vertexOutput VS_TransformAndTexture(vertexInput IN)
{
    vertexOutput OUT;
    float4x4 worldViewProj={IN.row1,IN.row2,IN.row3,IN.row4};
    OUT.hPosition = mul( float4(IN.position.xyz , 1.0) , worldViewProj);
    OUT.color = IN.color;
    return OUT;
}
technique textured
{
    pass p0
    {
        VertexShader = compile vs_1_1 VS_TransformAndTexture();
        PixelShader = null;
    }
}

Basta creare una matrice dalla struttura di input ed il gioco è fatto. Nonostante sia necessaria l'architettura 3.0 lo shader può essere anche un 1.1. Ora potete implementare quello che volete ed il risultato sarà super ottimizzato tanto che anche usando il reference anzichè l'hardware non avrete il blocco dell'applicazione. I casi di utilizzo più immediati sono asteroidi, flotte aeree, insomma, tutti quei sistemi che prevedono il rendering dello stesso oggetto centinaia di volte. Per modificare i valore sarete costretti a fare il lock dei vertici dell'istance buffer, cosa tuttavia molto più effeciente.

Esempio VB.Net