\\ Home : Articoli : Stampa
HLSL
Di RobyDx (del 05/03/2007 @ 16:20:47, in DirectX9, linkato 3275 volte)

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.