2.5. Un esempio più complesso

5
Il tuo voto: Nessuno Media: 5 (2 voti)

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.