Quello seguente è il codice completo di un file .fx più complesso del precedente che prevede l'illuminazione in phong-shading con normal-mapping (conosciuto impropriamente anche come bump-mapping). Questa volta la luce che illuminerà la scena non sarà direzionale, come nel capitolo precedente, ma puntiforme e omnidirezionale, dovremo conoscerne quindi la posizione.
1 // Matrici di trasformazione in ingresso
2 uniform float4x4 World;
3 uniform float4x4 WorldView;
4 uniform float4x4 ViewProj;
5
6 // Colori ambientale e diffuso del materiale
7 uniform float4 ambientColor;
8 uniform float4 diffuseColor;
9 // Esponenete per definire la forma del riflesso speculare
10 uniform float specularExponent;
11
12 // Posizione della telecamera in World-Space
13 uniform float3 eyePos;
14 // Posizione della sorgente luminosa omnidirezionale
15 uniform float3 lightPos;
16
17 // Textures in ingresso
18 texture diffuseTex;
19 texture normalTex;
20
21 // Configurazione dei samplers da utilizzare nel Pixel Shaders
22 sampler diffuseSampler : register(s0) = sampler_state
23 {
24 Texture = (diffuseTex);
25 AddressU = WRAP;
26 AddressV = WRAP;
27 AddressW = WRAP;
28 MIPFILTER = LINEAR;
29 MINFILTER = LINEAR;
30 MAGFILTER = LINEAR;
31 };
32
33 sampler normalSampler : register(s1) = sampler_state
34 {
35 Texture = (normalTex);
36 AddressU = WRAP;
37 AddressV = WRAP;
38 AddressW = WRAP;
39 MIPFILTER = LINEAR;
40 MINFILTER = LINEAR;
41 MAGFILTER = LINEAR;
42 };
43
44 // Struttura per gli elementi in uscita dal Vertex Shader
45 struct VS_OUT
46 {
47 float4 oPos : POSITION;
48 float2 oTexCoords : TEXCOORD0;
49 float3 oLightVec : TEXCOORD1;
50 float3 oViewVec : TEXCOORD2;
51 };
52
53 // Dichiarazione della funzione del Vertex Shader con elementi del vertice
54 // inseriti direttamente nella lista dei parametri.
55 VS_OUT mainVS(
56 in float3 Pos : POSITION,
57 in float3 Normal : NORMAL,
58 in float3 Tangent : TANGENT,
59 in float3 Binormal : BINORMAL,
60 in float2 TexCoords : TEXCOORD0)
61 {
62 // Dichiarazione della struttura per i dati in uscita
63 VS_OUT Out;
64
65 // Trasformazione del vertice da object-space a world-space
66 float4 p = mul(float4(Pos, 1.0), World);
67
68 // Passaggio della posizione trasformata in screen space...
69 Out.oPos = mul(p, ViewProj);
70 // ...e delle coordinate di textures al Pixel Shader
71 Out.oTexCoords = TexCoords;
72
73 // Calcolo della matrice di rotazione per la trasformazione da
74 // world-space a tangent-space
75 float3x3 objToTangentSpace;
76 objToTangentSpace[0] = mul(float4(Tangent, 0.0), World);
77 objToTangentSpace[1] = mul(float4(Binormal, 0.0), World);
78 objToTangentSpace[2] = mul(float4(Normal, 0.0), World);
79
80 // Calcolo dei vettori che vanno al vertice alla sorgente luminosa e
81 // dal vertice alla posizione della telecamera.
82 // Trasformazione dei vettori in tangent-space.
83 float3 LightVec = lightPos - p;
84 Out.oLightVec = mul(objToTangentSpace, LightVec);
85 Out.oViewVec = mul(objToTangentSpace, (eyePos - p));
86
87 return Out;
88 }
89
90 // Dichiarazione del Pixel Shader per il rendering con illuminazione
91 // da una luce puntiforme posizionata su "lightPos"
92 float4 litTexturedPS(
93 float2 TexCoord : TEXCOORD0,
94 float3 LightVec : TEXCOORD1,
95 float3 ViewVec : TEXCOORD2) : COLOR0
96 {
97 // Normalizzazione dei vettori provenienti dal VertexShader
98 LightVec = normalize(LightVec);
99 ViewVec = normalize(ViewVec);
100
101 // Recupero di tutti i texel delle varie componenti del materiale e
102 // modulazione con i rispettivi colori
103 float4 diffuse = tex2D(diffuseSampler, TexCoord) * diffuseColor;
104 float3 normal = tex2D(normalSampler, TexCoord).xyz;
105
106 // Espansione della normale recuperata dalla texture dal
107 // range 0.0 ... 1.0 a -1.0 ... 1.0 per poter essere utilizzata
108 // nei calcoli di illuminazione
109 normal = normalize((normal * 2.0f) - 1.0f);
110
111 // Calcolo della componente di illuminazione diffusa
112 float diff = saturate(dot(normal, LightVec));
113 // Calcolo della componente dell'hot-spot speculare
114 float spec = pow(saturate(dot(reflect(-ViewVec, normal), LightVec)), specularExponent);
115
116 // Calcolo del colore definitivo come somma delle componenti
117 return (ambientColor + diffuse * diff + spec);
118 }
119
120 // Dichiarazione della tecnica da utilizzare per un rendering phong-shaded
121 technique phongBumpMapped
122 {
123 pass P0
124 {
125 ZEnable = true;
126 ZWriteEnable = true;
127 AlphaTestEnable = false;
128 FillMode = solid;
129 VertexShader = compile vs_2_0 mainVS();
130 PixelShader = compile ps_2_0 litTexturedPS();
131 }
132 }
Anche se il codice è molto ci soffermeremo solamente sulle estensioni che questo shader ha avuto rispetto a quello presentato nei precedenti tre capitoli. Le novità sono: l'aggiunta di tre variabili che definiscono rispettivamente l'esponente utilizzato nel calcolo dello spot speculare, la posizione della telecamera in world-space e della luce che illuminerà la geometria ed una nuova texture che perturberà la normale della superfice dando l'impressione di aggiungere dettagli geometrici a livello di pixel. Ovviamente ci sono nuove linee di codice anche nel Vertex Shader e nel Pixel Shader che analizzeremo nello specifico saltando quelle più intuitive, data anche la presenza dei commenti. A questo proposito, è buona pratica commentare i file .fx proprio come si fa per i file di codice C#, soprattutto se siamo parte di un team e più persone si troveranno a lavorare sugli stessi files.
I due punti di maggiore interesse per questo esempio in particolare sono le linee 76 ... 78 e 83 ... 85 del Vertex Shader e le linee 98, 99, 109, 112 e 114 del Pixel Shader. Tutte le linee indicate precedentemente coinvolgono aspetti prettamente matematici presenti dietro il funzionamento dello shader e su cui è giusto porre l'attenzione.
Il primo blocco (linee 76 ... 79) rappresenta una matrice di rotazione che trasforma un qualsiasi vettore da world-space a tangent-space. Una matrice di rotazione è una matrice 3x3 ed è possibile calcolarla perchè composta da tre vettori che abbiamo in ingresso nel vertex shader e che devono essere calcolati quando la geometria viene esportata (ad esempio come file .x o .fbx): quello normale, quello tangente e quello binormale.
Il tangent-space è uno spazio di lavoro particolare che viene introdotto perchè nel Pixel Shader utilizzeremo una normal-map. Questo spazio di lavoro vede l'asse Z sempre perpendicolare alla superficie, l'asse X tangente alla superficie e l'asse Y perpendicolare agli altri due. Questa scelta è ovvia se osserviamo attentamente la texture di una normal-map notando che è in gran parte blu con varie sfumature di colore nei punti in cui ci sono dei dettagli e teniamo conto che in questo caso alle componenti di colore RGB corrispondono gli assi XYZ del tangent-space.
Come è facile intuire, per utilizzare in una equazione dei vettori dobbiamo fare in modo che facciano parte tutti dello stesso spazio altrimenti i valori ottenuti non saranno validi; questa è l'operazione che viene eseguita nel secondo blocco (linee 83 ... 85) in cui i vettori oLightVec e oViewVec vengono trasformati dalla matrice calcolata precedentemente. Questi due vettori vanno rispettivamente dal vertice corrente alla posizione della luce e dal vertice corrente alla posizione della telecamera e contribuiranno al calcolo di due differenti componenti del modello di illuminaizone.
Analizzando il codice del Pixel Shader è ovvio che le due linee che svolgono tutto il lavoro di ombreggiatura sono la 112 e la 114, ma prima è opportuno analizzare come le variabili utilizzate in queste due equazioni vengono "preparate", ricordandoci che siamo all'interno di un Pixel Shader e che quindi queste operazioni vengono eseguite per ogni pixel che la geometria rasterizzata ricopre.
Le prime due linee di codice (98 e 99) normalizzano i vettori calcolati nel Vertex Shader ed interpolati dal rasterizzatore. La normalizzazione si rende necessaria proprio perchè l'interpolazione effettuata è lineare (attenzione a non confondersi con i filtri applicati nei sampler) e sui vettori ha l'effetto, in questo caso non voluto, di modificare la loro lunghezza originaria, mentre abbiamo la necessiatà che restino di lunghezza pari a 1.0.
I due vettori precedenti vanno confrontati con la normale locale della superficie che, proprio per l'utilizzo della tecnica del normal-mapping ed in conseguenza del fatto che questi vettori si trovano in tangent-space, non è altro che il valore recuperato dal texel della texture normalTex. Tuttavia nel formato standard RGBA 8-bit le textures non possono registrare valori negativi (perchè rappresentate nell'hard-disk da un'intero con valore compreso tra 0 e 255 per ogni componente) e questo rende necessario la prensenza della linea 109 che modifica il range del vettore recuperato dalla texture dall'intervallo 0.0 ... 1.0 a quello -1.0 ... 1.0 ricreando così il vettore normale originario.
Ora che i tre vettori sono tutti normalizzati, tramite la legge di Lambert, che abbiamo già visto nei capitoli precedenti, possiamo calcolare l'intensità della componente diffusa del materiale (linea 112) e tramite un calcolo analogo anche la componente speculare (linea 114) per cui sono necessarie però delle operazioni aggiuntive.
Nell'ombreggiatura phong la componente speculare viene definita dall'equazione speculare = (R · L)n dove: n è l'esponente che definisce la forma e la dimensione dello spot speculare; L è nel nostro caso rappresentato dal vettore LightVec; R è il vettore riflesso di -ViewVec rispetto alla normale locale della superficie normal. La linea 114 esegue proprio questo calcolo utilizzando le funzioni proprie dell'HLSL dove: pow(a, b) calcola a elevato alla b; reflect(v, n) calcola il vettore riflesso di v rispetto alla normale n. È da notare che le definizioni delle funzioni HLSL date qui come in altri capitoli sono molto generiche perchè nella maggior parte dei casi possono essere utilizzate con diversi tipi di parametri (numeri, vettori, matrici).
Nell'immagine seguente è possibile visualizzare il vettore riflesso generato da un vettore origine e da uno facente da normale e l'individuazione grafica del tangent-space (R, G, B = X, Y, Z).

Ora che tutte le componenti sono state calcolate non rimane che sommarle per comporre il colore finale del pixel corrente.
