Continuamo l'introduzione alle nuove DirectX10. Nella prima parte abbiamo visto insieme cosa sono le DirectX e a cosa servono, un po' di storia e di esempi. Ora invece ci concentreremo sul rendering, ovveri i passi necessari a creare l'immagine su schermo.
Rendering
Il meccanismo del rendering è il seguente.
Attraverso opportune variabili ogni vertice viene posizionato nella sua posizione finale sullo schermo. Questa operazione si chiama proiezione e viene eseguita dal vertex shader. Oltre a questo vengono anche calcolati altri valori che verranno passati al pixel shader (esempio il colore del vertice). La fase di proiezione avviene tramite 3 passaggi chiamati “cambiamenti di spazio”.
La posizione in cui si trova l’oggetto si chiama Object Space.
Con la prima trasformazione l’oggetto viene posizionato in un punto preciso con una sua rotazione e scala (esempio il cubo viene creato con il centro in 0,0,0 mentre nella scena deve trovarsi nella posizione 30,40,50 ruotato di 45° lungo l’asse Y).
Questa posizione si chiama World Space.
Il modello però non è ancora visibile perché manca un dettaglio: da dove viene osservato?
La seconda trasformazione fa in modo che l’oggetto sia posizionato rispetto ad un punto di osservazione. Immaginate un set cinematografico. L’attore si trova in un punto preciso nella scena mentre ci sono telecamere che lo inquadrano da punti diversi producento immagini diverse. L’oggetto 3D cambierà di nuovo coordinate in modo da trovarsi centrato rispetto alla direzione della telecamera ed alla giusta distanza.
La nuova posizione si chiama ViewSpace.
L’ultima fase si può riassumere con una frase: “Quanto guardo?”. L’ultima fase infatti determina l’angolo della telecamera che sarà visibile (sia orizzontale che verticale). L’ultima trasformazione convertità le coordinate in Projection Space e farà in modo che tutti gli oggetti visibili abbiano coordinate XY comprese tra -1 e +1 mentre le Z da 0 ad 1.
Tutta la scena visibile rientrerà in questo intervallo.
Il vertex shader fa esattamente questo: data la posizione, la telecamera ed il tipo di proiezione, porta l’oggetto dalla sua posizione di inizio assegnata nell’editor alla sua posizione finale nell’intervallo -1, +1 chiamato “Spazio Omogeneo”.
Il tutto per ogni vertice della scena, cifre che con le schede moderne superano il miliardo di elaborazioni al secondo.
La fase successiva viene gestita in automatico. Posizionati tutti i triangoli vengono scartati quelli non visibili e poi “riempiti”. Per riempimento si intende una operazione che, pixel per pixel, traccia punti di colore che creeranno l’immagine finale sullo schermo. Questa operazione si chiama rastering.
Il processo prevede la lettura dei 3 vertici del triangolo che rappresenteranno i limiti. Al suo interno viene calcolata la media pesata dei valori contenuti nei vertici per ogni pixel. Facciamo un esempio.
Immaginiamo che, oltre alla posizione, il vertice contenga un valore numeri che chiamiamo T.
I 3 vertici che arrivano hanno valori di T uguali a 3, 6 e 15. Il rastering inizierà ad elaborare tutti i pixel e calcolare la media. Nel centro del triangolo T sarà uguale a 3 + 6 + 15 / 3 ossia 8. Più ci si avvicina ad uno dei vertici e più il valore si avvicinerà al valore del vertice (ad esempio il pixel a metà tra il primo ed il terzo vertice avrà T=9).
Ogni vertice ha un colore diverso che viene interpolato
Questo valore calcolato come media pesata verrà passato al Pixel Shader.
Il Pixel Shader riceve la struttura generata dal vertice con valori pari alla media pesata dei 3 vertici in quel pixel.
Partendo da questi valori il Pixel Shader calcolerà il colore finale del pixel restituendolo come struttura di 4 numeri (generalmente float che vanno da 0 ad 1 rappresentanti i colori RGB e la trasparenza Alpha).
Infine questo colore verrà confrontato con quello attualmente già presente sullo schermo e, in base alle impostazioni, potrà sovrascriverlo, lasciare il vecchio o fare una operazione di fusione e calcolare un nuovo valore finale.
Questo valore sarà l’esatto colore che comparirà sullo schermo.
Risultato Finale
Matematicamente
Le informazioni sulle trasformazioni (da Object Space a Projection Space) vengono comunemente memorizzate come matrici, griglie di numeri reali che in DirectX sono quadrate di dimensione 4x4.
Traslazioni, rotazioni, posizionamento di telecamere sono ottenute semplicemente effettuando un prodotto riga per colonna tra il vettore posizione e la matrice.
La posizione di un vertice è gestita come 4 float, XYZ e W. La W è chiamata coordinata omogenea e vale 1 (valore impostato da DirectX quindi non dovete preoccuparvi di salvarlo come dato). Lo scopo è puramente matematico in quanto l’unico modo per fare una traslazione con un prodotto vettore x matrice (potete verificare matematicamente la cosa ma verrà approfondita nei prossimi tutorial).
Se avete dubbi sulle Matrici Algebriche vi consiglio di consultare libri o di cercare su google.
La posizione finale del pixel equivale ad un prodotto del vettore per 3 matrici, la World, la View e la Projection.
Posizione Finale = Vettore x World x View x Transform
O per proprietà associativa
Posizione Finale = Vettore x Transform
Dove Transform è il prodotto delle 3 matrici. Ricordo (se non l’avete mai saputo studiate le Matrici Algebriche) che il prodotto tra matrici non è commutativo. V x T non è uguale a T x V.
Un esempio di pseudo codice shader che fa questo è il sequente
struct INPUT{
posizione;
valoreIngresso;
};
struct OUTPUT{
float4 posizione;
float4 valoreUscita1;
float4 valoreUscita2;
};
float4x4 matrice;
float4 costante;
OUTPUT Main(INPUT in){
OUTPUT out;
out. posizione = in. posizione * matrice;
out. valoreUscita1= in. valoreIngresso;
out. valoreUscita2= in. valoreIngresso + costante;
return outP;
}
Come potete vedere ho 2 strutture. In input ho la posizione (come float4 ossia una struttura contenente 4 float) ed un valore di ingresso mentre in output abbiamo posizione e due valori di uscita. Nello shader si moltiplica la posizione per la variabile matrice e la mettiamo in output insieme ai due valori di uscita.
La struttura OUTPUT verrà usata identica nel Pixel Shader ma il valore che arriverà ad ogni pixel sarà la media pesata dei 3 vertici del triangolo in cui si trova.
Una cosa importante da dire sono come i dati arrivano.
DirectX quando esegue il rendering riceve intere mesh che contengono anche milioni di triangoli. Sarà la libreria a scandire i vertici uno ad uno per passarli alla funzione Shader che avete scritto. Le costanti invece come “matrice” e “costante” usate nello shader di esempio vengono passate prima di ogni operazione di rendering (chiamate anche Draw) e saranno uguali per tutto il gruppo di triangoli. Le costanti sono condivise da tutti i tipi di shader e rimarranno in memoria finchè non le si modifica o si distrugge l’effetto.
Riassumento il processo è:
- Si attiva lo shader
- Si impostano le costanti per il gruppo di triangoli (ad esempio una mesh o una parte di essa)
- Si chiama il Draw del gruppo di triangoli
- Si ripetono i punti 2 e 3 per ogni altro oggetto che usa quello shader altrimenti si cambia shader e si renderizzano altri oggetti
Il Geometry Shader si inserisce appena dopo il Vertex Shader. I vertici formano triangoli che vengono passati nella loro interezza a questo shader. Sarà il codice a decidere se il triangolo sarà semplicemente passato alle fasi successive oppure subirà modifiche che potranno essere semplici variazioni nei valori, l’eliminazione o la moltiplicazione (potremo di fatto creare nuovi triangoli ed aggiungerli alla lista di quelli da processare).
La complessità degli Shader dipenderà da quello che vorrete realizzare ed il bello sarà proprio il fatto che non avrete limiti nella realizzazione dei vostri effetti. Addirittura molti stanno iniziando ad utilizzare gli Shader per fare elaborazione di dati utilizzando i vertici ed i pixel al posto di array di numeri.
Struttura dell’applicazione
Chi sviluppa applicazioni Windows è da tempo abituato alla gestione ad eventi. Il form rimane in attesa finchè succede qualcosa e quando accade viene chiamata la funzione addetta a quell’evento, come ad esempio il click del mouse.
Le applicazioni grafiche seguono invece un processo di tipo iterativo. Solitamente ci sono 3 fasi:
- Creazione: tutte le risorse necessarie vengono caricate da file o create. Questa è la fase più lunga ma va eseguita solo una volta (o comunque un numero limitato di volte)
- Rendering: le risorse vengono usate per creare l’immagine a video. Questa fase si ripete per tutta la durata dell’applicazione a ritmi di decine e decine di volte al secondo (applicazioni molto semplici e giochi datati rispetto alla scheda video girano anche migliaia di volte al secondo).
- Distruzione: tutte le risorse inutili vengono eliminate per liberare la memoria e poter reiniziare un nuovo ciclo di rendering o chiudere l’applicazione.
Solitamente la fase di rendering è un loop continuo in cui l’applicazione chiama la grafica e tutte le altre logiche.
Con cosa uso DirectX10?
Le DirectX 10 sono al momento della scrittura di questo tutorial una prerogativa del C++. Nonostante io sia un grande estimatore della piattaforma .Net, occorrerà quindi passare a questo linguaggio ed utilizzare questo ambiente di sviluppo.
I tutorial saranno quindi scritti in C++ con Visual Studio .Net 2005 ma gireranno perfettamente anche sulla versione Express che ricordo è gratuita e si trova sul sito Microsoft.
Avrete bisogno anche di avere un computer DirectX 10 compatibile. Ciò significa avere Windows Vista (qualsiasi edizione) ed una scheda video DirectX10 compatibile.