B10. HLSL: le basi
Hlsl è il linguaggio usato per scrivere gli shader, ossia i file da cui traggono origine gli effetti e le tecniche usate durante la costruzione 3D. Per avanzare ulteriormente nella programmazione di un buon paesaggio è assolutamente necessario conoscere questo linguaggio, poiché il resto delle tecniche usate sono attuate tutte non dal programma ma dalla GPU.
Un po' di teoria
Cominciamo coll'illustrare il funzionamento della GPU, passo per passo:
- Il programma calcola tutti i vertici, e li passa alla GPU, preoccupandosi anche di comunicare qual è il tipo di questi vertici e quante e quali informazioni esso contiene
- I vertici grezzi passano attraverso il Vertex Shader, che non è altro che una procedura di elaborazione. Esso ha il compito di leggere i dati contenuti in un vertice e da questi dedurre altre informazioni che serviranno in seguito, ad esempio: la posizione 2D sullo schermo del vertice (la cui posizione era definita in uno spazio 3D); il colore; la direzione della luce e la sua intensità; le coordinate texture; eccetera...
- L'output così prodotto passa attraverso altre due fasi "di lavorazione", che sono automatiche e dirette dalla GPU senza intervento umano, ossia l'interpolatore e il rasterizzatore. Il primo ha il compito di calcolare colore e posizione di tutti i punti dello spazio che si trovano tra tre vertici (procedimento di interpolazione lineare) e che costituiscono la superficie del triangolo definito dal programmatore. Il secondo ha il compito di trasformare la scena 3D (anche detta vettoriale) in un'immagine 2D che possa essere visualizzata sul monitor.
- Tutta l'informazione elaborata fino a questo punto passa al pixel shader, un'altra procedura di elaborazione. Il compito più importante di questa procedura consiste nel calcolare il colore corretto del pixel in questione, basandosi su tutti i fattori ottenuti dai precedenti step (colore originario o texture, luci, distanza dalla telecamera, ombre, riflessioni, eccetera...).
- C'è ancora un altro stadio prima del passaggio diretto al monitor: lo z-buffering, che calcola la profondità degli oggetti e stabilisce quali siano visibili in primo piano e quali siano nascosti dietro, o invisibili a causa della distanza elevata.
La parte XNA
XNA non interviene nella realizzazione di una tecnica, ma ha un ruolo importante nel definire il tipo di vertici usati. Infatti, il Vertex
Shader prende i dati direttamente dalle strutture dei vertici. Per fare un esempio, andiamo un po' indietro nelle lezioni, e ripeschiamo
la struttura VertexPositionNormalColor, un po' modificata:
È arrivata l'ora di spiegare quello che avevo tralasciato nelle lezioni scorse. Allora, la GPU non è molto intelligente, e per farle leggere una struttura in modo corretto dobbiamo comunicare esattamente quanta memoria occupa in totale e quanto spazio prende ciascuno dei suoi membri, in ordine di dichiarazione. Inoltre, poiché la struttura letta verrà passata al Vertex Shader, bisogna anche specificare alcuni parametri addizionali che indicano lo scopo per cui un membro viene usato.Private Structure VertexPositionNormalColorPublic PositionAs Vector3Public ColorAs ColorPublic NormalAs Vector3Public Shared SizeInBytesAs Int16 = 7 * 4Public Shared VertexElements()As VertexElement =New VertexElement() _ { _New VertexElement(0, 0, VertexElementFormat.Vector3, _ VertexElementMethod.Default, _ VertexElementUsage.Position, 0), _New VertexElement(0, 4 * 3, _ VertexElementFormat.Color, _ VertexElementMethod.Default, _ VertexElementUsage.Color, 0), _New VertexElement(0, 4 * 4, _ VertexElementFormat.Vector3, _ VertexElementMethod.Default, _ VertexElementUsage.Normal, 0) _ }End Structure
Innanzitutto viene la dichiarazione dei campi della struttura:
E fin qui niente di nuovo. Poi bisogna specificare come valore statico la variabile SizeInBytes, che rappresenta la dimensione della struttura in bytes. In questo caso abbiamo che è 7*4=28 bytes. La spiegazione è semplice: il tipo Vector3 non è altro che l'insieme di 3 valori X, Y e Z, ognuno dei queli è di tipo Single, la dimensione del quale è 4 bytes; perciò un Vector3 occupae 3*4=12 bytes. La struttura Color, invece, è formata da quattro semplici campi, A, R, G e B, ognuno dei quali occupa un solo byte di memoria. La dimensione di Color, quindi, è 4 bytes. Ora, per calcolare quanta memoria occupa VertexPositionNormalColor basta sommare la dimensione di tutti i suoi membri:Public PositionAs Vector3Public ColorAs ColorPublic NormalAs Vector3
Vector3 + Color + Vector3 = 12 + 4 + 12 = 28Fate bene attenzione a notare questo fatto: la dimensione di una struttura è data dalla somma della dimensione di tutti i tuoi campi d'istanza. Infatti, non abbiamo contato i campi Shared, che vengono salvati da un'altra parte. Lo stesso discorso non vale per le classi, le quali occupano, a parità di campi, più memoria.
Ma specificare la dimensione totale non basta. La GPU esige di sapere la posizione e la funzione di ogni membro della struttura. Questo viene fatto attraverso la creazione di un array di VertexElement, ognuno dei quali rappresenta un membro. Il costruttore del tipo VertexElement accetta sei diversi parametri:
- Stream As Int16 : numero del vertex stream da usare. Il vertex stream è il flusso di dati su cui vengono salvati i vertici. Noi usiamo un solo array, quindi un solo flusso, e perciò questo parametro è sempre 0
- Offset As Int16 : offset a cui si trova l'elemento (corrisponde alla somma delle dimensioni degli elementi precedenti). Ad esempio, nel nostro caso, Position si trova a offset 0 perchè è il primo elemento, mentre Color si trova dopo Position, ossia dopo 12 bytes dall'inizio della struttura
- ElementFormat As VertexElementFormat : semplicemente definisce il tipo del membro con un enumeratore
- ElementMethod As VertexElementMethod : metodo con cui viene elaborato il membro. Non entriamo in dettaglio
- ElementUsage As VertexElementUsage : scopo per il quale il membro viene usato. In pratica, questo enumeratore riferisce a cosa serve e cosa indica l'elemento in questione. Position indicherà una posizione, Normal una normale, Color un colore, e potrebbero esserci altri membri che definiscono delle coordinate texture, eccetera...
- UsageIndex As Byte : indice d'uso. Di default, questo parametro è sempre 0, ma può capitare che in una certa struttura ci siano due membri con la stessa funzione (ad esempio due coordinate texture). Per non confondere la GPU, il primo avrà UsageIndex impostato su 0, mentre il secondo su 1. Questo fatto viene evidenziato in maniera particolare nel codice Hlsl
Ottenuta cambiando un 4 in un 8
La parte HLSL
Veniamo al sodo. Gli shader vengono scritti in hlsl in file con estensione *.fx. La sintassi di questo linguaggio è molto simile
al C#, quindi dovrete fare un po' di sforzo per imparare.Tutti i parametri (ad esempio View, Projection, LightDirection) vengono dichiarati generalmente all'inizio del file, ma possono essere dislocati a piacere in punti strategici: l'importante è che vengano definiti prima di essere usati. In tutti i sorgenti che vedremo, dichiarerò i parametri delle tecniche all'inizio del file, per una questione di ordine. In hlsl i tipi vector3, vector4 e matrix vengono sostituiti dai corrispettivi float3, float4 e float4x4. Bisogna notare che i tipi hanno nomi significativi che non ci fanno venire in mente la loro funzione, ma la loro dimensione: infatti la GPU opera a basso livello ed è importante sempre tenere sott'occhio l'utilizzo della memoria. State attenti agli errori di battitura, però: sostituire un float3 con un float4 può causare disastri. La sintassi con cui si dichiara una variabile (perchè i parametri altro non sono che variabili interne allo shader, come i membri di una classe) è questa:
[Tipo] [Nome];Quindi, per dichiarare le nostre matrici, si userà questo codice:
Notare che dopo ogni istruzione è necessario un punto e virgola: coloro che conoscono C e derivati o pascal saranno forse abituati, ma per chi conosce solo VB, questo fatto può essere fonte di enormi frustrazioni, quindi, fate attenzione.float4x4 World;float4x4 View;float4x4 Matrix;
Anche in Hlsl si possono definire le strutture e si usa questa sintassi:
A prima vista può apparire difficile, ma se ci riflettete non lo è tanto. Infatti, potete considerare ogni membro simile a un VertexElement. Come abbiamo visto nel paragrafo precedente, ogni VertexElement è inizializzato specificandone, tra le altre cose, il tipo e la funzione. Quindi, possiamo rappresentare la struttura VertexPositionNormalColor in questo modo:struct [Nome] { [Tipo membro] [Nome membro] : [Funzione membro]; [Tipo membro 2] [Nome membro 2] : [Funzione membro 2]; ... };
Position è un float3, ossia il corrispondente di Vector3, e la sua funzione è quella di indicare una posizione. Color è un float4 poiché in hlsl i suoi campi A, R, G e B sono di tipo Single (in quanto variano tra 0 e 1 e non tra 0 e 255), e la sua funzione è di definire un colore: notate che ho scritto COLOR0, dove lo 0 rappresenta il sopracitato UsageIndex, poiché potrebbero esserci altri parametri con la stessa funzione. E Normal viene da sé. Le strutture non vengono usate per rappresentare vertici come ho fatto io in quello che era solo un esempio, ma vengono usate per passare dati dal vertex shader al pixel shader. Infatti, il vertex shader riceve in input la struttura che definisce un vertice, e produce in output una struttura quasi identica, ottenuta con qualche calcolo, la quale viene poi passata al pixel shader, che fornisce come risultato il colore finale del pixel.struct VertexPositionNormalColor {float3 Position :POSITION ;float4 Color :COLOR0; float3 Normal :NORMAL0 ; };
Per quanto riguarda il vertex shader e il pixel shader, essi sono rappresentati da comuni funzioni. La sintassi di una funzione in hlsl è questa:
[Tipo restituito] [Nome]([Parametri])
{
[Istruzioni]
}
Come i campi di struttura, i parametri devono presentare nome, tipo e funzione. Ma vedremo in dettaglio qualche esempio nel prossimo tutorial.Infine, una tecnica si dichiara in questo modo:
Notare che nella tecnica c'è un passo solo (Pass0). Come abbiamo detto all'inizio, ogni tecnica può avere più passi, ma tutte quelle che useremo ne avranno uno solo.technique [Nome] {pass Pass0 { VertexShader =compile vs_1_1 [vertex shader](); PixelShader =compile ps_1_1 [pixel shader](); } }
The Totem's Lair - Copyright (C) 2009
È vietata la riproduzione sia totale che parziale del sito.



