Primitive 3183 Visite) DirectX 11
Direct3D è un renderizzatore di triangoli. L’evoluzione ha portato alla possibilità di aumentarne sempre più la flessibilità, dai formati fissi di Direct3D7 fino alla completa libertà raggiunta con Direct3D9 ed in particolare con la versione 10.
I triangoli vengono raccolti all’interno di buffer di memoria chiamati Vertex Buffer. L’ordine con cui i vertici vengono renderizzati da Direct3D può essere l’ordine con cui sono caricati nel buffer o, preferibilmente, in un secondo buffer (Index Buffer) che contiene l’ordine con cui i vertici vanno presi, evitando così duplicati nel Vertex Buffer.
Un vertice può essere una qualsiasi struttura e per questo occorre far in modo che lo Shader legga correttamente il dato. Questa informazione va creata in apposito oggetto: l’input layout.
Prendiamo una struttura definita nel codice C#
Struct Vertex
{
float x; float y; float z;
float nx; float ny; float nz;
float tu; float tv;
};
E l’ equivalente struttura nel codice shader;
struct VertexHLSL
{
float3 pos:POSITION;
float3 nrm:NORMAL;
float2 tex:TEXCOORD;
};
Ora creiamo un layout che descriva come i dati della prima vengono mappati nella seconda.
new InputElement[] {
new InputElement("POSITION", 0, Format.R32G32B32_Float, 0, 0),
new InputElement("NORMAL", 0, Format.R32G32B32_Float, 12, 0),
new InputElement("TEXCOORD", 0, Format.R32G32_Float, 24, 0)
});
La struttura inputLayoutDesc indica il formato di ogni componente del vertice, la semantica e la posizione nel buffer dell’elemento. Tramite questa struttura lo shader saprà quali elementi andare a prendere. Non è infatti importante l’ordine, l’importante è che ogni valore nello shader sappia in quale punto della struttura leggere.
L’oggetto dedicato a contenere tale informazione è
ShaderSignature signature = ShaderSignature.GetInputSignature(vertexShaderByteCode);
InputLayout Layout = new InputLayout(Device, signature, elements);
Per creare un input layout è necessario il byte code compilato dal Vertex Shader. Tramite il GetInputSignature si ottiene la struttura di ingresso dello Shader che servirà a creare il layout.
Una cosa molto utile è sapere che un elemento composto da 2 o 3 float nel vertex buffer può essere passato ad un float4 in uno shader. Se quindi carichiamo la posizione XYZ può essere inserita in un float4. I valori mancanti verranno riempiti con 0 nel caso della Y o della Z e 1 nel caso di W (il quarto elemento).
Ora defininiamo un array di vertici. Il minimo elemento renderizzabile è 1 triangolo, quindi 3 vertici. Ora prendendo un oggetto Buffer del namespace SharpDx
Buffer.Create<VType>(device, BindFlags.VertexBuffer, vertices)
In questo modo abbiamo creato il nostro vertexBuffer e riempito.
Ora occorre creare un Index Buffer. Prima di ordinare gli indici occorre decidere la struttura da dare all’oggetto.
La topologia indica come sono organizzati i vertici. TriangleList indica che ogni 3 vertici viene chiuso un triangolo. Esistono altre topologie che permettono di risparmiare triangoli (esempio triangleStrip crea il ogni triangolo usando i 2 vertici precedenti). Osservate l’immagine sottostante.
I punti di adiancency indicano punti esterni al triangolo ma adiacenti allo stesso (il geometry shader ha bisogno di sapere quali sono i triangoli ad esso adiacenti).
Con i vertex buffer tutta la descrizione geometrica è contenuta sotto forma di array di strutture in un buffer.
Questo presenta un grosso svantaggio. Osservate questa immagine.
Per rappresentarla con il solo vertex buffer sono necessari 2 triangoli, quindi un array di 6 vertici.
Due di questi però sono in effetti dei duplicati. Nell'immagine del vertex buffer potete vedere che i vertici 0 e 4, come quelli 2 e 5 sono identici. Questo è uno spreco di memoria. Se ad esempio i vertici avessero contenuto posizione XYZ e coordinate texture UV sarebbero stati sprecati 5 float per vertice, un totale di 40byte su una oggetto di 120 byte, il 33%. All'aumentare della complessità aumenta lo spreco di memoria ma soprattutto aumentano i vertici da processare. Il vertex shader viene eseguito 1 volta per ogni vertice ed in questo modo sarebbe eseguito più volte del necessario.
Per questo esistono gli index buffer
Nei rendering indicizzati il vertex buffer contiene vertici senza duplicati, disposti senza un ordine. L'indexbuffer conterrà invece l'ordine con cui tali vertici devono essere presi per formare triangoli. Sono sufficienti valori short (2 byte) per indicizzare oggetti con 65000 vertici, ma potrete utilizzare anche interi (4byte) ed indicizzare oltre 4 miliardi di vertici. Nel nostro caso quindi avremo un array di 6 short, 12 byte ma ne risparmieremo 40 ed avremo 2 vertici in meno da far processare al vertex shader.
Per figure molto più complesse il vantaggio diventa esponenziale. Un cubo ad esempio ha 12 triangoli, 36 vertici. Con i vertex buffer occuperebbero
36 * 5 float = 720 byte.
Ma, dato che in realtà solo 8 vertici sono unici avremo un vertex buffer di soli
8 x 5 float = 40 byte
e un indexbuffer di 36 indici, quindi soli 72 byte.
Un cubo indicizzato occuperà solo 112 byte contro 720 e dovrà processare solo 8 vertici anziché 36. Numeri che in una scena complessa fanno la differenza. Mediamente risparmierete almeno il 30%.
Un indexbuffer si crea esattamente come un vertex buffer
Buffer.Create(device, BindFlags.IndexBuffer, indices)
Le proprietà sono le stesse del vertex buffer, l'unica differenza è che si deve indicare nel BindFlags il fatto che sia un indexbuffer. Ora che tutto è pronto si può procedere al rendering.
DeviceContext.InputAssembler.PrimitiveTopology = PrimitiveTopology.TriangleList;
DeviceContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(VertexBuffer, VertexSize, 0));
DeviceContext.InputAssembler.SetIndexBuffer(IndexBuffer, Format.R32_UInt, 0);
Device.DeviceContext.DrawIndexed(IndexCount, 0, 0);
Per prima cosa si imposta il tipo di Topologia, quindi si importano i VertexBuffer (la struttura mostra che si possono passare più VertexBuffer contemporaneamente, questo serve per alcuni tipi di effetti). Notate la variabile VertexSize, occorre infatti dire a DirectX che il nostro vertice occupa ad esempio 32bye (i nostri 8 float). L’ultima variabile è l’offset, usato per dire a DirectX di iniziare la lettura più avanti nel nostro buffer.
Quindi si imposta l’IndexBuffer (il formato serve ad indicare se sti stanno utilizzando Short che richiede un formato R16_Uint o un int con un formato R32_UInt). I due interi finali servono per indicare da quale punto dell’Index Buffer iniziare a leggere e da quale punto del VertexBuffer iniziare a leggere. Questo serve quando si vogliono fondere più buffer e quindi il primo indice e vertice nei buffer non corrispondono ai primi nell’oggetto.
Infine il metodo DrawIndexed effettua il rendering del numero di indici desiderati.
Vi rimando al tutorial numero 4 della serie (Github o in fondo dal primo tutorial)