Contents
INTRODUZIONE
Original Author: Damiano Vitulli
Translation by: Click here
Vettori e normali... parole forti! Certamente la maggior parte di voi avrà studiato queste cose a scuola durante le lezioni di fisica e di geometria. Non vi sareste mai immaginati però che un giorno avreste usato queste nozioni per realizzare un motore grafico! Ad ogni modo non vi preoccupate, non dovrete riaprire i vostri vecchi e polverosi libri, in questa lezione spiegheremo in dettaglio i vettori e realizzeremo appositamente una libreria per gestirli.
La prima applicazione pratica dei vettori riguarda i calcoli per l'illuminazione della scena. Molti di voi si staranno chiedendo: a che serve illuminare la scena quando i nostri oggetti sono già uniformemente illuminati?
Per la verità la scena che abbiamo fino ad ora disegnato presenta un'illuminazione uniforme e poco realistica, illuminare gli oggetti significa disegnarli con zone lucenti e zone in ombra, esattamente come avviene nella realtà. La realtà è disordine e caos! Ricordatevelo sempre quando realizzate un motore 3d... il vostro codice sorgente deve comunque rimanere ordinato mi raccomando =)
Con questa lezione incrementeremo ulteriormente il realismo della scena.
NOZIONI FONDAMENTALI SU RETTE, VETTORI, VERSORI, NORMALI
Ok ragazzi, ora dobbiamo affrontare un po' di teoria, tenete a mente che quello che stiamo per studiare è essenziale per eseguire i calcoli per illuminare la scena quindi anche stavolta prendetevi la solita camomilla e preparatevi.
Sapete cosa sono le rette ed i segmenti? Spero di si. Spiego brevemente le nozioni fondamentali:
LINEA RETTA
Una retta è una linea infinita, non ha origine, né fine, ma ha una direzione. Essa può essere rappresentata da una semplice equazione y=ax+b.
Due rette aventi la stessa direzione sono Rette Parallele.
Due rette aventi direzioni che formano un angolo di 90 gradi sono Rette Perpendicolari.
SEGMENTO
Dati due punti appartenenti ad una retta qualunque A e B, lo spazio compreso tra di essi è un Segmento. Per rappresentare il segmento AB si usa mettere un trattino che sovrasta i suoi punti estremi, es .
I punti A e B possono essere percorsi in due modi distinti: da A verso B e da B verso A. Un segmento al quale è stato assegnato un verso si chiama Segmento Orientato e si rappresenta utilizzando una freccia che sovrasta i suoi due punti estremi, es .
VETTORE
Ed ecco finalmente la definizione di vettore:
(1) Il vettore è una grandezza che rappresenta tutto l'insieme dei segmenti orientati aventi lo stesso verso, la stessa lunghezza e facenti parte di rette aventi la stessa direzione.
Ciò significa che due vettori sono uguali se hanno la stessa direzione, lo stesso verso e la stessa lunghezza a prescindere dalla loro origine.
Quella appena spiegata è una definizione geometrica che difficilmente ci fa capire la vera utilità di un vettore. In realtà il concetto di vettore trova le sue radici nel campo della fisica, ci sono alcune grandezze che possono essere misurate utilizzando semplicemente un numero, le grandezze scalari, quali ad esempio la temperatura o la massa, altre grandezze invece, come la velocità o la forza, oltre alla componente numerica (che però può essere solo positiva) hanno anche una direzione orientata (direzione e verso). Queste grandezze sono chiamate appunto Vettori.
Graficamente un vettore è rappresentato come una freccia, che ne definisce la direzione ed il verso. La lunghezza della freccia rappresenta invece la componente numerica o modulo (Magnitudine).
Per identificare un vettore si usano diverse notazioni.
- Se noi identifichiamo l'origine di un vettore nel punto O e la sua fine nel punto P allora algebricamente possiamo identificare un vettore usando i suoi estremi: OP.
- La notazione più usata è comunque una lettera maiuscola con una freccia che la sovrasta, esempio: .
La componente numerica o modulo (magnitudine) di un vettore è invece rappresentato in questo modo: Modulo di =
L' ultima definizione importante è il Versore:
VERSORE
Un versore è un vettore con lunghezza pari a 1 Spesso nel nostro motore avremo bisogno di trasformare alcuni vettori in versori, questa operazione si chiama normalizzare un vettore. Per identificare un versore useremo una lettera minuscola con una freccia che la sovrasta, ad es.
NOZIONI FONDAMENTALI SUI VETTORI
Ora iniziamo ad organizzare le nostre strutture dati per gestire questi strani vettori! La cosa migliore da fare quando si aggiunge una funzionalità particolare al motore grafico è quella di creare una libreria apposita per rendere il codice il più pulito possibile. Aggiungiamo quindi nel nostro ambiente di sviluppo il file mat_vect.cpp ed il relativo header mat_vect.h. Ho deciso di inserire il prefisso "mat" appunto per catalogare meglio questo tipo di libreria come una libreria matematica.
Diamo ora un'occhiata a questa figura:
Il nostro vettore è stato scomposto in tre vettori paralleli agli assi e può essere quindi rappresentato in questo modo:
(2) = [(x2-x1),(y2-y1),(z2-z1)]
Quindi:
Questa appena scritta si chiama Rappresentazione Cartesiana di un vettore.
Un altro modo per rappresentare un vettore è
dove , , sono i versori degli assi.
Ed ecco finalmente un pò di codice...
In base alla (3) creiamo subito la nostra struttura dati di tipo vettore:
typedef struct { float x,y,z; } p3d_type, *p3d_ptr_type;
Abbiamo identificato il vettore come un semplice punto 3d, perchè? Beh, grazie alla (1) possiamo considerare tutti vettori aventi come punto di partenza l'origine degli assi. Ricordate? Due vettori sono uguali se hanno la stessa direzione, lo stesso verso e la stessa lunghezza a prescindere dalla loro origine. Questo ci semplificherà di molto il lavoro.
CREAZIONE, NORMALIZZAZIONE E CALCOLO LUNGHEZZA
Per creare un vettore basta partire dalla formula (2). Creiamo una funzione in grado di accettare come parametri due punti nello spazio tridimensionale e di generare un vettore normalizzato.
void VectCreate (p3d_ptr_type p_start, p3d_ptr_type p_end, p3d_ptr_type p_vector) { p_vector->x = p_end->x - p_start->x; p_vector->y = p_end->y - p_start->y; p_vector->z = p_end->z - p_start->z; VectNormalize(p_vector); }
Tralasciamo per ora la funzione VectNormalize, che spiegheremo più avanti.
Per calcolare la lunghezza di un vettore (il modulo, la magnitude) dobbiamo usare due volte il Teorema di Pitagora, ed ora non mi venite a dire che non ve lo ricordate o non ne avete mai sentito parlare! Ve lo riassumo velocemente:
dato un triangolo rettangolo (con un lato di 90°) il quadrato dell'ipotenusa è uguale alla somma dei quadrati costruiti sui cateti.
Ora è necessaria un pò di fantasia... date un'occhiata al disegno, abbiamo creato un vettore che parte dall'origine degli assi:
Per trovare la lunghezza del vettore , ovvero il suo modulo = OP, applichiamo il teorema di pitagora al triangolo OPQ:
OP = Radice Quadrata di (Ay*Ay + OQ*OQ)
Ora Ay è noto, mentre per calcolare OQ dobbiamo applicare nuovamente il teorema di pitagora usando il triangolo OQR:
OQ * OQ = (Ax*Ax + Az*Az)
Applichiamo questa espressione alla equazione precedente e troviamo la lunghezza del vettore:
OP = Radice Quadrata di (Ax*Ax + Ay*Ay + Az*Az)
Ed ecco la funzione in C:
float VectLenght (p3d_ptr_type p_vector) { return (float)(sqrt(p_vector->x*p_vector->x + p_vector->y*p_vector->y + p_vector->z*p_vector->z)); }
Abbiamo già detto che normalizzare un vettore significa rendere la sua lunghezza uguale ad 1, ovvero è necessario dividere tutte le componenti Ax, Ay, Az del vettore stesso per la sua lunghezza.
void VectNormalize(p3d_ptr_type p_vector) { float l_lenght; l_lenght = VectLenght(p_vector); if (l_lenght==0) l_lenght=1; p_vector->x /= l_lenght; p_vector->y /= l_lenght; p_vector->z /= l_lenght; }
SOMMA E DIFFERENZA
Ci sono 4 operazioni fondamentali che si possono fare con i vettori: somma, differenza, prodotto scalare e prodotto vettoriale.
Somma di due vettori: Dati due vettori e la loro somma è un altro vettore , che è ottenuto applicando al vertice di l'origine di e congiungendo con una linea l'origine di con il vertice di .
Se rappresentiamo i due vettori secondo la notazione (3) allora la somma di due vettori può anche essere ottenuta con il metodo analitico:
+ = (Ax + Bx, Ay + By, Az + Bz)
Differenza di due vettori: La differenza di due vettori è un caso particolare della somma e si ottiene applicando al vertice di l'origine di - e congiungendo con una linea l'origine di con il vertice di -.
Non creeremo funzioni apposite per la somma e la differenza dei vettori perché per ora non ci sono utili.
PRODOTTO SCALARE
Per quanto riguarda la moltiplicazione tra vettori abbiamo due modalità:
- la prima è chiamata prodotto scalare il cui risultato restituisce un numero.
- la seconda è il prodotto vettoriale il cui risultato è invece un vettore.
Dati due vettori e il prodotto scalare si calcola moltiplicando il prodotto dei moduli dei due vettori per il coseno dell'angolo da essi formato.
In base a questa definizione è facile arrivare a queste relazioni di ortogonalità con i versori degli assi:
Esprimiamo ora due vettori come da definizione (4):
E calcoliamo il prodotto scalare:
dot = (Ax + Ay + Az) dot (Bx + By + Bz)
In base alla (5) ed alla (6) possiamo ricavare l'espressione del prodotto scalare secondo il metodo analitico.
Usando questa espressione creiamo la funzione VectScalarProduct
float VectScalarProduct (p3d_ptr_type p_vector1,p3d_ptr_type p_vector2) { return (p_vector1->x*p_vector2->x + p_vector1->y*p_vector2->y + p_vector1->z*p_vector2->z); }
Più facile di così!
PRODOTTO VETTORIALE
Dopo tutte queste nozioni vi starete senz'altro chiedendo cosa farvene di tutto ciò... ancora un pò di fatica ragazzi frà un pò metteremo tutto insieme è raggiungerete finalmente la tanto agognata "illuminazione" =)
Ed ecco finalmente il prodotto vettoriale...
Dati due vettori e si definisce prodotto vettoriale il vettore le cui componenti scalari e vettoriali sono calcolate in questo modo:
- La sua lunghezza è data dal prodotto dei moduli dei vettori e moltiplicato per il seno dell'angolo da essi formato, = x = AB sin().
- La sua direzione è ortogonale al piano formato da e e verso levogiro.
Come abbiamo già fatto per il prodotto vettoriale ricaviamoci le relazioni di ortogonalità con i versori degli assi:
Esprimiamo ora due vettori come da definizione (4):
x = (Ax + Ay + Az) x (Bx + By + Bz)
In base alle (7) e (8) dopo vari passaggi possiamo ricavare l'espressione del prodotto scalare secondo il metodo analitico.
x = (AyBz - AzBy) + (AzBx - AxBz) + (AxBy - AyBx)
Usando questa espressione creiamo la nostra funzione VectDotProduct
void VectDotProduct (p3d_ptr_type p_vector1,p3d_ptr_type p_vector2,p3d_ptr_type p_vector3) { p_vector3->x=(p_vector1->y * p_vector2->z) - (p_vector1->z * p_vector2->y); p_vector3->y=(p_vector1->z * p_vector2->x) - (p_vector1->x * p_vector2->z); p_vector3->z=(p_vector1->x * p_vector2->y) - (p_vector1->y * p_vector2->x); }
Wow, che sudata questi vettori! E' sembrata una vera e propria lezione universitaria non trovate? Ora rilassatevi e scatenate la vostra fantasia perchè ora ne combineremo di tutti i colori!
ILLUMINIAMO LA SCENA!
Ora è venuto il momento di svelare gli steps principali per effettuare una corretta illuminazione.
- Prima di tutto è necessario definire ed attivare almeno un punto luce nello spazio tridimensionale.
- Per ogni poligono nella scena bisogna determinare la quantità di luce che è in grado di trasmettere agli occhi della nostra telecamera virtuale.
- Nella fase di rendering coloriamo i vari poligoni in base al loro valore di illuminazione. Infatti per quanto ci riguarda, illuminare un oggetto significa disegnare i vari poligoni che lo costituiscono usando colori più o meno accesi.
Procediamo ora ad analizzare in dettaglio i vari punti.
DEFINIZIONE ED ATTIVAZIONE DI PUNTI LUCE
Ci sono due fattori principali da considerare per implementare l'illuminazione: le caratteristiche del punto luce e le caratteristiche del materiale
OpenGL considera la luce costituita da 3 componenti fondamentali: ambiente, diffusa e speculare.
- La componente ambiente è quel tipo di luce la cui direzione è impossibile da determinare poiché sembra provenire da tutte le direzioni. Si tratta perlopiù di luce riflessa varie volte. Un poligono viene sempre uniformemente illuminato dalla luce ambiente, non importa quale sia la sua orientazione e la sua posizione nello spazio.
- La componente diffusa è la luce che proviene da una direzione. A seconda dell'orientamento con il quale è inclinato rispetto alla sorgente di luce il poligono viene illuminato più o meno intensamente. Se il poligono si trova perpendicolare rispetto alla sorgente di luce allora viene illuminato al massimo viceversa nel caso il poligono è orientato parallelamente alla linea che lo collega alla sorgente di luce. E' interessante notare che la componente diffusa considera solo il grado di inclinazione del poligono rispetto alla sorgente di luce e non tiene in considerazione la posizione dell'osservatore.
- La componente speculare invece rispetta il grado di inclinazione del poligono ed anche la posizione dell'osservatore. La luce speculare proviene da una direzione e viene riflessa dal poligono secondo la sua inclinazione.
Torniamo ora nel nostro file main.c... Per ogni sorgente di luce dobbiamo specificare le varie componenti che la costituiscono (ambiente, diffusa, speculare):
GLfloat light_ambient[]= { 0.1f, 0.1f, 0.1f, 0.1f }; GLfloat light_diffuse[]= { 1.0f, 1.0f, 1.0f, 0.0f }; GLfloat light_specular[]= { 1.0f, 1.0f, 1.0f, 0.0f };
Come potete notare ogni componente è costituita da 4 valori float che rappresentano la tripletta RGB ed il canale alpha.
Abbiamo appena definito una luce bianca con poca componente ambiente e una massima componente diffusa e speculare. Esattamente quello che avviene nello spazio non trovate?
Specifichiamo la posizione nello spazio della sorgente di luce:
GLfloat light_position[]= { 100.0f, 0.0f, -10.0f, 1.0f };
Indichiamo ora ad OpenGL la sorgente di luce tramite la funzione glLightfv:
glLightfv (GL_LIGHT1, GL_AMBIENT, light_ambient); glLightfv (GL_LIGHT1, GL_DIFFUSE, light_diffuse); glLightfv (GL_LIGHT1, GL_SPECULAR, light_specular); glLightfv (GL_LIGHT1, GL_POSITION, light_position);
- void glLightfv( GLenum light, GLenum pname, const GLfloat *params ); assegna i valori dei parametri ad una singola sorgente di luce. Il primo argomento light è un nome simbolico per il punto luce, l'argomento pname specifica il parametro da modificare e può essere GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, GL_POSITION o altri, il terzo argomento è un vettore che specifica il valore da assegnare.
Attiviamo la sorgente di luce...
glEnable (GL_LIGHT1);
Infine ricordiamoci di attivare la gestione delle luci OpenGL
glEnable (GL_LIGHTING);
DETERMINAZIONE DELLA CAPACITA' DI RIFLESSIONE DEI POLIGONI
I MATERIALI
Anche i poligoni, come le luci, hanno delle proprietà fondamentali. I materiali che li costituiscono infatti possono avere una differente capacità di riflettere la luce. Per questo OpenGL permette di definire le caratteristiche del materiale utilizzato per costruire la scena.
Il materiale può avere 4 componenti fondamentali: ambiente, diffusa, speculare e emissiva.
Le prime tre proprietà rappresentano le capacità del materiale di riflettere le varie componenti di luce che abbiamo già spiegato nel precedente paragrafo, mentre l'ultima proprietà, la componente emissiva, è molto utile in quei casi in cui si vogliano simulare lampade accese. Considerate comunque che tale componente non abilita un altra sorgente di luce, l'oggetto viene illuminato senza essere influenzato dalle vere sorgenti di luce, come se brillasse di luce propria.
Definiamo ora un materiale con tutte le sue componenti fondamentali:
GLfloat mat_ambient[]= { 0.2f, 0.2f, 0.2f, 0.0f }; GLfloat mat_diffuse[]= { 1.0f, 1.0f, 1.0f, 0.0f }; GLfloat mat_specular[]= { 0.2f, 0.2f, 0.2f, 0.0f }; GLfloat mat_shininess[]= { 1.0f };
ed attiviamolo tramite la funzione glMaterialfv.
glMaterialfv (GL_FRONT, GL_AMBIENT, mat_ambient); glMaterialfv (GL_FRONT, GL_DIFFUSE, mat_diffuse); glMaterialfv (GL_FRONT, GL_SPECULAR, mat_specular); glMaterialfv (GL_FRONT, GL_SHININESS, mat_shininess);
- void glMaterialfv( GLenum face, GLenum pname, const GLfloat *params ); Questa funzione assegna i valori ai parametri del materiale. L'argomento face può essere GL_FRONT, GL_BACK or GL_FRONT_AND_BACK, il secondo argomento specifica il parametro oggetto della modifica e può essere GL_AMBIENT, GL_DIFFUSE, GL_SPECULAR, GL_SHININESS, GL_EMISSION, il terezo argomento è un array che specifica i valori da assegnare al parametro.
Abbiamo creato un materiale con una forte componente speculare e con una bassa componente diffusa e ambiente. Abbiamo simulato il comportamento di un materiale metallico illuminato da una sorgente di luce nello spazio profondo! ;)
INCLINAZIONE DEI POLIGONI RISPETTO AL PUNTO LUCE
Oltre alle caratteristiche fisiche del materiale l'elemento più importante nell'illuminazione è il grado di inclinazione dei poligoni rispetto al punto luce. E' chiaro infatti che più il piano del poligono è disposto perpendicolarmente rispetto al punto luce, più risulta illuminato.
Per calcolare il grado di inclinazione è necessario per prima cosa calcolare il vettore normale del poligono.
Il vettore normale di un poligono è un vettore di dimensione unitaria disposto ortogonalmente al piano formato dal poligono.
Per calcolare il vettore normale (anche chiamato solo "normale") del poligono basta effettuare il prodotto vettoriale di due suoi vettori complanari (si possono prendere ad esempio due suoi lati per creare due vettori complanari) ed in seguito normalizzare il vettore ottenuto.
In seguito è necessario creare un altro vettore la cui origine corrisponde al punto luce e la cui fine coincide con l'origine del vettore normale al poligono. Il vettore risultante dovrà essere normalizzato.
Eseguito queste operazioni, per calcolare il grado di illuminazione basterà poi fare il prodotto scalare dei due vettori (normale poligono dot vettore luce)!
Niente di più facile! Il valore ottenuto ha un range compreso tra 0 ed 1 ed è quindi facilmente utilizzabile per colorare il nostro poligono (si può ad esempio utilizzarlo come fattore).
FLAT SHADING
Se applichiamo questo procedimento per ogni poligono abbiamo illuminato la scena in una modalità particolare chiamata FLAT SHADING. Questa modalità non è più utilizzata nei moderni motori grafici a causa del suo basso livello di realismo, i poligoni vengono infatti disegnati con un colore uniforme e, osservando l'oggetto nella sua totalità, si notano le separazioni di colore con i poligoni adiacenti.
GOURAUD SHADING
Una tecnica molto più realistica ma più dispendiosa in termini di calcolo si chiama GOURAUD SHADING, anche chiamata SMOOTH SHADING. La differenza principale di questa tecnica è che invece di calcolare le normali dei poligoni vengono calcolate le normali dei vertici degli oggetti.
A dire la verità un vettore normale ad un vertice è una definizione che non ha senso! Infatti abbiamo bisogno di un piano per calcolare una normale ed i vertici, essendo punti, non hanno piani di riferimento! La corretta definizione potrebbe invece essere "Media delle normali dei poligoni adiacenti al vertice".
Per calcolare il vettore normale ad un vertice bisogna calcolare la media aritmetica delle normali dei poligoni adiacenti al vertice.
Una volta calcolate le normali dei vertici il procedimento per il calcolo del coefficiente di illuminazione è lo stesso, tuttavia questa volta ogni poligono avrà tre coefficienti di illuminazione e non più uno solamente.
Cosa ce ne facciamo di tre coefficienti? Semplice, quando coloriamo il poligono applichiamo l'illuminazione eseguendo un'interpolazione lineare dei valori dei coefficienti agli estremi di ogni riga di scansione. Il risultato è un'illuminazione con variazioni graduali, molto più realistica.
Ed ora vi devo confessare una cosa: noi dovremo solamente calcolare le normali dei vertici del nostro oggetto, OpenGL si occuperà di eseguire il resto del lavoro disegnando la scena in GOURAUD SHADING! Contenti? Ma non ditelo troppo in giro...
CALCOLO DELLE NORMALI
In questo paragrafo realizzeremo una funzione in grado di calcolare tutte le normali dei vertici in un oggetto 3d. Effettueremo questo calcolo solo durante la fase di inizializzazione del' oggetto, durante il rendering ci occuperemo solamente di fornire questi dati ad OpenGL.
Per migliorare la leggibilità del codice creiamo un altro nuovo file e chiamiamolo object.c, creiamo anche il relativo header object.h. In tali file salveremo tutte le funzioni relative alla gestione degli oggetti. Per prima cosa aggiungiamo in object.h un array in grado di contenere tutte le normali dei suoi vertici:
vertex_type normal[MAX_VERTICES];
Ora apriamo il nostro file object.c e scriviamo:
void ObjCalcNormals(obj_type_ptr p_object) {
Creiamo le nostre variabili di appoggio:
int i; p3d_type l_vect1,l_vect2,l_vect3,l_vect_b1,l_vect_b2,l_normal; int l_connections_qty[MAX_VERTICES];
E resettiamo tutte le normali dei vertici:
for (i=0; i<p_object->vertices_qty; i++) { p_object->normal[i].x = 0.0; p_object->normal[i].y = 0.0; p_object->normal[i].z = 0.0; l_connections_qty[i]=0; }
Per ogni poligono nell'oggetto:
for (i=0; i<p_object->polygons_qty; i++) { l_vect1.x = p_object->vertex[p_object->polygon[i].a].x; l_vect1.y = p_object->vertex[p_object->polygon[i].a].y; l_vect1.z = p_object->vertex[p_object->polygon[i].a].z; l_vect2.x = p_object->vertex[p_object->polygon[i].b].x; l_vect2.y = p_object->vertex[p_object->polygon[i].b].y; l_vect2.z = p_object->vertex[p_object->polygon[i].b].z; l_vect3.x = p_object->vertex[p_object->polygon[i].c].x; l_vect3.y = p_object->vertex[p_object->polygon[i].c].y; l_vect3.z = p_object->vertex[p_object->polygon[i].c].z;
Creiamo i due vettori complanari utilizzando due lati del poligono:
VectCreate (&l_vect1, &l_vect2, &l_vect_b1); VectCreate (&l_vect1, &l_vect3, &l_vect_b2);
Calcoliamo ora il prodotto vettoriale:
VectDotProduct (&l_vect_b1, &l_vect_b2, &l_normal);
e normalizziamo il vettore risultante, ecco a coi la normale del poligono
VectNormalize (&l_normal);
Per ogni vertice condiviso con questo poligono incrementiamo il numero di connessioni:
l_connections_qty[p_object->polygon[i].a]+=1; l_connections_qty[p_object->polygon[i].b]+=1; l_connections_qty[p_object->polygon[i].c]+=1;
ed aggiungiamo la normale al poligono:
p_object->normal[p_object->polygon[i].a].x+=l_normal.x; p_object->normal[p_object->polygon[i].a].y+=l_normal.y; p_object->normal[p_object->polygon[i].a].z+=l_normal.z; p_object->normal[p_object->polygon[i].b].x+=l_normal.x; p_object->normal[p_object->polygon[i].b].y+=l_normal.y; p_object->normal[p_object->polygon[i].b].z+=l_normal.z; p_object->normal[p_object->polygon[i].c].x+=l_normal.x; p_object->normal[p_object->polygon[i].c].y+=l_normal.y; p_object->normal[p_object->polygon[i].c].z+=l_normal.z; }
Ora eseguiamo la media delle normali dei poligoni adiacenti per ottenere la normale del vertice!
for (i=0; i<p_object->vertices_qty; i++) { if (l_connections_qty[i]>0) { p_object->normal[i].x /= l_connections_qty[i]; p_object->normal[i].y /= l_connections_qty[i]; p_object->normal[i].z /= l_connections_qty[i]; } } }
Richiamiamo questa funzione durante l'inizializzazione del nostro oggetto. Nel file object.c inseriamo quindi un'altra funzione che permette di inizializzare l'oggetto in ogni suo aspetto:
char ObjLoad(char *p_object_name, char *p_texture_name) { if (Load3DS (&object,p_object_name)==0) return(0); object.id_texture=LoadBMP(p_texture_name); ObjCalcNormals(&object); return (1); }
ED ORA DISEGNAMO LA SCENA ILLUMINATA
L'ultima modifica da fare riguarda la routine di disegno. Dobbiamo infatti fornire ad OpenGL le normali dell'oggetto. La funzione che si occupa di definire una normale in OpenGL è
- void glNormal3f( GLfloat nx, GLfloat ny, GLfloat nz ); specifica le coordinate x, y, z della normale.
Ecco la nuova funzione di disegno:
glBegin(GL_TRIANGLES); for (j=0;j<object.polygons_qty;j++) { //----------------- FIRST VERTEX ----------------- //Normal coordinates of the first vertex glNormal3f( object.normal[ object.polygon[j].a ].x, object.normal[ object.polygon[j].a ].y, object.normal[ object.polygon[j].a ].z); // Texture coordinates of the first vertex glTexCoord2f( object.mapcoord[ object.polygon[j].a ].u, object.mapcoord[ object.polygon[j].a ].v); // Coordinates of the first vertex glVertex3f( object.vertex[ object.polygon[j].a ].x, object.vertex[ object.polygon[j].a ].y, object.vertex[ object.polygon[j].a ].z); //----------------- SECOND VERTEX ----------------- //Normal coordinates of the second vertex glNormal3f( object.normal[ object.polygon[j].b ].x, object.normal[ object.polygon[j].b ].y, object.normal[ object.polygon[j].b ].z); // Texture coordinates of the second vertex glTexCoord2f( object.mapcoord[ object.polygon[j].b ].u, object.mapcoord[ object.polygon[j].b ].v); // Coordinates of the second vertex glVertex3f( object.vertex[ object.polygon[j].b ].x, object.vertex[ object.polygon[j].b ].y, object.vertex[ object.polygon[j].b ].z); //----------------- THIRD VERTEX ----------------- //Normal coordinates of the third vertex glNormal3f( object.normal[ object.polygon[j].c ].x, object.normal[ object.polygon[j].c ].y, object.normal[ object.polygon[j].c ].z); // Texture coordinates of the third vertex glTexCoord2f( object.mapcoord[ object.polygon[j].c ].u, object.mapcoord[ object.polygon[j].c ].v); // Coordinates of the Third vertex glVertex3f( object.vertex[ object.polygon[j].c ].x, object.vertex[ object.polygon[j].c ].y, object.vertex[ object.polygon[j].c ].z); } glEnd();
CONCLUSIONE
Finalmente siamo arrivati alla conclusione, e siamo anche sopravissuti! Quello appena letto è stato un tutorial molto tecnico, ma abbiamo imparato nozioni teoriche molto importanti che ci saranno utili nei prossimi tutorials.
Nella prossima lezione introdurremo le matrici che ci permetteranno di gestire finalmente la posizione e l'orientamento degli oggetti nella scena.
Siete stanchi della rotazione casuale della vostra astronave? ;)
SOURCE CODE
The Source Code of this lesson can be downloaded from the Tutorials Main Page