Programmazione degli Shaders in DirectX 9.0 (parte prima

Transcript

Programmazione degli Shaders in DirectX 9.0 (parte prima
In questa serie di articoli verrà spiegata la programmazione degli Shaders, ovvero dei programmi grafici 3D, eseguiti
dalla GPU della scheda grafica, che elaborano vertici e pixel. Tramite essi è possibile realizzare sensazionali effetti
grafici, prima alla portata solo delle produzioni cinematografiche.
Programmazione degli Shaders in DirectX 9.0 (parte prima) :
Introduzione ai Vertex Shaders
di Stefano Coppi
Negli ultimi anni si è assistito ad un notevole incremento di prestazioni da parte delle schede grafiche 3D per PC, grazie
soprattutto all’introduzione delle unità di Transform & Lighting, che si fanno carico dei pesanti calcoli vettoriali
necessari per le trasformazioni geometriche e per il calcolo dell’illuminazione della scena, lasciando libera la CPU di
dedicarsi ad altri compiti, come ad esempio l’elaborazione del modello fisico e dell’intelligenza artificiale. Tuttavia se
da una parte le performance sono aumentate, dall’altra è diminuita la libertà per il programmatore: le unità di T&L sono
a funzioni fisse, cioè gli algoritmi grafici sono stati scelti dai creatori del chip e memorizzati al suo interno: non è
possibile, per il programmatore, cambiarli. Per superare questo problema, i produttori hanno dotato le schede di vere e
proprie CPU programmabili, denominate GPU (Graphic Processing Unit); proprio questi programmi, eseguiti
direttamente dalla GPU, prendono il nome di shaders, dal verbo inglese to shade, che significa ombreggiare; infatti il
loro compito è proprio quello di calcolare l’ombreggiatura e più in generale l’aspetto degli oggetti tridimensionali.
Esiste un’ulteriore distinzione tra Vertex Shaders, ovvero quei programmi che elaborano i vertici degli oggetti 3d, e
Pixel Shaders , che elaborano i pixel in cui gli oggetti sono stati convertiti. Questi shaders possono essere scritti in
diversi linguaggi, sia di basso che di alto livello. Microsoft con DirectX 8.0 ha introdotto un linguaggio simile
all’assembler; con il rilascio delle DirectX 9.0 (cfr. [5], [6]), avvenuto ad inizio 2003, oltre allo shaders assembler,
giunto alla versione 2.0, è stato introdotto un nuovo linguaggio di alto livello, simile al C, chiamato High Level Shader
Language (HLSL). Un altro linguaggio di alto livello è stato creato dalla NVidia (cfr. [4]) : si tratta di CG ovvero C for
Graphics, anch’esso ispirato al C e compatibile con gli shaders di DirectX e OpenGL. Per quanto riguarda quest’ultima
API, in un primo momento il supporto agli shaders veniva offerto solo da estensioni proprietarie di ATI e NVidia, ma
con il rilascio della versione 1.4 dovrebbe essere standardizzato. Al momento, questa mancanza di standardizzazione, fa
preferire la API Microsoft per lo sviluppo degli shaders. In questo articolo verrà fornita un’introduzione ai Vertex
Shaders: il loro ruolo nella pipeline di Direct3D, la loro architettura: set di istruzioni, registri e tecniche di swizzling e
masking di questi ultimi; infine verrà scritto un primo vertex shader usando un tool visuale: ATI RenderMonkey.
La pipeline di Direct3D
Figura 1: la pipeline di Direct3D.
Gli oggetti in una scena tridimensionale sono formati da poligoni, aventi un certo numero di vertici; questi ultimi
vengono sottoposti ad una serie di trasformazioni che portano al disegno della scena stessa, chiamato in gergo
rendering. A questa serie di trasformazioni si dà il nome di pipeline, per l’analogia con una catena di montaggio. In
Figura 1 è rappresentata la struttura di quella di Direct3D: essa viene alimentata da un flusso(stream) di vertici, che
possono essere forniti direttamente dall’applicazione, oppure provenire dallo stadio di tessellation. Per spiegare il
compito di quest’ultimo, è necessario introdurre le high-order surfaces, ovvero delle superfici geometriche create a
partire da una famiglia di curve parametriche come Bezier, B-Spline e Catmull-Rom; attraverso di esse è possibile
rappresentare superfici curve in maniera molto realistica, senza la spigolosità di quelle approssimate da triangoli: sono
l’ideale per rappresentare parti anatomiche nei personaggi animati. Lo stadio di tesselation riceve in input i parametri
per la creazione di queste superfici, ad esempio i punti di controllo delle curve di Bezier, e fornisce in output uno stream
di vertici, in cui è stata decomposta la superficie di partenza. Questa operazione consente di aumentare il livello di
realismo della scena, infatti maggiore è il numero di vertici, migliore è la resa dell’illuminazione, dato che essa è
calcolata per vertice; inoltre, dato che l’intensità luminosa nei restanti punti del poligono viene ricavata interpolando
quella nei vertici, questa operazione permette un’interpolazione di ordine superiore al primo, con evidenti passi in
avanti in termini di realismo; sempre allo scopo di incrementare il numero di vertici, lo stadio di tesselation può
suddividere i triangoli che riceve in input, in un insieme di triangoli più piccoli, chiamato N-patch; inoltre, dato che la
scomposizione viene effettuata direttamente dal chip grafico, il traffico sul bus video AGP non aumenta. E’ conveniente
usare le high-order surfaces solo se vengono supportate dal chip grafico, anche se attualmente solo quelli di fascia alta
(Radeon 8500,9500,9700, GeForce 4 Ti 4200/4600) lo fanno. Uscito dallo stadio di tesselation, il flusso di vertici entra
in quello di Transformation and Lighting (T&L); il compito di quest’ultimo è di applicare ai vertici le trasformazioni
geometriche e calcolare l’intensità luminosa di ciascuno di essi. Per capire la necessità delle trasformazioni
geometriche, si deve sapere che normalmente le coordinate dei vertici degli oggetti memorizzati sono locali, cioè
riferite ad un sistema di riferimento centrato nel baricentro dell’oggetto. Per poter disegnare l’intera scena, è necessario
posizionare gli oggetti nel mondo: ciò si realizza moltiplicando tutti i vertici per una matrice di traslazione; in questo
modo si passa dal sistema di riferimento dell’oggetto a quello dell’intero mondo : questa trasformazione viene chiamata
world transformation. Dopo aver posizionato gli oggetti, è necessario aggiustare la posizione della telecamera con la
quale viene inquadrata la scena: è necessario traslare e ruotare gli oggetti in modo che la telecamera sia posta
nell’origine del sistema di riferimento. Questa operazione viene svolta dalla matrice view, o di vista, e viene chiamata
ovviamente view transformation. Siccome la superficie del monitor è bidimensionale, è necessaria un’ulteriore
trasformazione per passare da uno spazio a tre dimensioni a quello a due : la projection transformation, che effettua la
proiezione prospettica di ciascun vertice. Affinché Direct3D possa applicare queste trasformazioni ad ogni vertice, è
necessario fornirgli le 3 matrici world, view e projection. Questo stadio calcola anche l’intensità luminosa di ciascun
vertice, in base a un modello di illuminazione fissato dai creatori di Direct3D; il programmatore può impostare una serie
di parametri di esso quali la posizione e il colore delle luci, i materiali degli oggetti, l’attenuazione della luce etc… Se il
programmatore vuole implementare un modello diverso, può ricorrere ad uno stadio alternativo a quello di T&L: la
Vertex Shader Unit. Il compito di quest’ultima è di eseguire, per ogni vertice in input, un programma, chiamato vertex
shader, che implementa le trasformazioni geometriche e l’illuminazione. Di essa parleremo diffusamente in seguito.
Dopo essere stati trasformati e illuminati, i vertici vengono sottoposti al backface culling, operazione che rimuove le
facce opposte all’osservatore; per default queste sono quelle che hanno i vertici raggruppati in senso antiorario. Nello
stadio successivo vengono eliminati tutti i poligoni che non sono compresi nella parte di spazio individuata dalla
normale agli User Clip Planes, che sono molto utili nel portal rendering e possono essere al massimo 6. Quindi viene
effettuato il Frustum Clipping, utilizzando il viewing frustum, che è un tronco di piramide con la telecamera nel suo
vertice; il suo volume rappresenta la porzione di spazio visibile attraverso di essa. L’operazione di clipping è la
seguente: se un poligono giace completamente fuori dal volume del frustum, viene scartato; se giace completamente al
suo interno allora esso passa allo stadio successivo; se invece interseca il frustum, viene tagliato (da qui deriva il
termine clipping) e solo la parte in comune con il frustum passa allo stadio successivo. Questo è rappresentato
dall’homogeneus divide, in cui le coordinate x,y,z dei vertici vengono divise per quella omogenea w, per ottenere le
coordinate normalizzate: x,y sono comprese tra (–1,1) mentre la z è compresa in (0,1). Infine i vertici vengono
sottoposti al viewport mapping, che serve per mappare le coordinate normalizzate in quelle dello schermo, in base alla
risoluzione video di quest’ultimo. A questo punto i vertici vengono trasformati in pixel dallo stadio di triangle setup,
che insieme ai successivi sarà descritto quando parleremo dei pixel shaders.
Figura 2: registro della Vertex Shader Unit.
Architettura dei Vertex Shaders
Figura 3: architettura della VS Unit.
Dopo aver esaminato la parte della pipeline di Direct3D che processa i vertici, verrà analizzato lo stadio Vertex Shader
Unit. Esso riceve in input un vertice, applica ad esso le operazioni descritte nel vertex program e lo ritorna in output. E’
a tutti gli effetti un processore di tipo SIMD, cioè applica una stessa istruzione su dati multipli: infatti essi sono
contenuti in registri vettoriali (cfr. Figura 2), formati da 4 variabili float da 32 bit ciascuna, denominate rispettivamente
x,y,z,w. Questo tipo di dato è molto utile per rappresentare, ad esempio, la posizione di un vertice, e per le operazioni di
trasformazione geometrica, le quali implicano moltiplicazioni di vettori 1x4 per matrici 4x4. L’architettura di questa
unità è rappresentata in Figura 3: essa riceve i dati sul vertice (ad esempio: posizione, normale, colore, coordinate
texture) dai registri di input e può ricevere in input delle costanti (matrici, posizione della luce etc..) dagli appositi
registri; quindi esegue le istruzioni contenute nel vertex program, avvalendosi di registri temporanei a lettura/scrittura
per memorizzare risultati intermedi ed infine memorizza i risultati delle trasformazioni nei registri di output, che
conterranno il vertice trasformato ed illuminato. Esiste anche un ulteriore registro intero, denominato address register
a0, che serve per un indirizzamento relativo delle costanti. L’architettura appena descritta è relativa alla versione 1.1 dei
Vertex Shaders, introdotta con DirectX 8.0 e supportata dalla maggior parte delle schede grafiche in commercio; con
DirectX 9.0, è stata introdotta la versione 2.0, supportata, attualmente, solo dalle schede top di gamma (Radeon 9700 e
GeForceFX); in assenza di supporto hardware è possibile usare l’emulazione software della VS Unit, con prestazioni
inferiori, visto che sarà la CPU stessa ad eseguire il vertex program. La versione 2.0 differisce dalla precedente non solo
per il maggior numero di registri costanti (256 contro 96), ma anche per l’introduzione di 16 nuovi registri costanti di
tipo intero ed altrettanti di tipo booleano e di un registro intero che funge da contatore nei loop. Esiste anche la versione
3.0, che introduce altri 2 registri: predicate e sampler e rende possibile specificare la semantica dei registri di output;
attualmente questa versione viene supportata solo tramite emulazione software. In Tabella 1 vengono riassunte le
differenze tra le 3 versioni dell’architettura, limitatamente ai registri.
Tabella 1: registri nelle 3 architetture della VS Unit.
registri
tipo di dato dimensione permessi di I/O VS 1.1 VS 2.0 VS 3.0
address a0
input v#
constant float c#
constant integer i#
constant boolean b#
temporary r#
loop counter aL
predicate p0
sampler s#
oD#
oFog
oPos
oPts
oT#
o#
int
float
float
int
bool
float
int
bool
4D vettore
4D vettore
4D vettore
4D vettore
scalare
4D vettore
scalare
4D vettore
W
R
define/R
define/use
define/use
R/W
use
W/use
float
float
float
float
float
4D vettore
scalare
4D vettore
scalare
4D vettore
W
W
W
W
W
W
1
16
min 96
nd
nd
12
nd
nd
nd
2
1
1
1
8
nd
1
16
min 256
16
16
12
1
nd
nd
2
1
1
1
8
nd
1
16
min 256
16
16
min 12
1
1
4
nd
nd
nd
nd
nd
12
Set di istruzioni
La sintassi delle istruzioni ricalca quella di un tipico assembler per CPU RISC:
NomeOp dest, [-]s0 [,[-]s1 [,[-]s2]] ; commento
dove NomeOp è il nome dell’istruzione, dest è l’operando destinazione, mentre s0,s1,s2 sono quelli sorgenti; le
parentesi quadre indicano che l’operando è opzionale, come pure lo è il segno meno davanti ad uno di essi. Oltre alle
istruzioni normali, ci sono le macro-ops, che combinano quelle normali per fornire funzionalità di alto livello come
moltiplicazione di un vettore per una matrice, prodotto vettoriale etc.. Ciascuna istruzione può occupare uno o più slots :
ad esempio le istruzioni normali ne occupano uno mentre le macro-ops più di uno; ci sono anche delle istruzioni da zero
slots: si tratta di quelle per impostare le costanti. Per la specifica 1.1 un programma può occupare al massimo 128 slots,
mentre nella 2.0 questo limite è stato portato a 256 mentre nella 3.0 si può superare 512. In Tabella 2 sono riassunte le
istruzioni per la versione 1.1 mentre in Tabella 3 quelle nuove introdotte dalla versione 2.0. Si può notare come le
novità riguardino l’introduzione della chiamata a subroutine, dell’istruzione condizionale if, dei loop, del prodotto
vettoriale, della normalizzazione, dell’interpolazione lineare e della funzione potenza. Nella versione 1.1 l’esecuzione
delle istruzioni è rigorosamente lineare : non sono possibili salti né subroutine.
Tabella 2: set di istruzioni della versione 1.1 dei VS.
istruz.
parametri
s
m
add
dest,src0,src1
1
NO somma i vettori src0,src1.
azione
dcl_usage dest
0
def
dest,v0,v1,v2,v3
0
dp3
dest,src0,src1
1
NO dichiara l’utilizzo del registro di input dest. Possibili valori per _usage sono:
_position, _normal, _texcoord, _color etc...
Esempio: dcl_position v0 ;posizione del vertice in v0
NO definisce una costante. dest è uno dei registri costanti float, mentre
v0,v1,v2,v3 sono 4 numeri floating point.
NO prodotto scalare a 3 componenti dei registri sorgente.
dest.x = src0.x + src1.x; analogamente per y,z,w.
dest.x=dest.y=dest.z=dest.w= (src0.x*src1.x) + (src0.y*src1.y) +
(src0.z*src1.z);
dp4
dest,src0,src1
1
NO prodotto scalare a 4 componenti dei registri sorgente
dest.w
=(src0.x*src1.x)+(src0.y*src1.y)+(src0.z*src1.z)+(src0.w*src1.w);
dest.x=dest.y=dest.z= unused ;
dst
dest,src0,src1
1
NO calcola un vettore distanza, utile per calcolare l’attenuazione di una luce
puntiforme.
dest.x=1; dest.y=src0.y*src1.y ; dest.z=src0.z ; dest.w=src1.w;
exp
dest,src.w
10 SI
expp
dest,src.w
1
se src0=[ignored,d*d,d*d,ignored] src1=[ignored,1/d,ignored,1/d]
allora dest=[1,d,d*d,1/d] che rappresenta l’attenuazione.
esponenziale 2x, con precisione piena.
dest.x=dest.y=dest.z=dest.w=pow(2,src.w);
SI
esponenziale 2x, con precisione parziale a 10 bit.
dest.x
dest.y
dest.z
dest.w
=
=
=
=
pow(2,(int)src.w)
frc(src.w); parte frazionaria di src.w
pow(2,src.w) & 0xffffff00
1
frc
dest,src
3
SI
ritorna la parte frazionale di ciascuna componente del vettore src.
lit
dest,src
1
NO calcola i coefficienti di illuminazione.
dest.x = src.x – floor(src.x); analogamente per y,z,w.
src.x = N*L; prodotto scalare tra normale e direzione della luce
src.y = N*H; prodotto scalare tra normale e half vector
src.z = ignorato;
src.w = esponente; valore tra –128.0 e +128.0
dest.x = 1;
dest.y = 0;
dest.z = 0;
dest.w = 1;
float power = src.w;
if (src.x > 0)
{
dest.y = src.x;
if (src.y > 0)
dest.z = (float)(pow(src.y, power));
log
dest,src.w
10 SI
}
calcola il log2(x) con precisione piena.
float v = abs(src.w);
if(v != 0)
dest.x=dest.y=dest.z=dest.w=log(v)/log(2);
else
dest.x=dest.y=dest.z=dest.w=-FLOAT_MAX;
logp
m3x2
dest,src.w
dest,src0,src1
1
2
SI
SI
calcola il log2(x) con precisione parziale a 10 bit. Vedasi log.
calcola il prodotto tra un vettore a 3 componenti e una matrice 2x3.
m3x2
dp3
dp3
r0.xy, r1, c0;
r0.x, r1, c0
r0.y, r1, c1
verrà espansa in:
m3x3
m3x4
m4x3
m4x4
mad
dest,src0,src1
dest,src0,src1
dest,src0,src1
dest,src0,src1
dest,src0,src1,src2
3
4
3
4
1
SI
SI
SI
SI
NO
calcola il prodotto tra un vettore a 3 componenti e una matrice 3x3.
calcola il prodotto tra un vettore a 3 componenti e una matrice 3x4.
calcola il prodotto tra un vettore a 4 componenti e una matrice 4x3.
calcola il prodotto tra un vettore a 4 componenti e una matrice 4x4.
moltiplica e somma i vettori sorgenti.
max
dest,src0,src1
1
NO calcola il massimo tra i vettori sorgenti.
min
mov
mul
dest,src0,src1
dest,src
dest,src0,src1
1
1
1
dest.x=(src0.x >= src1.x) ? src0.x : src1.x;analogamente per y,z,w.
NO calcola il minimo tra i vettori sorgenti.
NO sposta i dati tra 2 registri. dest.x = src.x; analogamente per y,z,w.
NO moltiplica le componenti corrispondenti dei 2 vettori sorgenti.
dest,src.w
0
1
NO non viene svolta nessuna operazione.
NO calcola il reciproco dello scalare nel registro sorgente.
dest.x = src0.x * src1.x + src2.x; analogamente per y,z,w.
dest.x = src0.x * src1.x;analogamente per y,z,w.
nop
rcp
if(src.w==0)
dest.x = dest.y = dest.z = dest.w = FLOAT_MAX;
else
dest.x = dest.y = dest.z = dest.w = 1.0/src.w;
rsq
dest,src.w
1
NO calcola il reciproco della radice quadrata del registro sorgente.
float f = fabs(src.w);
if (f==0)
f = FLOAT_MAX;
else
f = 1.0f/sqrt(f);
dest.x = dest.y = dest.z = dest.w = f;
sge
dest,src0,src1
1
slt
sub
vs
dest,src0,src1
dest,src0,src1
1
1
0
NO se il primo operando è maggiore o uguale al secondo, setta dest a 1.
dest.x = (src0.x >= src1.x) ? 1.0f : 0.0f; analogamente per y,z,w.
NO se il primo operando è minore o uguale al secondo, setta dest a 1.
NO sottrae i vettori sorgenti: dest.x=src0.x – src1.x; analogamente per y,z,w.
NO specifica la versione del vertex shader; sintassi: vs_mainVer_subVer
mainVer = 1,2,3
subVer = 1,0,sw,x
Tabella 3: nuove istruzioni della versione 2.0 dei VS.
istruz. parametri
s m
abs
dest,src
1 NO Calcola il valore assoluto di src.
azione
call
callnz
l#
l#,boolReg
crs
dest,src0,src1
2 NO Effettua una chiamata alla subroutine contrassegnata dalla label l#.
3 NO Effettua una chiamata a subroutine condizionale, se il boolReg è diverso da
zero. boolReg è un registro booleano costante b#.
2 NO Calcola il prodotto vettoriale, con la regola della mano destra.
dest.x = fabs(src.x); analogamente per y,z,w
dest.x = src0.y * src1.z - src0.z * src1.y;
dest.y = src0.z * src1.x - src0.x * src1.z;
dest.z = src0.x * src1.y - src0.y * src1.x;
defb
dest,boolValue
defi
dest,i0,i1,i2,i3
else
endif
endloop
endrep
if
boolReg
0 NO Definisce una costante booleana; dest deve essere un registro costante
booleano b#; mentre boolValue = {TRUE,FALSE}
0 NO Definisce un valore intero costante; dest deve essere un registro costante
intero i#; mentre i0,i1,i2,i3 sono dei numeri interi a 32 bit.
1 NO Inizia un blocco else di un’istruzione condizionale if.
1 NO Indica la fine di un blocco if...else.
2 NO Indica la fine di un blocco loop...endloop.
2 NO Indica la fine di un blocco rep...endrep.
3 NO Esegue il blocco di istruzioni compreso tra if ed else se il registro booleano è
diverso da zero(true). Esempio:
defb b3, TRUE
if b3
// istruzioni da eseguire se b3 è diverso da zero
else
// istruzioni da eseguire altrimenti
label
loop
l#
aL,intReg
endif
0 NO Contrassegna la successiva istruzione con la label l#.
3 NO Esegue iterativamente il blocco di istruzioni tra loop ed endloop.
aL è il loop counter register
intReg è un registro costante intero
int iterationCount = intReg.x;
int initialValue
= intReg.y;
int increment
= intReg.z;
for(aL = initialValue; aL<iterationCount; aL+=increment)
{
// blocco di istruzioni
}
lrp
dst,src0,src1,src2,src3 2 SI
Effettua un’interpolazione lineare.
dest.x = src0.x * (src1.x - src2.x) + src2.x;
mova
nrm
a0,src
dst,src
Analogamente per y,z,w.
1 NO Sposta i dati tra un registro float e l’address register a0.
3 SI Normalizza un vettore a 3 componenti.
squareRootOfTheSum = sqrt(src0.x*src0.x + src0.y*src0.y +
src0.z*src0.z);
dest.x = src0.x * (1 / squareRootOfTheSum);analogamente per y,z,w
pow
dst,src0.w,src1.w
3 SI
Elevazione a potenza.
rep
intReg
ret
sgn
dst,src0,src1,src2
3 NO Ripete il blocco di istruzioni comprese tra rep...endrep.
intReg è un registro costante intero i#, che specifica nella componente x il
numero di ripetizioni.
1 NO Ritorna da una subroutine chiamata con call o callnz.
3 SI Calcola il segno del registro di input src0.
dst.x=dst.y=dst.z=dst.w=pow(abs(src0.w),src1.w);
for each component in src0
{
if (src0.component < 0)
dest.component = -1;
else
if (src0.component == 0)
dest.component = 0;
else
dest.component = 1;
}
sincos
dst,src0.w,src1,src2
8 SI
src1,src2 devono essere 2 registri temporanei diversi.
Calcola sin e cos in radianti.
dst.x = cos(src0.w); -pi < src0.w < pi
dst.y = sin(src0.w);
dst.z = indefinito;
src1,src2 devono essere 2 registri costanti caricati con le costanti
D3DSINCOSCONST1, D3DSINCOSCONST2.
legenda:
s n.ro di instruction slots
m macro-ops
Swizzling e masking dei registri
Dopo aver fatto una panoramica sul set di istruzioni e sui registri, verranno spiegate alcune operazioni che consentono
di manipolare il contenuto di questi ultimi. Come è stato già detto, essi sono composti da 4 componenti floating-point a
32 bit, denominate rispettivamente x,y,z,w. Quando un registro viene usato come operando sorgente di un’istruzione, è
possibile scambiare le sue componenti x,y,z,w: questa operazione viene chiamata swizzling; questo concetto, viene
chiarito mediante un esempio:
mov r1,r2.yxyz
il risultato di questa istruzione sarà il seguente:
r1.x
r1.y
r1.z
r1.w
=
=
=
=
r2.y
r2.x
r2.y
r2.z
Questa tecnica può essere utile, ad esempio, per realizzare un prodotto vettoriale:
; r0 = r1 x r2
mul r0, r1.yzxw, r2.zxyw
mad r0, -r2.yzxw, r1.zxyw, r0
Quando si usa un registro come destinazione di un’istruzione, è possibile decidere quali componenti scrivere,
mascherando le altre: si parla di masking di un registro. Ad esempio:
mov r1.x, r2
con questa istruzione solamente la componente x di r1 verrà scritta con il valore di r2.x, mentre le componenti y,z,w di
r1 rimarranno invariate. Alcune istruzioni richiedono un uso esplicito dello swizzling o del masking di un operando.
Un’ulteriore possibilità è la negazione di un registro sorgente, facendolo precedere dall’operatore -, come si è già visto
in occasione dell’esempio di prodotto vettoriale.
ATI RenderMonkey
RenderMonkey, realizzato da uno dei maggiori produttori di schede grafiche 3d: ATI Technologies inc., è un ambiente
integrato per lo sviluppo visuale di shaders. Attualmente è disponibile in una versione 0.9Beta, che supporta DirectX 9,
liberamente scaricabile dal sito in [2]. Richiede Windows 98,ME,2000(sp2),XP e DirectX 9, 128MB di Ram e 100MB
di spazio libero su disco. E’ rivolto sia ai programmatori che agli artisti 3d; mette a disposizione editors per vertex e
pixel shaders e per modificare le variabili. Salva tutti i dati in formato xml, per facilitare lo scambio con altri
programmi. Fornisce una finestra di preview per visualizzare rapidamente gli effetti creati. Viene fornita un’ampia
libreria di effetti di esempio.
Riquadro 1: ATI RenderMonkey
Esempio di Vertex Shader con ATI RenderMonkey
Fino a questo punto, l’articolo ha avuto carattere prettamente teorico, ma adesso è giunta l’ora di mettere in pratica le
nozioni appena spiegate: si scriverà un semplicissimo vertex shader, con l’ausilio di un tool visuale, chiamato ATI
RenderMonkey (cfr. Riquadro 1). Data la natura visuale di questo tool, nel resto di questo paragrafo verranno descritte
una serie di operazioni da effettuare tramite il mouse, i menu e le dialog box; per seguire meglio il discorso, è
consigliabile avere davanti il computer, con il programma RenderMonkey in esecuzione ed applicare passo dopo passo
le operazioni descritte di seguito, fino a realizzare l’esempio completo. Appena eseguito, RenderMonkey, si presenta
con la finestra raffigurata in Figura 4; si può notare che essa è divisa in 3 aree principali: a sinistra il workspace, che
tramite una vista ad albero, rappresenta tutti gli elementi che compongono un effect, termine con cui viene identificato
un effetto grafico realizzato mediante vertex, pixel shaders e modelli 3D; in basso c’è la output window, in cui vengono
visualizzati tutti i messaggi sulle operazioni effettuate dal programma; al centro troviamo la preview window, che
mostra il modello 3D con l’effetto applicato. Tramite il menu file è possibile caricare alcuni workspace d’esempio, che
sono in formato xml. Per vedere un effetto nella preview window, bisogna renderlo attivo, cliccando con il tasto destro
del mouse sulla sua icona nell’albero del workspace e selezionare la voce Set As Active Effect. Adesso verrà spiegato
come creare un nuovo effetto: selezionando la voce new dal menu file, apparirà un workspace vuoto. Cliccando con il
tasto destro su di esso, selezionare la voce Add Default Effect: verrà creato un effetto di default; si espanda la vista ad
albero, cliccando sull’icona a forma di più; si potrà notare che sono stati creati alcuni elementi: una matrice, denominata
view_proj_matrix , che serve per le trasformazioni geometriche e viene automaticamente inizializzata da
RenderMonkey; un elemento standard mapping che serve per impostare i registri di input del vertex shader; facendo un
doppio clic su quest’ultimo appare una finestra intitolata D3DStreamMapping con un pulsante AddChannel e una riga
di dati: questa riporta il registro di input (v0 in questo caso), il suo uso (position), l’indice in caso di più set di
coordinate texture(0), e il tipo (FLOAT3); per i nostri scopi, sono sufficienti le impostazioni di default, se si vuole
aggiungere un altro registro di input basta cliccare sul pulsante Add Channel. Si trova quindi un elemento model che
rappresenta un modello 3d in formato 3ds; facendo doppio clic su di esso, appare la dialog box standard di scelta file: si
scelga teapot.3ds , presente nella directory ATIRenderMonkey/Directx 9.0 build/effects/media : dovrebbe apparire una
teiera rossa nella finestra di preview. Il successivo elemento aggiunto al workspace è Default Effect che rappresenta
l’effetto che si sta creando; sotto di esso è presente Pass1; il rendering di un effetto può essere suddiviso in più passi,
ciascuno dei quali può contenere un vertex e un pixel shader; per i nostri scopi ne basta uno. Sotto Pass1 si trova un
riferimento al modello 3d, rappresentato dalla freccia sotto l’icona model, un altro allo standard mapping e le icone vs e
ps che rappresentano rispettivamente il vertex e il pixel shader. Purtroppo gli shaders creati di default usano il
linguaggio di alto livello HLSL, come si nota dalla scritta HL presente nell’icona; siccome si intende usare il linguaggio
assembly, bisogna cancellare gli elementi vs e ps, selezionandoli e premendo il tasto CANC; si noti che il modello 3d
scomparirà dalla preview window. Adesso si deve creare un vertex shader in assembly: cliccando con il tasto destro del
mouse su Pass1, selezionare la voce Add Vertex Shader e nella dialog box che apparirà selezionare assembly; verrà
creato un nuovo elemento denominato Vertex Shader, sotto Pass1. Per editarlo, fare un doppio clic sull’elemento
corrispondente nel workspace: apparirà la finestra di editing; quest’ultima è divisa in due parti: in quella superiore
vengono rappresentate le costanti, mentre nella inferiore il programma. Per prima cosa, bisogna impostare una costante,
contenente la matrice proj_view_matrix: cliccando con il tasto sinistro sulla casella Name, nel rigo della costante c0,
apparirà un menu da cui selezioneremo Matrices e quindi proj_view_matrix; così facendo questa matrice sarà
assegnata alle costanti c0-c3. A questo punto è necessaria un’altra costante per memorizzare il colore da assegnare
all’oggetto; cliccando con il destro su Pass1, selezionare Add Variable : apparirà una dialog box con due caselle: dalla
prima selezionare il tipo di variabile: COLOR mentre nella seconda si digiti il nome : color. Facendo doppio clic
sull’elemento color appena creato, apparirà una dialog box per scegliere il colore da associare alla variabile. Non resta
che associare questa variabile con il registro costante c4, con il procedimento visto prima per la matrice. Adesso si può
scrivere il codice del vertex shader:
Figura 4: finestra principale di RenderMonkey.
vs_1_1
dcl_position v0
m4x4 oPos, v0, c0
mov oD0,c4
La prima istruzione indica la versione del vs, in questo caso è sufficiente la 1.1 vista la semplicità dell’esempio. Tramite
la seconda istruzione si dichiara la posizione del vertice nel registro di input v0; quindi si moltiplica la matrice 4x4
view_proj _matrix, contenuta in c0, per il vettore v0, contenente la posizione del vertice, mettendo il vettore risultante
nel registro di output della posizione oPos: in questo modo si applica la trasformazione geometrica al vertice. Per poter
visualizzare il modello, si deve assegnare ad ogni vertice almeno un colore diffuso: ciò viene fatto dall’ultima istruzione
che pone il colore impostato nella costante c4 nel registro di ouput del colore diffuso. A questo punto è possibile
chiudere la finestra dell’editor del vertex shader, confermando di voler salvare i cambiamenti. Per poter osservare gli
effetti di questo brevissimo vertex shader, è necessario rendere attivo l’effetto; nella finestra di preview dovrebbe
apparire un modello 3d (in questo caso una teiera) nel colore scelto: tendendo premuto il tasto sinistro del mouse è
possibile ruotarlo, mentre con i tasti Z,X si può intervenire sullo zoom della telecamera. Si noti come RenderMonkey
aggiorni la matrice view_proj_matrix quando si ruota il modello. Dal menu file è possibile salvare il workspace in un
file xml.
Conclusioni
In questo articolo, per ragioni di spazio, è stato necessario limitarsi a scrivere un primo, semplicissimo, Vertex Shader
usando un tool visuale; nei prossimi si vedrà come integrarlo in un’applicazione Win32 e si scriveranno degli shaders
più sofisticati per implementare vari modelli di illuminazione. Continuate a seguire questa rubrica !!!
Bibliografia
[1]
AA.VV.– “ShaderX Vertex and Pixel Shaders Programming”, Worldware Inc., 2002
Riferimenti
[2]
http://www.ati.com/developer/sdk/radeonSDK/html/Tools/RenderMonkey.html, Home page di RenderMonkey
[3]
http://www.ati.com/developer, documentazione e software su shaders per DX e OpenGL
[4]
http://developer.nvidia.com, documentazione e software su shaders per DX e OpenGL
[5]
http://msdn.microsoft.com/downloads/list/directx.asp, pagina di download del DirectX 9.0 SDK
[6]
http://msdn.microsoft.com/library, articoli e documentazione ufficiale di DX9
Stefano Coppi è laureando in Ingegneria Elettronica, presso il Politecnico di Bari. Si occupa di programmazione Object
Oriented in C++ e Java e di Computer Graphics tridimensionale. Ha realizzato un engine 3D chiamato PortalX.
Attualmente sta lavorando alla sua tesi di laurea sul riconoscimento automatico del linguaggio naturale, per effettuare
query su una knowledge base.