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 Segment.png.

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 Oriented segment.png.

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).

Vector gr.png

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: Vector a.png.

La componente numerica o modulo (magnitudine) di un vettore è invece rappresentato in questo modo: Modulo di Vector a.png = Vector a magnitude.png

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. Versor a.png

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:

Vector def.png

Il nostro vettore è stato scomposto in tre vettori paralleli agli assi e può essere quindi rappresentato in questo modo:

(2) Vector a.png = [(x2-x1),(y2-y1),(z2-z1)]

Quindi:

(3) Vector a.png = (Ax,Ay,Az)

Questa appena scritta si chiama Rappresentazione Cartesiana di un vettore.

Un altro modo per rappresentare un vettore è

(4) Vector a.png = AxVersor i.png+AyVersor j.png+AzVersor k.png

dove Versor i.png, Versor j.png, Versor k.png 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:

Vector length.png

Per trovare la lunghezza del vettore Vector a.png, ovvero il suo modulo Vector a.png = 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 Vector a.png e Vector b.png la loro somma è un altro vettore Vector c.png, che è ottenuto applicando al vertice di Vector a.png l'origine di Vector b.png e congiungendo con una linea l'origine di Vector a.png con il vertice di Vector b.png.

Vector sum.png

Se rappresentiamo i due vettori secondo la notazione (3) allora la somma di due vettori può anche essere ottenuta con il metodo analitico:

Vector a.png + Vector b.png = (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 Vector a.png l'origine di -Vector b.png e congiungendo con una linea l'origine di Vector a.png con il vertice di -Vector b.png.

Vector difference.png

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à:

  1. la prima è chiamata prodotto scalare il cui risultato restituisce un numero.
  2. 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.

Vector a.png dot Vector b.png = Vector a magnitude.png * Vector b magnitude.png * cos (Alpha.png)

Vector scalar product.png

In base a questa definizione è facile arrivare a queste relazioni di ortogonalità con i versori degli assi:

(5) Versor i.png dot Versor i.png = Versor j.png dot Versor j.png = Versor k.png dot Versor k.png = 1

(6) Versor i.png dot Versor j.png = Versor j.png dot Versor k.png = Versor k.png dot Versor i.png = 0

Esprimiamo ora due vettori come da definizione (4):

Vector a.png = AxVersor i.png + AyVersor j.png + AzVersor k.png

Vector b.png = BxVersor i.png + ByVersor j.png + BzVersor k.png

E calcoliamo il prodotto scalare:

Vector a.png dot Vector b.png = (AxVersor i.png + AyVersor j.png + AzVersor k.png) dot (BxVersor i.png + ByVersor j.png + BzVersor k.png)

In base alla (5) ed alla (6) possiamo ricavare l'espressione del prodotto scalare secondo il metodo analitico.

Vector a.png dot Vector b.png = Ax*Bx + Ay*By + Az*Bz

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 Vector a.png e Vector b.png si definisce prodotto vettoriale il vettore Vector c.png le cui componenti scalari e vettoriali sono calcolate in questo modo:

  1. La sua lunghezza è data dal prodotto dei moduli dei vettori Vector a.png e Vector b.png moltiplicato per il seno dell'angolo da essi formato, Vector c.png = Vector a.png x Vector b.png = AB sin(Alpha.png).
  2. La sua direzione è ortogonale al piano formato da Vector a.png e Vector b.png e verso levogiro.

Vector vector product.png

Come abbiamo già fatto per il prodotto vettoriale ricaviamoci le relazioni di ortogonalità con i versori degli assi:

(7) Versor i.png x Versor i.png = Versor j.png x Versor j.png = Versor k.png x Versor k.png = 0

(8) Versor i.png x Versor j.png = Versor k.png Versor j.png x Versor k.png = Versor i.png Versor k.png x Versor i.png = Versor j.png

Esprimiamo ora due vettori come da definizione (4):

Vector a.png = AxVersor i.png + AyVersor j.png + AzVersor k.png

Vector b.png = BxVersor i.png + ByVersor j.png + BzVersor k.png

Vector a.png x Vector b.png = (AxVersor i.png + AyVersor j.png + AzVersor k.png) x (BxVersor i.png + ByVersor j.png + BzVersor k.png)

In base alle (7) e (8) dopo vari passaggi possiamo ricavare l'espressione del prodotto scalare secondo il metodo analitico.

Vector a.png x Vector b.png = (AyBz - AzBy)Versor i.png + (AzBx - AxBz)Versor j.png + (AxBy - AyBx)Versor k.png

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.

  1. Prima di tutto è necessario definire ed attivare almeno un punto luce nello spazio tridimensionale.
  2. Per ogni poligono nella scena bisogna determinare la quantità di luce che è in grado di trasmettere agli occhi della nostra telecamera virtuale.
  3. 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)!

Flat shading.png

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.

Gouraud shading.png

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