********************************************************** ****************************************************************** ********************************************************************** ********* ********* ******** REAL TIME COMPUTER GRAPHICS 3D ******** ******* ******* ******* tECNICHE e aLGORITMI aPPLICATIVI ******* ******* pER lA rEALIZZAZIONE dI mOTORI ******* ******** iN gRAFICA 3d cOL cOMPUTER ******** ********* ********* ********************************************************************** ****************************************************************** ********************************************************** Realizzato da : -+- Cristiano Tagliamonte -+- Aceman/RamJam -+- Ultima revisione : 22 Luglio 1998 Versione : 0.02 ! ! _..-/\ |\___/| /\-.._ ./||||||\\. ||||||| .//||||||\. ./||||||||||\\|.. ||||||| ..|//|||||||||\. ./||||||||||||||\|||||||||||||||||/|||||||||||||\. ./||||||||||||||||||||||||||||||||||||||||||||||||||\. /||||||||||||||||||||||||||||||||||||||||||||||||||||||\ '||||||||||||||||||||||||||||||||||||||||||||||||||||||||' '||||' '|||||/' ''\|||||||||||||/'' '\||||||' '|||' |/' '\|/ \!|||||||!/ \|/' '\| V V \|||||/ V V ' ' \|||/ ' ' \./ V ========================================================================== ============================ pREFAZIONE ================================== ========================================================================== Questo breve testo vuol essere di aiuto a chi vuol intraprendere l'arduo ed affascinante cammino verso la programmazione di un motore grafico 3D, scoprendone inizialmente i concetti di base per poi affrontare i piu' complessi e spettacolari effetti realizzabili. Col termine "motore" si indica l'insieme di routine atte alla gestione e alla manipolazione di specifici dati che una volta elaborati nella maniera dovuta daranno come risultato la visualizzazione dell'ambiente 3D in tempo reale. Il seguente testo non vuol essere un corso di programmazione orientato alle applicazioni 3D, bensi' un'opera nella quale vengano forniti piu' semplicemente i concetti sui quali si fondano molti motori 3D. Il tutor e' stato suddiviso in parti, in cui ogni parte e' composta da capitoli tra loro indipendenti, ovvero (esclusi alcuni casi in cui certi argomenti sono legati tra loro intrinsecamente) la comprensione di un argomento citato in un determinato capitolo non dipende dagli altri capitoli. In questo modo si rende piu' efficace ed immediata la consultazione del presente testo. Naturalmente un capitolo potrebbe richiedere la conoscenza di particolari argomenti, pertanto all'inizio di ognuno di essi vengono elencati eventuali titoli dei capitoli in cui vengono fornite le informazioni necessarie affinche' sia possibile comprendere appieno gli argomenti trattati. Inoltre, nella lista degli argomenti necessari alla comprensione, potranno essere presenti eventuali sigle "n.p." atte ad indicare che tali argomenti, a causa della loro natura, non sono presenti nel seguente testo. Nel caso il lettore non abbia le dovute conoscenze del linguaggio C si raccomanda di porgere priorita' alla consultazione dell'Appendice A, nella quale vengono forniti chiarimenti riguardo la sintassi dello pseudocodice utilizzato, rispresa appunto dal C. Per concludere si raccomanda la conoscenza di base della trigonometria, dell'algebra lineare e di un linguaggio di programmazione (meglio se non troppo evoluto). ========================================================================== ============================= iNDICE ===================================== ========================================================================== Parte 0 : Cenni di grafica 2D relativa agli 80x86 con VGA Introduzione ............................................. 0.0 Apertura di uno schermo 320*200 a 256 colori ............. 0.1 Organizzazione della memoria video ....................... 0.2 Tracciamento di un pixel ................................. 0.3 Settare la palette di colori ............................. 0.4 Sincronismo del processore col refresh video ............. 0.5 Parte 1 : Geometry engine Introduzione ............................................. 1.0 Trasformazioni prospettiche .............................. 1.1 Concetto di prospettiva ............................... 1.1.0 Implementazione della prospettiva ..................... 1.1.1 Traslazione .............................................. 1.2 Applicazione delle traslazioni ........................ 1.2.0 Trasformazioni in scala .................................. 1.3 Principio Algoritmo Realizzazione Rotazione ................................................ 1.4 Concetto di rotazione ................................. 1.4.0 Rotazione intorno l'origine del sistema Rotazione intorno un'asse arbitrario Ottimizzazioni Affine transformations Principio Algoritmo Realizzazione Gestione delle telecamere in una scena 3D Principio Algoritmo Realizzazione Gerarchia degli oggetti Principio Algoritmo Realizzazione Parte 2 : Hidden surface removal Introduzione ............................................. 2.0 Back face culling Principio Realizzazione Algoritmo del pittore Algoritmi di ordinamento Radix sort Byte sort Z-Buffer Clipping 2D Principio Clipping di una linea Algoritmo per interpolazione Algortimo di Liang-Barsky Clipping di un poligono Algoritmo di Sutherland-Hodgman Algoritmo di Liang-Barsky Ray casting Principio Algoritmo Realizzazione Binary space partition Principio BSP trees 2D Algoritmo Realizzazione BSP trees 3D Algoritmo Realizzazione Parte 3 : Tecniche di rendering Introduzione ............................................. 3.0 Wireframe Principio Tracciamento di una linea Algoritmo di Bresenham Algoritmo per interpolazione Filled vector Principio Algoritmo Realizzazione Ottimizzazioni Metodi di ombreggiatura Calcolo della luminosita' Algoritmo Realizzazione Ottimizzazioni Ambient shading Flat shading Principio Algoritmo Realizzazione Depth cueing Principio Algoritmo Realizzazione Gouraud shading Principio Algoritmo Realizzazione Phong shading Principio Algoritmo Realizzazione Metal shading Modello di illuminazione di Phong Metodi di mappatura delle texture Affine texture mapping Principio Algoritmo Realizzazione Free direction texture mapping Principio Algoritmo Realizzazione Texture mapping bilineare Principio Algoritmo Realizzazione Texture mapping biquadratico Principio Algoritmo Realizzazione Correzione prospettica Principio Algoritmo Realizzazione Reflection mapping Principio Algoritmo Realizzazione Phong mapping Environment mapping Tecniche avanzate e ibride Principio Texture + Flat Texture + Depth Texture + Gouraud Texture + Metal Texture trasparenti Bump mapping Principio Algoritmo Realizzazione Parte 4 : Effetti Speciali Introduzione Morphing dell'oggetto Principio Algoritmo Realizzazione Antialiasing Principio Motion blur Algoritmo Realizzazione Lens flare Principio Algoritmo Realizzazione Parte 5 : Appendici A: Caratteristiche dello pseudocodice utilizzato B: Rendering pipeline C: Notazione in virgola fissa D: Coordinate polari E: Gestione degli oggetti F: Interpolazione lineare G: Cenni di algebra lineare .............................. G.0 I vettori ............................................. G.1 Definizione di vettore ............................. G.1.0 Operazioni tra vettori ............................. G.1.1 Vettore unitario ................................... G.1.2 Vettore normale .................................... G.1.3 Le matrici ............................................ G.2 Definizione ........................................ G.2.0 Operazioni tra matrici ............................. G.2.1 H: Tecniche di wrapping/mapping della texture............. H.0 Mapping normalizzato .................................. H.1 Mapping planare ....................................... H.2 Mapping sferico ....................................... H.3 Note finali ========================================================================== ======== pARTE 0 : cENNI dI gRAFICA 2d rELATIVA aGLI 80X86 e vga ========= ========================================================================== Argomenti necessari alla comprensione: assembly 80x86 (n.p.) ***** 0.0 iNTRODUZIONE In questa sezione non approfondiremo l'utilizzo delle VGA e SVGA, bensi' tratteremo le piu' elementari funzioni atte alla manipolazione di strutture bidimensionali, indispensabili per poter realizzare progetti in computer grafica 3D. In altre parole verrano date le nozioni minime per poter comprendere ed applicare i concetti sulla computer grafica 3D, senza esporre in maniera peculiare le caratteristiche delle VGA/SVGA, il cui studio richiederebbe una documentazione dedicata, che non e' l'obiettivo di questo tutor. A tal proposito verra' analizzato il solo modo video 320*200 a 256 colori, il piu' immediato da gestire. Tutti gli argomenti presentati in questa sezione richiedono espressamente la conoscienza dell'Assembly relativo agli 80x86 presumendo di lavorare in modalita' protetta 32 bit con un modello di memoria di tipo flat. ***** 0.1 aPERTURA dI uNO sCHERMO 320*200 A 256 cOLORI Analizziamo questa manciata di istruzioni assembly: mov eax,13h ; imposta EAX al cui valore corrisponde ; la modalita' video da aprire. Nel nostro ; caso 13h ci permette l'apertura di uno ; schermo 320*200 a 256 colori. int 10h ; interrupt per la gestione della grafica. Come possiamo vedere e' molto semplice utilizzare una risoluzione 320*200 a 256 colori: basta eseguire giusto queste due istruzioni assembly e tale modo video viene aperto! Nel caso volessimo ripristinare la vecchia modalita' testo (standard del dos) non dovremo far altro che seguire queste istruzioni: mov eax,03h ; imposta EAX al cui valore corrisponde ; la modalita' video da aprire. Nel nostro ; caso 03h ci permette l'apertura di uno ; schermo in modalita' testo 80*25. int 10h ; interrupt per la gestione della grafica. Questa coppia di istruzioni dovranno essere eseguite appena prima dell'uscita all' MS-DOS del nostro programma. ***** 0.2 oRGANIZZAZIONE dELLA mEMORIA vIDEO Uno schermo 320*200 a 256 colori ha come indirizzo fisico di partenza 000A0000h. A partire da tale indirizzo ogni byte rappresentera' un pixel visualizzato su schermo e di conseguenza, dato che un byte puo' coprire un range di valori da 0 a 255, potremo utilizzare al massimo 256 colori. Il byte corrispondente l'indirizzo 000A0000h e' il pixel estremo superiore a sinistra, mentre ai relativi pixel adiacenti corrisponderanno i byte immediatamente successivi a all'indirizzo 000A0000h. Il 321esimo byte (a partire da 000A0000h, quindi 000A0140h) risulta il pixel estremo sinistro della seconda riga. Vediamo un semplice schema per chiarire le idee: +---- 000A0001h . = ipotetico pixel sul monitor |+--- 000A0002h ||+-- 000A0003h vvv 000A0000h -> ............................ <- 000A013Fh 000A0140h -> ............................ <- 000A027Fh 000A0280h -> ............................ ............................ ............................ ............................ ............................ ............................ ............................ ............................ ............................ ............................ ............................ ............................ ............................ ............................ 000AF8C0h -> ............................ <- 000AF987h E' possibile notare che l'area di memoria interessata alla visualizzazione dei pixel e' compresa tra 000A0000 e 000AF987, che corrisponde a 64000 byte (difatti 320*200=64000). ***** 0.3 tRACCIAMENTO dI uN pIXEL Per visualizzare un pixel conoscendone le coordinate X e Y dovremo calcolare la giusta locazione di memoria su cui e' posto in memoria. Per far cio' occorrera' utilizzare una legge matematica che a partire dalle coordinate X e Y ricavi l'indirizzo effettivo del pixel, ovvero: x <- coordinata x del pixel (range: da 0 a 319) y <- coordinata y del pixel (range: da 0 a 199) destaddr <- indirizzo fisico effettivo del pixel destaddr = 000A0000 + 320*y + x La formula appena vista permette si' un corretto calcolo dell'indirizzo fisico del pixel, ma non la si puo' propriamente definire ottimizzata in quanto e' presente una moltiplicazione; cerchiamo di sostituirla con qualche operazione meno pesante: 320*y = (256 + 64)*y = 256*y + 64*y = y<<8 + y<<6 destaddr = 000A0000 + y<<8 + y<<6 + x Abbiamo eliminato la moltiplicazione utilizzando due snelli shift a sinistra (256 e 64 sono potenze di 2) che rendono il nostro codice sicuramente piu' efficiente. ***** 0.4 sETTARE lA pALETTE dEI cOLORI Abbiamo gia' detto che ogni byte rappresenta il colore di un pixel, tale byte si definisce "chunky pixel". Questo byte non e' altro che un indice di una tabella dove sono indicati i 256 colori da utilizzare. Infatti l'insieme dei colori esistenti nella nostra risoluzione (320*200) sono 262144, di cui possiamo sceglierne 256 da poter visualizzare. Analizziamo dove e come definire tale tabella. Ogni colore idealmente realizzabile puo' essere scomposto da tre componenti: il rosso, il verde e il blu. Mescolando questi tre colori secondo intensita' differenti e' possibile ottenere tutti i colori rappresentabili dalla nostra VGA. Nel nostro caso, per dimensionare un qualsiasi colore, definiremo le relative componenti R (red), G (green) e B (blue). L'insieme delle componenti RGB rappresenta il colore che vogliamo ottenere. Una componente R, G o B non e' altro che un byte in cui vengono presi in considerazione i 6 bit meno significativi; in altre parole per ogni componente abbiamo a disposizione un range di valori compreso tra 0 e 63, ovvero 64 combinazioni (infatti 64*64*64=262144 ovvero l'intera gamma di colori a disposizione). Un valore basso indica una bassa intensita' (tonalita' scura), al contrario piu' il valore sara' alto e maggiore sara' l'intensita' di quella componente (tonalita' chiara). Per settare la palette (ovvero la tavolozza di colori che si vuol utilizzare), ci serviranno solo le due porte hardware di indirizzo 03C8h e 03C9h. Ecco la procedura da seguire per settare un colore: - scrivere nella porta 03C8h un byte, il quale indichera' quale colore andremo a settare (0 e' il primo colore, 255 e' l'ultimo); - scrivere nella porta 03C9h il byte contenente la componente R (rosso); - scrivere nella porta 03C9h il byte contenente la componente G (verde); - scrivere nella porta 03C9h il byte contenente la componente B (blu). Da notare che una volta settato un colore si potra' continuare a scrivere nella porta 03C9h i successivi colori senza dover memorizzare (di volta in volta) il numero del colore successivo nella porta 03C8h. Tutto cio' tradotto in assembly porta ad un codice che puo' essere scritto nella seguente forma: lea esi,[palette] ; puntatore alla tabella dei colori mov edx,03C8h ; porta 03C8h xor eax,eax ; puliamo eax out dx,al ; iniziamo a settare il primo colore inc edx ; porta 03C9h mov ecx,256 ; numero di iterazioni del loop @@loop: mov al,[esi] ; leggiamo la componente Red... out dx,al ; ... e la memorizziamo mov al,[esi+1] ; leggiamo la componente Green... out dx,al ; ... e la memorizziamo mov al,[esi+2] ; leggiamo la componente Blue... out dx,al ; ... e la memorizziamo add esi,3 ; prossimo colore da settare dec ecx jnz @@loop ... ... ; altro codice ... palette: ; la tabella dei colori db 00, 00, 00 ; colore 00h : R, G, B db 11, 23, 45 ; colore 01h : R, G, B ... ... ; altri colori della tavolozza ... db 63, 63, 63 ; colore FFh : R, G, B Il sorgente appena esaminato non fa altro che leggere i byte nella tabella dei colori a partire dall'indirizzo 'palette' e li scrive nella porta 03C9h non prima di aver precisato nella porta 03C8h che intendiamo iniziare a settare la palette a partire dal colore 00h, fino ad arrivare all'ultimo, ovvero il colore FFh. ***** 0.5 sINCRONISMO dEL pROCESSORE cOL rEFRESH vIDEO Utilizzando una risoluzione come la 320*200 a 256 colori, lo schermo viene aggiornato 70 volte al secondo (70 Hz). In altre parole quel che si visualizza sul monitor viene tracciato 70 volte al secondo, cio' permette l'illusione di realizzare animazioni fluide. Il refresh non e' che il procedimento fisico per cui lo schermo viene aggiornato (illuminando in maniera opportuna ogni pixel). Mettiamo caso di voler realizzare l'animazione di un oggetto 3D renderizzato in tempo reale. Per rendere il movimento di tale oggetto piu' fluido possibile dovremo calcolare un intero frame in un 70esimo di secondo. Consideriamo che le routine che abbiamo realizzato siano abbastanza ottimizzate da permettere che l'elaboratore calcoli oltre 70 fotogrammi per secondo. In questo caso dovremo scrivere una routine che, una volta renderizzato un frame, permetta al processore di attendere il termine del refresh video prima di calcolare il prossimo fotogramma. Questo e' cio' che si intende con "sincronizzazione del processore col refresh video". Ora vediamo come realizzare il tutto in Assembly evitando di analizzare dettagliatamente il codice: mov edx,3DAh @@wait_refresh1: in al,dx test al,8 jz @@wait_refresh1 @@wait_refresh2: in al,dx test al,8 jnz @@wait_refresh2 In questo spezzone di codice il processore non fa altro che attendere (grazie al primo loop) che il pennello elettronico (che aggiorna lo schermo) giunga ad una ben determinata linea video, quindi (nel secondo loop) si aspetta che tale linea venga completamente tracciata. Questa tecnica viene applicata per evitare di calcolare un nuovo fotogramma ancor prima di averlo visualizzato. ========================================================================== ==================== pARTE 1 : gEOMETRY eNGINE =========================== ========================================================================== ***** 1.0 iNTRODUZIONE In questa sezione studieremo la geometria che si cela dietro la computer grafica 3D, in quanto ogni elemento elaborato da un motore 3D consiste in un insieme di valori numerici che attribuiscono una o piu' proprieta' geometriche, quindi il movimento compiuto da un solido proiettato su monitor e' frutto di opportune trasformazioni geometriche. ***** 1.1 tRASFORMAZIONI pROSPETTICHE Argomenti necessari alla comprensione: i vettori [G.1] ***** 1.1.0 cONCETTO dI pROSPETTIVA Riguardo le nostre applicazioni (relative alla realizzazione di un motore 3d), alla base della prospettiva risiede il principio con cui una qualsiasi entita' (punto, linea, poligono o oggetto che sia) da uno spazio tridimensionale possa essere rappresentata in uno spazio a due dimensioni. Difatti uno dei primissimi problemi che intercorre nella scrittura di un engine 3d e' come visualizzare tali entita' su uno spazio 2d, ossia il monitor, quando all'origine ogni cosa viene rappresentata matematicamente in uno spazio 3d. Per far fronte a questa esigenza viene in aiuto la prospettiva che ci permette si applicare la "conversione" da uno spazio 3d ad uno spazio 2d. Innanzitutto va sottolineato che lo studio di trasformazioni prospettiche sara' limitato ai soli vettori. Infatti ogni entita' e' derivabile da un insieme ordinato di uno o piu' vettori (un punto e' rappresentabile con un vettore; una linea si puo' rappresentare matematicamente come quella coppia di vettori coincidenti con gli estremi della linea stessa; un poligono e' una sequenza ordinata di vertici, i quali possono essere indicati con vettori; infine un oggetto e' un insieme ben definito di poligoni). Dati un punto di vista e un piano di proiezione perpendicolare alla direzione di osservazione e posto di fronte al suddetto punto vista, il concetto di prospettiva e' di tracciare un raggio passante sul punto di vista e sul vettore 3d di cui si vuol conoscere la proiezione sul piano (questo avviene esclusivamente a livello teorico, a livello pratico non si traccia nessun raggio!); quindi il vettore coincidente con il punto di intersezione tra il raggio ed il piano, rappresentera' la proiezione di quel vettore. Dato un ipotetico punto di vista e un piano di proiezione (rispetto ai quali dovremo ricavare le coordinate 2d), consideriamo che la direzione di osservazione sia perpendicolare al nostro piano e che tale piano coincida con il piano XY del nostro spazio 3d (ovvero il piano di equazione z = 0). Inoltre la retta coincidente con la direzione di osservazione passa per l'origine dei tre assi del nostro sistema di riferimento XYZ. Questo implica che tale retta coincide con l'asse z, sul quale sara' quindi posto il punto di vista. Il monitor e' inteso come quella parte (ovvero quel sottospazio) di piano visibile. Vediamo praticamente la nostra situazione: + monitor +----------+ monitor asse Z | _ | | <---------| - - - - - ¢_> | . | | occhio | [0,0,0] | + +----------+ Questa sitazione e' molto importante, in quanto permette di semplificare concettualmente e algoritmicamente le trasformazioni prospettiche. ***** 1.1.1 iMPLEMENTAZIONE dELLA pROSPETTIVA Abbiamo un vettore di cui conosciamo le coordinate [x, y, z]. Il nostro obiettivo consiste nel calcolare le coordinate [xs, ys] proiettate sullo schermo, quindi dobbiamo applicare una trasformazione prospettica. Consideriamo un punto, denominato A, nello spazio visibile dal video. Facciamo finta che il nostro monitor sia trasparente e che quel punto sia davvero presente nello spazio. Ora, la nostra situazione vista lateralmente e' la seguente: A. | |-_ | | -_ | | -_|A' | +_ | | -_ | | -_ | | -_ _ /______|______|_______- ¢_> (occhio) \ z B |B' C | |y v Mentre questo e' il caso analogo visto dall'alto: ^ |x A. | |-_ | | -_ | | -_|A' | +_ | | -_ | | -_ | | -_ _ /______|______|_______- ¢_> (occhio) \ z B |B' C | A = vettore nello spazio [x, y, z] rappresentante il punto a cui si vuol applicare la trasformazione prospettica; B = vettore avente la stessa coordinata z del vettore A, ma con con coordinate x e y uguali a zero, e' del tipo [0, 0, z]; C = vettore coincidente con il punto vista, per comodita' e' posto sull'asse z (quindi ha le coordinate x,y = 0); A' = vettore coincidente con la proiezione su video del vettore A, e' della forma [xs, ys, 0]; B' = vettore avente la stessa coordinata z del vettore A', ma con con coordinate x e y uguali a zero, e' esattamente [0, 0, 0], cio' significa che B' coincide con l'origine degli assi; Si ricorda, come precedentemente definito, che il piano di proiezione e' quello di equazione z = 0, ovvero il piano XY, su cui coincide, appunto, il monitor. Quindi sara' chiaro perche' le coordinate [xs, ys] sono esattamente le coordinate 2d rappresentanti il nostro vettore 3d su schermo. Vediamo adesso come ricavare queste coordinate. Sia nella vista dall'alto che in quella laterale i triangoli ABC e A'B'C sono simili in quanto hanno un angolo in comune ed entrambi hanno un angolo retto, quindi possiamo scrivere la seguente proporzione: A'B' / AB = B'C / BC che corrisponde a: A'B' = AB * B'C / BC Possiamo scrivere BC come BB'+B'C. Dato che B' coincide con l'origine, la distanza BB' equivale alla coordinata z del vettore B. Inoltre definiamo "d" come la lunghezza del lato B'C. La nostra formula si traduce nella seguente: A'B' = d * AB / (z + d) Rispetto alla vista laterale AB coincide con la coordinata y di A, mentre A'B' coincide con ys; sostituendo questi valori otteniamo proprio la formula per calcolare la y proiettata su schermo: ys = d * y / (z + d) Nel caso della vista dall'alto il discorso e' analogo, ma relativo alla coordinata x proiettata: xs = d * x / (z + d) Il valore d rappresenta la distanza tra il punto di vista e l'origine; e come tale puo' essere fissato arbitrariamente. Un buon valore con cui istanziare d e', ad esempio, 256. Bisogna tener conto che nel video le coordinate hanno come origine il punto in alto a sinistra, mentre per la trasformazione 3d->2d sono state considerate al centro del monitor. Per ovviare questo problema basta sommare, al termine dei calcoli per la proiezione, una costante a xs e a ys grazie alle quali i relativi assi verranno traslati di un numero di pixel equivalente alla costante. Riassumendo, ponendo d = 256, le coordinate proiettate sullo schermo sono equivalenti a: xc = larghezza schermo/2, per traslare l'ascissa a destra yc = altezza schermo/2, per traslare l'ordinata in basso xs = 256 * x / (z + 256) + xc ys = 256 * y / (z + 256) + yc Attenzione al fatto che la coordinata z del punto non puo' coincidere con il punto di vista dato che si verificherebbe una divisione per zero. Nemmeno si deve porre la profondita' di un punto inferiore a quella del punto di vista: sarebbe assurdo poter visualizzare punti posti dietro l'osservatore! Finalmente siamo in grado di svolgere trasformazioni prospettiche, che tuttavia risultano veramente semplici da applicare. ***** 1.2 tRASLAZIONE Argomenti necessari alla comprensione: i vettori [G.1] ***** 1.2.0 aPPLICAZIONE dELLE tRASLAZIONI Dato un vettore V che rappresenta un punto nello spazio, traslare V rispetto ad un vettore T significa trovare un ulteriore vettore, chiamiamolo W, che rappresenta il vettore V spostato, in direzione coincidente con il verso di T, di uno spazio equivalente alla lunghezza di T. Tutto cio' si traduce in una banalissima somma tra vettori, piu' precisamente tra il vettore V e T, quindi: V[xv, yv, zv] = vettore da traslare T[tx, ty, tz] = vettore coincidente la traslazione da applicare W[xw, yw, zw] = vettore coincidente V traslato rispetto a T W = V + T xw = xv + tx yw = yv + ty zw = zv + tz ***** 1.3 tRASFORMAZIONI iN sCALA Argomenti necessari alla comprensione: i vettori [G.1] ***** 1.3.0 aPPLICAZIONE dELLE tRASFORMAZIONI iN sCALA ***** 1.4 rOTAZIONE Argomenti necessari alla comprensione: i vettori [G.1], le matrici [G.2] ***** 1.4.0 cONCETTO dI rOTAZIONE Consideriamo un orologio analogico, le sue lancette ruotano intorno l'asse passante per il centro dell'orologio e perpendicolare l'orologio stesso: \ \ ______________ X____________ / //\ 12 // orologio // \ / // //9 \/___ 3// // // // // // 6 // //___________/X /_____________/ \ asse immaginario su cui \ ruotano le lancette Tecnicamente parliamo di rotazione di un punto su di un asse, quando il punto si muove sul piano appartenente al punto stesso e perpendicolare all'asse in modo da non alterare la distanza punto-asse (che rimane costante). In questo modo il punto compie un movimento circolare intorno l'asse, ossia ci ruota intorno, e la distanza punto-asse funge da raggio per la circonferenza descritta dalla rotazione del punto in questione. La rotazione intorno l'asse y si puo' immaginare come la traiettoria percorsa da un punto che giri intorno l'asse y senza modificare la propria ordinata, che rimane appunto inalterata. ***** 1.4.1 rOTAZIONE 2D ***** 1.4.2 rOTAZIONE 3D ========================================================================== ========================= pARTE 5 : aPPENDICI ============================ ========================================================================== ****************\ *****************> Appendice D : coordinate polari ****************/ ***** D.0 iNTRODUZIONE Le coordinate polari vengono utilizzate in questo tutor al fine di comprendere il principio che sta alla base dell'applicazione delle rotazioni. ****************\ *****************> Appendice G : cenni di algebra lineare ****************/ ***** G.0 iNTRODUZIONE La seguente appendice e' orientata a tutti coloro che non possiedono le nozioni di algebra lineare necessarie alla comprensione del testo ed in particolare della parte 1, relativa al geometry engine. ***** G.1 i vETTORI Argomenti necessari alla comprensione: cenni di geometria piana (n.p.) ***** G.1.0 dEFINIZIONE dI vETTORE Un vettore non e' altro che un oggetto matematico che rappresena una quantita' di valore con una direzione e un verso, o in termini spicci una linea. Nel contesto verranno sempre specificati vettori che vanno da un punto di coordinate [0, 0, 0] (l'origine degli assi) verso un altro punto [x, y, z], cosi' facendo si puo' affermare che questo vettore ha come quantita' [x, y, z]. _ | /| | / [0,0,0]|/ x ----+------------> /|\ z / | \ | \ | \ | \. V y| v Un vettore viene indicato con una lettera, ad esempio nella precedente figura e' rappresentato il vettore V[x, y, z]. Il sistema di riferimento usato e' dato da tre variabili, immaginabile come un semplice piano cartesiano perpendicolare all'asse z. Per semplicita' consideriamo la crescita della y verso il basso (e non verso l'alto) in modo da ridurre i calcoli da svolgere per la visualizzazione di un punto sullo schermo. L'asse z cresce quando esso si allontana dall'osservatore, mentre diminuisce nel caso si avvicini all'osservatore. Utilizzeremo i vettori per definire ogni punto nello spazio (e in particolare i vertici di ogni oggetto 3d), cio' non significa in termini pratici che un vettore costituisca una linea visualizzata su schermo, bensi' indica il segmento immaginario compreso tra l'origine degli assi ed un punto nello spazio. ***** G.1.1 oPERAZIONI tRA vETTORI Innanzitutto va specificato che faremo uso fondamentalmente di vettori 2d e 3d in quanto il nostro motore gestira' punti nello spazio piano (per il tracciamento di poligoni su monitor) e tridimensionale (per eventuali trasformazioni geometriche), quindi vettori del tipo: v2 = [ x , y ] <--- generico vettore 2d v3 = [ x , y , z ] <--- generico vettore 3d Tutte le operazioni tra vettori sono possibili solo tra vettori che hanno la stessa dimensione, quindi una qualsiasi operazione tra un vettore 2d ed uno 3d sono impossibili. Di seguito vediamo algebricamente quali sono queste operazioni: - ADDIZIONE: il risultato della somma di due vettori e' ancora un vettore della stessa dimensione dei due operandi: v1 = [ x1 , y1 , z1 ] v2 = [ x2 , y2 , z2 ] v3 = v1 + v2 = [ x1+x2 , y1+y2 , z1+z2 ] dove nel nostro caso x1, x2, y1, y2, z1 e z2 sono ovviamente numeri reali (che possono essere rappresentati come numeri in virgola fissa oppure preferibilmente come numeri in virgola mobile). Da notare che il vettore [0, 0, 0] coincide con l'operando nullo di questa operazione, ovvero addizionando tale vettore ad un qualsiasi vettore V otteremo come risultato sempre il vettore V. Inoltre tale operazione gode della proprieta' commutativa. Fisicamente e' possibile immaginare la somma di due vettori 2d nella seguente maniera: v1 = [ x1 , y1 ] v2 = [ x2 , y2 ] Ora immaginiamo di trovarci sul vettore v1. Quindi il risultato della somma v1+v2 si puo' intendere come il risultato dello spostamento (dalla posizione indicata da v1) di x2 posizioni orizzontali e y2 posizioni verticali, queto implica la nostra posizione finale coincidera' col vettore [x1+x2 , y1+y2], ovvero il risultato dell'addizione. - NEGAZIONE: il risultato della negazione di un vettore v1 e' ancora un vettore della stessa dimensione di v1 e che e' equidistante dall'origine rispetto a v1, ma e' di verso opposto (in altre parole e' simmetrico rispetto alla retta passante per l'origine, la quale e' perpendicolare al vettore v1): v1 = [ x , y , z ] v2 = - v1 = [ -x , -y , -z ] Anche in questo caso l'elemento nullo di questa operazione coincide con il vettore dell'origine [0, 0, 0]. - PRODOTTO CON UN FATTORE: il prodotto tra un vettore v1 ed un fattore (inteso come un numero reale o in virgola mobile) e' ancora un vettore delle stesse dimensioni di v1: v1 = [ x , y , z ] k = numero reale v2 = k * v1 = [ k*x , k*y , k*z ] Il fattore k viene chiamato 'scalare' (da non confondere con il prodotto scalare!), ed il vettore risultante v2 conservera' lo stesso verso di v1 ma la sua dimensione verra' scalata rispetto al valore di k. Questa operazione e' commutativa. - PRODOTTO SCALARE: il risultato prodotto scalare tra due vettori e' uno scalare (quindi un numero reale o in virgola mobile): v1 = [ x1 , y1 , z1 ] v2 = [ x2 , y2 , z2 ] k = v1 * v2 = x1*x2 + y1*y2 + z1*z2 Geometricamente il prodotto scalare puo' essere definito come il prodotto tra le distanze dei due vettori con l'origine ed il coseno dell'angolo compreso tra i due vettori. Anche questa operazione gode della proprieta' commutativa. - PRODOTTO IN CROCE: il risultato del prodotto in croce tra due vettori e' uno scalare (quindi un numero reale o in virgola mobile): v1 = [ x1 , y1 ] v2 = [ x2 , y2 ] k = v1 x v2 = x1*y2 - x2*y1 Per semplicita' citiamo solo la formula del prodotto in croce tra due vettori 2d dato che utilizzeremo questa operazione solo su tali vettori. Il prodotto in croce non gode della proprieta' commutativa. ***** G.1.2 vETTORE uNITARIO Un vettore unitario e' un vettore cui lunghezza pari all'unita'. La lunghezza di un vettore e' data dalla distanza del punto di coordinate specificate dal vettore stesso con l'origine. La lunghezza di un generico vettore si ricava dal teorema di Pitagora svolgendo la radice quadrata della sommatoria dei quadrati delle componenti del vettore, piu' semplicemente: v2 = [ x2 , y2 ] v3 = [ x3 , y3 , z3 ] l2 = |v2| = sqrt(x2*x2 + y2*y2) l3 = |v3| = sqrt(x3*x3 + y3*y3 + z3*z3) Rendere un generico vettore v1 come vettore unitario significa trovare quel vettore che conserva lo stesso verso di v1 ma che ha lunghezza pari ad 1. Nella pratica si traduce nel dividere ogni componente del vettore per la relativa lunghezza; in relazione al precedente esempio possiamo affermare: u2 = [ x2/l2 , y2/l2 ] u3 = [ x3/l3 , y3/l3 , z3/l3 ] Che coincide col moltiplicare il vettore per uno scalare equivalente all'inverso della lunghezza del vettore: v = [ x , y , z ] k = 1 / |v| = 1 / sqrt(x*x + y*y + z*z) u = k * v ***** G.1.3 vETTORE nORMALE Un vettore 3d normale su di un piano (quindi su uno spazio bidimensionale) significa che la retta passante per tale vettore e' perpendicolare a quel piano. Un vettore normale su una retta r significa che la retta passante per tale vettore e' perpendicolare alla retta r. Un vettore normale ad altro vettore implica che il primo vettore sia normale alla retta passante per il secondo vettore. Vediamo un esempio di vettore normale ad un altro vettore: v1. .v2 \ / \ / \ / \ / \./ \ \ \. v3 In questo caso v1 e v3 sono vettori normali rispetto a v2, mentre quest'ultimo e' vettore normale sia rispetto a v1 che a v3. Infatti i vettori v1 e v3 giaciono sulla stessa retta. ***** G.2 lE mATRICI Argomenti necessari alla comprensione: i vettori [G.1] ***** G.2.0 dEFINIZIONE dI mATRICE Una matrice e' una tabella di elementi (che nel nostro caso sono numeri razionali) del tipo: _ _ | | | a(1,1) a(1,2) ... a(1,m) | | a(2,1) a(2,2) ... a(2,m) | M(n,m) = | ... | | ... | | ... | | a(n,1) a(n,2) ... a(n,m) | |_ _| dove a(i,j) e' il generico elemento della matrice mentre n*m rappresenta la dimensione della matrice, in cui valgono le seguenti regole: n, m > 0 0 < i <= n 0 < j <= m n, m, i, j sono quindi numeri interi maggiori di zero. Il valore "n" e' il numero di righe della matrice, mentre "m" e' il numero di colonne. I valori "i" e "j" rappresentano rispettivamente gli indici di riga e di colonna di un elemento della matrice. Nel nostro caso intendiamo ogni elemento a(i,j) come un numero reale o un numero in virgola mobile. Per semplicita' si fara' spesso riferimento ad una matrice denotando solo la lettera che la etichetta. Ovvero spesso si utilizzera' spesso la notazione "M" al posto di "M(n,m)". Casi particolari di matrici: - MATRICE QUADRATA: M(n,n) il numero di righe e di colonne sono uguali. Esempio: _ _ | | | 1 2 3 | M(n,n) = | 4 5 6 | | 7 8 9 | |_ _| La DIAGONALE principale della matrice quadrata e' definita come quell'insieme di elementi della matrice stessa tali che gli indici di riga e colonna siano uguali, quindi: D = tutti gli a(i,i) per 0 < i <= n ovvero: D = { a(1,1) , a(2,2), ... , a(n,n) } nel precedente esempio la diagonale principale e' uguale a: D = { 1 , 5 , 9 } - MATRICE TRASPOSTA: Mt(n,n) data una matrice quadrata M(n,n), la sua trasposta Mt(n,n) e' quella matrice che contiene gli stessi elementi della M, ma con diverso ordine. Tale ordine e' determinato dagli indici di riga e colonna, i quali vengono scambiati. Piu' formalmente il generico elemento a(i,j) appartenente a M coincide con l'elemento t(i,j) di Mt tale che: t(i,j) = a(j,i) 0 < i <= n 0 < j <= n Per chiarire le idee vediamo un esempio pratico relativo ad una matrice di dimensione 3*3: _ _ _ _ | | | | | 1 2 3 | | 1 4 7 | M = | 4 5 6 | la sua trasposta e' Mt = | 2 5 8 | | 7 8 9 | | 3 6 9 | |_ _| |_ _| Da notare che gli elementi posti sulla diagonale principale rimangono nella posizione originaria. - MATRICE RIGA: M(1,m) una matrice riga e' coincide, a livello logico, con la definizione di vettore [G.1], quindi tale matrice e' intesa come una matrice con una sola riga, quindi nella forma: M(1,m) = [ a(1,1) a(1,2) ... a(1,m) ] o piu' semplicemente: M(m) = [ a(1) a(2) ... a(m) ] Le ultime due definizioni di matrice per righe sono logicamente equivalenti, la differenza sostanziale sta nel secondo caso, in cui non si fa specificatamente riferimento alla notazione matriciale, la quale implica che si faccia riferimento ad entrambi gli indici di riga e colonna (infatti abbiamo il solo indice di colonna). Etichettando gli elementi di una matrice per riga senza il relativo indice si ottiene un oggetto matematico equivalente alla matrice per riga che viene denominato ENNUPLA. Vediamone un esempio: E = ( a , b , c , d ) - MATRICE COLONNA: M(n,1) e' un modo alternativo (rispetto alla matrice riga) di rappresentare un vettore secondo la notazione matriciale, ovvero: _ _ | | | a(1,1) | | a(2,1) | M(n,1) = | ... | | ... | | ... | | a(n,1) | |_ _| o piu' semplicemente: _ _ | | | a(1) | | a(2) | M(n) = | ... | | ... | | ... | | a(n) | |_ _| - MATRICE NULLA: M(n,m) e' quella matrice in cui tutti gli elementi sono uguali a 0, ovvero: a(i,j) = 0 0 < i <= n 0 < j <= m - MATRICE IDENTITA': I(n) e' un particolare caso di matrice quadrata in cui tutti gli elementi sono uguali a 0 fatta esclusione per quegli elementi presenti sulla diagonale principale che sono uguali a 1. O in termini piu' formali: 0 < i <= n 0 < j <= n a(i,j) = 0 per i diverso da j a(i,j) = 1 per i uguale a j Faremo riferimento alla generica matrice identita' n*n con la notazione I(n) (basta specificare un solo indice in quanto la matrice identita' e' sempre quadrata, quindi l'indice di colonna e' sottointeso). Vediamo un esempio relativo alla matrice identita' 4*4: _ _ | | | 1 0 0 0 | I(4) = | 0 1 0 0 | | 0 0 1 0 | | 0 0 0 1 | |_ _| ***** G.2.1 oPERAZIONI tRA mATRICI In relazione all'obiettivo di realizzare un motore 3d possiamo anticipare che principalmente ci occuperemo di applicare operazioni tra matrici 3*3, 4*3 e 4*4. - SOMMA: la somma tra due matrici A e B e' applicabile se e solo se A e B hanno lo stesso numero di righe e colonne. Il risultato e' ancora una matrice, che per convenzione chiamiamo C, delle stesse dimensione degli operandi. Ogni elemento di C e' la somma del corrispondente elemento di A e di B (che quindi si trovano nella stessa posizione dell'elemento di C): A(n,m) = matrice cui generico elemento e' a(i,j) B(n,m) = matrice cui generico elemento e' b(i,j) C(n,m) = matrice cui generico elemento e' c(i,j) c(i,j) = a(i,j) + b(i,j) 0 < i <= n 0 < j <= m _ _ | | | a(1,1)+b(1,1) a(1,2)+b(1,2) ... a(1,m)+b(1,m) | | a(2,1)+b(2,1) a(2,2)+b(2,2) ... a(2,m)+b(2,m) | C = A + B = | ... | | ... | | ... | | a(n,1)+b(n,1) a(n,2)+b(n,2) ... a(n,m)+b(n,m) | |_ _| Questa operazione gode delle seguenti proprieta': commutativa => A + B = B + A associativa => (A + B) + C = A + (B + C) - NEGAZIONE: la negazione di una matrice A(n,m) e' una matrice di dimensione ancora n*m in cui ogni elemento di A e' di segno opposto: A(n,m) = matrice cui elementi sono a(i,j) B(n,m) = matrice cui elementi sono b(i,j) B = - A b(i,j) = - a(i,j) 0 < i <= n 0 < j <= m - PRODOTTO PER UNO SCALARE: svolgere il prodotto di una matrice A per uno scalare k significa moltiplicare ogni elemento di A con k, il quale non e' altro che un numero reale. La matrice cosi' ottenuta e' ovviamente delle stesse dimensioni della matrice originaria: k = numero reale A(n,m) = matrice cui elementi sono a(i,j) B(n,m) = matrice cui elementi sono b(i,j) B = k * A b(i,j) = k * a(i,j) 0 < i <= n 0 < j <= m - PRODOTTO TRA MATRICI: il prodotto tra due matrici A e B e' applicabile se il numero di colonne di A e' uguale al numero di righe di B. Il risultato sara' una matrice C con un numero di righe equivalente a quello di A ed un numero di colonne uguale al corrispondente di B. Ogni elemento c(i,j) di C coincide con il prodotto scalare dei due vettori intesi come la i-esima riga di A e la j-esima colonna di B: A(n,t) = matrice cui generico elemento e' a(i,k) B(t,m) = matrice cui generico elemento e' b(k,j) C(n,m) = matrice cui generico elemento e' c(i,j) C = A * B c(i,j) = a(i,1)*b(1,j) + a(i,2)*b(2,j) + ... + a(i,t)*b(t,j) 0 < i <= n 0 < j <= m che equivale a: c(i,j) = a(i,k)*b(k,j) 0 < k <= t 0 < i <= n 0 < j <= m Questa operazione gode della proprieta' associativa, ovvero: (A * B) * C = A * (B * C) che, come vedremo in seguito, risultera' particolarmente utile per la progettazione del geometry engine. Inoltre il prodotto NON gode della proprieta' commutativa. Da notare che l'elemento neutro di questa operazione e' esattamente la matrice identita': A(n,m) = I(n) * A(n,m) = A(n,m) * I(m) Inoltre il prodotto per la matrice identita' e' l'unico caso in cui vale la proprieta' commutativa. ========================================================================== =========================== nOTE fINALI ================================== ========================================================================== Spesso vagheggiano nella mia mente pensieri del tipo "Ne vale davvero la pena consumare tanto tempo per realizzare un motore 3d?", oppure "Ora c'e' da fare altro di piu' prioritario che starsene qui a scrivere questo engine...", dubbi che affliggono molte di quelle persone che si propongono di programmare un motore 3d, in particolare chi e' alle prime armi oppure chi ne ha avuto a che fare per molto (forse troppo) tempo. Tristo e' quel discepolo che non avanza il suo maestro. Leonardo. Se qualcuno e' interessato a ricevere gratuitamente l'ultima versione di questo file di testo o per comunicarmi eventuali errori puo' contattarmi per e-mail : acemann@tin.it su irc : #demo-ita #coders scrivendo a : Cristiano Tagliamonte Via Maddaloni, 12 Interno B21 00177 Roma Tel. 06-272815 Cristiano Tagliamonte Via Filippo Masci, 86/G 66100 Chieti Tel. 0871-347826 _____ _____ _____ __ __ _____ __ __ : _ Y _ Y _ Y " Y _ Y \ : .-----| _ | l__j ___| Y | _ | _ |---------------------------. |:::::| | | | | | | | | | | ::::: aCEmAN/rAmJaM ::::::| |.....| |__j.____j_____j l__j |__j |__j ::::: acemann@tin.it :::::| `-----`--' -------------`--' -`--' -`--' -----------------------------' ______ ______ ____ _____ /\_____\ /\_____\ /\___\/\____\ ___________ ____ / / __ \/ / __ \/ / \/ /\ / __ \ \/ \ / / /_/\ \/ /_/\ \/ / \/ /_/\ \ \ / / /__\/ / /__\/ / /\ \ \_\_\ \ \ / / ____/ / _ /\ \ \ \ _ \ / / __ \/ / ____ / /\// /__\_\ \ ____ \ \\/\ \ / / / \ \/ / / / / // / //\ \ \__/\ \ \\ \ \ \/__/ \_/__/ \/__/___/ \/___/ \ \________\__\ \ \__\___\\ \___\ \/________/__/ \/__/___/ \/___/ rOTAZIONE ========================================================================== Adesso vediamo realmente come effettuare rotazioni. Utilizzando come sistema di riferimento un piano a 2 dimensioni e' possibile convertire le coordinate cartesiane (x,y) di un punto in coordinate polari (r,t): _ V V(x,y)=V'(r,t) _ V' r=distanza y| /| | /| punto-origine | / r=sqrt(x*x+y*y) | r/ | / t=arctan(y/x) | / t=angolo compreso |/ |/) t tra il vettore +-------> x=r*cos(t) +-------> ed il semiasse x y=r*sin(t) positivo x Dopodiche' per ruotare il punto intorno l'origine si dovra' sommare l'angolo di cui si vuol ruotare il punto alla variabile t e convertire le risultanti coordinate polari in cartesiane. Questo metodo risulta troppo lento per applicazioni in real time in quanto sono presenti quadrati, arcotangenti e radici quadrate; inoltre introducendo la variabile z si andrebbe incontro ad una pesante gestione delle coordinate polari (utilizzo di integrali tripli, ecc.): meglio trovare una soluzione piu' semplice e veloce. Immaginiamo di avere un vettore con ordinata nulla V(x,0) e vogliamo ruotarlo di un angolo a: y vettore y _ Vr(xr,yr) vettore | coincidente | /| ruotato | l'ascissa | / di a | | / radianti | V(x,0) |/) a +----->-> +-------> x x Se volessimo trasformare in polari le coordinate di V risulterebbe r=x (x=sqr(x*x+0*0)) e t=0 (0=arctan(0/x). Per ruotare il vettore basta sommare a t l'angolo di rotazione a. Quindi: (V = generico vettore in coordinate cartesiane) (V' = generico vettore in coordinate polari) (Vr = vettore ruotato di a radianti in coordinate cartesiane) (Vr' = vettore ruotato di a radianti in coordinate polari) (Vxr = vettore ruotato con la sola componente x in coordinate cartesiane) (Vyr = vettore ruotato con la sola componente y in coordinate cartesiane) V(x,y) = V'(r,t) Vr(xr,yr) = Vr'(r,t+a) -> xr=r*cos(t+a) yr=r*sin(t+a) Andiamo a sostituire xr e yr con le relative formule: Vr(r*cos(t+a),r*sin(t+a)) -> r=x t=0 Andiamo a sostituire r con x e t con 0: Vxr(x*cos(a),x*sin(a)) E questa e' la formula per la rotazione di un vettore che non ha la componente y. Ora cosideriamo un vettore che ha l'ascissa nulla V(0,y): r=y (=sqr(0*0+y*y)) t=pi/2 (=arctan(y/0)=arctan(infinito)) Vyr(y*cos(pi/2+a),y*sin(pi/2+a)) Un paio di formule trigonometriche ci dicono: cos(pi/2+a)=-sin(a) sin(pi/2+a)=cos(pi/2) ma siccome utilizziamo un sistema di riferimento con l'asse y "rovesciato" dobbiamo cambiare un segno ad entrambe le formule per poterle sfruttare, che quindi diventano: cos(pi/2+a)=sin(a) sin(pi/2+a)=-cos(a) Adesso la formula per ruotare un vettore senza componente x e' uguale: Vyr(y*sin(a),-y*cos(a)) Ma se guardiamo al caso generale, abbiamo un vettore V che ha entrambe le componenti x e y. Difatti un generico vettore con con le componenti x e y e' uguale a: V1(x,0)+V2(0,y)=V(x+0,0+y)=V(x,y) Ora possiamo usare le formule di rotazione dei singoli casi per calcolare il caso generale con un'addizione tra vettori: Vxr(x*cos(a) ,x*sin(a) ) + Vyr( +y*sin(a), -y*cos(a)) = ------------------------------------------ Vr (x*cos(a)+y*sin(a),x*sin(a)-y*cos(a)) Grazie a questa formula e' possibile ruotare un qualsiasi vettore su uno spazio bidimensionale. In un ambiente 3D la formula appena descritta coincide con la rotazione intorno l'asse z (non c'e' nessuna coordinata z cambiata). Per ruotare il punto intorno un altro asse basta lasciar fuori la relativa variabile e utilizzare le altre nella precedente espressione, il che si puo' sintetizzare con: intorno l'asse z intorno l'asse y intorno l'asse x -------------------- -------------------- -------------------- xr=x*cos(a)+y*sin(a) xr=x*cos(a)+z*sin(a) yr=y*cos(a)+z*sin(a) yr=x*sin(a)-y*cos(a) zr=x*sin(a)-z*cos(a) zr=y*sin(a)-z*cos(a) oTTIMIZZAZIONE dELLE rOTAZIONI ========================================================================== Dato un angolo di rotazione per ogni asse (x,y e z) con le precedenti formule sarebbero occorse ben 12 moltiplicazioni per poter ruotare un solo punto. Qui vedremo come effettuare rotazioni eseguendo 9 moltiplicazioni per ogni punto. Consideriamo: ax = angolo di rotazione intorno l'asse x s1 = sin(ax) c1 = cos(ax) ay = angolo di rotazione intorno l'asse y s2 = sin(ay) c2 = cos(ay) az = angolo di rotazione intorno l'asse z s3 = sin(az) c3 = cos(az) Ognuna delle variabili x,y e z influenza le rotazioni intorno a due assi (nella rotazione intorno al proprio asse la variabile rimane inalterata), possiamo cosi' indicare con x' y' e z' le variabili parzialmente ruotate (cioe' dopo la prima rotazione) e con x'' y'' e z'' le variabili completamente ruotate. Detto cio' le formule viste in precedenza corrispondono a: x' = x*c1 + y*s1 y' = x*s1 - y*c1 x''= x'*c2 + z*s2 <- coordinata x ruotata completamente z' = x'*s2 - z*c2 y''= y'*c3 + z'*s3 <- coordinata y ruotata completamente z''= y'*s3 - z'*c3 <- coordinata z ruotata completamente che equivale a scrivere: x''= (x*c1+y*s1)*c2+z*s2=c2*c1 *x + c2*s1 *y + s2 *z y''= (x*s1-y*c1)*c3+((x*c1+y*s1)*s2-z*c2)*s3= c3*s1 *x - c3*c1 *y + s3*s2*c1 *x + s3*s2*s1 *y - s3*c2 *z= (s3*s2*c1+c3*s1) *x + (s3*s2*s1-c3*c1) *y + (-s3*c2) *z z''= (x*s1-y*c1)*s3-((x*c1+y*s1)*s2-z*c2)*c3= s3*s1 *x - s3*c1 *y - c3*s2*c1 *x - c3*s2*s1 *y + c3*c2 *z= (-c3*s2*c1+s3*s1) *x + (-c3*s2*s1-c3*c1) *y + (c3*c2) *z z''= (x*s1-y*c1)*s3-((x*c1+y*s1)*s2-z*c2)*c3= s3*s1 *x - s3*c1 *y - c3*s2*c1 *x - c3*s2*s1 *y + c3*c2 *z= (-c3*s2*c1+s3*s1) *x + (-c3*s2*s1-s3*c1) *y + (c3*c2) *z ^^^^^^^^^^^^^^^^^ zx ^^^^^^^^^^^^^^^^^ zy ^^^^^^^ zz Dall'ultimo passaggio di ognuna di queste formule si puo' notare che non vengono calcolate coordinate parzialmente ruotate e che ogni coordinata ruotata equivale alla somma delle variabili (non ruotate) moltiplicate per un determinato fattore. Se precalcoliamo questi fattori potremo utilizzarli per tutti quei punti che devono essere ruotati nella stessa direzione. In questo modo avremo svolto semplicemente 9 moltiplicazioni per ogni punto (esclusi i precalcoli per i fattori). In sostanza dobbiamo calcolare prima queste costanti: xx = c2*c1 xy = c2*s1 xz = s2 yx = c3*s1+s3*s2*c1 yy = -c3*c1+s3*s2*s1 yz = -s3*c2 zx = s3*s1-c3*s2*c1 = s2*c1+c3*s1 zy = -s3*c1-s3*s2*s1 = c3*c1-s2*s1 zz = c3*c2 poi per ogni punto si devono svolgere questi calcoli (sfruttando gli stessi fattori): x'' = xx * x + xy * y + xz * z y'' = yx * x + yy * y + yz * z z'' = zx * x + zy * y + zz * z Ed otterremo le tre coordinate ruotate. Questo algoritmo risulterebbe meno efficiente del precedente se si utilizzassero pochi punti, mentre nel caso di una elevata quantita' di vettori e' possibile raggiungere notevoli risparmi in termini di calcolo. wIREFRAME ========================================================================== Il wireframe e' la piu' semplice ed antica tecnica atta a riprodurre poligoni. Consiste semplicemente nel tracciare linee che uniscano i vertici del poligono da rappresentare, nient'altro. Il tracciamento delle linee dovra' svolgersi sfruttando le coordinate proiettate dei punti (e non quelle con tre variabili xyz). Vediamo quindi come tracciare linee nel caso si stia programmando in un linguaggio che non permetta direttamente di svolgere questa funzione (come il C e l'Assembly). Analizziamo l'algoritmo di Bresenham. Abbiamo due punti P1(x1,y1) e P2(x2,y2) e vogliamo visualizzarne la linea che possa unirli: P1(x1,y1) .------______ ^ ------______ P2(x2,y2) | dy ------______. v dx <------------------------------------> consideriamo: x2 > x1 y2 > y1 dx = x2-x1 dy = y2-y1 dx > dy Tutti gli altri tipi di linee sono derivarivabili da questo tipo. Dopodiche' andiamo a calcolare questi valori: xl = x1 -> ascissa attuale del punto yl = y1 -> ordinata attuale del punto d = 2*dx-dy -> variabile di decisione d1 = 2*dy -> incremento di d (se d<0) d2 = 2*(dy-dx) -> incremento di d (se d=>0) Finalmente vediamo l'algoritmo vero e proprio: > loop di dx iterazioni > visualizza pixel alla posizione (xl,yl) > xl=xl+1 > se d<0 allora: > d=d+d1 > altrimenti: > d=d+d2 > yl=yl+1 > prossima iterazione Una linea e' formata da un insieme di pixel, nel nostro caso il numero di pixel che compone la linea equivale a dx, quindi dobbiamo realizzare un loop che si ripete dx volte in cui per ogni iterazione bisogna visualizzare un punto. Ma quali coordinate dovra' avere questo punto? Indichiamo con xl e yl le coordinate del punto da proiettare su video, le quali inizialmente coincideranno con quelle di P1(x1,y1). Al termine di ogni iterazione andiamo ad incrementare xa, in questo modo uscendo dal ciclo xa coincidera' con x2 (perche' x2=x1+dx). Cosa succede invece alla ordinata del pixel? La incrementiamo semplicemente quando la variabile d e' positiva. E' possibile utilizzare anche un altro algoritmo per tracciare linee, il quale risulta spesso piu' efficiente di quello di Bresenham (soprattutto se realizzato in Assembly) che sfrutta il principio di interpolazione lineare, lo vedremo piu' dettagliatamente nel paragrafo relativo al fill e scan line. hIDDEN fACE ========================================================================== Hidden face significa faccia nascosta, in questo paragrafo vedremo come eliminarla. Difatti nella realta' in un solido non trasparente non sono visibili tutte le facce per ovvi motivi. Visualizzare sul monitor solo le facce visibili e' sicuramente piu' realistico rispetto a tracciarle tutte. Il piu' semplice e intuitivo algoritmo e' quello del "pittore". Consiste nell'ordinare le facce che compongono l'oggetto in base alla componente z. In seguito si devono disegnare le facce partendo da quella piu' lontana sino a quella piu' vicina; in questo modo le facce disegnate per ultime saranno visibili, mentre sulle nascoste verranno disegnate quelle visibili. A suo discapito questo algoritmo include un pesante spreco di tempo macchina, inoltre e' praticamente inutilizzabile per grafica in wireframe, pero' permette a qualsiasi oggetto (che non sia wireframe) di essere visualizzato correttamente. Un'altra via consiste nel calcolare la normale (la retta perpendicolare) su ogni faccia, controllare se punta verso l'osservatore ed in caso contrario non visualizzarla. Questo algoritmo e' valido solo se i vertici che delimitano la faccia sono posti in memoria in senso orario, in quanto i calcoli che vedremo sfruttano questa caratteristica. Inoltre gli oggetti dovranno necessariamente essere convessi, ossia non devono esserci facce che possano "oscurare" (non nascondere!) altre facce. La retta normale la possiamo considerare una grandezza vettoriale che nello spazio viene indicata come un comune vettore con tre componenti. La visibilita' di un poligono dipende esclusivamente dalla sua orientazione lungo l'asse z. Essendo piu' precisi possiamo dire che la sola componente z e' necessaria per sapere se la faccia e' nascosta o meno. Consideriamo la nostra faccia come la seguente: per individuare un piano bastano tre punti. Di A(x1,y1) conseguenza, per ricavare la normale sul piano /\ coincidente la nostra faccia, saranno sufficienti / \ le coordinate dei primi tre vertici della faccia. D / \ B(x2,y2) Lasciando da parte eventuali dimostrazioni, e' \ / possibile affermare che la componente z della \ / normale e' equivalente a: \/ C(x3,y3) (x2-x1)*(y3-y1)-(x3-x1)*(y2-y1) Se il risultato e' minore o uguale a zero la faccia e' nascosta, altrimenti e' visibile. Per sapere semplicemente se questa componente z e' maggiore o minore di zero potremmo anche utilizzare le coordinate proiettate, ovvero: (xp2-xp1)*(yp3-yp1)-(xp3-xp1)*(yp2-yp1) Questo e' quanto basta per sapere se un poligono e' o meno visibile. aLGORITMO dEL pITTORE ========================================================================== L'algoritmo del pittore consiste semplicemente nel visualizzare un oggetto (o una scena) 3D tracciando i poligoni partendo da quello piu' distante verso quello piu' vicino al punto di vista. Il suo scopo e' di risolvere il problema causato dal tracciamento delle facce parzialmente oscurate da altre (ovvero visualizzare oggetti concavi). Questo semplice principio e' lo stesso che un qualsiasi pittore utilizza quando deve dipingere un quadro. Difatti si inizia sempre col disegnare (ad esempio) i monti (che si trovano piu' distanti dal punto di vista) sino a disegnare gli elementi piu' vicini al pittore, quali possono essere un ruscello o una persona. Per realizzare l'algoritmo del pittore e' sufficiente utilizzare un array monodimensionale a cui ogni coppia di posizioni corrispondano il puntatore o l'indice di ogni faccia e la componente z di quella faccia. Consideriamo di poter numerare le facce del nostro oggetto, quindi di poter indicare una singola faccia con un determinato numero (si consulti l'Appendice C per ulteriori consigli). Il numero di elementi necessari al questo buffer sono equivalenti a: Dimensione Buffer = 1 + (numero facce oggetto)*2 Nella prima posizione viene salvato il numero di facce da tracciare. Invece nelle successive posizioni vengono salvati (per ogni poligono) il numero identificatore della faccia (che puo' essere un indice o un puntatore e la componente z di quella faccia di cui abbiamo appena parlato. La coordinata z relativa alla faccia e' equivalente alla media aritmetica delle componenti z dei vertici di quel poligono. Successivamente dovremo ordinare in ordine crescente gli elementi di tale struttura in base alla coordinata z. Una volta ordinato l'array traccieremo i poligoni a video nella successione definita dagli identificatori delle facce presenti nel nostro array gia' ordinato (partendo dallo specificatore con la z maggiore sino ad arrivare allo specificatore della faccia con la z minore); cosi' facendo saremo sicuri di visualizzare le facce dalle piu' lontane alle piu' vicine; cio' ci permette di rappresentare correttamente sullo schermo anche oggetti concavi. Per snellire questo buffer possiamo calcolare la componente z della normale di ogni faccia (Nz) e, solamente se Nz risultera' positiva, salveremo l'identificatore e la z di quella faccia; in caso contrario (se Nz<0) passiamo direttamente alla prossima faccia. In altre parole andiamo a considerare solo le facce orientate verso l'osservatore (consultare il paragrafo relativo all'hidden face per ulteriori approfondimenti). Se applicato nella maniera dovuta (ovvero utilizzando un algoritmo di ordinamento ottimizzato), l'algoritmo del pittore puo' considerarsi uno dei metodi piu' veloci nella rimozione delle facce nascoste durante il tracciamento di un oggetto o di una scena 3D. I suoi principali svantaggi risiedono nella difficolta' di implementazione nel caso di intersezioni tra poligoni; nell'imprecisione visiva in certi mondi 3D complessi e nell'impossibilita' di tracciare correttamente i poligoni in alcuni casi (si immagini di voler visualizzare una scena in cui vi siano un enorme poligono che rappresenti un pavimento su cui giace un piccolo cubo. Poniamo caso caso che la componente z del centro di tale cubo corrisponda con quella del poligono coincidente il pavimento. Dopo aver tracciato i poligoni su schermo puo' accadere che alcune facce del cubo vengano disegnate prima del pavimento, quindi alcuni poligoni, che dovrebbero essere visibili, non verrano visualizzati su schermo). fILLED vECTOR e sCAN lINE ========================================================================== I filled vector non rappresentano altro che poligoni "riempiti" di un determinato colore. Realizzare una routine di fill significa colorare il contenuto di un poligono conoscendone le coordinate proiettate dei vertici. Immaginiamo che il poligono da riempire sia il seguente: | A|\ potremmo procedere nella seguente maniera: | | \ a partire dalla coordinata y minore del poligono | | \ (in questo caso quella di A) e via via arrivando | | \ all'ultima posizione verticale (ossia y di D) | | \ coloriamo la riga delimitata dalle posizioni x | | \ B dei lati in quella posizione y. | D \ | | \ | Il principio del fill si basa sul riempimento di | \ | righe orizzontali partendo dalla riga superiore | \ | del poligono sino a quella inferiore. |y \ | v \|C Vediamo un esempio relativo ad una sola riga: | A|\ | | \ dobbiamo riempire la riga indicata nella figura. | | \ Partiamo dalla coordinata x del lato AD. | riga | \ Iniziamo col colorare questo punto. | da -->|****\ Passiamo al punto successivo (ossia quello a | riempire| \ B destra) e coloriamo anch'esso; procediamo col | D \ | colorare i pixel successivi fin quando arriviamo | \ | a colorare il punto posto sul lato AB e passiamo | \ | alla prossima riga. | \ | Cio' significa che per ogni riga ci servono le |y \ | coordinate x del punto estremo a destra e del v \|C punto estremo a sinistra. Ora che abbiamo capito cosa fare vediamo come realizzare il tutto. Bisogna utilizzare due tabelle (matrici unidimensionali) in memoria di dimensioni equivalenti al numero di pixel verticali rappresentabili sullo schermo (es.: in una risoluzione 320*200 dobbiamo avere due tabelle di 200 valori ciascuna). Consideriamo ogni posizione delle tabelle una posizione y nello schermo ed il contenuto del primo array come la corrispondente componente x del punto estremo a sinistra mentre il valore del secondo array come la componente x del punto estremo a destra. Cosi' facendo bastera' colorare tutti i pixel alla riga corrispondente la posizione delle tabelle partendo dalla posizione x contenuta dalla prima tabella sino alla posizione x contenuta dalla seconda (tutti i punti di una riga hanno la stessa ordinata). Facciamo un esempio pratico: 0| x=0 ->A. <- x=0 per semplicita' consideriamo i segmenti AB e CD 1| x=0 -> .. <- x=1 inclinati a 45 gradi . I vertici sono: 2| x=0 -> . . <- x=2 A(0,0) B(5,5) C(5,10) D(0,5) 3| x=0 -> . . <- x=3 le nostre due tabelle saranno: 4| x=0 -> . . <- x=4 +-------------------------------------------+ 5| x=0 ->D. .B<- x=5 |TAB1| 0| 0| 0| 0| 0| 0| 1| 2| 3| 4| 5|..|..| 6| x=1 -> . . <- x=5 +-------------------------------------------+ 7| x=2 -> . . <- x=5 |TAB2| 0| 1| 2| 3| 4| 5| 5| 5| 5| 5| 5|..|..| 8| x=3 -> . . <- x=5 +-------------------------------------------+ 9| x=4 -> .. <- x=5 Per riempire il poligono bastera' colorare i 10|y x=5 -> .C<- x=5 pixel compresi tra i corrispondenti valori v delle due tabelle utilizzando come componente y l'indice delle tabelle (che e' lo stesso per entrambe). A volte e' possibile eliminare i due valori estremi delle tabelle, quando in quelle righe vi e' un solo pixel (come nel nostro esempio). Adesso non ci rimane altro che ricavarci il contenuto di questi due array. Per definizione si indica col termine "scanline" una linea orizzontale su schermo del poligono. Un "edge" non e' altro che uno dei due pixel ai bordi della scanline, quindi gli array di cui dovremo ricavarci il contenuto possiamo chiamarli "edge buffer". Le tabelle contengono semplicemente le coordinate x di tutti i punti che compongono i lati del poligono; inoltre queste ascisse sono ordinate in base alla loro componente y. In pratica dobbiamo svolgere una routine di tracciamento di linee per tutti i lati della faccia, in cui non visualizziamo i pixel, bensi' salviamo la componente x in un array la cui posizione equivale alla y di quel punto. Questa procedura e' denominata "scan conversion" e praticamente consiste nel suddividere il poligono in un insieme di righe e colonne. Per realizzare la scan conversion e' possibile utilizzare l'algoritmo di Bresenham, pero' conviene sfruttare il procedimento di interpolazione lineare, che risulta piu' efficiente. Vediamo sinteticamente in cosa consiste. Consideriamo due generici punti A(x1,y1) e B(x2,y2) dove y2>y1. Ora calcoliamo: dx = x2 - x1 <-- lunghezza della linea che unisce A e B dy = y2 - y1 <-- altezza della linea che unisce A e B stepx = dx / dy <-- numero di pixel orizzontali su ogni riga Mentre l'algoritmo generale e': > x = x1 > y = y1 > loop di dy iterazioni > se e' libera la posizione y della tab1: > salva x in posizione y della tab1 > altrimenti: > salva x in posizione y della tab2 > x = x + stepx > y = y + 1 > prossima iterazione Questo algoritmo permette di riempire parzialmente un edge buffer nel caso y2>y1, se invece risulta y1>y2 bastera' scambiare entrambe le coordinate dei due punti (ossia considerare y1 come y2 e x1 come x2). Per riempire completamente l'edge buffer bastera' ripetere tale procedura per ogni lato del poligono. La tab1 e' l'array che contiene i punti estremi a sinistra mentre la tab2 punti estremi a destra. Col nostro algoritmo puo' capitare che alcuni i valori scritti nella tab1 appartengano alla tab2, e viceversa. Cio' significa che le coordinate x presenti nella tab1 potranno essere gli edge a destra mentre i valori presenti nella tab2 potranno essere gli edge a sinistra. Vediamo cosa fare per evitare questo inconveniente. Se abbiamo i punti salvati in senso orario il tutto risulta piu' semplice e veloce. Dati due punti A(x1,y1) e B(x2,y2) posti in senso orario, se y1 e' maggiore di y2 allora l'insieme dei punti che formano quel lato appartiene alla tab1 (quella contenente le posizioni x minori), in caso contrario quei punti apparterranno alla tab2 (contenente le x maggiori). Ecco l'algoritmo completo per la scan conversion relativo ad un singolo lato: > confronta y1 con y2 > se y1=y2: > esci dalla procedura! > se y1>y2: > la giusta tab e' tab1 > se y1 la giusta tab e' tab2 > scambia y1 con y2 > scambia x1 con x2 > dy = y1 - y2 > dx = x1 - x2 > stepx = dx / dy > x = x2 > y = y2 > loop di dy iterazioni > salva x in posizione y nella giusta tab > x = x + stepx > y = y + 1 > prossima iterazione Una volta eseguita la scan conversion del poligono dovremo calcolare la componente y minore dei quattro punti che compongono i vertici del poligono ed l'altezza in pixel del poligono stesso. La coordinata y minore rappresenta l'indice delle tabelle da cui partire per riempire il poligono e quindi la posizione y superiore del poligono. L'altezza del poligono e' equivalente alla differenza tra la y maggiore e la y minore e ci serve per sapere quante righe dovremo riempire per l'attuale poligono. Riassumendo, per effettuare il fill di un poligono si devono svolgere i seguenti passi: - definire in memoria due tabelle dimensionate ad ys valori (dove ys rappresenta l'altezza in pixel dello schermo); - calcolare la y minore dei vertici e l'altezza del poligono; - svolgere la scan conversion del poligono salvando le coordinate x iniziali e finali (edge) di ogni scanline nell'edge buffer; - partendo dalla posizione y minore, riempire la riga delimitata dalle posizioni x contenute dall'edge buffer stesso per un numero di volte equivalente all'altezza del poligono. fLAT sHADING ========================================================================== Siamo arrivati all'analisi del primo (e piu' semplice) algoritmo di shading, grazie al quale potremo attribuire ad ogni poligono comprendente l'oggetto una precisa intensita' di luce, la quale verra' determinata in base all'orientazione della faccia rispetto alla sorgente di luce. Il flat shading permette di attribuire ad ogni faccia un solo colore che determinera' quanto il poligono sia illuminato. Facciamo un esempio: +-------------+ vista | | dall'alto | | <--- oggeto in 3D teorico (teorico | | perche' in realta' non c'e', noi | | andiamo a visualizzare le +-------------+ coordinate proiettate) | | <--- direzione della sorgente di luce ______________|_____________ <--- schermo del monitor | | o <--- punto di vista dell'osservatore Consideriamo che la sorgente di luce corrisponda con il punto di vista, la sua direzione e' perpendicolare lo schermo. Definiamo l'angolo di inclinazione della faccia rispetto la sorgente di luce come l'angolo compreso tra la retta corrispondente la direzione della luce ed il versore della faccia (ovvero la retta normale). Piu' questo angolo e' piccolo e piu' il poligono risulta orientato verso l'osservatore. Possiamo intuire che tanto la faccia e' posta di fronte l'osservatore e tanto sara' maggiore l'intensita' di luce applicata su quella faccia. Di conseguenza ad un angolo minore corrispondera' una luminosita' maggiore della faccia. Ecco un altro esempio: /\ / \ vista dall'alto / \ oggetto 3D --> / \ immaginario / \ / \ \ / \ / a = angolo compreso tra il versore della \ / faccia e la direzione della luce /|\ / /-| \ / / a| \/ versore / | faccia --> / | <-- direzione luce (in questo caso / | coincidente il punto di vista) / | -----------|------------- <-- schermo del monitor o <-- punto di vista dell'osservatore L'insensita' di luce attribuibile al poligono e' proporzionale al coseno di questo angolo. Sappiamo che il generico risultato di cos(a) e' compreso tra -1 e 1. Da notare che se il poligono e' visibile il nostro angolo varia da 0 a 90 gradi, altrimenti la faccia risulta nascosta (e' possibile utilizzare questa caratteristica per l'eliminazione dell'hidden face!). Quindi il valore del coseno relativo al nostro angolo copre un range di valori da 0 e 1. Inoltre, utilizzando 256 colori, bastera' moltiplicare il coseno per 256 (oppure effettuare uno shift di 8 bit a sinistra) e otterremo il pixel chunky col quale dovremo riempire la relativa faccia! Vediamo come ricavare questo valore. Innanzitutto dobbiamo specificare la palette dei colori da utilizzare. Cio' e' possibile definendo nella memoria video una tavolozza che parte dal colore di luminosita' minore sino ad arrivare gradualmente al colore piu' chiaro. Per il calcolo del coseno conviene sfruttare la regola di Lambert, la quale afferma che il prodotto scalare tra due rette espresse come grandezze vettoriali e' equivalente al prodotto della lunghezza dei relativi vettori e del coseno dell'angolo limitato dalle rette stesse, ovvero l'angolo a. Quindi, per conoscere cos(a), non dovremo far altro che svolgere questo prodotto scalare e dividere il risultato per il prodotto delle lunghezze dei due vettori. Per calcolare il prodotto scalare di due vettori si esegue il prodotto delle corrispondenti componenti e poi si sommano i risultati, ad esempio: H=(xh,yh,zh) ; K=(xk,yk,zk) H*K=xh*xk+yh*yk+zh*zk <-- prodotto scalare Per ricavare una lunghezza di un vettore possiamo ricorrere al teorema di Pitagora grazie al quale si puo' affermare che la lunghezza e' equivalente alla radice quadrata della somma dei quadrati di ogni componente. Verifichiamo come calcolare i coefficienti x, y e z del versore della faccia: Nx=(y2-y1)*(z3-z1)-(y3-y1)*(z2-z1) <-+--- i tre coefficienti Ny=(z2-z1)*(x3-x1)-(z3-z1)*(x2-x1) <-| della retta normale Nz=(x2-x1)*(y3-y1)-(x3-x1)*(y2-y1) <-+ N.B.: i punti devono essere posti in memoria in senso orario! x1, y1, z1 = componenti del primo punto del poligono x1, y2, z2 = componenti del secondo punto del poligono x3, y3, z3 = componenti del terzo punto del poligono Infine ecco la formula per calcolare il pixel chunky: Nx*lx + Ny*ly + Nz*lz cos(a)=------------------------------------------------- sqrt(Nx*Nx+Ny*Ny+Nz*Nz) * sqrt(lx*lx+ly*ly+lz*lz) pixel chunky = 256*cos(a) a = angolo tra il versore e la direzione della sorgente di luce lx = componente x della sorgente di luce ly = componente y della sorgente di luce lz = componente z della sorgente di luce Le coordinate lx, ly e lz rappresentano la posizione della sorgente di luce. Nel caso la luce coincida con il punto di vista dell'osservatore, le relative coordinate risulteranno: lx=0 ; ly=0 ; lz=-256 zl e' uguale all'opposto della distanza tra l'osservatore e lo schermo (nel nostro caso la distanza tra l'osservatore e lo schermo e' 256). oTTIMIZZAZIONI pER iL cALCOLO dELLA sORGENTE dI lUCE ========================================================================== In questa parte vediamo come velocizzare il nostro motore 3D nel caso comprenda l'implementazione di una sorgente di luce reale. Una prima ottimizzazione consiste nell'utilizzare un buffer dove andremo a precalcolare tutte le normali di ogni faccia (o di ogni vertice nel caso del gouraud shading); poi anziche' calcolare ad ogni frame tutte le normali, ruotiamo i nostri versori precalcolati dello stesso angolo con cui ruotiamo i vertici dell'oggetto sfruttando la stessa identica procedura descritta in precedenza (utilizzando preferibilmente l'algoritmo a 9 moltiplicazioni). Abbiamo detto che il prodotto scalare tra il vettore normale ed il vettore corrispondente la sorgente di luce moltiplicato 256 ci permette di conoscere il pixel chunky. Ora vediamo come eliminare subito le radici quadrate e la divisione. Analizziamo di nuovo la formula: Nx*lx + Ny*ly + Nz*lz cos(a)=------------------------------------------------- sqrt(Nx*Nx+Ny*Ny+Nz*Nz) * sqrt(lx*lx+ly*ly+lz*lz) Per attuare questa ottimizzazione dobbiamo rendere unitario il vettore normale ed il vettore corrispondente la sorgente di luce. Rendere unitario un vettore significa dividere ognuna delle componenti per la sua distanza dall'origine; cio' fa si' che le nuove componenti abbiano un range compreso tra -1 e +1, ecco perche' si dice unitario. Vediamo algebricamente come rendere unitario un qualsiasi versore: Nx uNx=--------------------------- sqrt(Nx*Nx + Ny*Ny + Nz*Nz) Ny uNy=--------------------------- sqrt(Nx*Nx + Ny*Ny + Nz*Nz) Nz uNz=--------------------------- sqrt(Nx*Nx + Ny*Ny + Nz*Nz) Piu' genericamente, dato un vettore V(x,y,z), per calcolare le componenti del relativo vettore unitario uV(ux,uy,uz): x ux=--------------------- sqrt(x*x + y*y + z*z) y uy=--------------------- sqrt(x*x + y*y + z*z) z uz=--------------------- sqrt(x*x + y*y + z*z) Per rendere unitaria la sorgente di luce possiamo anche evitare di dividere ogni sua componente per la lunghezza della medesima, infatti siamo noi a decidere dove si trova, quindi arbitrariamente possiamo assegnare coordinate unitarie. Ad esempio tornando al caso che la luce coincida col punto di vista: ulx=0 ; uly=0 ; ulz=-1 Quindi la formula per calcolare l'intensita' di luce viene ridotta a: cos(a) = uNx*ulx + uNy*uly + uNz*ulz pixel chunky = 256*cos(a) Rendere unitario un vettore e poi ruotarlo o ruotare un vettore e poi renderlo unitario significa svolgere la stessa funzione; quindi al momento in cui andiamo a precalcolare le normali possiamo renderle subito unitarie, in seguito andremo a ruotare i versori gia' resi unitari. In questo modo evitiamo di eseguire 2 radici quadrate e una divisione per vertice ad ogni frame! N.B.: nel caso si voglia muovere la sorgente di luce, utilizzando questa ottimizzazione non si potranno attuare traslazioni della sorgente di luce, ma solo rotazioni, in quanto la distanza origine-luce deve restare costante. L'ultima ottimizzazione consiste nel mantere fissa in un punto la sorgente di luce, piu' precisamente diciamo che deve coincidere sempre con il punto di vista dell'ossevatore. Naturalmente utilizzeremo coordinate unitarie per i versori e la luce. Vediamo la formula per calcolare il pixel chunky in questo particolare caso: pixel chunky = 256*( uNx*ulx + uNy*uly + uNz*ulz) = = 256*( uNx*0 + uNy*0 + uNz*(-1))= = -256*uNz Ora il nostro pixel chunky dipende solo da uNz, quindi possiamo precalcolare per ogni vertice -256*uNz al posto del semplice uNz, ruotarlo e utilizzare subito questo valore come pixel chunky. In questo modo evitiamo 3 moltplicazioni e 2 addizioni. Inoltre siccome ci serve solo uNz possiamo benissimo evitare di ruotare uNx e uNy, risparmiando la bellezza di altre 6 moltiplicazioni per faccia (o per vertice nel caso del gouraud). In totale risparmiamo ben 9 moltiplicazioni e 2 addizioni per faccia (o per vertice nel gouraud)! Naturalmente dovremo precalcolare oltre a -256*uNz anche uNx e uNy moltiplicati 256 (256*uNx, 256*uNy) che servono per poter ruotare -256*uNz. Inoltre se invertiamo la nostra palette potremo utilizzare 256*uNz al posto di -256*uNz. gOURAUD sHADING ========================================================================== Questo algoritmo di ombreggiatura permette di sfumare l'interno di ogni poligono al contrario del flat shading col quale si assegna un unico colore per faccia. Innanzitutto bisogna calcolare il versore di ogni vertice dell'oggetto anziche' di ogni poligono. Le componenti della normale sul vertice sono equivalenti alla media aritmetica delle componenti delle normali di tutte le facce che toccano quel vertice. Facciamo un esempio: ____ sia V un generico vertice di un cubo appartenente /f2 /\ alle facce f1, f2 e f3. Consideriamo le normali di /___/V \ codeste facce, chiamiamo questi versori N1,N2,N3. \f1 \f3/ \___\/ N1(Nx1,Ny1,Nz1) N2(Nx2,Ny2,Nz2) N3(Nx3,Ny3,Nz3) Allora la normale su V e' equivalente a: NV( (Nx1+Nx2+Nx3)/3, (Ny1+Ny2+Ny3)/3, (Nz1+Nz2+Nz3)/3 ) In questo caso le facce appartenenti a V sono 3, a seconda dell' oggetto che si vuol utilizzare il numero di poligoni appartenenti ad un vertice cambiano. Una volta precalcolate tutte le normali (preferibilmente gia' unitarie) su ogni spigolo dovremo calcolare la quantita' di luce che cade su ognuno dei vertici, ovvero il pixel chunky, utilizzando la legge gia' studiata nel flat shading (magari sfruttando eventuali ottimizzazioni citate nel precedente paragrafo). In seguito facciamo la scan conversion (spiegata nel paragrafo dedicato al fill e scan line) di tutti i poligoni visibili. Adesso dobbiamo interpolare linearmente per ogni faccia i pixel chunky appartenenti ai vertici di quella faccia. In pratica dovremo svolgere una semplice scan conversion del poligono utilizzando i pixel chunky al posto delle coordinate x dei vertici, tutto qui. Naturalmente cio' va svolto solamente se la faccia risulta visibile. Non rimane altro che effettuare il fill vero e proprio dei poligoni. Come per un normale fill bisogna eseguire un loop ad iterazioni equivalenti all'altezza in pixel del poligono. Ad ogni iterazione preleviamo di volta in volta le coordinate x iniziali e finali dalle tabelle delle scan line (come per un normale fill), pero' stavolta preleviamo anche i pixel chunky iniziali e finali. Ora dobbiamo interpolare il pixel chunky iniziale con quello finale partendo dalla coordinata x iniziale sino ad arrivare a quella finale. Per far cio' basta utilizzare l'algoritmo per tracciare una scan line con le seguenti modifiche: - utilizzare il pixel chunky iniziale al posto della coordinata x1; - utilizzare il pixel chunky finale al posto della coordinata x2; - utilizzare la x iniziale al posto della coordinata y1; - utilizzare la x finale al posto della coordinata y2; - utilizzare la riga dello schermo chunky da "fillare" come tabella dove la scan line verra' salvata. Ed ecco realizzato il gouraud shading! pHONG sHADING ========================================================================== Il phong shading permette di assegnare ad ogni pixel la sua reale intensita' di luce, al contrario del gouraud nel quale si generano sfumature all'interno di ogni faccia tra le intensita' di luce di ogni vertice dell'oggetto. La maggiore definizione che si ottiene col phong rispetto al gouraud comporta allo stesso tempo un drastico aumento delle operazioni che il processore deve svolgere. La pesantezza dei calcoli da eseguire e' tale da impedire agli attuali elaboratori di tracciare soddisfacienti scene in phong shading in tempo reale. Nel gouraud calcoliamo la reale intensita' di luce su ogni vertice, dopodiche' ogni colore viene intepolato lungo ogni lato del poligono, infine si interpolano i colori posti sui lati estremi a sinistra con i colori posti sui lati estremi a destra in modo da riempire l'intero poligono. Nel phong invece interpoliamo sempre le normali, non si interpolano mai i colori. Una volta determinati i versori su ogni vertice, questi devono essere interpolati lungo ogni lato; successivamente i versori posti lungo i lati estremi a sinistra verranno interpolati con quelli posti lungo i lati estremi a destra, quindi per ogni singolo verra' calcolato il colore sfruttando la tradizionale formula piu' volte studiata. Il phong ci impedisce di sfruttare le diverse ottimizzazioni possibili col gouraud shading e col flat shading. Infatti nel phong e' impossibile utilizzare normali unitarie in quanto nel momento in cui queste vengono interpolate la loro lunghezza (equivalente al risultato dell'espressione sqrt(Nx*Nx+Ny*Ny+Nz*Nz)) puo' variare. Quindi occore eseguire almeno una divisione ed una radice quadrata per pixel, il che non e' poco. rEFLECTION mAPPING ========================================================================== Se un solido riflette sempre e solo una singola immagine (in gergo denominata "texture") allora possiamo affermare che su quel solido e' stato applicato il reflection mapping. Nel caso la texture corrisponda approsimativamente alla rappresentazione bidimensionale di una luce (es.: un cerchio il cui centro risulta molto chiaro mentre agli orli viene sfumato in una tinta piu' scura), e' possibile raggiungere effetti simili (e a volte superiori) al phong e al gouraud. Spesso questo effetto viene erroneamente confuso con l'environment mapping, il quale permette invece di riflettere un intero ambiente che circonda l'oggetto (il quale ambiente e' spesso definito per semplicita' come un cubo, quindi in questo caso sul solido verranno riflesse sei immagini). Tuttavia e' possibile considerare il reflection mapping come un'approssimazione dell'environment mapping. In questo paragrafo verra' descritto come realizzare il reflection mapping utilizzando esclusivamente texture di dimensioni 256*256 pixel. L'implementazione di texture di dimensioni differenti e' facilmente derivabile. Vediamo dettagliatamente come realizzare un comune oggetto in reflection mapping. Inizialmente ci andiamo a precalcolare tutti i versori unitari su ogni vertice (come per il gouraud) e li moltiplichiamo per 128 (oppure applichiamo un semplice scorrimento di 7 bit a sinistra), il che matematicamente si traduce in: _ _ | PVx = 128*Nx / sqrt(Nx*Nx + Ny*Ny + Nz*Nz) | PV | PVy = 128*Ny / sqrt(Nx*Nx + Ny*Ny + Nz*Nz) | |_ PVz = 128*Nz / sqrt(Nx*Nx + Ny*Ny + Nz*Nz) _| Chiamiamo PV il vettore che ha come componenti questi 3 valori. La normale unitaria ha come coordinate 3 valori che comprendono numeri reali tra -1 e +1. Ora abbiamo PVx, PVy e PVz che rispetto ai versori unitari sono molplicati 128, questo vuol dire che copriranno un raggio di valori compresi tra -128 e +128 (anche se in realta' tali valori non superano mai +127). E qui finisce la fase di precalcolo. In tempo reale dobbiamo ruotare il vettore PV per ogni vertice (casomai sfruttando gli stessi fattori di rotazione dei punti se si ha realizzato la rotazione a 9 moltiplicazioni). Del vettore PV ci servono solo le componenti x e y ruotate, quindi possiamo anche non ruotare PVz evitando cosi' di svolgere almeno 3 moltiplicazioni e 2 addizioni per vertice (naturalmente e' necessario precalcolare PVz per ogni vertice per poter ruotare PVx e PVy). Ad ognuna delle componenti ruotate x e y del vettore PV addizioniamo il valore 128. Al termine di questi calcoli, il range dei valori che PVx e PVy potranno comprire sara' compreso tra 0 e 255. In realta' ((PVx ruotato)+128) e ((PVy ruotato)+128) rappresentano le coordinate della texture da mappare (ossia tracciare) sul poligono. Cio' significa che se abbiamo un poligono delimitato da 4 punti, dobbiamo mappare su quel poligono la parte di texture delimitata dai 4 relativi PVx e PVy ruotati (e sommati con 128). Quindi bastera' mappare il "pezzo" di texture su quel poligono e ripetere il tutto per ogni faccia visibile per realizzare il reflection mapping! Ora vediamo come tracciare la parte di texture una volta calcolati i nuovi PVx e PVy. Innanzitutto dobbiamo svolgere la scan conversion del poligono, in piu' bisogna interpolare PVx e PVy lungo tutti i lati della nostra faccia, il che significa fare 2 ulteriori scan conversion del poligono utilizzando i PVx e i PVy al posto delle coordinate x dei vertici. Quindi in tutto vi sono 3 scan conversion: la prima e' quella tradizionale, la seconda viene fatta sostituendo PVx alle x dei vertici, mentre la terza utilizza i PVy al posto delle x (facciamo esattemente come per le normali nel phong, con la differenza che consideriamo 2 componenti (PVx e PVy) al posto di 3 (Nx, Ny e Nz)). Abbiamo eseguito la scan conversion del poligono, quel di cui abbiamo bisogno e' un algoritmo che permetta di associare ad ogni punto appartenente alla faccia un determinato pixel della texture. Consideriamo la seguente figura come la nostra faccia proiettata a video: . Dopo aver interpolato PVx e PVy e svolto . . la scan conversion, per ogni coppia di . . punti sulla stessa posizione y dello P1 -> . . <- P2 schermo, ad esempio P1 e P2, conosciamo . . la loro coordinata x assieme a PVx e PVy. . . Adesso dobbiamo interpolare PVx e PVy . . dal punto P1 al punto P2, in questo modo . . sapremo il valore di PVx e PVy per tutti . i punti del poligono. Per interpolare questi 2 valori lungo una riga si deve P1 -> x1, y, PVx1, PVy1 applicare l'algoritmo generale per il P2 -> x2, y, PVx2, PVy2 tracciamento di una scan line utilizzando dPVx = PVx1-PVx2 dx al posto di dy, dPVx (per interpolare dPVy = PVy1-PVy2 PVx) e dPVy (per interpolare PVy) al posto dx = x1-x2 di dx, proprio come abbiamo fatto nel gouraud per interpolare i pixel chunky. Come gia' accennato, PVx e PVy rappresentano le coordinate del pixel texture da tracciare. Appena svolte le 3 scan conversion conoscevamo PVx e PVy appartenenti ad ogni vertice della faccia. Quindi, interpolando PVx e PVy lungo tutto il poligono, ricaveremo le coordinate dei punti della texture per tutti i punti della faccia! Ora, con delle semplici operazioni di copia, potremo mappare il poligono associando, ad ogni suo punto, il pixel chunky della texture in posizione (PVx,PVy). aFFINE tEXTURE mAPPING ========================================================================== Questo effetto permette il tracciamento di un'intera immagine su di un poligono, in pratica e' come se "incollassimo" su ogni faccia una texture (ovvero una semplice immagine chunky posta in memoria). In questo paragrafo tratteremo del texture mapping senza prospettiva (il cosidetto "affine texture mapping"), il quale risulta essere uno degli algoritmi piu' veloci per mappare un'immagine su di un poligono, ma allo stesso tempo anche il meno realistico. Per completezza definiamo gli assi relativi alla texture (posta in memoria, non quella visualizzata su schermo) hanno come origine il punto piu' in alto a sinistra. L'ascissa di tale sistema viene denominata mentre l'ordinata ; quindi un generico punto della texture puo' essere indicato come P(u,v). Da notare che le componenti u e v non possono assumere valori negativi. Consideriamo di utilizzare poligoni formati da 4 lati, ogni spigolo del poligono coincide con uno spigolo della faccia, cio' che dobbiamo fare e' tracciare tutta la texture sul poligono. In pratica e' come se dovessimo realizzare il reflection mapping sapendo che le coordinate della texture da mappare sono sempre costanti e coincidono esattamente con i 4 vertici della texture stessa. Se la texture e' 256*256 pixel realizzeremo un algoritmo di reflection mapping sapendo che PV1(0,0), PV2(255,0), PV3(255,255), PV4(0,255) sono gli stessi per ogni poligono. In altre parole andiamo ad interpolare le coordinate x e y della texture (ovvero u e v)lungo tutto il poligono, in modo tale da sapere per ogni pixel appartenente alla faccia da tracciare quale sia il relativo punto della texture. Tutto qui. Per chiarire meglio le idee osserviamo un semplice esempio: A +-----------------+ B Abbiamo il nostro poligono ABCD in cui | | intendiamo applicarci una texture. | | Vogliamo che al punto piu' in alto a | | sinistra della texture corrisponda il | | vertice A, al punto in alto a destra | | il vertice B, a quello in basso a | | destra C, mentre al punto in basso a | | sinistra il vertice D. | | Facciamo finta che le coordinate del D +-----------------+ C poligono siano gia' state ruotate. Al punto A corrisponde il punto (0,0) della texture, al punto B le coordinate (255,0), a C corrisponde (255,255) ed infine a D corrisponde il pixel (0,255) della texture. Quindi svolgiamo 2 ulteriori scan conversion della faccia (oltre a quella classica per "fillare" il poligono) utilizzando una volta le iniziali e finali al posto delle x, mentre nella seconda volta si devono sfruttare le iniziali e finali al posto delle x; in questo modo (interolando la e la per ogni scanline) sapremo le coordinate (u,v) della texture di ogni pixel della faccia. fREE dIRECTION tEXTURE mAPPING ========================================================================== tEXTURE mAPPING bILINEARE ========================================================================== tEXTURE mAPPING bIQUADRATICO ========================================================================== oTTIMIZZAZIONE dEL fILL ========================================================================== In questo paragrafo studieremo un alternativo algoritmo per realizzare il fill dei poligoni nel caso avessimo bisogno di interpolare una o piu' variabili lungo tutto il poligono (come nel caso del texture mapping, del gouraud e del phong shading). L'unico vero svantaggio nell'utilizzare tale metodo consiste nel fatto che tutti poligoni dovranno per forza essere dei triangoli. Consideriamo di dover mappare una texture (o parte di essa) su di un poligono. Applicando i metodi precedentemente descritti avremmo eseguito 2 divisioni per scanline, ovvero: du = u2 - u1 dove (u1,v1) rappresentano le coordinate dv = v2 - v1 della texture per l'edge a sinistra, mentre stepu = du / dx (u2,v2) valgono per l'edge a destra. stepv = dv / dx L'ottimizzazione notevole che si ottiene con l'algoritmo che vedremo, sta nel calcolare stepu e stepv, detti constant slope, una sola volta per tutto il poligono. In altre parole al posto di svolgere 2 divisioni per scanline, eseguiremo 2 divisioni per poligono, non male! In un triangolo qualsiasi, gli stepu e stepv sono sempre costanti lungo tutto il poligono, quindi potremo calcolarci i costant slope una volta sola (per faccia) e riutilizzarli per tutte le scanline di tale poligono. Per ottenere risultati il piu' precisi possibile, calcoleremo gli stepu e stepv relativi alla scanline piu' lunga di tutto il poligono, poi utilizzeremo tali constant slope per tutte le scanline. Questo e' il concetto fondamentale per poter ottimizzare il fill. Consideriamo un generico triangolo: A(x1,y1) . A(x1,y1) e' il vertice superiore, _/ \ che sta piu' in alto; _/ \ B(x2,y2) e' il vertice che sta a _/ \ L1 media altezza; L3 _/ \ C(x3,y3) e' il vertice inferiore, _/ \ che sta piu' in basso; _/ ____\. L1 e' il lato AB; L2 e' il lato BC _/ ____---- B(x2,y2) mentre L3 e' il lato CA. C(x3,y3) ./____---- L2 La scanline piu' lunga e' sempre quella che giace sul vertice B, ossia quel vertice vediamo come calcorarne la relativa lunghezza assieme ai constant slope: temp = (y2-y1) / (y3-y1) scan = temp * (x3-x1) + (x1-x2) <- lunghezza scanline maggiore stepu = (temp * (u3-u1) + (u1-u2)) / scan <- constant slope u stepv = (temp * (v3-v1) + (v1-v2)) / scan <- constant slope v Il valore di puo' essere positivo o negativo, il che ci permette di determinare se il vertice B e' il punto estremo a destra o a sinistra dell'intero poligono: se (scan = 0) allora il poligono e' vuoto, non tracciare il poligono; se (scan > 0) allora il vertice B e' posto a sinistra, quindi i lati L1 e L2 appartengono all'edge di destra, mentre solo L3 appartiene all'edge di sinistra; se (scan < 0) allora il vertice B e' posto a destra, quindi i lati L1 e L2 appartengono all'edge di sinistra, mentre solo L3 appartiene all'edge di destra; cORREZIONE pROSPETTICA ========================================================================== cLIPPING 2d ========================================================================== Nella visualizzazione di un oggetto o un mondo 3D, bisogna considerare il problema del tracciamento di poligoni o linee parzialmente visibili sullo schermo, ovvero di facce o linee che "escono" (anche parzialmente) fuori dal monitor. Il procedimento atto a risolvere questo problema e' chiamato clipping 2D. Questo tipo di clipping e' 2D in quanto ci si propone di "tagliare" i nostri oggetti solo rispetto al singolo piano coincidente lo schermo. Come condizioni iniziali consideriamo i valori XMIN e XMAX come le componenti x estreme a sinistra e a destra dello schermo, mentre YMIN e YMAX come le componenti y estreme in alto e in basso dello schermo. Quindi i 4 vertici immaginari dello schermo avranno le seguenti coordinate: (XMIN,YMIN) (XMAX,YMIN) +-----------------+ | | | | | | | | | | | | | | +-----------------+ (XMIN,YMAX) (XMAX,YMAX) Ad esempio, nell'ipotetico caso di uno schermo di risoluzione 320x200: XMIN = 0 ; XMAX = 319 ; YMIN = 0 ; YMAX = 199 Analizziamo subito come clippare una singola linea, in seguito vedremo il clipping relativo ai poligoni (il quale si basa sullo stesso principio per tagliare una linea). Caso 1: la linea esce dal bordo superiore dello schermo: P1 . consideriamo P1 il punto che sta piu' in alto \ e P2 il punto che sta piu' in basso. \ P1c Quel che dobbiamo fare e' calcolarci le nuove +-----+-------+ coordinate (clippate) di P1 in modo tale che | \ | risulti posizionato sul bordo superiore dello | \. | schermo, chiamiamo questo punto P1c. | P2 | | | P1c(xclip,yclip) | | P1(x1,y1) ; P2(x2,y2) +-------------+ xclip = x1 + (YMIN-y1)*(x1-x2)/(y1-y2) yclip = YMIN Una volta ricavate le coordinate di P1c non dovremo far altro che tracciare la linea tra P1c e P2. Caso 2: la linea esce dal bordo inferiore dello schermo: +-------------+ P1 e' il punto superiore mentre P2 e' il punto | | inferiore. P2c e' il punto di cui dobbiamo | .P1 | calcolarci le coordinate. | / | | / | P2c(xclip,yclip) | P2c / | P1(x1,y1) ; P2(x2,y2) +-----+-------+ P2 ./ xclip = x2 + (YMAX-y2)*(x2-x1)/(y2-y1) yclip = YMAX La linea clippata sara' compresa tra i punti P1 e P2c. Caso 3: la linea esce dal bordo sinistro dello schermo: +-------------+ P1 e' il estremo della linea a sinistra e P2 P1.___|P1c | e' il punto estremo a destra della linea. |---___. P2 | | | P1c(xclip,yclip) | | P1(x1,y1) ; P2(x2,y2) | | +-------------+ xclip = XMIN yclip = y1 + (XMIN-x1)*(y1-y2)/(x1-x2) La linea clippata sara' compresa tra i punti P1c e P2. Caso 4: la linea esce dal bordo destro dello schermo: +-------------+ P1 e' il estremo della linea a sinistra e P2 | | e' il punto estremo a destra della linea. | ___|_.P2 | P1.___---P2c| P2c(xclip,yclip) | | P1(x1,y1) ; P2(x2,y2) | | +-------------+ xclip = XMAX yclip = y2 + (XMAX-x2)*(y2-y1)/(x2-x1) La linea clippata sara' compresa tra i punti P1 e P2c. Dato che l'argomento relativo al clipping 2D dei poligoni e' stato gia' affrontato esaurientemente da CDS/DeathStar, ho preferito evitare di rispiegare tale argomento e ho deciso di riportare qui di seguito il testo che ha scritto CDS. L'algoritmo di Sutherland-Hodgman permette di eseguire il clipping di un poligono convesso o concavo rispetto ad una finestra di clipping convessa. Solitamente la finestra di clipping e' un rettangolo (lo schermo video), ma in alcuni casi e' necessario "clippare" un poligono rispetto ad un altro (ad esempio nel calcolo delle ombre portate). Nel caso di finestra rettangolare SH ha bisogno di 4 passi, uno per ogni lato della finestra di clipping. SH lavora su una lista di vertici e produce in output, ad ogni passo, una nuova lista di vertici che si trovano dalla parte visibile rispetto al lato di clipping. Siano P1 il vertice attuale nella lista, P2 il vertice seguente e T l'eventuale punto di intersezione fra la retta passante per P1 e P2 e il lato di clipping. Bisogna considerare 4 casi: 1) P1 interno P2 interno Nella lista di destinazione si inserisce P2. 2) P1 interno P2 esterno Nella lista di destinazione si inserisce T. 3) P1 esterno P2 interno Nella lista di destinazione si inseriscono T e P2. 4) P1 esterno P2 esterno Non si inserisce nessun vertice. Le condizioni interno/esterno nel caso di clipping con una finestra rettangolare sono dei semplici confronti, nel caso di finestra convessa non rettangolare e' necessario eseguire un prodotto scalare. Ad esempio il test P1 interno nel caso di clipping con il lato sinistro dello schermo e' semplicemente P1.x>0 . Un esempio di clipping di un triangolo con una finestra rettangolare: /| A +---------------+ / | | |/ | | /| | | / | | | / | | | / | | +---------------+ | / | +----------------+ B C Passo 1 (clipping con il lato superiore) : T1 T2 --------+---------------+--x--x----- | |/ | | /| | | / | | | / | | | / | | +---------------+ | / | +----------------+ A e' esterno e B e' interno, si inseriscono T1 e B. B e' interno e C e' interno, si inserisce C. C e' interno e A e' esterno, si inserisce T2. Si ottiene un nuovo poligono con i due vertici temporanei T1 e T2. Passo 2 (clipping con il lato sinistro): | | T1 T2 +---------------+--x--x | |/ | | /| | | / | | | / | | | / | | +---------------+ | | / | | +----------------+ | Passo 3 (clipping con il lato inferiore) : T1 T2 +---------------+ x--x | |/ | | /| | | / | | | / | | | / | | --------+------x--------+-----x------- T3 T4 Si ottiene un nuovo poligono con i due vertici temporanei T3 e T4. Passo 4 (clipping con il lato destro) : | | +---------------+ | | | /x T6 | / | | / | | / | +------x--------x T5 T3 | | | Il poligono definitivo ha per vertici T3, T5 e T6. CDS/DeathStar z-bUFFER ========================================================================== Lo Z-Buffer e' un'area di memoria dedicata all'eliminazione delle facce completamente o parzialmente nascoste. E' particolarmente utile per tracciare poligoni che si intersecano e non comporta limitazioni nella scena da tracciare (al contrario dell'algoritmo del pittore). Lo spazio occupato in memoria dallo Z-Buffer coincide con le dimensioni dello schermo (es.: per uno schermo 320*200 avremo bisogno di un buffer di 64000 valori). Consideriamo lo Z-Buffer come il nostro schermo chunky, con la differenza che ogni posizione non rappresenta un pixel, bensi' la componente Z di quel pixel; in questo modo possiamo sapere la coordinata Z di tutti i punti visualizzati su schermo. Ora si procede nella seguente maniera. Per una ed una sola volta per ogni frame andiamo ad inizializzare lo Z-Buffer copiandoci su ogni posizione un valore arbitrario piuttosto elevato. In seguito ci andiamo ad interpolare la coordinata Z di ogni vertice lungo tutto il poligono (ossia facciamo come nel gouraud, ma consideriamo Z al posto di Nz). Nel loop nel quale tracciamo i pixel (ovvero il loop piu' interno del fill), confrontiamo la Z che stiamo interpolando con il valore corrispondente nello Z-Buffer (la cui posizione deve coincidere con quella dello schermo chunky) e, soltanto nel caso la nostra Z sia minore rispetto a quella dello Z-Buffer, copiamo la nostra Z nello Z-Buffer e scriviamo il pixel nello schermo chunky. Nel caso in cui la nostra Z sia maggiore o uguale rispetto al valore dello Z-Buffer, allora non dobbiamo ne' aggiornare lo Z-Buffer, ne' tracciare il pixel su schermo (comunque in ogni caso dobbiamo continuare ad interpolare sia la Z che altre eventuali variabili). gESTIONE dELLE tELECAMERE iN uNA sCENA 3d ========================================================================== bSP tREES ========================================================================== bUMP mAPPING 2d ========================================================================== Nel texture mapping abbiamo la possibilta' di rivestire un oggetto con un'immagine qualunque, quindi la superficie dell'oggetto sara' data dalla texture ad essa applicata, quindi la superficie puo' considerarsi liscia, ovvero piana, completamente piatta. Il bump mapping ha lo scopo di rendere una texture "pseudotridimensionale", il che significa che l'immagine e' solida (e non piatta) a livello ottico, ma in realta' viene elaborata come uno strumento 2D (in pratica non c'e' nessun asse z per la texture). Per creare l'effetto "simil 3D" applichiamo sull'immagine ombre e luci, utilizzando casomai una sorgente di luce in movimento. Per realizzare il bump mapping dovremo utilizzare una texture un po' particolare, la quale deve avere una determinata disposizione dei colori. Mettiamo caso che la nostra immagine utilizzi 256 colori, partendo dal primo colore sino al duecentocinquantaseiesimo specifichiamo le relative componenti RGB; nel nostro caso il primo colore dovra' essere il piu' scuro, il successivo dovra' essere quello un po' meno scuro, il terzo quello poco piu' chiaro del secondo e cosi' via, in modo tale che l'ultimo colore sia il piu' chiaro di tutti. La texture dovra' trovarsi in memoria secondo una successione continua e ordinata di righe (ovvero: ogni riga di pixel dovra' trovarsi esattamente nella locazione di memoria successiva rispetto alla locazione in cui e' presente l'ultimo pixel della riga precedente). Inoltre ogni pixel dovra' essere rappresentato come un singolo byte. Premesso cio', possiamo fare alcune osservazioni. Facciamo finta che la texture da "bumpare" sia la rappresentazione di una catena montuosa vista da un aereo, la cui tavolozza di colori e' composta da 256 tonalita' di grigio. Prendiamo un pixel qualunque di questa immagine, il pixel chunky e' un byte, quindi un valore compreso tra 0 e 255; tanto questo valore sara' maggiore e tanto quel pixel risultera' chiaro (come colore). Un pixel piu' e' chiaro e piu' riflettera' luce; i punti che riflettono piu' luce sono quelli maggiormente vicini alla sorgente di luce: in sostanza possiamo affermare che piu' e' alto il valore del pixel chunky e piu' questo risultera' vicino alla sorgente di luce; al contrario possiamo dire che piu' e' basso il valore del pixel e piu' sara' lontano dalla sorgente di luce. Adesso vediamo come realizzare praticamente il bump mapping. L'algoritmo si puo' suddividere principalmente in 2 parti: nella prima svolgiamo un precalcolo, ovvero calcoliamo una tabella in memoria; la seconda parte consiste nel calcolo vero e proprio della texture "bumpata", la quale parte (a differenza della prima) viene svolta in tempo reale. Per una singola texture, il precalcolo della tabella viene svolto solamente una volta. La tabella occupa esattamente 4 volte lo spazio occupato dalla texture. Per ricavarla dobbiamo fare l'intera scansione della nostra immagine. Supponiamo che P1 sia l'attuale pixel chunky scansito, che P2 sia il successivo punto sulla destra di P1 e che P3 sia esattamente quello posto al di sotto di P1, quindi le coordinate di questi 3 punti saranno: P1( x , y ) <- punto attuale P2( x+1 , y ) <- punto sulla destra di P1 P3( x , y+1 ) <- punto al di sotto di P1 Ecco i calcoli da eseguire: px = P1-P2 <- differenza di pixel chunky py = P1-P3 <- differenza di pixel chunky ox = h*sin(px) oy = h*sin(py) of = oy*tx+ox <- valore della tabella da precalcolare tx = 256 <- larghezza della texture in pixel h <- valore arbitrario tra 0 e 255 Da notare che px e py coprono un range di valori tra -255 e +255. Nel nostro caso -255 rappresenta -pigreco/2 (ovvero -90 gradi), mentre +255 indica pigreco/2 (+90 gradi). Se avessimo una funzione seno che accettasse in ingresso un angolo in radianti, sin(px) sarebbe equivalente a: sin(px*512/PI). E' consigliabile calcolare una tabella di sin(x) in cui x varia da -90 gradi a +90 gradi formata da 512 valori. La tabella e' composta semplicemente da un insieme di of, e precisamente uno per ogni pixel dell'immagine. Of e' un valore a 32 bit, quindi per una texture 256*256 la tabella risulta lunga 256kb. Il valore e' il coefficiente di perturbazione, cioe' indica il grado dell'effetto bump. In altre parole tanto h sara' maggiore e tanto si notera' che la texture e' tridimensionale, tanto h sara' minore e tanto la texture risultera' piatta. E qui finisce la fase di precalcolo. Vediamo la parte da svolgere in tempo reale. Innanzitutto ci serve una ulteriore texture, la quale verra' utilizzata per "simulare" la luce applicata alla texture da bumpare. Questa immagine e' la riproduzione vera e propria della luce; in pratica rappresenta una serie di cerchi concentrici, di cui il cerchio piu' interno (quindi il piu' piccolo) e' riempito con il massimo valore attribuibile ad un pixel chunky (ovvero 255), mentre il cerchio piu' esterno (il piu' grande) e' riempito con il minor valore attribuibile al pixel chunky (che sarebbe 0). E' possibile utilizzare la stessa texture che si applica nel reflection mapping per simulare una sorgente di luce. Un buon esempio di questo tipo di immagini e' una sfera (di dimensioni arbitrarie) piu' luminosa al centro e resa scura ai bordi. Consideriamo di utilizzare 3 variabili (o registri) che puntano rispettivamente al buffer dove salvare la bump-texture (che puo' anche essere lo schermo chunky), alla texture che rappresenta la luce e alla tabella precalcolata. Per ogni byte del buffer della texture da calcolare noi preleviamo sequenzialmente un valore dalla tabella precalcolata (ossia un valore della tabella per ogni byte del buffer). In seguito utilizziamo questo valore come offset da applicare al puntatore della texture che rappresenta la luce (ovvero addizioniamo al valore della tabella il puntatore della texture coincidente la sorgente di luce) e copiamo il relativo byte nel buffer destinazione. Dopo aver svolto il tutto per ogni pixel avremo ottenuto il bump mapping. aPPENDICE a: nOTAZIONE iN vIRGOLA fISSA ========================================================================== Nell'elaborazione di oggetti 3D si ha spesso a che fare con numeri reali, non interi. La maggior parte dei linguaggi evoluti permettono la diretta manipolazione di tali numeri, casomai sfruttando un eventuale coproccessore matematico oppure emulandoli via software. L'emulazione che apportano gli attuali compilatori risulta decisamente lenta per applicazioni in tempo reale, inoltre se si lavora in Assembly non si ha a disposizione la diretta gestione di numeri reali a meno che non venga utilizzato il coprocessore matematico. In alternativa alla FPU (che sfrutta numeri in virgola mobile) si puo' optare per il formato in virgola fissa che, nonostante una minore precisione rispetto al formato in virgola mobile, rimane la scelta migliore in quanto le operazioni svolte in tale formato risultano piu' veloci. In linea di principio, in un elaboratore elettronico, tutti i numeri (anche quelli reali) vengono rappresentati come interi, la notazione in virgola fissa si basa sulla diretta semplificazione di tale rappresentazione, vediamo come. Un numero reale viene rappresentato come quel valore intero dato dal prodotto del numero reale moltiplicato per una costante definita a priori. E' proprio da questa costante che dipende la precisione con la quale possono essere rappresentati numeri non interi. Ecco un esempio: 3.25 <- numero reale 256 <- costante 3.25*256 = 832 <- 3.25 in virgola fissa In questo modo possiamo rappresentare tutti i numeri reali con un discreto margine di errore che per le nostre applicazioni risulta praticamente ininfluente. E' conveniente utilizzare come costante una potenza di 2 (es.: 256, 65536), con la quale e' possibile velocizzare la manipolazione di numeri in tale notazione. Difatti e' noto che il computer rappresenta un qualsiasi numero come una sequenza di bit, quindi, utilizzando come costante una potenza di 2, e' possibile definire 2 campi di bit per ogni cifra: una dedicata alla parte intera mentre l'altra dedicata alla parte frazionaria. Se un numero in virgola fissa ha un numero di bit dedicati alla parte intera uguale ad e un numero di bit dedicati alla parte frazionaria uguale a , allora si dice che quel numero e' nel formato "a:b". Bisogna inoltre sfecificare che la parte intera di una cifra in virgola fissa appartiene al campo di bit piu' alto, mentre la parte frazionaria appartiene al campo di bit piu' basso. 3.25 <- numero reale 256=2^8 <- costante 832 <- 3.25 in virgola fissa (ovvero 3.25*256) 8:8 <- formato del numero in virgola fissa. Se utilizziamo 1 word (16 bit) abbiamo gli 8 bit piu' significativi dedicati alla parte intera e gli altri 8 bit meno significativi per quella frazionaria. Vediamo come convertire un numero intero nel formato in virgola fissa e viceversa: numero intero = (numero virgola fissa) / (costante) numero virgola fissa = (numero intero) * (costante) Infine occupiamoci di comprendere come effettuare le 4 operazioni con tali numeri: (a:b) + (c:d) = impossibile!! (a:b) + (a:b) = a:b (a:b) * (c:d) = (a+c):(b+d) (a:b) / (c:d) = (a-c):(b-d) Ci accorgiamo subito che risulta impossibile sommare 2 numeri in virgola fissa di diverso formato, bisogna prima rendere omogenee le 2 cifre (significa che le 2 cifre devono avere lo stesso formato). Da notare che una qualsiasi cifra intera puo' essere intesa come un numero in virgola fissa nel formato "a:0"; quindi e' possibile svolgere direttamente moltiplicazioni e divisioni tra numeri in virgola fissa ed interi. aPPENDICE b: cOORDINATE pOLARI ========================================================================== Come gia' sappiamo, per rappresentare un generico punto su di un piano possiamo utilizzare gli assi cartesiani. Le componenti x ed y non rappresentano altro che le proiezioni del nostro punto sull'ascissa e sull'ordinata. Immaginiamo invece di voler indicare un punto utilizzando un altro sistema di riferimento, nel nostro caso le coordinate polari. ^ Consideriamo r la distanza tra il punto P e y | .P(x,y) l'origine, mentre t come l'angolo compreso tra il | / segmento OP ed il semiasse positivo x. | / E' possibile indicare qualsiasi punto utilizzando | r/ queste 2 variabili (r e t), le quali rappresentano | / proprio le coordinate polari. |/) t Per ogni P(x,y) corrisponde un P'(r,t). Vediamo +------------> come effettuare queste conversioni. O x Sia R(x,0) la proiezione di P(x,y) sull'asse x ^ (ovvero il punto di ordinata 0 e di equivalente y | .P(x,y) ascissa di P). Il triangolo ORP e' rettangolo in R, | /| quindi per il teorema di Pitagora: | / | | r/ | r = OP = sqrt( x*x + y*y ) | / | |/) t | Possiamo anche affermare che: +-----+------> O R x PR = r*sin(t) => sin(t) = PR/r OR = r*cos(t) => cos(t) = OR/r Se facciamo attenzione possiamo affermare che (considerando il punto P(x,y)) PR=y e OR=x. La tangente di un angolo e' equivalente al rapporto del seno di quell'angolo col relativo coseno, quindi: tan(t) = sin(t)/cos(t) = (PR/r)/(OR/r) = PR/OR = x/y x/y = tan(t) t = arctan(x/y) r = sqrt(x*x+y*y) x = r*cos(t) y = r*sin(t) Ora sappiamo come convertire le coordinate cartesiane in polari e viceversa, cio' risultera' utile per comprendere come effettuare la rotazione di un punto. aPPENDICE c: gESTIONE dEGLI oGGETTI ========================================================================== Vogliamo tracciare il nostro oggetto su schermo sia esso in wireframe, gouraud shading, texture mapping o in altra tecnica di rendering che piu' ci va a genio; sappiamo perfettamente come visualizzare una singola faccia, ma come potremmo gestire tutte le facce che compongono il solido tridimensionale? Bisogna definire un "formato" con cui l'oggetto risiede in memoria, in base al quale potremo visualizzare qualsiasi figura 3D utilizzando sempre le stesse routine che compongono il nostro motore 3D. Iniziamo col vedere un esempio pratico: V5 _____________ V6 teniamo conto di dover definire come /| /| oggetto un semplice cubo, per prima / | / | cosa potremmo indicarne il numero di / | / | vertici e di facce di cui e' composto; / | / | in seguito elenchiamo tutte le V1 /____|_______ /V2 | coordinate (x,y,z) dei vertici del cubo; | | | | infine indichiamo le caratteristiche di | V8|_______|_____|V7 tutte le facce. Nel caso piu' semplice | / | / per definire una faccia basta indicarne | / | / i vertici (casomai ordinati in senso | / | / orario per facilitare la rimozione | / | / dell'hidden face e il calcolo della |/____________|/ normale). Ecco come e' possibile V4 V3 definire in memoria un cubo: 8 <- numero di vertici dell'oggetto 6 <- numero di facce dell'oggetto -50,-50,-50 <- coordinate x,y,z del vertice V1 +50,-50,-50 <- coordinate x,y,z del vertice V2 +50,+50,-50 <- coordinate x,y,z del vertice V3 -50,+50,-50 <- coordinate x,y,z del vertice V4 -50,-50,+50 <- coordinate x,y,z del vertice V5 +50,-50,+50 <- coordinate x,y,z del vertice V6 +50,+50,+50 <- coordinate x,y,z del vertice V7 -50,+50,+50 <- coordinate x,y,z del vertice V8 1,2,3,4 <- indici al buffer dei vertici della faccia 1 2,6,7,3 <- indici al buffer dei vertici della faccia 2 6,5,8,7 <- indici al buffer dei vertici della faccia 3 5,1,4,8 <- indici al buffer dei vertici della faccia 4 5,6,2,1 <- indici al buffer dei vertici della faccia 5 4,3,7,8 <- indici al buffer dei vertici della faccia 6 Analizziamo come vengono definiti i poligoni, prendiamo la faccia 1: 1,2,3,4 <- significa che la faccia e' composta dai primi 4 punti posti nella lista dei vertici, ovvero: -50,-50,-50 <- coordinate x,y,z del vertice V1 +50,-50,-50 <- coordinate x,y,z del vertice V2 +50,+50,-50 <- coordinate x,y,z del vertice V3 -50,+50,-50 <- coordinate x,y,z del vertice V4 2,6,7,3 <- significa che la faccia e' composta dai lati delimitati dai punti 2 e 6, 6 e 7, 7 e 3 e dai punti 3 e 2. Ogni lato e' rappresentato dalla congiunzione lineare di 2 vertici, nell'esempio appena proposto i lati della faccia 1 sono i segmenti delimitati dal primo al secondo punto (V1 e V2), dal secondo al terzo (V2 e V3), dal terzo al quarto (V3 e V4) e dal quarto al primo punto (V4 e V1). Nel nostro caso ogni faccia e' un quadrilatero, naturalmente e' possibile realizzare il proprio motore 3D che utilizzi un diverso numero di lati, l'importante e' che ogni poligono sia convesso, altrimenti l'algoritmo di scan conversion risulterebbe decisamente piu' complesso di quello studiato nel paragrafo relativo al fill e scan line.