Quando guardiamo la superficie dell'acqua, essa non appare sempre uniforme, ma, dato che riflette anche la luce del sole, ci saranno certi
punti molto più luminosi di altri. Questo è quello che succede, infatti, quando si guarda l'immagine riflessa del sole, ad
esempio, in una pozzanghera. Nella cube map che abbiamo usato come skybox c'è il "disegno" di un sole, ed esso viene correttamente
riflesso sulla sull'acqua senza problemi. La situazione non impone di aggiungere ulteriori riflessi, ma lo si può fare. Questa aggiunta
rappresenta una raffinatezza usabile anche in altri casi.
Per trovare quei pixel che vengono colpiti dalla luce solare, basta semplicemente riflettere la direzione della luce sulla superficie.
Questa operazione viene effettuata usando la funzione reflect di hlsl, che accetta come primo parametro il vettore da riflettere, e come
secondo parametro la normale del punto di riflessione. Aggiungiamo quindi questo codice alla fine del pixel shader, dopo il calcolo di
WaterColor:
In questo modo, si ottiene il vettore riflesso di LightDirection rispetto alla normale del punto colpito dalla luce. Potete osservare
il risultato che si dovrebbe ottenere in questo schema:
Luce riflessa
Tuttavia, la funzione reflect non restituisce il vettore verde esattamente com'è nella figura, ma invertito, con la punta rivolta verso
O (ossia esattamente simmetrica al vettore Luce rispetto alla normale). Per questo motivo dobbiamo mettere un meno davanti al risultato
della funzione per ottenere il vettore giusto.
Ora, sappiamo che la luce intensa riflessa dai punti colpiti dal sole ci giunge solo se stiamo guardando verso quei punti. Perciò, per
sapere quanta luce l'osservatore riceva, dobbiamo valutare di quanto il vettore riflessione sia uguale al vettore sguardo. Se vi ricordate,
per calcolare "quanto coincidenti" (passatemi il termine) siano due vettori, si utilizza il prodotto scalare dot, che restituisce il coseno
dell'angolo compreso tra i due. Immettiamo questo risultato in un fattore che chiamiamo Specular:
//Vector e Normal sono definiti nella parte precedente
//del pixel shaderfloat3 ReflectionVector = -reflect(LightDirection, Normal);
//Ricordate che dot richiede, in questo caso, due vettori di
//lunghezza unitariafloat Specular = dot(normalize(ReflectionVector), normalize(Vector));
Possiamo ora sommare questo valore al colore di output del pixel shader, per esaltarne la luminosità:
Output.Color.rgb += Specular;
E otterremo questo magnifico risultato:
Un effetto alquanto strano
Non era proprio quello che ci si era aspettati, a dire il vero. Questo succede perchè la luce viene riflessa ed espansa su tutta la
superficie disponibile e quando il coseno risulta negativo (ossia quando si voltano le spalle alla luce) il colore non viene esaltato ma
scurito. Possiamo eliminare questo vistoso difetto elevando il coefficiente Specular ad una potenza molto grande (e pari): in questo modo
non solo si elimina l'area scura che deturpa la bellezza del paesaggio, ma si restringe anche la luce ad un'area molto piccola. Infatti,
usando una funzione esponenziale, i valori bassi del coseno tenderanno ad annullarsi e quelli alti a divenire ancora più evidenti.
Potete capire meglio questo concetto osservando il grafico seguente
Approssimazione della funzione
Questo grafico rappresenta la funzione |2cos(x/4)|5. Il coefficiente 2 serve per rendere evidente i picchi (se avessi lasciato solo coseno,
il grafico sarebbe oscillato tra 0 e 1: in questo modo oscilla tra 0 e 25); il fattore 1/4 nell'argomento del coseno
serve per allargare la funzione in modo da renderla meglio osservabile (altrimenti sarebbe stata molto stretta); il valore assoluto, invece, serve per annullare l'oscillazione
negativa del coseno, in quanto la potenza 5 è dispari e fa permanere il segno. Insomma, tralasciando tutti gli accorgimenti che ho
immesso per rendere la funzione "più bella" esteticamente, potete osservare che, solo con la quinta potenza, essa oscilla vertiginosamente,
e raggiunge picchi molto alti in un intervallo esiguo. Pensate al suo andamento se elevassimo, ad esempio, alla 256-esima potenza! Se non
riuscite a pensarlo, allora provate (usando la funzione pow, che è identica a Math.Pow):
Un risultato molto migliore di prima, vero? Tuttavia, possiamo renderlo ancora migliore. Infatti, in questo codice presumiamo che la normale
sia la stessa (0,1,0) in tutti i punti. Ma nei tutorial precedenti abbiamo introdotto apposta una bump map per rendere la superficie
increspata. Introduciamo questa modifica anche sulla luce: