Il geometry instancing è una tecnologia che permette di replicare un Vertex Buffer numerose volte in modo da creare copie di un oggetto a video in modo ottimizzato.
Tramite il geometry instancing possiamo caricare un soldato ed in un'unica istruzione di draw, mandarne a video migliaia di copie, ognuna con caratteristiche di posizione, colore o texture differenti l'uno dall'altro.
La cosa più interessante è la totale gestione in memoria video del processo, permettendo delle prestazioni impensabili normalmente. Infatti ogni volta che si esegue una istruzione di draw ci sono dati che passano dalla RAM alla scheda video o tra zone diverse di quest'ultima: sarà direttamente la GPU ad operare la moltiplicazione dei vertici e degli indici dell'oggetto.
Il geometry instancing prevede l'utilizzo di 2 vertex buffer: il primo contenente i vertici dell'oggetto, il secondo contenente un array di strutture dati specifiche per ogni copia. Ad esempio se vogliamo rappresentare 1000 oggetti, ognuno con posizione e colore diverso, dovremo creare una struttura contenente posizione e colore e crearne un array di 1000 elementi. La funzionalità geometry instancing non farà altro che fondere ogni vertice con ogni elemento del secondo buffer. Nel vertex shader troveremo una struttura di input che sarà la somma dei 2 buffer. Dal punto di vista dello shader sarà quindi sufficiente prevedere dei dati extra da utilizzare per modificare la posizione ed il colore dell'oggetto. Per prima cosa è necessario creare un buffer per contenere i dati
ID3D11Buffer* instanceBuffer;
this->stride=stride;
this->offset=0;
this->vertexCount=vertexCount;
D3D11_BUFFER_DESC bd;
bd.Usage = D3D11_USAGE_DYNAMIC;
bd.ByteWidth = stride * elementCount;
bd.BindFlags = D3D11_BIND_VERTEX_BUFFER;
bd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
bd.MiscFlags = 0;
D3D11_SUBRESOURCE_DATA InitData;
InitData.pSysMem = data;
device->CreateBuffer( &bd, &InitData, &instanceBuffer);
Come vedete è simile a quanto usato per il vertex buffer. Per poter utilizzare l'instancing occorre creare un layout appropriato
D3D11_INPUT_ELEMENT_DESC layout[] =
{
{ "POSITION", 0, DXGI_FORMAT_R32G32B32_FLOAT, 0, 0, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "TEXCOORD", 0, DXGI_FORMAT_R32G32_FLOAT, 0, 12, D3D11_INPUT_PER_VERTEX_DATA, 0 },
{ "EXTRADATA", 0, DXGI_FORMAT_R32G32B32A32_FLOAT, 1, 0, D3D11_INPUT_PER_INSTANCE_DATA, 1 },
};
Nell' instancing il layout è formato dalla somma dei due formati vertici. Il secondo buffer è descritto dalla seconda parte dell'array. Si può notare infatti che l'indice è impostato ad 1 e non a 0 (perchè sarà estratto dal secondo buffer), l'offset è a zero (perchè si conta l'offset del buffer, non totale del vertice) e si imposta il tipo ad Instance_Data. L'ultimo valore, instance_repetition, va impostato ad 1.
Ora quello che si dovrò fare in fase di rendering è passare i 2 vertex buffer contemporaneamente (quello con i dati e quello con i dati per l'instancing).
Ad esempio nel caso del solo Vertex Buffer
device->IASetInputLayout(this->layout);
device->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
ID3D11Buffer* buffers[2]={vertexBuffer,instanceVertexBuffer};
UINT strideA[2]={stride,instanceStride};
UINT offsetA[2]={0,0};
device->IASetVertexBuffers(0,2,buffers,strideA,offsetA);
device->DrawInstanced(VertexCount,istanceCount,0,0);
O nel caso ci sia anche Index Buffer
device->IASetVertexBuffers(0,2,buffers,strideA,offsetA);
device->IASetIndexBuffer(indexBuffer,DXGI_FORMAT_R16_UINT,0);
device->DrawIndexedInstanced(FaceCount*3,istanceCount,0,0,0);
In questo esempio vedete come ho eseguito il rendering. Ho creato un array con due buffer e li ho passati al device insieme ai due stride ed i due offset. Nel mio esempio avrò uno stride di 20 byte per il primo vertex buffer (POSITION e TEXCOORD) e 16 per il secondo (EXTRADATA). Successivamente passo l'indexbuffer ed utilizzo l'istruzione DrawIndexedInstanced per mandare il rendering (gli si passa alla funzione il numero degli indici ed il numero di istance ossia quante copie dell'oggetto volete). Con la funzione DrawIndexedInstance potrete renderizzare oggetti da un minimo di zero ad un massimo pari al numero di elementi nel buffer dell'instance.
Nello shader non noterete differenza
struct VS_INPUT
{
float4 Pos : POSITION;
float2 tex:TEXCOORD;
float4 posIstance:EXTRADATA;
};
Ogni vertice del primo buffer sarà combinato con ogni vertice (anche se in realtà sono dati anche se gestiti come vertici). Quindi se abbiamo un cubo e 1000 vettori posizione otterremo 8 (vertici del cubo) * 1000 = 8000 vertici e quindi 8000 chiamate allo shader con tutte le combinazioni. Nello shader ad esempio potremo utilizzare il vettore per spostare il cubo in modo da assegnare ad ognuno una posizione diversa. Passando 4 float4 avrete una intera matrice. Potete passare quanti dati volete.
Nella struttura di input potete utilizzare un campo non definito nei due buffer con sintatti SV_InstanceID
uint indice:SV_InstanceID
Questo campo contiene l'indice dell'instanza che stiamo renderizzando (che varierà quindi da 0 a 999 nell'esempio).
Vi lascio al demo che renderizza fino a 1000 cubi contemporaneamente.
Demo CPP
Demo .Net