I vertex shader rappresentano senza dubbio la più importante tecnologia nell’ambito della computer grafica in real time permettendo effetti unici ed impensabili appena pochi anni fa. Ma cosa sono esattamente? Osservate questa immagine
Questo è uno schema che mostra cosa fa DirectX per renderizzare una scena. Potete notare che i vertex data (cioè i vertici contenuti ad esempio in una mesh) vengono trattati da tante parti fino ad ottenere il risultato finale. Potete subito vedere che i vertex shaders appartengono al primissimo strato di tale processo. I vertex shader sono infatti responsabili della trasformazione geometrica dei vertici sullo schermo. Sono infatti i vertex shader ad elaborare secondo precise formule matematiche le matrici, i vertici e tutte le altra impostazioni puramente geometriche per generare l’oggetto come vediamo. Solo dopo vengono applicati tutti gli altri effetti. Fino alla versione 7 di DirectX si poteva intervenire molto poco e venivano utilizzati dei vertex shader fissi incorporati nelle librerie. Dalla versione 8 però ci viene data la possibilità di creare noi degli shader sotto forma di codice assembler in modo da cambiare completamente il modo di visualizzare la scena. Molte tecniche oggi applicate per i videogiochi più moderni utilizzano appunto degli shader che gestiscono in modo personalizzato luci, colori e posizioni per ottenere deformazioni dell’ambiente, illuminazioni più realistiche, grafica a cartone animato (Cell shading) effetti di texture a rilievo etc. Ecco qualche immagine che mostrano cosa è ottenibile con l’uso di shader personalizzati.
Questo è 3D ma in Cell shading (un mio esempio)
Anche questo è 3D ma in Charcoal rendering (esempio dell’ATI)
Riuscireste ad illuminare una teiera in questo modo?
I vertex shader permettono quindi di ottenere degli effetti veramente professionali e allo stesso tempo con ottime prestazioni (nel limite dell’umano). Come ho detto i vertex shader sono codice assembler che viene caricato in DirectX ed utilizzato al posto di quello standard. Caricare il codice è abbastanza semplice, utilizzarlo anche, ma scrivere codice comincia a diventare difficile e creare un effetto veramente ottimo richiede conoscenze matematiche più che elevate e molto lavoro. Tutto dipende ovviamente da cosa volete realizzare. Il codice assembler destinato ai vertex shader è fortunatamente più semplice di quello destinato ai processori e nel prossimo tutorial pubblicherò una guida al codice assembler per i vertex shader. Il codice può essere scritto in un banalissimo file testuale (anche un txt va benissimo) e non ha bisogno di nient’altro che essere corretto. Sarà DirectX che lo compilerà per noi in fase di caricamento. A differenza di DirectX8 in cui la gestione dei vertex shader era molto complessa in DirectX9 i vertex shader sono oggetti semplici e robusti e possono essere applicati direttamente alle mesh (chi li ha utilizzati con DirectX8 starà saltando sulla sedia visto che era necessario fare un certo numero di operazioni veramente insopportabili per visualizzare un oggetto). I vertex shader ci hanno quindi guadagnato in semplicità e soprattutto in velocità. Prima di iniziare con il codice sarà bene spiegare cosa è un codice assembler per vertex shader, soprattutto per chi non ha mai usato il normale assembler per i processori.
Vertex shader Assembler
Questo schema mostra il funzionamento dell’ALU del processore della scheda video (GPU) responsabile dei calcoli matematici sui vertici. Nel linguaggio assembler a differenza dei linguaggi di alto livello come il C o il VB lo spazio per la memorizzazione di variabili e istruzioni viene gestito direttamente dal programmatore. Ogni processore ha infatti una spazio particolare (memoria cache) molto veloce ma molto limitata in cui carica le istruzioni del programma e memorizza i valori che gli servono. Questi valori non vengono usati direttamente ma vengono caricati in zone del processore ancora più veloci chiamate registri e che sono in numero molto limitati a secondo del processore. Il programma carica quindi le istruzioni e le variabili nei registri ed il processore esegue i calcoli producendo un risultato che va a finire in uno o più registri di uscita. Questi poi vengono copiati nella cache e solo in seguito nella memoria centrale ossia nella RAM che è molto lenta se paragonata alla velocità del processore. Nella GPU esistono un certo numero di registri (4 tipi) che sono destinati a gestire i vertici. Alcuni registri sono per la lettura e la scrittura (utilizzati come variabili per i calcoli), altri sono in sola lettura e contengono i valori che gli passiamo dal programma (ad esempio i vertici dell’oggetto o le matrici view, matrix e projection) mentre altri sono solo in scrittura e sono dei punti in cui dobbiamo depositare i risultati (ad esempio la posizione finale del vertice o il colore). Ecco un banale codice assembler che modifica le coordinate texture in fase di esecuzione.
vs.1.0
dcl_position v0
dcl_normal v3
dcl_texcoord0 v7
dp4 r0.x, v0, c0
dp4 r0.y, v0, c1
dp4 r0.z, v0, c2
dp4 r0.w, v0, c3
mov oPos, r0 ; Emette la posizione
mov oD0, c4 ; Emette il colore
mul r1,v7,c5
mov oT0, r1
In questo esempio (non è necessario capirlo tutto per il momento) vengono mostrati alcuni dei registri. Ad esempio v0 è il registro che contiene la posizione del vertice (presa dalla mesh) mentre oD0 è un registro destinazione del colore. L’unico registro su cui possiamo (e dobbiamo) intervenire durante l’esecuzione del programma sono quelli c (c0, c1 etc). In questi registri dovremo infatti inserire noi dei valori durante l’esecuzione dai numeri(per passare ad esempio l’intensità della luce) alle matrici (gli shader che scriviamo noi non prendono le matrici dal device, dobbiamo passargliele noi). Ad esempio l’istruzione
mov oD0, c4
Muove nel registro oD0 (colore finale del vertice) il registro c4 che dobbiamo passargli. Nel registro c4 ci inseriremo un colore che potremo modificare dinamicamente in fase di esecuzione. Ogni registro altro non è che un vettore da 4 single (le matrici sono considerate ad esempio 4 vettori da 4). Per ora non è necessario capire il codice: sappiate solo che visualizza un oggetto con un colore scelto da noi e con la possibilità di moltiplicare per un valore a scelta le coordinate texture.
Codice
Prima di utilizzare i vertex shader controlliamo che la scheda sia compatibile. Altrimenti saremo costretti a creare un device in modalità REF (troppo lenta per qualsiasi gioco).
Dim caps As Direct3D.Caps
caps = Direct3D.Manager.GetDeviceCaps(0, Direct3D.DeviceType.Hardware)
caps.VertexShaderVersion.Major()
caps.VertexShaderVersion.Minor()
Major deve essere maggiore di 0 altrimenti la vostra scheda non supporta la modalità.
Anche la minor è importante. Se ad esempio ha valore 0 la vostra versione massima sarà la 1.0 mentre con 1 sarà 1.1. Se avete schede molto recenti (nel 2003 non dovreste averne) potete arrivare a versioni fino alla 3.0
Per questo tutorial si spera abbiate almeno la 1.0.
Caricare il codice non è troppo difficile. Copiate il codice proposto in un file di testo e salvatelo sul disco. Create il device come di consueto e dichiarate 2 oggetti:
Dim vShad As VertexShader = Nothing
Dim vDichi As VertexDeclaration = Nothing
Il primo è il vertex shader che conterrà e gestirà il codice assembler mentre il secondo serve a contenere la struttura del vertice.
Per caricare da file lo shader dichiarate un oggetto GraphicsStream
Dim code As GraphicsStream = Nothing
Createlo
code = ShaderLoader.FromFile(strFilename, Nothing, 0)
Dove strFilename è il path del file. Gli altri parametri lasciateli così. Infine create il vertex shader
vShad =New VertexShader(device, code)
Il vertex shader è ora creato.
La dichiarazione corrisponde alla struttura del vertice che adoperiamo per il vertex shader.
Dim elementi() As VertexElement = { New VertexElement(0, 0, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Position, 0), New VertexElement(0, 12, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Normal, 0), New VertexElement(0, 24, DeclarationType.Float2, DeclarationMethod.Default, DeclarationUsage.TextureCoordinate, 0), VertexElement.VertexDeclarationEnd}
Attenzione è tutta una righa.
vDichi =New VertexDeclaration(device, elementi)
Come vedete creo un array di vertex element e lo riempio (ora vi spiego come) ed infine creo l’oggetto VertexDeclaration.
L’array deve contenere i dati sulla struttura del vertice. Io ad esempio ho adoperato il tipo vertex che ha un vettore da 3 per la posizione, un vettore da 3 per le normali e un vettore da 2 per le coordinate texture. L’array deve quindi contenere in ordine la descrizione di questi 3 elementi. Ad esempio il primo per la posizione
New VertexElement(0, 0, DeclarationType.Float3, DeclarationMethod.Default, DeclarationUsage.Position, 0)
Il primo 0 è il flusso che si usa. Se usate le mesh usate sempre 0, la seconda è il byte da dove iniziare. Si parte sempre da 0 e si aggiunge la dimensione del tipo che abbiamo usato nel precedente elemento. Potete infatti vedere che quella successiva, le normali, iniziano da 12 perché quella precedente (la posizione) è un vector3 composto da 3 single (4 byte x 3=12byte). Le coordinate partiranno quindi da 24 (12 della posizione e 12 delle normali). Si dichiara poi il tipo di dichiarazione (lasciate il default) e l’utilizzo dell’elemento (position, normal etc). Lasciate a 0 l’ultimo valore. L’ultimo elemento deve essere sempre
VertexElement.VertexDeclarationEnd
per chiudere il tutto.
Ho detto che il vertice ha posizione e normale in vector3 e un set di coordinate texture con un vector2. Quindi l’end va posizionato nel punto 12+12+8=24byte.
Ricordate che la dichiarazione dipende dal tipo di vertice che usate e come l’ho scritta va inserita tutta in un’unica riga (potete usare _ per andare a capo e continuare).
Ora che avete creato questi due oggetti potete usarli.
Rendering
Prima di renderizzare l’oggetto dovete impostare questi tre campi:
device.VertexDeclaration = vDichi
device.VertexShader = vShad
device.VertexFormat = Vertex.Format
In questo modo settate la dichiarazione dei vertici, lo shader ed il formato. Ora dovete passare i valori ai registri (ricordate che con i shader il device ignora le matrici world, view, projection e le impostazioni delle luci). I registri che conterranno tali valori sono i registri C.
Per passare i valori si usa
device.SetVertexShaderConstant(numeroRegistro, oggetto())
Il numero registro è appunto il numero del registro C in cui depositare il valore. Oggetto() è un array contenente i valori da passargli.
device.SetVertexShaderConstant(9, New Single() {.R / 256, .G / 256, .B / 256, 0})
In questo esempio gli ho passato un array di 4 single (un vector4) nel registro 9 (ricordo che un registro contiene 4 valori single).
device.SetVertexShaderConstant(5, New Matrix() {m})
In questo gli ho passato una matrice. La matrice è trattata come 4 array di single e riempirà i registri 5,6,7 e 8.
Dim v4(0) As Vector4
v4(0).X = 0
v4(0).Y = 0.5
v4(0).Z = 1
v4(0).W = 2
device.SetVertexShaderConstant(0, v4)
Ed in questo un vettore precedentemente creato. Riempirà completamente il registro 0
Potete passargli anche booleani o interi.
Ora passate tutto il necessario allo shader (questo dipende solo dal codice assembler) e renderizzate tutto come di consueto usando texture e mesh (non i materiali che devono essere passati con i registri sotto forma di valori ad esempio i quattro colori di diffuse rgba).
Il risultato sarà quindi quanto programmato nel codice dello shader.
Potete caricare molti shader ed alternarli continuamente per usarne di diversi per ogni oggetto.
Per tornare a quello standard (visualizzazione standard).
device.VertexDeclaration = Nothing
device.VertexShader = Nothing
device.VertexFormat = Vertex.Format
Vi lascio ad un primo semplice esempio per farvi capire come si carica e usa un semplice vertex shader. Vi rimando al prossimo tutorial che spiegherà come scrivere un codice shader. Ricordate che:
1)Cosa passare ai registri dipende dal codice assembler creato da voi.
2)Matrici, telecamere, materiali, nebbia, luci devono essere inserite nei registri (la modalità dipende dal codice assembler).
3)Texture, vertici, mesh, vengono passate come di consueto.
4) Potete alternare gli shader in qualsiasi momento, anche dopo ogni drawSubset
5)Ora potete usare le mesh!!!!!!!!!!!!
6)Se non passate niente al registro gli verrà passato 0
Esempio VB