2.2. Dichiarazione delle funzioni del Vertex Shader e del Pixel Shader

5
Il tuo voto: Nessuno Media: 5 (1 vote)

Il codice seguente è un esempio della dichiarazione di funzioni per i Vertex Shader ed i Pixel Shader e della definizione di strutture per il loro corretto funzionamento.

 28 // Struttura per gli elementi in uscita dal Vertex Shader
 29 struct VS_OUT
 30 {
 31     float4 oPos : POSITION;
 32     float2 oTexCoords : TEXCOORD0;
 33     float3 oNormal : TEXCOORD1;
 34 };
 35 
 36 // Dichiarazione della funzione del Vertex Shader con elementi in input
 37 // del vertice inseriti direttamente nella lista dei parametri.
 38 VS_OUT mainVS(
 39     in float3 Pos  : POSITION,
 40     in float3 Normal : NORMAL,
 41     in float2 TexCoords : TEXCOORD0)
 42 {
 43     // Dichiarazione della struttura per i dati in uscita
 44     VS_OUT Out;
 45 
 46     // Trasformazione del vertice da object-space a world-space
 47     float4 p = mul(float4(Pos, 1.0), World);
 48 
 49     // Passaggio della posizione trasformata in screen-space al pixel shader
 50     Out.oPos = mul(p, ViewProj);
 51 
 52     // Passaggio delle coordinate di texture e della normale
 53     // del vertice corrente al pixel shader
 54     Out.oTexCoords = TexCoords;
 55     Out.oNormal = Normal;
 56 
 57     return Out;
 58 }
 59 
 60 // Dichiarazione del Pixel Shader per il rendering senza alcuna illuminazione
 61 float4 texturedPS(
 62     float2 TexCoord : TEXCOORD0) : COLOR0
 63 {
 64     // Recupero del texel corrispondente alle coordinate specificate e
 65     // modulazione con il colore diffuso del materiale
 66     float4 base = tex2D(diffuseSampler, TexCoord) * diffuseColor;
 67 
 68     // Calcolo del colore definitivo
 69     return (ambientColor + base);
 70 }
 71 
 72 // Dichiarazione del Pixel Shader per il rendering con
 73 // illuminazione da una luce direzionale
 74 float4 litTexturedPS(
 75     float2 TexCoord : TEXCOORD0,
 76     float3 Normal : TEXCOORD1) : COLOR0
 77 {
 78     // Normalizzazione dei vettori provenienti dal VertexShader
 79     Normal = normalize(Normal);
 80 
 81     // Recupero del texel della texture
 82     float4 diffuse = tex2D(diffuseSampler, TexCoord) * diffuseColor;
 83 
 84     // Calcolo dell'intensità di illuminazione diffusa
 85     float diff = saturate(dot(Normal, lightVec));
 86 
 87     // Calcolo del colore definitivo come somma delle componenti
 88     return (ambientColor + diffuse * diff);
 89 }

Per chi non è a digiuno di C o conosce un po' il C#, sarà palese il fatto che queste funzioni si dichiarano esattamente come i metodi di una classe: viene dichiarato il tipo del valore di ritorno (che proprio come nel C/C# può essere anche una struttura), si specifica il nome del metodo che tra parentesi conterrà l'elenco dei parametri ed infine tra le due parentesi graffe si dichiara il blocco di codice da eseguire (una volta per vertice per un Vertex Shader ed una volta per pixel per un Pixel Shader). Anche se non ci sono restrizioni riguardo il nome della funzione, è buona norma aggiungere il suffisso VS se si tratta di un Vertex Shader oppure il suffisso PS se si tratta di un Pixel Shader permettendo un riconoscimento immediato. Se l'aggiungere un suffisso sembra un'eccesso vi invito a pensare che adesso, rispetto a qualche tempo fa, gran parte delle operazioni che si possono svolgere in un Pixel Shader sono ammesse anche in un Vertex Shader, come nel caso delle Vertex Textures (delle texture utilizzate all'interno dei Vertex Shader).

Nell'HLSL ci sono due modi differenti di specificare i dati che arrivano e che escono dal Vertex Shader e quelli che arrivano al Pixel Shader. Uno è quello di creare delle strutture dati (come VS_OUT) che contengano il pacchetto di dati ed utilizzare questa struttura come parametro o valore di ritorno. L'altro metodo è quello di dichiarare direttamente tra le parentesi tonde della funzione tutti i parametri in ingresso facendoli precedere dalla parola chiave in e tutti quelli in uscita utilizzando invece la parola chiave out divisi da una virgola. Specificatamente in quest'ultimo metodo è buona pratica (per evitare duplcazione di nomi dei parametri) inserire come prefisso la lettera "o" (out) per i parametri in uscita e, opzionalmente, la lettera "i" per quelli in ingresso. In entrambi i casi per ogni elemento è necessario indicare il suo utilizzo tramite la parola chiave posta dopo i due punti, ossia la semantica della variabile.

Non ci sono differenze nell'utilizzare l'uno piuttosto che l'altro metodo, tuttavia per chi inizia è forse più comodo utilizzare le strutture, almeno per i dati in uscita dal Vertex Shader e per quelli in entrata nel Pixel Shader. In questo modo si avrà sempre la corrispondenza perfetta del pacchetto di informazioni dato che la struttura è definita una sola volta e che un'eventuale modifica sia al nome dei campi che alla loro semantica dovrà essere effettuata in un solo luogo ansi che in locazioni multiple del file .fx.

Come è visibile dal codice nulla vieta di utilizzare una mescolanza di entrambi i metodi rappresentando i dati in uscita dal Vertex Shader con una struttura e specificando invece singolarmente i parametri nel o nei Pixel Shaders. Questo approccio è utile soprattutto quando un Vertex Shader viene condiviso tra più tecniche ed i dati vengono quindi ricevuti da differenti Pixel Shaders. In questo caso gli elementi che vengono omessi nel Pixel Shader, perchè non utilizzati, non sono un problema e non generano ne errori di compilazione ne causano instabilità nel codice.

In questo capitolo non voglio dilungarmi troppo in spiegazioni sulla logica degli shaders, anche per la presenza di commenti nel codice, ma voglio comunque soffermarmi un attimo su alcune linee di codice che sono il cuore del funzionamento del Vertex Shader e dei due Pixel Shader proposti dato che questa implementazione sarà parte integrante di alcuni shader proposti successivamente.

Le linee di codice principali qui sono: la 47 e la 50, la 66 e la 85.

La linea 47 trasforma un vertice dallo spazio object-space in quello world-space "spostando l'oggetto nella posizione definitiva della scena attuale"; ciò significa che i valori delle componenti del vertice saranno riscalati, ruotati e traslati secondo le informazioni contenute nella matrice World. Per quelli di voi che utilizzano un programma di grafica come Blender o 3D studio max possono pensare che i tre assi che definiscono l'object-space corrispondono al manipolatore che appare cliccando sopra un oggetto quando lo si vuole modificare.

La linea 50 trasforma il vertice da world-space in view-space e successivamente in screen-space. Questi tre sono i tre spazi di lavoro che sono sempre coinvolti in un'applicazione 3D. Il primo che rappresenta il sistema di coordinate dell'intera scena e di solito è impostato ad Identità (una matrice impostata ad identità non trasforma un vettore o un'altra matrice); il secondo può essere pensato come la posizione della telecamera, quindi in questo spazio l'orgine ddegli assi è la posizione corrente della telecamera; l'ultimo è lo spazio riferito allo schermo e in XNA vede l'asse X parallelo al bordo orizzontale con direzione da sinistra a destra, l'asse Y parallelo al bordo verticale con direzione dal basso in alto e l'asse Z nella direzione perpendicolare allo schermo con direzione verso l'esterno.

La linea 66 è sicuramente la più utilizzata in un qualsiasi shader; la funzione tex2D(sampler s, float2 uv) è iinfatti utilizzata per recuperare il colore della texture legata al sampler s nella posizione uv.

La linea 85 è quella che da sola incorpora l'ombreggiatura di tipo diffuso e per fare questo sfrutta la legge di Lambert per determinare l'intensità apparente di un determinato punto di una superficie conoscendo l'angolo che c'è tra la normale di quel punto e il vettore che definisce la direzione della sorgente luminosa. Più precisamente indica che l'intensità apparente equivale al coseno dell'angolo specificato prima. Nella grafica 3D il coseno dell'angolo creato da due vettori di lunghezza unitaria è equivalente al prodotto scalare dei due vettori ( Normal · lightVec ); questa operazione è effettuata proprio dalla funzione dot(float3 u, float3 v).

Ora rimane solamente da definire il comportamento della funzione saturate(a) che fa in modo di troncare il parametro in ingresso all'intervallo 0.0 ... 1.0. L'uso di questa funzione si rende necessario perchè il coseno di un angolo può assumere valori negativi ed è sempre buona norma che un Pixel Shader non ritorni valori negativi oppure superiori a 1.0 (anche se vedermo che in alcuni casi i valori possono essere maggiori di 1.0) per non avere effetti inaspettati.