Il codice shader ci permette di gestire la nostra grafica trasformando i nostri vertici da ciò che ci arriva tramite il vertex buffer ad uno o più valori float4 che diventano i nostri colori sullo schermo. Passiamo in dettaglio lo shader.
Uno shader è molto simile ad un listato di codice C seppur molto semplificato e senza la presenza dei puntatori. All'interno del codice è possibile inserire molte funzioni, strutture ed effettuare anche include ad altri file (permettendo di includere quindi altri file e crearvi delle librerie di effetti).
Alla fine del file viene posizionata la technique che descrive quali funzioni vanno inserite nel device per vertex, pixel e geometry shader.
Come detto nei precedenti tutorial lo shader viene eseguito per tutti i vertici ed i pixel coinvolti nell'operazione di Draw. Questo significa che difficilmente potremo avere il controllo per ogni singolo vertice (ne tantomeno per i pixel). Di conseguenza tutti i dati saranno passati all'inizio e resteranno tali per tutta la durata del rendering.
Il primo input è il vertex buffer che contiene i vertici della scena. Nel precedente tutorial abbiamo visto come, attraverso il vertex layout e le semantiche, sia possibile ottenere i vertici contenuti nel buffer. Di fatto per ogni elemento presente viene eseguito una volta il vertex shader. Di conseguenza un primo modo di inserire dati è proprio nel vertex buffer utilizzando le variabili al suo interno. Questo per dati che devono essere unici per ogni vertice. Ci sono dati però che sono comuni a tutti i vertici. Questi si chiamano costanti. Le costanti vanno inserite all'interno del codice shader ma fuori dalle funzioni (come variabili globali quindi). Queste sono visibili in tutto lo shader ma non possono essere modificate dal codice (farlo non restituisce errori ma il loro stato tornerà al valore iniziale dopo ogni ciclo). Questo quindi vi impedisce di conservare dati (esempio usare una variabile per contare il numero delle volte che viene fatta un'operazione).
L'unico modo per passarle è dal device.
float4 valore:MyValue;
struct VS_INPUT
{
float4 Pos : POSITION;
float4 color:COLOR;
};
struct PS_INPUT
{
float4 Pos : SV_POSITION;
float4 color:COLOR;
};
PS_INPUT VS( VS_INPUT input )
{
PS_INPUT output = (PS_INPUT)0;
output.Pos = input.Pos;
output.color=input.color;
return output;
}
float4 PS( PS_INPUT input) : SV_Target
{
return input.color + valore;
}
DepthStencilState EnableDepth
{
DepthEnable = TRUE;
DepthWriteMask = ALL;
DepthFunc = LESS_EQUAL;
};
technique10 Render
{
pass P0
{
SetVertexShader( CompileShader( vs_4_0, VS() ) );
SetGeometryShader(NULL);
SetPixelShader( CompileShader( ps_4_0, PS() ) );
SetDepthStencilState( EnableDepth, 0 );
}
}
In questo esempio in cima ho definito una costante di tipo float4 con nome "valore" e semantica "MyValue". Nel pixel shader l'ho utilizzata per incrementare il valore di input.color. La semantica è utile all'interno di DirectX10 per poter dare un nome significativo dell'utilizzo della variabile ed è buona pratica utilizzarla.
All'interno del blocco pass potete vedere il SetDepthStencilState che viene utilizzando usando come parametro la variabile EnableDepth definita poco sopra. Questa è la maniera in cui è possibile valorizzare i parametri del device (in questo caso la gestione delle profondità). Più avanti avremo modo di approfondire. Nel frattempo ricordatevi di utilizzare questa opzione del depth-stencil in quando a partire dalla SDK di giugno 2007 DirectX ha lasciato disattivata la gestione delle profondità e quindi avreste un rendering errato se non l'attivate.
In HLSL ci sono i seguenti tipi primitivi
- bool : true o false
- int : 32 bit
- uint : 32 bit unsigned (quindi solo valori positivi)
- half : 16 bit (rappresenta un float con metà della precisione ma in realtà è un tipo fittizio lasciato per retrocompatibilità. In DirectX10 viene convertito in float dal compilatore)
- float : 32 bit
- double : 64-bit
Maggiore precisione dà risultati migliori a discapito della velocità. In HLSL è possibile far precedere le variabili con i suffissi snorm ed unorm
unorm float myValue;
I suffissi snorm fanno in modo che il valore sia limitato tra -1 ed 1, mentre unorm da 0 ad 1. Sembra sciocco ma in realtà negli shader si usano moltissimo valori compresi in questi intervalli.
E' possibile inizializzare le variabili con un valore a nostro piacimento. Questo sarà ovviamente sovrascritto se dal device passeremo un valore.
In HLSL abbiamo i tipi vettori, molto importanti per i nostri scopi. Un tipo vettore è un tipo base ripetuto. Ad esempio float2 significa un vettore con 2 float.
I valori vanno da 1 a 4 (non è possibile utilizzare vettori più grandi). Un valore di tipo vettore si utilizza come una struttura ed i suoi sottovalori saranno X,Y, Z e W.
Esempio
float3 valore;
valore.x=0;
I vettori possono essere utilizzati come i tipi primitivi possedendo tutte le operazioni primitive (+,-,*,/). Nel caso di vettori queste sono applicate componenti per componenti.
Esempio
float3 v1; float3 v2;
v1 + v2 <=> (v1.x+ v2.x , v1.y+ v2.y , v1.z+ v2.z)
I componenti possono essere valorizzati o letti in diverse maniere. Ad esempio scrivere float4 v=5; significa passare il valore 5 a tutte le componenti. E' possibile anche scrivere in questo modo:
float4 v1;
v1.xy=5;
In questo caso x ed y saranno valorizzati a 5.
Potrete anche scambiare i vari componenti a vostro piacimento.
Il terzo tipo fondamentale è la matrice. Una matrice è un vettore di vettori. Ad esempio
Float4x4 myMatrix;
Questa è una matrice composta da 16 float. Per poter leggere i suoi valori si usa questa scrittura
matrix.m11;
Potrete creare anche strutture.
struct myStruct
{
int val1;
float val2;
};
Per gli array dovrete usare dimensioni fisse.
float val[10];
Questi sono i primi tipi base in DirectX10. Ora andiamo a vedere come questi valori vengono impostati dal nostro codice.
Ogni variabile creata nello shader può essere recuperata tramite il suo effect.
ID3D10EffectVariable* myVariable;
myVariable=effect->GetVariableByIndex(index);
myVariable=effect->GetVariableByName(name);
myVariable=effect->GetVariableBySemantic(semantic);
Potete decidere di richiamare una variabile tramite l'ordine in cui è stata dichiarata, il suo nome o la sua semantica.
Ciò che otterrete sarà una variabile generica che potete specializzare attraverso i metodi di ID3D10EffectVariable;
ID3D10EffectVectorVariable* var1 = myVariable->AsVector();
ID3D10EffectMatrixVariable* var2 = myVariable->AsMatrix();
ID3D10EffectScalarVariable* var3 = myVariable->AsScalar();
In questo modo potrete creare variabili specializzate. Sarà responsabilità vostra specializzare nel modo corretto le variabili. Per passare i dati vi basterà usare una delle istruzioni set contenute (ne esistono diverse specifiche per ogni tipo). Ad esempio per il vettore
var1->SetFloatVector(value);
var1->SetIntVector(value);
Dove value è, nel primo caso, un puntatore ad un array di float, nel secondo ad un array di int. E' possibile usare anche
var1->SetFloatVectorArray(data,offset,count);
Questo serve per valorizzare interi array. Data è il nostro array di float, offset il punto dell'array da dove iniziare la lettura e count il numero di float da leggere.
Per quanto riguarda le strutture dovrete utilizzare la prima effectVariable per ottenere i suoi membri attraverso l'istruzione
myVariable->GetMemberByIndex(index);
myVariable->GetMemberByName(name);
myVariable->GetMemberBySemantic(semantic);
queste restituiranno a loro volta un effectVariable. Volendo potrete passare dati in maniera più generica con l'istruzione
myVariable->SetRawValue(data,offset,count);
dove data è un puntatore a void. Potrete in questo modo valorizzare una intera struttura con un'unica istruzione.
Ultima nota, l'istruzione GetDesc vi permetterà di avere una descrizione della variabile.
Per passare il valore allo shader vi basterà utilizzare le varie SetValue prima del draw e queste andranno a valorizzare le variabili. Questi valori rimarranno nello shader anche dopo, quindi per variabili che cambiano raramente il loro valore potete anche evitare di dover reinserire valori già presenti.
Nel prossimo tutorial realizzeremo un vertex shader completo.