Il rendering delle ombre è sicuramente uno degli aspetti più complessi da analizzare. In attesa che directX gestisca in maniera automatica questo problema vi propongo un nuovo metodo, alternativo allo stencil: lo shadow mapping.
Il presupposto da cui partire è la definizione di ombra. Un ombra è una schermatura tra tutto quello che si trova tra la luce e l'oggetto. Lo shadow mapping si basa su questo: renderizzare su una texture quello che si trova tra la luce e l'oggetto. L'oggetto ombreggiato semplicemente userà questa texture per creare zone d'ombra.
Una sorgente luminosa (il cerchio arancione) illumina un oggetto (sfera rossa), parzialmente coperto da un secondo oggetto(sfera blu). Attorno alla luce è evidenziata un quadrato che rappresenta la nostra cubemap. Su di essa, centrando la telecamera nell'origine, renderizzeremo gli oggetti utilizzando come colore la distanza tra esso e la luce. Quello che la mappa vedrà in questo esempio saranno le 2 sfere ma il valore di quella blu sarà minore perchè più vicina alla luce. L'oggetto rosso quindi caricherà questa mappa su se stesso posizionando le coordinate texture in modo da andare a prendere la zona verso la luce. Il valore abbiamo detto che rappresenta la distanza: se la distanza tra di lui e la luce è maggiore del valore sulla mappa in quel pixel allora aggiungerà l'ombra su se stesso perchè significa che sulla cubemap c'è un oggetto davanti a lui che ha inserito un valore minore. In questo caso l'oggetto avrà una zona in ombra ed il resto in luce perchè tra di lui e la luce non c'è nulla (valore minore a quello sulla cubemap che avrà sfondo massimo, quindi bianco). Ogni oggetto sulla scena utilizzerà lo stesso algoritmo caricando la shadowmap ed il risultato sarà l'ombreggiatura perfetta della scena.
Il rendering si divide in 2 pass.
Il primo pass renderizzerà sulla cubemap la scena vista dai suoi 6 lati restituendo nel pixel shader la distanza.
Il secondo pass (quello finale) renderizza gli oggetti con questa mappa. Ecco la formula da usare (e da inserire nello shader).
lightDirection=vertexPosition-lightPosition;
distanza=distance(vertexPosition,lightPos);
float shadow=texCUBE(Shadow,normalize(lightDirection));
float ombra=distanza<(shadow);
Tramite lightdirection prendiamo la cubemap verso la direzione della luce e tramite l'operatore < avremo 0 se la distanza + maggiore dell'ombra, 1 altrimenti. Questo valore è ora a disposizione per farci quello che vogliamo.
Caratteristiche delle shadowmapping:
Pro
- tutto il calcolo è sulla gpu
- universale, prende in considerazione ogni elemento della scena
- l'ombra viene proiettata anche sullo stesso oggetto
Contro
- richiedono il rendering della scena più volte (con shader molto semplice)
- occorre creare una cubemap per ogni sorgente
- dipende fortemente dalla qualità della cubemap (dimensione, formato)
L'algoritmo deve essere ulteriormente migliorato. In prossimità dell'oggetto dove la distanza è minima è possibile che ci siano degli artefatti (difetti). Al valore shadow occorre aggiungere una correzione. La distanza per un valore (esempio 0.03) mi sembra una buona approssimazione che fa ottenere una scena più che buona. Un'altra ottimizzazione sta nel fatto che spesso non si devono renderizzare tutte le facce. Se una luce si trova in cima alla scena allora non serve la faccia superiore. Il sistema funziona per la luce puntiforme. Per le luci direzionali è sufficiente una normale texture da orientare verso la scena. Le formule sono leggermente diverse ma non difficili da calcolare. Magari saranno argomento di successivi tutorial. Per risparmiare risorse potete anche calcolare la shadow map non ad ogni frame. Infine se avete ancora un pò di frame liberi potete fare un blur delle mappe per migliorare ancora la scena.
Vi lascio all'esempio:
Esempio VB.Net