La più grande innovazione che directX8 aveva portato è stata senza dubbio l'introduzione della tecnologia shader. Il poter intervenire direttamente sulla renderizzazione grafica dei vertici e dei pixel direttamente tramite accellerazione hardware ha portato a nuovi ed impensabili effetti grafici. DirectX9 ha poi aggiunto nuovi versioni di linguaggio assembler ma subito si è notata una cosa: il linguaggio assembler usato nei vertex e pixel shader doveva essere sostituito con un linguaggio più vicino ai programmatori. Nasce HLSL, un linguaggio simile al C per la programmazione di vertex e pixel shader. Non esiste paragone tra la semplicità di codice C rispetto al codice assembler. Il codice HLSL viene caricato in maniera simile a quello assembler ed utilizzato in maniera perfettamente identica. Descrivere in un solo tutorial la ricchezza di un linguaggio così avanzato rispetto al codice assembler è cosa abbastanza difficile. Introdurrò quindi la base della sintassi e le istruzioni. Solo la pratica aiuterà ad impadronirsi della tecnica.
Scrivere codice HLSL
struct OUTPUT{
float4 position:POSITION;
float4 diffuse:COLOR0;
float2 texture0:TEXCOORD0;
};
struct INPUT{
float4 position:POSITION;
float4 normal:NORMAL;
float2 texture0:TEXCOORD0;
};
float4x4 matrice;
float4 materiale;
OUTPUT Main(INPUT inP){
OUTPUT outP;
outP.position=mul(inP.position,(float4x4)matrice);
outP.diffuse=materiale;
outP.texture0=inP.texture0;
return outP;
}
Questo è un esempio di codice shader. In questo esempio potete vedere una funzione main che riceve un input e restituisce un output. Le 2 strutture sono definite in alto. Come vedete è semplice codice c. La funzione moltiplica la matrice "matrice" per la posizione di input e la restituisce nell'Output. Le strutture che possono avere il nome che volete (non per forza INPUT e OUTPUT) servono per definire gli ingressi, i classici vertexdeclaration e le uscite. La funzione di avvio (che arbitrariamente ho chiamato Main) deve avere lo stesso nome di quella caricata da codice basic. IMPORTANTE: HLSL è case sensitive. Questo significa che c'è differenza tra maiuscole e minuscole (cosa che in VB non c'è). Nel codice avete moltissima libertà ma andiamo ad iniziare.
Sintassi
La sintassi è identica al C. I commenti sono preceduti da // e ogni istruzione è seguita da ; I blocchi (funzioni, if, for) usano come delimitatori le parentesi graffe ottenibili tenendo premuto alt e digitando sul tastierino numerico 123 o 125.
Tipi
In HLSL si possono usare come in un normale linguaggio delle variabili. Ecco i tipi a disposizione.
bool
int 32
half 16
float 32-bit
double 64-bit
Dichiarare una variabile è molto semplice.
Es.
float I;
float var=3.1;
float var[3];
float var[]={1,2,3};
Come vedete si possono utilizzare anche array e inizializzarli in fase di creazione. Tramite opportune parole chiave è possibile dichiarare variabili come costanti o globali. Questo per il fatto che si possono usare funzioni e dividere il programma in subroutine. Const : per la definizione di costanti Static : permette di essere usata solo nel codice HLSL e non può essere modificata da codice VB Extern : puà essere modificata tramite le istruzioni di settaggio dei registri Uniform : rimangono costanti per tutto il codice pixel o vertex Shared : comuni a tutto il codice Effect (solo per l'uso tramite Effects)
Vettori
E' possibile creare variabili che contengano vettori (importanti per ogni cosa visto che tutto funziona con componenti .X,Y,Z e W). Ecco degli esempi
bool bVector;
bool1 bVector;
int1 iVector;
half2 hVector;
float3 fVector;
double4 dVector;
Il numero da 1 a 4 stabilisce quanti canali possiedono (3 indica ad esempio XYZ). Se non c'è valore equivale ad 1. Le matrici invece hanno la loro descrizione completa.
float4x4 matrix;
double4x4 matriceDouble;
per accedere ai singoli elementi si usa _mXY
ad esempio
matrice._m00=5;
Matematica
Si possono usare tutte le operazioni matematiche (+,-,*,/); I prodotti saranno per componente. Quindi il prodotto fra matrici non sarà una concatenazione con il *. Ovviamente c'è una istruzione per la concatenazione. Anche le matrici possono essere inizializzate.
es
float2x2 matrice={1,2,3,4};
Booleani
Per i booleani si usano gli stessi operatori usati in C. &&, ||, <, >, ==, !=, <=, >=. Nell'ordine: AND, OR, minore, maggiore, uguale (sono 2 gli =), diverso, minore uguale, maggiore uguale.
Espressioni di controllo
If (condizione){
istruzioni
}else{
istruzioni
}
for(int i=0;i<10;i++){
istruzioni
}
do{
istruzioni
}while(condizione)
while(condizione){
istruzioni
}
Strutture
structure gruppo{
float4 a;
float4x4 matrice;
};
le strutture possono essere usate per raggruppare dati di interesse.
Impostare uno shader
Il sistema migliore per impostare uno shader è quello di creare una strutture di input, una di output e una funzione principale che prenda l'imput, lo elabori e restituisca un output tramite l'istruzione return. Per associare un registro di input ad uno di output basta far seguire la variabile da :TIPOdiRegistro.
es
float4 posizione:POSITION;
Ovviamente l'input e l'output devono essere impostate correttamente. L'input dei vertex shader è la struttura del vertice (quindi solitamente userete il vertice standard). Per l'uscita esporterete una posizione (POSITION), il colore diffuse e specular (2 variabili COLOR) ed un certo numero di coordinate texture (TEXCOORDn). Per il pixel shader invece l'input è 2 registri color (diffuse e specular) ed un certo numero di registri texture. e l'uscita un color. Ecco degli esempi di strutture per l'input e l'output
struct uscita{
float4 color:COLOR;
};
struct entrata{
float4 diffuse:COLOR0;
float4 specular:COLOR1;
float2 texC1:TEXCOORD0;
};
Funzioni una funzione è strutturata così: tipoRestituito Nomefunzione(input).
ad esempio
float somma(float a, float b){
float s=a+b;
return s;
}
le funzioni vanno dichiarate prima della funzione che la usa. Ricordate che la funzione principale deve restituire i dati minimi degli shader (posizione per i vertex e colore per pixel)
Istruzioni
Nome,sintassi
Descrizione
abs(value a)
restituisce il valore assoluto
acos(x)
restituisce l'arcocoseno
all(x)
restituisce true se tutti i componenti sono diversi da zero
any(x)
restituisce true se almeno un componente è diverso da zero
asin(x)
restituisce l'arcoseno
atan(x)
restituisce l'arcotangente
atan2(y, x)
restituisce l'arcotangente di y/x
ceil(x)
restituisce il più piccolo intero maggiore o uguale a x (arrotondamento in eccesso)
clamp(x, min, max)
limita x tra min e max
clip(x)
se uno dei canali è uguale a zero non disegna il pixel
cos(x)
restituisce il coseno
cosh(x)
restituisce il coseno iperbolico
cross(a, b)
restituisce il cross product
D3DCOLORtoUBYTE4(x)
converte x per farlo diventare un colore per hardware che non supportano UByte4
ddx(x)
Calcola la percentuale di cambio nella direzione X
ddy(x)
Calcola la percentuale di cambio nella direzione y
degrees(x)
converte radianti in gradi
determinant(m)
restituisce il determinante di una matrice
distance(a, b)
restituisce la distanza fra a e b
dot(a, b)
esegue il dot product
exp(x)
restituisce e^x
exp2(value a)
restituisce 2^x
faceforward(n, i, ng)
restituisce -n * sign(dot(i, ng))
floor(x)
restituisce il più grande intero minore uguale a x (approssimazione difetto)
fmod(a, b)
restituisce il resto della divisione
frac(x)
restituisce la parte frazionaria di x
frexp(x, out exp)
restituisce la parte frazionaria di x e in exp inserisce la mantissa
fwidth(x)
restituisce abs(ddx(x)) + abs(ddy(x)).
isfinite(x)
restituisce true se x è finito
isinf(x)
restituisce true se x è infinito (+ o -)
isnan(x)
restituisce true se x è indeterminato (n/zero)
ldexp(x, exp)
restituisce x * 2exp
length(v)
restituisce la lunghezza del vettore
lerp(a, b, s)
restituisce l'interpolazione lineare tra a e b in base ad s
lit(n • l, n • h, m)
restituisce un vettore illuminazione dato il vettore diffuse, specular e ambient
log(x)
restituisce il logaritmo di x
log10(x)
restituisce il logaritmo in base 10 di x
log2(x)
restituisce il logaritmo in base 2 di x
max(a, b)
restituisce il massimo tra a e b
min(a, b)
restituisce il minimo tra a e b
modf(x, out ip)
restituisce la parte frazionaria di x e mette la parte intera in ip
mul(a, b)
restituisce il prodotto fra vettori. Per le matrici usate il cast es
mul(vettore,(float4x4 matrice)
importante perchè sostituisce m4x4
noise(x)
non implementata
normalize(v)
restituisce il vettore normalizzato
pow(x, y)
restituisce x^y
radians(x)
converte gradi in radianti
reflect(i, n)
calcola la riflessione data la direzione incidente i e la normale n
refract(i, n, ri)
calcola la rifrazione data la direzione incidente i, la normale n e l'angolo di rifrazione ri
round(x)
arrotonda x
rsqrt(x)
restituisce 1/radice quadrata di x
saturate(x)
limita x tra 0 e 1
sign(x)
restituisce il segno di x come -1,0,1
sin(x)
restituisce il seno di x
sincos(x, out s, out c)
restituisce il seno di x in s e il coseno in c
sinh(x)
restituisce il seno iperbolico di x
smoothstep(min, max, x)
restituisce 0 se x < min, 1 se maggiore di max altrimenti esegue una funzione di Hermite
value sqrt(value a)
restituisce la radice quadrata di x
step(a, x)
restituisce 1 se x>=a, 0 altrimenti
tan(x)
restituisce la tangente di x
tanh(x)
restituisce la tangente iperbolica di x
tex1D(s, t)
legge una texture ad 1 dimensione dal sampler s e le coordinate t
tex1D(s, t, ddx, ddy)
legge una texture ad 1 dimensione dal sampler s e le coordinate t usando un cambio di percentuale ddx e ddy
tex1Dproj(s, t)
legge una texture ad 1 dimensione dal sampler s e le coordinate t dividento t per l'ultimo valore
tex1Dbias(s, t)
legge una texture ad 1 dimensione dal sampler s e le coordinate t: il mip map è deciso da t.w
tex2D(s, t)
legge una texture a 2 dimensioni dal sampler s e le coordinate t
tex2D(s, t, ddx, ddy)
legge una texture a 2 dimensioni dal sampler s e le coordinate t usando un cambio di percentuale ddx e ddy
tex2Dproj(s, t)
legge una texture a 2 dimensioni dal sampler s e le coordinate t dividento t per l'ultimo valore
tex2Dbias(s, t)
legge una texture a 2 dimensioni dal sampler s e le coordinate t: il mip map è deciso da t.w
tex3D(s, t)
legge una texture a 3 dimensioni dal sampler s e le coordinate t
tex3D(s, t, ddx, ddy)
legge una texture a 3 dimensioni dal sampler s e le coordinate t usando un cambio di percentuale ddx e ddy
tex3Dproj(s, t)
legge una texture a 3 dimensioni dal sampler s e le coordinate t dividento t per l'ultimo valore
tex3Dbias(s, t)
legge una texture a 3 dimensioni dal sampler s e le coordinate t: il mip map è deciso da t.w
texCUBE(s, t)
legge una texture cubica dal sampler s e le coordinate t
texCUBE(s, t, ddx, ddy)
legge una texture cubica dal sampler s e le coordinate t usando un cambio di percentuale ddx e ddy
texCUBEproj(s, t)
legge una texture cubica dal sampler s e le coordinate t dividento t per l'ultimo valore
texCUBEbias(s, t)
legge una texture cubica dal sampler s e le coordinate t: il mip map è deciso da t.w
transpose(m)
restituisce la trasposta di una matrice
Come potete vedere le istruzioni sono moltissime ma una scheda video compatibile con i pixel shader 2.0 è consigliata per avere il maggior numero possibile di istruzioni. Ogni istruzione HLSL occupa un certo numero di istruzioni assembler e quindi potreste facilmente raggiungere con poche istruzioni un numero altro di istruzioni assembler.
ecco un esempio completo di codice HLSL
struct OUTPUT{
float4 position:POSITION;
float4 diffuse:COLOR0;
float4 specular:COLOR1;
float2 texture0:TEXCOORD0;
};//struttura di outPut
struct INPUT{
float4 position:POSITION;
float4 normal:NORMAL;
float2 texture0:TEXCOORD0;
};//struttura di input
//variabili
float4x4 transform;
float4x4 worldview;
float3 lightDir;
OUTPUT main(INPUT inP){
OUTPUT outP;
float4x4 worldview=transpose(worldview);
float3 N=normalize(mul(inP.normal,(float3x3)worldview));//trasforma la normale in view space
float3 L=lightDir;
L.y=-L.y;//per il settaggio della camera occorre invertire la y
outP.diffuse=max(0,dot(N,L));//calcolo diffuse per materiale bianco
float3 P = mul(inP.position,(float4x3)worldview);
float3 R = normalize(2 * dot(N, L) * N - L);
float3 V = -normalize(P);
outP.specular=pow(max(0, dot(R, V)), 16);//16 è lo specular, per non passarlo da registro
outP.position=mul(inP.position, (float4x4) transform);
outP.texture0=inP.texture0;
return outP;
}
Codice ASM equivalente
// transform c0 // worldview c4
// lightDir c8
vs_1_1
def c9, 0, 0, 0, 0
dcl_position v0
dcl_normal v1
dcl_texcoord v2
mul r0.xyz, v1.y, c5
mad r0.xyz, v1.x, c4, r0
mad r0.xyz, v1.z, c6, r0
dp3 r1.x, r0, r0
rsq r0.w, r1.x
mul r0.xyz, r0, r0.w
mov r1.y, -c8.y
mov r1.xz, c8
dp3 r3.x, r0, r1
add r0.w, r3.x, r3.x
mul r1.xyz, v0.y, c5
mov r2.xz, -c8
mad r1.xyz, v0.x, c4, r1
mov r2.y, c8.y
mad r1.xyz, v0.z, c6, r1
mad r0.xyz, r0.w, r0, r2
mad r1.xyz, v0.w, c7, r1
dp3 r4.x, r0, r0
dp3 r2.x, r1, r1
rsq r1.w, r4.x
rsq r0.w, r2.x
mul r0.xyz, r0, r1.w
mul r1.xyz, r1, r0.w
dp3 r0.x, r0, -r1
max r0.w, r0.x, c9.x
mul r0.w, r0.w, r0.w
mul r0.w, r0.w, r0.w
max oD0, r3.x, c9.x
mul r0.w, r0.w, r0.w
mul oD1, r0.w, r0.w
dp4 oPos.x, v0, c0
dp4 oPos.y, v0, c1
dp4 oPos.z, v0, c2
dp4 oPos.w, v0, c3
mov oT0.xy, v2
Come potete vedere la semplicità d'uso è senz'altro migliore, difficilmente vedo infatti usare ad esempio l'istruzione mad per diminuire il numero di slot di istruzioni assembler. HLSL sostituisce quindi il codice assembly per tutte le versioni e per tutti i linguaggi. Potrete ad esempio compilare differentemente lo stesso codice assembler per diverse versioni vertex shader (a volte il codice in 2_0 occupa meno istruzioni della 1_1). Inoltre tramite l'utilizzo di funzioni potrete più facilmente riutilizzare codice.