Programmazione degli Shaders in DirectX 9.0 (parte seconda): i

Transcript

Programmazione degli Shaders in DirectX 9.0 (parte seconda): i
In questo articolo, mediante un esempio, vedremo come creare un’applicazione Win32 che utilizzi i vertex shaders, sia
in linguaggio assembly che in HLSL.
Programmazione degli Shaders in DirectX 9.0 (parte seconda):
i Vertex Shader nelle applicazioni Win32
di Stefano Coppi
Nel precedente articolo, è stata introdotta l’architettura dei Vertex Shaders ed abbiamo scritto un primo semplice shader
utilizzando il tool visuale RenderMonkey. Questo articolo avrà un taglio decisamente pratico: mediante un esempio,
vedremo come creare un’applicazione Win32 che utilizzi i vertex shaders, sia in linguaggio assembly che in HLSL. Per
poter realizzare tale applicazione servono i compilatori Microsoft Visual C++ 6.0 oppure Visual Studio .NET, e il
DirectX 9.0 SDK (scaricabile da [5]). E’ inoltre richiesta una buona conoscenza del linguaggio C++ e dei concetti base
di DirectX. Il codice completo, con progetti per i due compilatori sopraelencati, è prelevabile dal sito ftp.infomedia.it In
generale, per poter realizzare una applicazione che usa DirectX, è necessario scrivere un bel po’ di codice di
inizializzazione; tuttavia, per sollevare noi pigri programmatori da questo noioso compito, la Microsoft ha incluso, nel
SDK, una robusta libreria di classi C++ per l’inizializzazione e la gestione delle operazioni più ripetitive di DirectX,
chiamata Common Files Framework. Addirittura, il SDK installa un comodo Wizard per Visual Studio 6 e .Net che crea
un’applicazione DirectX minimale, basata sul suddetto framework.
Il DirectX 9 AppWizard
Cominciamo a creare l’applicazione di esempio, utilizzando il sopraccitato Wizard di Visual C++ 6.0; dopo aver
eseguito il programma, selezioniamo la voce New dal menu File: apparirà la finestra di dialogo per la creazione di nuovi
progetti: selezioniamo DirectX 9 AppWizard, ed inseriamo il nome dell’applicazione, VSDemo, nella apposita casella;
dopo aver premuto il pulsante OK, appariranno i 3 passi con i quali il Wizard permette di scegliere il tipo di
applicazione da creare (vedasi Figura 1). Le opzioni più significative da selezionare sono: tipo di applicazione single
document window, supporto per Direct3D e DirectInput, cominciare con una finestra vuota, supporto per i font e solo
supporto per la tastiera tramite DirectInput, senza action mapping. Alla fine verrà presentata una finestra riepilogativa
delle scelte fatte e delle classi C++ che verranno create; premendo OK verrà creato il nuovo progetto.
Il Microsoft Common Files Framework
Selezionando il tab Files, del Workspace di Visual C++ 6.0, si possono vedere i principali files del framework, inseriti
dal Wizard: i più importanti sono d3dapp.cpp e d3dapp.h che contengono la classe CD3DApplication, che rappresenta
una tipica applicazione Windows che utilizzi Direct3D. Questa classe definisce sette metodi virtuali, che l’utente del
framework deve ridefinire per specificare il comportamento della propria applicazione: OneTimeSceneInit(),
InitDeviceObjects(), RestoreDeviceObjects(), DeleteDeviceObjects(), Render(), FrameMove(), FinalCleanUp(). Il
Wizard ha creato per noi una classe derivata da CD3DApplication, chiamata CMyD3DApplication, e posta nei file
VSDemo.cpp, VSDemo.h, corrispondenti al nome dell’applicazione. In quest’ultima classe, sono state create le versioni
ridefinite dei sette metodi sopra citati, in cui verrà inserito tutto il codice che scriveremo successivamente. Per capire
quando vengono invocati questi metodi, analizziamo il codice della funzione WinMain(), che è il punto di ingresso di
qualsiasi applicazione per Windows, ed è contenuta nel file VSDemo.cpp: si nota che innanzitutto viene creato
l’oggetto d3dApp, istanza della classe CMyD3DApplication, che rappresenta la nostra applicazione; quindi si invoca il
metodo d3dApp.Create(), il quale crea la finestra dell’applicazione e richiama nell’ordine i metodi: ConfirmDevice(),
OneTimeSceneInit(), InitDeviceObjects(), RestoreDeviceObjects(). Successivamente, WinMain() chiama il metodo
d3dApp.Run() , il quale esegue in un loop i metodi FrameMove() e Render(). Quando l’utente chiude l’applicazione,
premendo il tasto ESC o l’apposito pulsante sulla finestra, il framework richiama, nell’ordine, i metodi:
InvalidateDeviceObjects(), DeleteDeviceObjects(), FinalCleanUp(). Se l’utente ridimensiona la finestra, vengono
richiamati i metodi InvalidateDeviceObjects(), RestoreDeviceObjects(). Infine se l’utente cambia le impostazioni di
visualizzazione, mediante la finestra di dialogo che viene richiamata premendo il tasto F2, oppure selezionando la voce
Change Device del menu File, vengono chiamati i metodi: InvalidateDeviceObjects(), DeleteDeviceObjects(),
InitDeviceObjects(), RestoreDeviceObjects(). Dopo aver visto l’ordine temporale con cui vengono richiamati i metodi
virtuali, spieghiamo il loro compito: ConfirmDevice() controlla che la scheda video abbia le capacità richieste
dall’applicazione, ad esempio il supporto hardware per i vertex shader. OneTimeSceneInit() effettua le inizializzazioni
che non dipendono dal device D3D (l’oggetto di Direct3D che rappresenta la scheda video 3d); tipicamente qui viene
caricata la scena da un file. Questo metodo è “accoppiato” con FinalCleanUp() , il quale rilascia le risorse create da
OneTimeSceneInit(). La coppia di metodi InitDeviceObjects()/DeleteDeviceObjects() inizializza/distrugge i dati che
dipendono dal device e che non vengono perduti con l’evento di ridimensionamento della finestra; questo è il posto
giusto per creare i vertex buffers, i vertex shaders e caricare le textures. La coppia di metodi
RestoreDeviceObjects()/InvalidateDeviceObjects(), inizializza/distrugge i dati che dipendono dalle dimensioni della
finestra come ad esempio la matrice di proiezione, i vari stati del motore di rendering etc... Il metodo FrameMove(),
serve per aggiornare lo stato degli oggetti prima di disegnare il frame successivo, quindi è utile per le animazioni,
mentre il metodo Render() disegna un frame della scena.
Integrazione di un Vertex Shader in un’applicazione Win32
Dopo aver visto la dinamica d’invocazione ed il compito dei metodi virtuali del framework, possiamo iniziare ad
esaminare le operazioni necessarie per gestire un vertex shader, sia in linguaggio assembly che in HLSL:
• controllo del supporto hardware
• dichiarazione del formato dello stream di vertici in input al vertex shader
• impostazione delle costanti
• caricamento e compilazione
• creazione dell’oggetto vertex shader di D3D
• impostazione del vertex shader attivo
• rilascio dell’oggetto vertex shader
Controllo del supporto hardware per i Vertex Shaders
Per controllare se la scheda video supporta una data versione dei Vertex Shaders, si utilizza il campo
VertexShaderVersion, della struttura D3DCAPS9, che riporta la massima versione dei vs supportati. Questo controllo va
effettuato nel metodo ConfirmDevice(), il quale riceve in input la struttura D3DCAPS9, già inizializzata con le capacità
supportate dalla scheda video e ritorna S_OK se le capacità richieste dal programma sono supportate, altrimenti ritorna
E_FAIL; in quest’ultimo caso, il framework, provvede ad attivare l’emulazione software dei vertex shaders.
HRESULT CMyD3DApplication::ConfirmDevice(D3DCAPS9* pCaps,DWORD dwBehavior,D3DFORMAT Format )
{
if(pCaps->VertexShaderVersion < D3DVS_VERSION(1,1) )
return E_FAIL;
else
return S_OK;
}
Dichiarazione del formato dei vertici in input al VS
Il passo successivo è definire il formato dello stream dei vertici in input allo shader, per mezzo di un array di strutture
D3DVERTEXELEMENT9, ad esempio:
D3DVERTEXELEMENT9 dwDecl[] =
{
{0, 0,D3DDECLTYPE_FLOAT3,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_POSITION,0},
{0,12,D3DDECLTYPE_FLOAT2,D3DDECLMETHOD_DEFAULT,D3DDECLUSAGE_TEXCOORD,0},
D3DDECL_END()
};
Ogni elemento di questo array rappresenta un dato da inserire in uno dei registri di input del VS; tramite il campo
usage, si indica l’uso che si intende fare del dato, mentre, tramite l’istruzione dcl_usage del VS stesso, si assegna tale
dato, in base all’uso, ad un registro di input. Ad esempio, per assegnare la posizione al registro di input v0 si userà:
dcl_position v0
Vediamo il significato degli altri campi della struttura D3DVERTEXELEMENT9, prendendo come esempio il secondo
elemento dell’array dwDecl: il primo è il numero dello stream, in questo caso 0; poi abbiamo l’offset, in bytes,
dall’inizio dello stream a quello del dato; nel nostro caso, il numero 12 indica che le coordinate texture si trovano 12
bytes dopo quelle della posizione; il terzo campo indica il tipo di dato, nel nostro esempio un vettore di 2 float; quindi
troviamo il metodo di processing usato dal tesselator, in questo caso quello di default; il quinto parametro è il più
importante e rappresenta l’uso che si farà del dato nel vertex shader; infatti solo tramite questo si può assegnare il dato
ad un registro di input; infine l’ultimo parametro è un indice: se usassimo più set di coordinate texture, indicherebbe
proprio il set usato; in questo caso è posto a 0 visto che usiamo solo un set. L’array dwDecl viene utilizzato dal metodo
CreateVertexDeclaration() per creare un oggetto della classe IDirect3DVertexDeclaration9, accessibile tramite il
puntatore m_pVertexDeclaration; successivamente si rende attiva questa dichiarazione dei vertici tramite il metodo
SetVertexDeclaration(); queste operazioni sono svolte dal seguente frammento di codice, posto nel metodo
InitDeviceObjects():
m_pd3dDevice->CreateVertexDeclaration( dwDecl, &m_pVertexDeclaration );
m_pd3dDevice->SetVertexDeclaration( m_pVertexDeclaration );
Impostazione delle costanti
Il terzo passo per configurare un VS è l’impostazione dei suoi registri costanti. Nella nostra applicazione di esempio,
l’unica costante da caricare è la matrice matClip, calcolata moltiplicando a destra le matrici m_matWorld, m_matView,
m_matProj. Poiché, nel nostro esempio, vogliamo far ruotare il logo di DirectX intorno all’asse y, è necessario
aggiornare la matrice m_matWorld ad ogni frame, quindi anche la matrice clip cambierà ad ogni frame e di conseguenza
la costante del VS va impostata nel metodo FrameMove(). Il seguente frammento di codice realizza quanto detto:
D3DXMatrixRotationY(&m_matWorld, m_fTime*1.5f);
D3DXMATRIX matClip = m_matWorld*m_matView*m_matProj;
D3DXMATRIX matClipTrasp;
D3DXMatrixTranspose(&matClipTrasp,&matClip);
if (m_bHLSL) // imposta la costante dello shader HLSL
m_pEffect->SetValue("matClip", (void*)(FLOAT*)matClip, sizeof(D3DXMATRIX));
else // imposta le costanti dello shader assembly
m_pd3dDevice->SetVertexShaderConstantF(0,(float*)&matClipTrasp,4);
La prima istruzione assegna una matrice di rotazione intorno all’asse y a m_matWorld; bisogna notare che l’angolo di
rotazione intorno all’asse y, in radianti, è dato da una costante, 1.5f, per il tempo corrente m_fTime; in questo modo
l’angolo di rotazione ha un incremento lineare con il tempo; questa tecnica è molto utilizzata per rendere i movimenti
degli oggetti grafici indipendenti dalla velocità del computer su cui si esegue l’applicazione. Quindi viene calcolata la
matrice clip e la sua trasposta. A questo punto è necessario distinguere tra lo shader assembly e quello HLSL: se è attivo
lo shader HLSL (flag m_bHLSL == true), si usa il metodo SetValue() per impostare la variabile denominata “matClip”
nel file HLSL con la matrice clip, avente le dimensioni di una D3DXMATRIX. Invece se è attivo lo shader in assembly,
si impostano i suoi registri costanti con il metodo SetVertexShaderConstantF(), che riceve come primo parametro il
numero iniziale del registro, come secondo un puntatore all’array di costanti ed infine il numero di registri che le
costanti occuperanno. In questo caso la matrice matClipTrasp occupa 4 registri, a partire dal registro 0. La necessità di
passare al VS in assembly la matrice clip trasposta verrà spiegata nel seguito, quando verrà esaminato in dettaglio il VS.
Esistono anche le funzioni SetVertexShaderConstantB() e SetVertexShaderConstantI() per caricare, rispettivamente, i
registri costanti booleani e interi.
Caricamento, compilazione e creazione del VS
Il passo successivo è quello di caricare il codice sorgente del vertex shader da un file e di compilarlo. E’ necessario
distinguere tra VS in assembly e in HLSL; cominciamo dal primo: poiché questo codice di inizializzazione dipende dal
device D3D, va inserito nel metodo InitDeviceObjects():
LPD3DXBUFFER pCode; // buffer contenente il codice assemblato del VS
HRESULT hr = D3DXAssembleShaderFromFile("basic-vshader.vsh",NULL,
NULL,0,&pCode,NULL);
if (FAILED(hr)) return E_FAIL;
La funzione D3DXAssembleShaderFromFile(), carica il codice sorgente assembly dello shader dal file di testo “basicvshader.vsh” e lo assembla, inserendolo nel buffer pCode. L’ultimo parametro della funzione è un puntatore ad un
buffer dove vengono messi gli eventuali messaggi di errore verificatisi durante l’assemblaggio; qui non viene usato.
Con il codice oggetto del VS, contenuto nel buffer puntato da pCode, possiamo creare l’oggetto Vertex Shader:
m_pd3dDevice->CreateVertexShader( (DWORD*)pCode->GetBufferPointer(), &m_pVertexShader);
Il primo parametro è un puntatore al buffer contenente il codice oggetto, mentre il secondo è l’indirizzo del puntatore
m_pVertexShader, all’oggetto della classe IDirect3DVertexShader9 che si sta creando. Il VS in HLSL, va caricato,
compilato e reso attivo nel metodo RestoreDeviceObjects(), in quanto esso deve essere reinizializzato ad ogni reset del
device, in seguito al cambio delle dimensioni della finestra o del modo video. Il seguente frammento di codice realizza
quanto detto:
HRESULT hr = D3DXCreateEffectFromFile(m_pd3dDevice,"shader.fx",NULL,NULL,0,NULL,&m_pEffect,NULL);
if(hr != S_OK) return E_FAIL;
D3DXHANDLE hTechnique;
m_pEffect->FindNextValidTechnique(NULL, &hTechnique);
m_pEffect->SetTechnique(hTechnique);
Il codice sorgente del VS HLSL viene caricato e compilato con il metodo D3DXCreateEffectFromFile(), i cui parametri
più significativi sono i primi due, rispettivamente, il device D3D e il nome del file, ed il penultimo, che è l’indirizzo di
un puntatore ad un oggetto della classe I3DXEffect, che rappresenta un effetto grafico. Quest’ultimo è memorizzato in
un file con estensione .fx e contiene una collezione di tecniche di rendering, ciascuna delle quali può essere suddivisa in
passi; in ciascuno di essi si può usare un vertex e un pixel shader, scritti sia in HLSL, come in questo caso, che in
assembly. Dopo che l’effetto è stato creato, è necessario trovare la prima tecnica valida, usando il metodo
FindNextValidTechnique(), che restituisce nel secondo parametro l’indirizzo dell’handle che la identifica; infine occorre
renderla attiva con il metodo SetTechnique(), che riceve come unico parametro l’handle hTechnique.
Impostazione del VS attivo
A questo punto, il nostro VS è pronto per essere attivato: questa operazione va effettuata nel metodo Render(),
immediatamente prima dell’istruzione di disegno DrawPrimitive(); tutti i vertici disegnati da questa istruzione saranno
processati dal vertex shader attivo. Per rendere attivo il VS in assembly, puntato da m_pVertexShader è sufficiente
l’istruzione:
m_pd3dDevice->SetVertexShader(m_pVertexShader);
Per attivare il VS in HLSL, contenuto nell’effetto puntato da m_pEffect è necessario il seguente codice:
UINT uPasses; // numero di passi dell'effetto
m_pEffect->Begin(&uPasses, 0 );
for(UINT uPass = 0; uPass < uPasses; uPass++)
{
m_pEffect->Pass(uPass);
m_pd3dDevice->DrawPrimitive(D3DPT_TRIANGLESTRIP,0,2);
}
m_pEffect-> End();
Il metodo Begin() comincia l’applicazione della tecnica correntemente attiva nell’effetto, restituendo in uPasses il
numero di passi di questa tecnica. Quindi si effettua un ciclo sul numero di passi, e per ciascuno di questi si impostano i
parametri con il metodo Pass(); in questo caso viene attivato il VS; successivamente si disegna la geometria con il
metodo DrawPrimitive(). Infine si termina l’applicazione della tecnica corrente, con il metodo End().
Rilascio delle risorse impiegate dal VS
In fase di chiusura dell’applicazione è necessario rilasciare le risorse impiegate dal VS; per quello in assembly, questa
operazione va fatta nel metodo DeleteDeviceObjects(), visto che esso è stato creato in InitDeviceObjects():
SAFE_RELEASE( m_pVertexShader );
Mentre per il VS in HLSL, visto che è stato creato in RestoreDeviceObjects(), bisogna rilasciarlo nel metodo
corrispondente InvalidateDeviceObjects(), tramite l’istruzione:
SAFE_RELEASE( m_pEffect );
In entrambi i casi, viene usata una macro definita dal framework, che consente il rilascio sicuro di oggetti creati
dinamicamente e referenziati tramite puntatore.
Semplice Vertex Shader in assembly v1.1
Dopo aver visto i passi necessari per gestire un vertex shader, scriviamo il codice sorgente di uno semplice, che utilizzi
il set di istruzioni in versione 1.1, visto che la maggior parte delle schede video supporta ancora solo questa versione e
che non sono necessarie le potenti istruzioni della versione 2.0. Il compito di questo VS sarà molto elementare: si
limiterà a trasformare la posizione del vertice da coordinate locali a quelle clip e ad impostarne le coordinate texture e il
colore diffuso. Il codice completo è contenuto nel file basic-vshader.vsh, mentre di seguito vedremo i frammenti più
significativi. Si comincia con la parte dichiarativa:
vs_1_1
dcl_position v0
dcl_texcoord v1
def c12,0.9,0.9,0.9,0.9
La prima istruzione dichiara la versione dell’architettura: 1.1; con le due istruzioni successive si dichiara l’uso dei
registri di input: in v0 ci sarà la posizione del vertice, mentre in v1 le sue coordinate texture. Come già fatto notare a
proposito della dichiarazione del formato dello stream di input, le istruzioni dcl sono strettamente legate agli elementi
dell’array dwDecl. Infine l’istruzione def definisce nel registro c12, una costante che rappresenta la luminosità; le
componenti di questa sono tutte uguali tra loro e rappresentano la luminosità in percentuale, nell’esempio 0.9 cioè 90%.
Questa istruzione equivale ad una chiamata del metodo SetVertexShaderConstantF(), visto in precedenza.
Il compito principale di questo VS è di trasformare la posizione del vertice da coordinate locali all’oggetto a quelle clip;
per fare ciò è necessario moltiplicare il vettore contenente la posizione per la matrice di clip. Come è noto, per
moltiplicare un vettore 1x4 per una matrice 4x4 bisogna fare il prodotto scalare del vettore stesso per ciascun vettore
colonna della matrice; tuttavia, tramite le istruzioni dello shader, abbiamo accesso soltanto alle righe, ma questo non è
un problema perché l’operazione di trasposizione scambia le colonne con le righe; questo spiega perché in c0-c3
abbiamo memorizzato la matrice clip trasposta. Le seguenti istruzioni calcolano la nuova posizione del vertice in
coordinate clip, inserita nel registro di output oPos:
dp4
dp4
dp4
dp4
oPos.x,v0,c0
oPos.y,v0,c1
oPos.z,v0,c2
oPos.w,v0,c3
Come si nota, ciascuna istruzione effettua il prodotto scalare del vettore posizione v0 per una riga della matrice clip
trasposta, che corrisponderà ad una colonna della matrice clip non trasposta. Al posto di queste 4 istruzioni, si può usare
la equivalente macro-istruzione:
m4x4 oPos,v0,c0
A questo punto impostiamo il registro di output del colore diffuso oD0, con la costante c12, che rappresenta la
luminosità dell’oggetto. Infine impostiamo il registro di output delle coordinate texture oT0 con quelle provenienti dal
registro di input v1. Il codice che implementa ciò è il seguente:
mov oD0,c12
mov oT0,v1
Per poter verificare la correttezza sintattica di un VS, si può provare a compilarlo usando l’assemblatore fornito nel
SDK : vsa.exe, posto nella directory /DXSDK/Bin/DXUtils; dopo aver aggiunto tale directory al path di sistema, basta
digitare:
vsa basic-vshader.vsh
Questa riga dice all’assemblatore di compilare il VS contenuto nel file di nome basic-vshader.vsh e di generare un file,
con lo stesso nome di quello sorgente, ma con estensione .vso, contenente il codice oggetto del VS. La cosa importante
è che, in caso di errori, l’assemblatore segnalerà la riga e il tipo di errore.
Semplice Vertex Shader in HLSL
Adesso vedremo come scrivere un VS, di funzionalità equivalenti al precedente, usando il linguaggio di alto livello
HLSL. Il codice sorgente completo si può trovare nel file shader.fx, qui ne esamineremo i frammenti più significativi.
Cominciamo con la dichiarazione di alcune variabili:
float4x4 matClip;
float4
luminosita
= { 0.9f, 0.9f, 0.9f, 0.0f };
La sintassi è simile al C, quindi viene specificato prima il tipo, poi il nome della variabile. La prima variabile è una
matrice di 4x4 float, che viene caricata con la matrice clip tramite il metodo SetValue(), come visto in precedenza. A
differenza dell’assembly, non è necessario trasporre la matrice clip. La seconda variabile è un vettore di 4 float,
inizializzato localmente con la luminosità percentuale.
A questo punto, è necessario definire una struttura per l’output del VS:
struct VS_OUTPUT
{
float4 Pos
: POSITION;
float4 Diff
: COLOR0;
float2 TexCoord : TEXCOORD0;
};
Analogamente al C, la parola chiave struct definisce un nuovo tipo di dato, composto dall’aggregazione di tipi semplici;
la novità è costituita dalla necessità di specificare la semantica di ciascun membro della struct: ciò si realizza facendo
seguire il nome del membro dai due punti e da un identificatore che specifica l’uso che se ne farà nel VS; ad esempio il
membro Pos è un vettore di 4 float, con semantica POSITION, cioè verrà usato per memorizzare la posizione del vertice
trasformato dal VS stesso.
Il codice del VS viene racchiuso in una funzione,VSmain(), definita con sintassi simile al C:
VS_OUTPUT VSmain(float4 Pos
: POSITION,
float2 TexCoord : TEXCOORD0)
{
VS_OUTPUT Out = (VS_OUTPUT) 0;
Out.Pos = mul(Pos,matClip);
Out.TexCoord = TexCoord;
Out.Diff = luminosita;
return Out;
}
Il tipo di ritorno della funzione è la struttura VS_OUTPUT, dichiarata in precedenza. I parametri di input sono: un
vettore di 4 float Pos, rappresentante la posizione del vertice, e da un vettore di 2 float TexCoord, rappresentante il set 0
di coordinate texture. Si noti che è necessario specificare la semantica d’uso per ciascun parametro di input e che ci
deve essere una corrispondenza 1:1 tra il numero e la semantica d’uso degli elementi della vertex declaration e dei
parametri di input del VS HLSL. Esaminando il corpo della funzione, vediamo che viene creata ed inizializzata a zero,
una variabile Out, di tipo VS_OUTPUT, che conterrà l’output del VS. Quindi si trasforma la posizione del vertice in
coordinate clip, moltiplicandola per la matrice matClip; si noti che mul() è una funzione predefinita di HLSL, come
abs(), acos(), etc ... La posizione in clip space viene assegnata al campo Pos della struttura di output. Quindi si
assegnano al campo Out.TexCoord le coordinate texture ricevute in input, visto che non è necessario trasformarle.
Infine si assegna al campo Out.Diff, che rappresenta il colore diffuso del vertice, la variabile luminosita,
precedentemente inizializzata. L’ultima istruzione ritorna in output la variabile Out.
Siccome il vertex shader in HLSL viene definito all’interno di un effect file, dobbiamo specificare le tecniche e i passi
usati da quest’ultimo:
technique T0
{
pass P0
{
VertexShader = compile vs_1_1 VSmain();
}
}
Con l’istruzione technique T0 si specificano i passi contenuti nella tecnica di rendering denominata T0, racchiusi tra le
parentesi graffe. Questi ultimi si specificano con l’istruzione pass seguita dal nome del passo P0 e da un blocco di
istruzioni racchiuse tra le parentesi graffe. Per i nomi di tecniche e passi si usa la convenzione che devono cominciare
rispettivamente con le lettere T e P maiuscole, seguite da un numero progressivo. All’interno del blocco pass troviamo
l’istruzione compile, seguita da una stringa che identifica il tipo e la versione di shader ed infine dal nome della
funzione che racchiude il codice dello shader. Questa istruzione compila lo shader e lo assegna all’oggetto
VertexShader, predefinito dal linguaggio HLSL.
Vertex Buffers
Dopo aver visto il codice legato alla gestione dei VS, esaminiamo sinteticamente quello restante, necessario
all’implementazione dell’applicazione d’esempio, a beneficio dei lettori che non hanno molta familiarità con DirectX.
Per poter disegnare un qualunque oggetto tridimensionale, è necessario specificarne i vertici. In DirectX, questi vanno
memorizzati in un buffer apposito, chiamato appunto VertexBuffer, rappresentato dalla classe IDirect3DVertexBuffer9.
Il VB va creato usando il metodo CreateVertexBuffer() della classe IDirect3DDevice9. Nel nostro esempio, volendo
disegnare un quadrato, verrà creato un VB contenente 4 vertici, nel metodo InitDeviceObjects():
m_pd3dDevice->CreateVertexBuffer( 4*sizeof(CUSTOMVERTEX),
D3DUSAGE_WRITEONLY, D3DFVF_CUSTOMVERTEX,
D3DPOOL_MANAGED, &m_pVB, NULL);
Il primo parametro è la dimensione del buffer, in bytes; il secondo è una costante che rappresenta l’uso: in questo caso
sola scrittura; il terzo rappresenta il formato del vertice, dichiarato per mezzo di macro, come vedremo in seguito; il
quarto parametro è una costante che indica la classe di memoria dove verrà collocato il buffer; il valore
D3DPOOL_MANAGED indica che il buffer verrà creato nella memoria di sistema e sarà automaticamente caricato in
quella video della scheda 3D; ne esisteranno quindi 2 copie, a discapito dell’efficienza, ma in compenso il buffer non
dovrà essere reinizializzato in seguito ad un reset del device, quando ad esempio si cambia risoluzione o le dimensioni
di una finestra. Il valore D3DPOOL_DEFAULT indica che il buffer sarà memorizzato nella memoria video, per
ottenere maggiore efficienza; l’inconveniente è dato dal fatto che sarà necessario reinizializzare il buffer in seguito ad
un reset del device; questo tipo di buffer va creato nel metodo RestoreDeviceObjects() del framework. Il quinto
parametro è l’indirizzo di un puntatore ad un oggetto della classe IDirect3DVertexBuffer9, che rappresenta il VB che si
sta creando; il sesto parametro va messo sempre a NULL. In precedenza abbiamo parlato del formato del VB;
supponendo che il nostro vertice sia rappresentato dalla seguente struttura:
struct CUSTOMVERTEX
{
D3DXVECTOR3 position;
D3DXVECTOR2 texCoord;
};
// posizione
// coordinate texture
è possibile definire il formato del vertice, detto flexible vertex format, tramite la seguente macro:
#define D3DFVF_CUSTOMVERTEX (D3DFVF_XYZ | D3DFVF_TEX1)
Si combinano le costanti D3DFVF_XYX e D3DFVF_TEX1 per indicare la presenza della posizione e di 1 set di
coordinate texture. Si veda la documentazione di DX9 per tutte le altre costanti.
Per poter inserire dei dati nel VB appena creato, è necessaria l’operazione di lock; siccome la collocazione in memoria
di un VB è gestita automaticamente da DX, che potrebbe anche spostare il buffer in altre locazioni di memoria per
evitare la frammentazione, è necessario dire a DX di bloccare il buffer, cioè di non spostarlo, e di ritornare un puntatore
all’area di memoria dove è allocato. Tutto ciò è svolto dal metodo Lock(), che riceve come primo parametro l’offset, in
bytes, ai dati da bloccare nel buffer; il secondo parametro rappresenta la dimensione dell’area da bloccare in bytes;
assegnando 0 si blocca l’intero buffer; il terzo è l’indirizzo del puntatore ai dati, che va convertito al tipo VOID; il
quarto sono dei flags, che possono essere impostati a 0. Il seguente frammento di codice, sempre in InitDeviceObjects(),
blocca il VB, inizializza i vertici e quindi lo sblocca, usando il metodo Unlock():
CUSTOMVERTEX* pVertices;
m_pVB->Lock(0,0,(VOID**)&pVertices,0);
// vertici del quadrilatero
pVertices[0].position = D3DXVECTOR3( -1.0f, -1.0f, 0.0f );
pVertices[0].texCoord = D3DXVECTOR2( 0.0f, 1.0f);
....
m_pVB->Unlock();
Textures in Direct3D
Dopo aver creato i vertici del nostro quadrato, dobbiamo caricare la texture con il logo di DirectX, rappresentata dalla
classe IDirect3DTexture9; vediamo come creare un oggetto di questa classe, nel metodo InitDeviceObjects():
hr = D3DXCreateTextureFromFile(m_pd3dDevice,"dx5_logo.bmp",&m_pTexture);
if(hr!=D3D_OK) return E_FAIL;
La funzione D3DXCreateTextureFromFile() crea un oggetto texture, leggendone i dati da un file. Il primo parametro è
il device; il secondo il nome del file, che può essere nei seguenti formati: .bmp, .dds, .dib, .jpg, .png, .tga.; il terzo
parametro è l’indirizzo di un puntatore all’oggetto texture che verrà creato, della classe IDirect3DTexture9. Il valore di
ritorno della funzione è di tipo HRESULT, quindi se è diverso da D3D_OK significa che si è verificato un errore.
L’oggetto texture, come qualsiasi altra risorsa di D3D, va rilasciato nel metodo DeleteDeviceObjects() tramite la macro:
SAFE_RELEASE( m_pTexture );
Render states
Prima di poter disegnare un oggetto sul video, è necessario impostare lo stato della MultiTexturing Unit, ovvero quella
parte delle pipeline che si occupa dell’applicazione delle texture. Questa unità fa parte della pipeline fissa, ed è
costituita da otto stadi in cascata; ciascuno di questi riceve in input 2 argomenti che possono essere: colore diffuso,
speculare, texture oppure il risultato di uno stadio precedente; a questi 2 argomenti viene applicata una operazione e il
risultato passa allo stadio successivo o va in output. Nei prossimi articoli vedremo come sostituire questa unità con un
Pixel Shader. Per il momento utilizzeremo solo uno stadio o “stage” di questa unità: riceverà in input il colore diffuso
del vertice e il corrispondente texel della texture, effettuerà il prodotto tra i due e stamperà a video il pixel risultante. In
questo modo si farà variare la luminosità della texture in base al colore diffuso impostato nel VS. Questa operazione
viene effettuata nel metodo RestoreDeviceObjects(), in quanto i render states vanno reimpostati ogni qualvolta si
effettua il reset del device:
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLOROP ,D3DTOP_MODULATE );
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG1,D3DTA_TEXTURE );
m_pd3dDevice->SetTextureStageState( 0, D3DTSS_COLORARG2,D3DTA_DIFFUSE );
Per impostare uno stadio della Multitexture Unit, viene usato il metodo SetTextureStageState(), della classe
IDirect3DDevice9. Il primo parametro è il numero dello stage(0-7); il secondo è lo stato da settare, specificato tramite
costanti; ad esempio l’operazione è rappresentata da D3DTSS_COLOROP etc...; il terzo parametro è il valore da
assegnare allo stato, ad esempio D3DTOP_MODULATE indica il prodotto degli argomenti.
Un altro importante render state da impostare è il filtraggio delle texture, che va specificato per ogni texture stage usato
tramite il metodo:
m_pd3dDevice->SetSamplerState(0,D3DSAMP_MINFILTER,D3DTEXF_LINEAR);
m_pd3dDevice->SetSamplerState(0,D3DSAMP_MAGFILTER,D3DTEXF_LINEAR);
m_pd3dDevice->SetSamplerState(0,D3DSAMP_MIPFILTER,D3DTEXF_LINEAR);
Il primo argomento è il numero di texture stage, il secondo è una costante che specifica il tipo di filtro; ad esempio filtro
per la riduzione, per l’ingrandimento e per il mip-mapping; il terzo parametro specifica il tipo di filtro, qui lineare.
Disegno della scena
Dopo la fase preparatoria, possiamo finalmente disegnare la scena, ovvero il nostro quadrato ricoperto dalla texture con
il logo di DirectX. Il disegno avviene nel metodo Render(). Prima di disegnare è necessario pulire il frame buffer e lo zbuffer, con il metodo:
m_pd3dDevice->Clear( 0L, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
0x00000000, 1.0f, 0L );
Il terzo parametro rappresenta le superfici da pulire, in questo caso il frame buffer e lo z-buffer; il quarto parametro è il
colore con cui riempire il frame buffer, nero in questo caso; il quinto parametro è il valore della z con cui riempire lo zbuffer: qui si assegna 1.0, cioè il valore max per la z. La successiva operazione è quella di specificare lo stream dei
vertici che conterrà le primitive geometriche da disegnare, e il formato di tale stream, tramite i seguenti metodi:
m_pd3dDevice->SetStreamSource( 0, m_pVB, 0, sizeof(CUSTOMVERTEX) );
m_pd3dDevice->SetFVF( D3DFVF_CUSTOMVERTEX );
SetStreamSource() riceve come primo parametro il numero dello stream, 0 in questo caso; il secondo parametro è un
puntatore al vertex buffer da associare allo stream; il terzo indica l’offset dei dati dall’inizio del vertex buffer; qui viene
impostato a 0; il quarto parametro rappresenta le dimensioni di un vertice, in bytes. Tramite il metodo SetFVF() si
imposta il formato del vertex buffer, nel modo già visto per la creazione del VB stesso. A questo punto è necessario
specificare la texture da usare con il metodo:
m_pd3dDevice->SetTexture(0,m_pTexture);
Il primo parametro rappresenta la texture stage, 0 in questo caso; il secondo è un puntatore all’oggetto texture. Adesso
bisogna attivare il vertex shader che processerà i vertici, come visto nel paragrafo relativo alla gestione dei VS.
Infine bisogna disegnare il quadrato utilizzando il metodo DrawPrimitive():
m_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, 2 );
Questo riceve come primo parametro il tipo di primitiva, in questo caso una strip di triangoli, cioè una lista di triangoli,
aventi 2 vertici in comune; in questo modo per disegnare i 2 triangoli che formano il quadrato bastano 4 vertici, dato
che 2 sono in comune. L’uso di strip consente una maggiore efficienza di disegno, perché riduce il numero di primitive
disegnate.
Altri
tipi
di
primitiva
supportati
sono:
D3DPT_POINTLIST,
D3DPT_LINELIST,
D3DPT_TRIANGLELIST, D3DPT_TRIANGLEFAN; rispettivamente, i vertici vengono interpretati come: punti
isolati, segmenti, triangoli, triangoli aventi un vertice in comune. Il secondo parametro di DrawPrimitive() indica il
vertice da cui cominciare a disegnare, in questo caso 0; il terzo parametro indica il numero di primitive da disegnare,
qui 2. L’intero codice di disegno deve essere racchiuso tra le chiamate:
m_pd3dDevice->BeginScene();
// disegno della scena
m_pd3dDevice->EndScene();
Conclusioni
A questo punto non ci resta che provare a compilare la nostra applicazione di esempio; se tutto procede bene, si
dovrebbe vedere un’immagine di un quadrato con il logo di DirectX, rotante intorno all’asse y, come in Figura 2.
Riassumendo, in questo articolo abbiamo appreso i concetti essenziali per realizzare applicazioni grafiche 3D con
DirectX 9.0, come l’uso del Common Files Framework, i vertex buffers, gli oggetti texture, i render states, la
DrawPrimitive(); inoltre abbiamo visto i passi necessari ad integrare un Vertex Shader in un’applicazione; abbiamo
anche visto un esempio di VS scritto con il nuovo linguaggio di alto livello HLSL. Nel prossimo articolo vedremo come
implementare alcuni modelli di illuminazione tramite Vertex Shader, in modo da conferire maggior realismo agli
oggetti disegnati. A presto !!!
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.