Output Stream 2636 Visite) DirectX 11
L’Output Stream è una delle nuove funzionalità di DirectX10, che insieme al Geometry Shader permette di trasportare la gestione dei vertici dalla CPU alla GPU.
L’Output Stream è l’equivalente del Rendering su Texture applicato però al vertex buffer. Quando si utilizza un Output Stream i vertici invece di essere mandati al pixel shader vengono inviati in questo buffer che provvede a conservare questi dati. Successivamente si potrà usare l’Output Stream come una normale mesh contenente tutto ciò che ci abbiamo renderizzato sopra.
L’utilizzo di questa tecnologia permette di accelerare il rendering in modo veramente incredibile.
Facciamo un esempio.
Immaginiamo di creare una scena in cui dobbiamo assemblare molti oggetti diversi tra loro ma che quasi sempre rimarranno immobili. Normalmente renderizzeremo ad ogni frame tutti gli oggetti, con pesanti impatti sulle prestazioni. Ricordo infatti che renderizzare 1000 oggetti da 100 poligoni è immensamente più pesante rispetto a renderizzare 1 oggetto da 100000 poligoni.
L’istancing non ci può aiutare, visto che si limita a creare copie di un solo oggetto, di conseguenza quello che si faceva era creare un’unica mesh contenente tutti i poligoni delle mesh che componevano la scena (tecnica nota come Batching).
Ogni modifica alla scena prevedeva la modifica dei buffer usando la CPU, processo molto più lento.
L’output stream invece lavora usando l’accellerazione hardware permettendoci di fondere più mesh con la stessa velocità con cui le renderizzeremmo.
In ogni momento potremo svuotare il buffer e ricominciare da capo, sempre considerando che la velocità dell’operazione sarà comunque elevatissima (più veloce di un rendering normale in quanto non viene processato alcun pixel shader).
Altro esempio sono i rendering multipass. Capita infatti che una scena durante un frame debba essere processata più volte (ombre, zonly, riflessioni). Ogni volta si deve quindi eseguire il vertex shader ed il geometry shader. Con gli output stream si fa una volta e si conserva il risultato per tutti i passaggi successivi.
Passiamo ora ad illustrare in pratica la tecnologia.
BufferDescription desc = new BufferDescription()
{
SizeInBytes = size,
BindFlags = BindFlags.VertexBuffer | BindFlags.StreamOutput,
Usage = ResourceUsage.Default,
};
Buffer buffer = new Buffer(Device, desc);
Il buffer per l’output stream è creato utilizzando come Bind i flags VertexBuffer e StreamOuput. Occorre poi passare una dimensione (variabile size), misurata in byte. Questa sarà la dimensione massima da destinare ai vertici. Per fare un calcolo, se vogliamo conservare 1000 triangoli con vertice da 32byte (posizione, normale e coordinata texture ad esempio), la dimensione sarà 32 x 3 x 1000= 96000 byte.
La dimensione sarà un tetto oltre il quale DirectX impedirà il rendering (senza tuttavia creare errori).
L’attivazione dell’output stream è molto simile a quella di un render target
DeviceContext.StreamOutput.SetTarget(_buffer, 0);
notate che è possibile passare fino a 4 output stream contemporaneamente, valorizzabili come volete.
Da questo momento non renderizzerete più a video, ma sul buffer. Nel momento in cui inserirete lo stream questo verrà anche svuotato dei precedenti rendering.
Al termine usate questo codice per tornare allo stato originario
DeviceContext.StreamOutput.SetTargets(new StreamOutputBufferBinding[] { new StreamOutputBufferBinding() });
In questo momento il buffer è riempito con quello che avete restituito dal geometry shader. Per il rendering si procede in questo modo
DeviceContext.InputAssembler.SetVertexBuffers(0, new VertexBufferBinding(_buffer, vertexSize, 0));
DeviceContext.DrawAuto();
Si utilizza come un normale vertex buffer ad eccezione dell’istruzione DrawAuto. Questa provedderà a stampare tutto il buffer fino alla fine senza dovervi preoccupare di nulla (non potete sapere infatti quanti vertici ci sono dentro).
Ora passiamo allo shader che deve essere un po’ diverso. Innanzitutto non dovete usare pixel shader, solo la geometria verrà esportata. Il vertex ed il geometry shader saranno assolutamente normali (ovviamente dovrete ragionare nell’ottica che ciò che renderizzate non va a video ma su un buffer da usare come mesh). Non dovrete neanche usare le semantiche di sistema (ad esempio SV_POSITION). Il geometry shader (obbligatorio) sarà compilato con l’istruzione ConstructGSWithSO. Qui andrete a specificare il geometry shader da usare e la semantica di ciò che esporterete. Questo sarà anche l’input layout da utilizzare quando userete il buffer per il rendering.
StreamOutputElement[] soDeclaration = new StreamOutputElement[]
{
new StreamOutputElement(){SemanticName="POSITION",ComponentCount=3},
new StreamOutputElement(){SemanticName="TEXCOORD",ComponentCount=2}
};
int[] size = new int[] { description.GeometrySO.Select(e => e.ComponentCount * 4).Sum() };
GeometryShader = new GeometryShader(Device, geometryShaderByteCode, soDeclaration, size, -1);
Usate sempre l’output stream quando
- La stessa scena viene renderizzata più volte in un frame quando cambiano pochi parametri (esempio solo la view matrix)
- La scena è composta da tanti oggetti che possono essere fusi insieme
- Avete una scena a cui dovete solo aggiungere o eliminare ogni tanto delle parti
Vi rimando al tutorial numero 14 della serie (Github o in fondo dal primo tutorial)