\\ Home : Articoli : Stampa
Multiple Render Target
Di RobyDx (del 27/07/2007 @ 15:41:50, in DirectX9, linkato 2991 volte)

La possibilità per directX di renderizzare una scena su una texture anzichè direttamente nello schermo è senza dubbio la caratteristica più importante per la realizzazione dei moderni effetti grafici, specie se in Vertex e Pixel shader. Lo stile che si adopera è chiamato multi pass: la scena viene renderizzata su una texture e questa poi viene elaborata ed infine presentata sullo schermo. Il motivo di ciò sta nel fatto che molti effetti sono notevolmente complessi e richiedono quindi il passaggio per varie fasi.

Ad esempio riportato nel sito è il depth of field. Questo richiede 3 passaggi:

1)rendering normale su una texture

2)rendering del cosiddetto depth space cui il colore della scena è la distanza dal punto di messa a fuoco con colore in scala di grigio

3)sfocatura finale in cui finalmente sullo schermo viene renderizzata la prima texture a pieno schermo sfocata tramite i dati contenuti nella seconda.

Essendo impossibile sfocare i poligoni è necessario trasformare la scena in una texture renderizzandola alla fine come un rettangolo che copra l'intero schermo.

Questo è solo un semplice esempio. Molti giochi, come ad esempio Half Life 2, richiedono decine di rendering separati che vengono dal modello iniziale generano luci, rilievi e riflessioni che vengono poi assemblati per ottenere il risultato finale.

halfLifeS

Immagine presa Half-Life 2 shading pubblicato da ATI. Analizzando le immagini vediamo che ci sono ben 9 rendering su texture prima di arrivare all'immagine finale della sezione di roccia: ad ognuno è applicato uno shader (vertex e/o pixel). Tutti questi rendering sono oltremodo pesanti per qualsiasi sistema. Osservate normal, albedo, lightmaps, specular factor e cubeMap (in pratica i primi rendering da cui inizia l'effetto). Questi partono tutti dal rendering del modello della roccia. Renderizzarli separatamente richiede 5 vertex shader (o 5 rendering normali) e 5 pixel shader per generare le 5 texture da assemblare. DirectX ci viene incontro migliorando le prestazioni con il multiple render target. In pratica il codice shader può essere indirizzato contemporaneamente su più texture contemporaneamente. Qualcuno potrebbe pensare: a che serve avere 5 immagini uguali? Semplice. Se il vertex shader restituisce gli stessi output per tutte e 5 le texture (ad esempio posizione coordinate texture e normali), il pixel shader può mandare colori diversi ad ognuna di esse. In pratica uno stesso codice shader controlla contemporaneamente più rendering. Il risparmio è impressionante.

I vertici e i pixel vengono calcolati una sola volta invece di 5 e anche se devono calcolare più cose diverse tra loro possono sfruttare calcoli che hanno in comune per ridurre il codice. Decine di render pass possono quindi essere ridotti anche ad un paio. Innanzitutto controllate la compatibilità della vostra scheda

Dim c As Caps
c = Manager.GetDeviceCaps(0, DeviceType.Hardware)
c.NumberSimultaneousRts()

l'ultimo valore conterrà il numero di render target che la scheda può effettuare contemporaneamente. Non potremo utilizzare la comoda renderToSurface per questa operazione. Dovremo agire manualmente. Innanzitutto salvate il backBuffer ed il viewPort originali per non perderli

Dim vpB As Viewport = device.Viewport
Dim backBuf As Surface
backBuf = device.GetBackBuffer(0, 0, BackBufferType.Mono)

Ora creiamo un renderTarget, ossia una texture per il renderToSurface

Dim t1 As Texture
Dim s1 As Surface
t1 = New Texture(device, 512, 512, 1, Usage.RenderTarget, Format.A8R8G8B8, Pool.Default)
s1 = t1.GetSurfaceLevel(0)

Ovviamente potete settare a piacimento la dimensione ed il formato

Ora la superficie s1 è connessa alla texture. Createne tante texture e superfici a seconda di quante ve ne servono (NOTA, il numero controllato con la compatibilità non centra nulla con il numero di texture che potete creare).

Ora passiamo le superfici al device (s2 e s3 sono ad esempio superfici generate come s1 da altre texture  t2 e t3 inizializzate in modo uguale a t1).

device.SetRenderTarget(0, s1)
device.SetRenderTarget(1, s2)
device.SetRenderTarget(2, s3)

Ora impostiamo il viewPort in modo da farlo entrare perfettamente nella texture

Dim vp As Viewport
vp.Width = 512
vp.Height = 512
vp.MaxZ = 1
device.Viewport = vp

Ora utilizzate il device come preferite

device.Clear(ClearFlags.Target Or ClearFlags.ZBuffer, Color.White, 1, 0)
device.BeginScene()

'rendering della scena
device.EndScene()

Non utilizzate ovviamente il present. Il risultato si troverà ora sulle superfici s1,s2 e s3 e quindi (dato che sono collegate) anche sulle texture t1,t2 e t3 ora a disposizione dei prossimi passaggi.

Così come è ora il risultato verrà renderizzato solo sulla prima superficie. Per differenziare il rendering dovete agire con i pixel shader come mostrerò fra poco.

Dopo aver finito il rendering dei vari pass per tornare al device normale

device.SetRenderTarget(0, backBuf)
device.SetRenderTarget(1, Nothing)
device.SetRenderTarget(2, Nothing)
device.Viewport = vpB

Ora renderizzate le ultime cose e fate il present. Quello che vedrete sullo schermo dipenderà da quello che avrete saputo fare.

Differenziare il rendering con il pixel shader

Come dicevo questa tecnica ha poco valore se sulla stessa texture si renderizzasse la stessa cosa (cosa che non accade, di default si renderizza solo sulla prima). Il valore finale del pixel shader determinerà dove il colore finale apparirà. La procedura si differenzia tra pixel shader in assembler o in HLSL.

In assembler ho spiegato che per restituire un colore occorre passare un registro a oC0

es

mov oC0,r0

ebbene se invece utilizzate il multiple render target è sufficiente variare l'indice di oC#. Quindi con oC0 il colore sarà inserito sulla prima superficie, oC1 sulla seconda e così via. Se renderizzate su una superficie che non è inserita (nel mio caso un'ipotetica oC4) non succede nulla.

In HLSL è più semplice, basta creare una struttura di variabili associate a COLOR, COLOR1, COLOR2 etc.

struct INPUT{
..................................
};
struct OUTPUT{
float4 color:COLOR;
float4 color1:COLOR1;
float4 color2:COLOR2;
};
sampler2D t:register(s0);
samplerCUBE t2:register(s1);
OUTPUT Main(INPUT inP){
OUTPUT outP;
...........................................
outP.color=risultato1;
outP.color1=risultato2;
outP.color2=risultato3;
return outP;
}

In questo modo color apparirà sulla superficie s1, color1 sulla superficie s2 e così via.

Ora non vi resta che elaborare degli effetti. Attenzione però: il multiple render target va usato con intelligenza perchè non tutti i casi sono adatti e spesso capita di voler a tutti i costi renderizzare contemporaneamente cose troppo diverse fra loro con il risultato di scrivere codice troppo complesso. Ricordate infatti che uno shader ha un suo limite come numero di istruzioni e quindi per voler mettere tutto insieme potreste superare questo limite. Il risultato è che il codice non funzionerà su sistemi con supporto per poche istruzioni. Se ben utilizzato invece rappresenta un sistema vincente. Come consiglio posso solo dirvi che se state creando un effetto molto complesso sarebbe meglio prendere carta e penna e capire quello che si sta facendo. Con un pò di studio potete trovare il modo di realizzare ciò che volete con meno istruzioni e passaggi raggruppando i rendering in modo intelligente. Vi lascio all'esempio che è documentato nella sezione shader library.

Esempio VB.Net

Esempio C#