Programmazione degli Shaders in DirectX 9.0 (parte prima) : Introduzione ai Vertex Shaders
←
→
Trascrizione del contenuto della pagina
Se il tuo browser non visualizza correttamente la pagina, ti preghiamo di leggere il contenuto della pagina quaggiù
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 int 4D vettore W 1 1 1 input v# float 4D vettore R 16 16 16 constant float c# float 4D vettore define/R min 96 min 256 min 256 constant integer i# int 4D vettore define/use nd 16 16 constant boolean b# bool scalare define/use nd 16 16 temporary r# float 4D vettore R/W 12 12 min 12 loop counter aL int scalare use nd 1 1 predicate p0 bool 4D vettore W/use nd nd 1 sampler s# nd nd 4 oD# float 4D vettore W 2 2 nd oFog float scalare W 1 1 nd oPos float 4D vettore W 1 1 nd oPts float scalare W 1 1 nd oT# float 4D vettore W 8 8 nd o# W 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 azione add dest,src0,src1 1 NO somma i vettori src0,src1. dest.x = src0.x + src1.x; analogamente per y,z,w. dcl_usage dest 0 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 def dest,v0,v1,v2,v3 0 NO definisce una costante. dest è uno dei registri costanti float, mentre v0,v1,v2,v3 sono 4 numeri floating point. dp3 dest,src0,src1 1 NO prodotto scalare a 3 componenti dei registri sorgente. 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; 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. exp dest,src.w 10 SI esponenziale 2x, con precisione piena. dest.x=dest.y=dest.z=dest.w=pow(2,src.w); expp dest,src.w 1 SI esponenziale 2x, con precisione parziale a 10 bit. dest.x = pow(2,(int)src.w) dest.y = frc(src.w); parte frazionaria di src.w dest.z = pow(2,src.w) & 0xffffff00 dest.w = 1 frc dest,src 3 SI ritorna la parte frazionale di ciascuna componente del vettore src. dest.x = src.x – floor(src.x); analogamente per y,z,w. lit dest,src 1 NO calcola i coefficienti di illuminazione. 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 dest,src.w 1 SI calcola il log2(x) con precisione parziale a 10 bit. Vedasi log. m3x2 dest,src0,src1 2 SI calcola il prodotto tra un vettore a 3 componenti e una matrice 2x3. m3x2 r0.xy, r1, c0; verrà espansa in: dp3 r0.x, r1, c0 dp3 r0.y, r1, c1 m3x3 dest,src0,src1 3 SI calcola il prodotto tra un vettore a 3 componenti e una matrice 3x3. m3x4 dest,src0,src1 4 SI calcola il prodotto tra un vettore a 3 componenti e una matrice 3x4. m4x3 dest,src0,src1 3 SI calcola il prodotto tra un vettore a 4 componenti e una matrice 4x3. m4x4 dest,src0,src1 4 SI calcola il prodotto tra un vettore a 4 componenti e una matrice 4x4. mad dest,src0,src1,src2 1 NO moltiplica e somma i vettori sorgenti. dest.x = src0.x * src1.x + src2.x; analogamente per y,z,w. max dest,src0,src1 1 NO calcola il massimo tra i vettori sorgenti. dest.x=(src0.x >= src1.x) ? src0.x : src1.x;analogamente per y,z,w. min dest,src0,src1 1 NO calcola il minimo tra i vettori sorgenti. mov dest,src 1 NO sposta i dati tra 2 registri. dest.x = src.x; analogamente per y,z,w. mul dest,src0,src1 1 NO moltiplica le componenti corrispondenti dei 2 vettori sorgenti. dest.x = src0.x * src1.x;analogamente per y,z,w. nop 0 NO non viene svolta nessuna operazione. rcp dest,src.w 1 NO calcola il reciproco dello scalare nel registro sorgente. 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 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. slt dest,src0,src1 1 NO se il primo operando è minore o uguale al secondo, setta dest a 1. sub dest,src0,src1 1 NO sottrae i vettori sorgenti: dest.x=src0.x – src1.x; analogamente per y,z,w. vs 0 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 azione abs dest,src 1 NO Calcola il valore assoluto di src. dest.x = fabs(src.x); analogamente per y,z,w call l# 2 NO Effettua una chiamata alla subroutine contrassegnata dalla label l#. callnz l#,boolReg 3 NO Effettua una chiamata a subroutine condizionale, se il boolReg è diverso da zero. boolReg è un registro booleano costante b#. crs dest,src0,src1 2 NO Calcola il prodotto vettoriale, con la regola della mano destra. 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 0 NO Definisce una costante booleana; dest deve essere un registro costante booleano b#; mentre boolValue = {TRUE,FALSE} defi dest,i0,i1,i2,i3 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. else 1 NO Inizia un blocco else di un’istruzione condizionale if. endif 1 NO Indica la fine di un blocco if...else. endloop 2 NO Indica la fine di un blocco loop...endloop. endrep 2 NO Indica la fine di un blocco rep...endrep. if boolReg 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 endif label l# 0 NO Contrassegna la successiva istruzione con la label l#. loop aL,intReg 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
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.
Puoi anche leggere