• Non ci sono risultati.

4.7 Shadow Mapping con Tecnica EVSM

4.7.2 Filtraggio della Shadow Map

Il secondo problema che bisogna arontare è la realizzazione di un ltraggio eciente da poter eettuare ad ogni fotogramma sulla shadow map. Il ltro utilizzato deve essere un ltro di convoluzione che combina i valori presenti nella mappa producendo dei momenti utilizzati poi per la stima della quantità di luce ricevuta. La funzione utilizzata per la convoluzione può essere di vario tipo (nel caso più semplice la media dei valori in un certo intorno). Nel nostro caso usiamo una funzione gaussiana per cercare di dare un peso ragionevole ad ognuno dei texel nell'intorno esaminato. Il ltro che ci si propone di realizzare è pertanto un ltro di blur gaussiano (gaussian blur).

Il ltraggio di una texture è un'operazione semplice ma normalmente onerosa: per produrre un singolo texel della mappa ltrata è necessario elaborare diversi elementi della texture originale, combinandoli tra loro con operazioni in virgola mo- bile. Data la natura dei calcoli da eseguire, risulta conveniente eettuare il ltraggio sfruttando i core della GPU in parallelo (questo vale per tutte le moderne schede vi- deo). Ne deriva che per ottenere una procedura eciente si opera predisponendo un nuovo FBO con una nuova texture e renderizzando la texture ltrata direttamente all'interno della nuova texture, combinando i texel dell'immagine direttamente nello stadio di fragment shading (sfruttando dunque la pipeline OpenGL).

Nella pratica questo è fattibile disegnando un piano che copre l'intero viewport (corrispondente alle dimensioni della texture) e calcolando il colore di ogni frammen- to combinando diversi campioni dalla shadow map originale (vista come texture). L'accesso alla shadow map non ltrata avviene direttamente mediante le coordinate del frammento nel viewport.

Supponiamo adesso che ci venga dato il kernel quadrato del ltro da utilizza- re (di dimensione m). Se l'immagine da ltrare è quadrata di dimensione n (in pixel) avremo una complessità computazionale complessiva per ltrare gli n2 pixel

di O (n2m2) in termini di operazioni elementari: per ognuno degli n2 pixel bisogna

eettuare m2 moltiplicazioni per i coecienti del kernel e m2− 1somme.

Questa complessità è considerevole e proibisce di espandere il kernel del ltro (aumentare m) oltre un certo livello (nonostante il ltraggio non venga eettuato durante il rendering della scena, a dierenza del caso PCF presentato nel paragrafo 3.3.1).

Per migliorare le prestazioni si ricorre ad un ltro separabile. Un ltro si dice separabile quando è possibile eettuare il ltraggio in due passate ognuna delle quali

opera in una singola direzione (con un kernel monodimensionale). Il ltro gaussiano è separabile nelle due direzioni e dunque per ottenere un approccio più eciente possiamo scomporre il ltraggio in due diversi ltraggi con kernel monodimensionali (ltraggio orizzontale e a seguire ltraggio verticale).

Per ottenere lo stesso eetto del ltro bidimensionale con kernel quadrato di dimensione m si deve ricorrere ad un doppio ltraggio con un kernel monodimensio- nale di dimensione m, prima orizzontale e poi verticale. La complessità di uno dei due passi di ltraggio diventa O (n2m)perché per ognuno degli n2 pixel si eettuano

solamente m moltiplicazioni per i coecienti e m − 1 somme. La complessità del ltraggio in due passate risulta pertanto O (n2m).

Ne deriva che useremo un diverso shader program per ognuna delle due passate. La shadow map ltrata dopo il primo passo viene costruita in una nuova texture di un nuovo FBO; questa viene poi fornita al secondo passo di ltraggio come immagine in ingresso.

Il vertex shader è identico nelle due passate e si occupa semplicemente di ssare la posizione dei vertici del piano, la sua struttura è la seguente:

# version 330 core // GLSL version 3.30 , core OpenGL profile in vec2 vertex ; // plane vertex position ( clip space , (x,y) only ) void main ( void ) {

gl_Position = vec4 ( vertex ,0.0 ,1.0); // clip coordinates }

dove notiamo che le coordinate (x, y) per i vertici del piano vengono già fornite in clip space in modo tale da riempire tutta l'area del viewport indipendentemente dalle sue dimensioni.

Il fragment shader utilizzato invece dipende dal particolare ltraggio eseguito (orizzontale o verticale), la forma è simile nei due casi e come esempio diamo solo la struttura dello shader scritto per eseguire il ltraggio orizzontale della shadow map:

# version 330 core // GLSL version 3.30 , core OpenGL profile uniform sampler2D texSampler ; // sampler for the image to filter uniform vec2 texSize ; // image size (in pixels )

CAPITOLO 4. REALIZZAZIONE DELLE TECNICHE PRESENTATE 131

out vec4 fragColor ; // filtered value for this fragment

const float coefficients0 [10] = float [10]( 0.9734 , 0.5984 , 0.3990 , 0.2994 , 0.2396 , 0.1996 , 0.1712 , 0.1498 , 0.1332 , 0.1198 ); const float coefficients1 [10] = float [10]( 0.0133 , 0.1942 , 0.2421 ,

0.2260 , 0.2001 , 0.1762 , 0.1562 , 0.1396 , 0.1260 , 0.1146 ); const float coefficients2 [10] = float [10]( 0.0000 , 0.0066 , 0.0540 ,

0.0972 , 0.1166 , 0.1212 , 0.1186 , 0.1131 , 0.1067 , 0.1002 ); const float coefficients3 [10] = float [10]( 0.0000 , 0.0000 , 0.0044 ,

0.0238 , 0.0474 , 0.0648 , 0.0749 , 0.0796 , 0.0808 , 0.0799 ); const float coefficients4 [10] = float [10]( 0.0000 , 0.0000 , 0.0000 ,

0.0033 , 0.0134 , 0.0270 , 0.0393 , 0.0486 , 0.0547 , 0.0584 ); const float coefficients5 [10] = float [10]( 0.0000 , 0.0000 , 0.0000 ,

0.0000 , 0.0027 , 0.0088 , 0.0172 , 0.0258 , 0.0332 , 0.0390 ); const float coefficients6 [10] = float [10]( 0.0000 , 0.0000 , 0.0000 ,

0.0000 , 0.0000 , 0.0022 , 0.0063 , 0.0119 , 0.0180 , 0.0237 ); const float coefficients7 [10] = float [10]( 0.0000 , 0.0000 , 0.0000 ,

0.0000 , 0.0000 , 0.0000 , 0.0019 , 0.0048 , 0.0087 , 0.0132 ); const float coefficients8 [10] = float [10]( 0.0000 , 0.0000 , 0.0000 ,

0.0000 , 0.0000 , 0.0000 , 0.0000 , 0.0017 , 0.0038 , 0.0067 ); const float coefficients9 [10] = float [10]( 0.0000 , 0.0000 , 0.0000 ,

0.0000 , 0.0000 , 0.0000 , 0.0000 , 0.0000 , 0.0015 , 0.0031 ); const float coefficients10 [10] = float [10]( 0.0000 , 0.0000 , 0.0000 ,

0.0000 , 0.0000 , 0.0000 , 0.0000 , 0.0000 , 0.0000 , 0.0013 ); void main ( void ) {

float blurSize = 1.0/ texSize .x;

vec2 fragTexCoord = gl_FragCoord .xy/ texSize ; vec4 sum = vec4 (0.0);

switch ( filterSize ) { case 9:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

10.0* blurSize , fragTexCoord .y)) * coefficients10 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

10.0* blurSize , fragTexCoord .y)) * coefficients10 [ filterSize ]; case 8:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

9.0* blurSize , fragTexCoord .y)) * coefficients9 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

9.0* blurSize , fragTexCoord .y)) * coefficients9 [ filterSize ]; case 7:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

8.0* blurSize , fragTexCoord .y)) * coefficients8 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

8.0* blurSize , fragTexCoord .y)) * coefficients8 [ filterSize ]; case 6:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

7.0* blurSize , fragTexCoord .y)) * coefficients7 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

7.0* blurSize , fragTexCoord .y)) * coefficients7 [ filterSize ]; case 5:

6.0* blurSize , fragTexCoord .y)) * coefficients6 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

6.0* blurSize , fragTexCoord .y)) * coefficients6 [ filterSize ]; case 4:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

5.0* blurSize , fragTexCoord .y)) * coefficients5 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

5.0* blurSize , fragTexCoord .y)) * coefficients5 [ filterSize ]; case 3:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

4.0* blurSize , fragTexCoord .y)) * coefficients4 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

4.0* blurSize , fragTexCoord .y)) * coefficients4 [ filterSize ]; case 2:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

3.0* blurSize , fragTexCoord .y)) * coefficients3 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

3.0* blurSize , fragTexCoord .y)) * coefficients3 [ filterSize ]; case 1:

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

2.0* blurSize , fragTexCoord .y)) * coefficients2 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

2.0* blurSize , fragTexCoord .y)) * coefficients2 [ filterSize ]; }

sum += texture ( texSampler , vec2 ( fragTexCoord .x -

blurSize , fragTexCoord .y)) * coefficients1 [ filterSize ]; sum += texture ( texSampler , vec2 ( fragTexCoord .x +

blurSize , fragTexCoord .y)) * coefficients1 [ filterSize ]; sum += texture ( texSampler ,

vec2 ( fragTexCoord .x, fragTexCoord .y)) * coefficients0 [ filterSize ]; fragColor = sum ;

}

dove notiamo che:

• Nella variabile di uscita fragColor si scrivono i valori ltrati per il texel (frammento) corrente sui 4 canali di colore (R, G, B e A).

• È possibile specicare un raggio per il ltro gaussiano tramite la variabile uniform filterSize (che va da 0 a 9): dato il valore di questa variabile, il kernel bidimensionale equivalente al ltraggio in due passate applicato avrà ampiezza (in pixel) pari a 2 · filterSize + 3.

• I coecienti nel kernel monodimensionale da applicare in questa passata sono memorizzati in una serie di vettori costanti di numeri reali. Ogni vettore contiene tutti i coecienti ad una certa distanza dal centro del ltro (un coeciente per ogni filterSize legale).

CAPITOLO 4. REALIZZAZIONE DELLE TECNICHE PRESENTATE 133 • Il costrutto switch si comporta in modo analogo all'omonimo costrutto in C. • Si usa la variabile gl_FragCoord built-in nel linguaggio GLSL per calcolare le coordinate con cui accedere alla texture. Possiamo considerare il contenuto di questa variabile come le window coordinates del frammento (anche se è una semplicazione).

• Le dimensioni dell'immagine da ltrare vengono passate esplicitamente come variabile uniform (texSize). Si potrebbe pensare in alternativa di estrarre le dimensioni con delle funzioni apposite di GLSL che interrogano il siste- ma OpenGL a partire dal sampler, tuttavia la soluzione risulterebbe meno eciente.

Naturalmente il ltraggio verticale avviene con un fragment shader molto simile a quello sopra, in cui cambiano solamente dei dettagli di accesso alla texture (shadow map) da ltrare.