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.
ID3D10Buffer *buffer;
D3D10_BUFFER_DESC bufferDesc =
{
bufferSize,
D3D10_USAGE_DEFAULT,
D3D10_BIND_VERTEX_BUFFER | D3D10_BIND_STREAM_OUTPUT,
0,
0
};
HRESULT hr = device-> CreateBuffer( &bufferDesc, NULL, & buffer );
Il buffer per l’output stream è creato utilizzando come Bind i flags VERTEX_BUFFER e STREAM_OUTPUT. Occorre poi passare una dimensione (variabile bufferSize), 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
UINT offset[]={0};
ID3D10Buffer* temp[1];
temp[0]= buffer;
device-> SOSetTargets( 1, temp, offset );
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
UINT offset[]={ 0};
ID3D10Buffer* temp[1];
temp[0]=NULL;
device-> SOSetTargets( 1, temp, offset );
In questo momento il buffer è riempito con quello che avete restituito dal geometry shader. Per il rendering si procede in questo modo
UINT offset[]={0};
UINT strideSize[]={stride};
ID3D10Buffer* temp[]={buffer};
device->IASetInputLayout(layout);
device->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
device->IASetVertexBuffers(0,1,temp,strideSize,offset);
device->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. Innanzittutto 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 (nel mio caso POSITION, NORMAL, TEXCOORD).
Questo sarà anche l’input layout da utilizzare quando userete il buffer per il rendering.
Ricordate anche di disabilitare il depth buffer. DepthStencilState DisableDepth
{
DepthEnable = FALSE;
DepthWriteMask = ZERO;
};
GeometryShader gsS = ConstructGSWithSO( CompileShader( gs_4_0, GS_OUT() ), "POSITION.xyz; NORMAL.xyz; TEXCOORD.xy" );
technique10 Render
{
pass P1
{
SetVertexShader( CompileShader( vs_4_0, VS_OUT() ));
SetGeometryShader(gsS);
SetPixelShader( NULL );
SetDepthStencilState( DisableDepth, 0 );
}
}
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
Nel demo qui sotto ho implementato una sorta di Snake 3D (ovviamente senza logica di gioco). Ogni secondo aggiungo un blocco alla testa del “serpente” nella direzione desiderata in modo che io abbiamo una buffer contenente tutti i blocchi da mandare in rendering con una sola istruzione. Nell’esempio uso 2 output stream. Ogni volta che aggiungo un blocco attivo uno dei due stream, e sopra ci renderizzo l’altro che contiene tutti i blocchi fino a quel momento, ed il nuovo blocco.
Demo
I commenti sono disabilitati.