INTRODUZIONE

In questa lezione introdurremo le Matrici, una tecnica matematica che sarà molto utile al nostro engine, ed aggiungeremo la possibilità di caricare più oggetti.

A cosa servono le Matrici?

Le Matrici ed i Determinanti vengono maggiormente usati nell’algebra lineare e nei calcoli relativi agli spazi vettoriali e alle loro trasformazioni lineari. Matrici e Determinanti sono anche utili per rappresentare sistemi di equazioni lineari.

Ti è piaciuta questa definizione? Cosa dici? Non hai capito nulla?

Tranquillo, la tua sensazione è perfettamente comprensibile. Quella appena scritta è infatti la tipica definizione che si trova comunemente in un qualsiasi testo matematico ed, ahimè, anche nella maggior parte delle fonti presenti attualmente su internet. Prima di scrivere questo tutorial confesso di aver rispolverato qualche testo scolastico ed ho fatto alcune ricerche sul web cercando qualche informazione sul calcolo Matriciale. La scuola è finita da un pezzo per me e quindi la mia memoria aveva bisogno di un refresh. I risultati delle mie ricerche sono stati, come prevedibile, molto deludenti: non sono riuscito a trovare alcuna fonte che spiegasse chiaramente, cosi come piace a me, l'utilità delle matrici.

E' facile immaginare quindi le difficoltà che deve affrontare giornalmente uno sfortunato programmatore grafico durante la stesura del proprio Engine. Tuttavia sono contento di informarti che la sfortuna è finita! Ora ci sono i tutorial di Spacesimulator.net! La parola chiave per me è: passo dopo passo! Quindi rilassati e goditi la lezione, qui sei nel posto giusto!

UTILITA' DELLE MATRICI

La prima cosa da dire sulle matrici quindi sarà:

Le Matrici sono oggetti matematici fantastici che ci permettono di Traslare, Ruotare e Scalare a nostro piacimento ogni singolo oggetto del nostro mondo tridimensionale.

Meglio vero?

Cosa significa Traslare, Ruotare e Scalare? Presto detto:

  • Traslare: Muovere un oggetto, spostandolo da un punto dello spazio ad un altro mantenendo l'orientamento originale dell'oggetto stesso.
  • Ruotare: Ruotare un oggetto tramite uno specifico centro di rotazione (in genere il centro dell'oggetto).
  • Scalare: Cambiare le dimensioni dell'oggetto, ad esempio ingrandirlo o rimpicciolirlo. Anche questa operazione sarà fatta considerando il centro dell'oggetto come punto di partenza.

I BENEFICI DELLE MATRICI

Ora che ci è chiara l'utilità delle matrici potrebbe sorgere una domanda: esistono altri modi per eseguire le trasformazioni di Traslazione, Rotazione e Scaling oltre alle Matrici? La risposta è: Si, esistono altri metodi ed alcuni di questi sono anche più semplici da implementare rispetto alle Matrici.

Per eseguire una traslazione di un oggetto ad esempio, ovvero un banale movimento nello spazio, potresti pensare che sia molto più semplice aggiungere ad ogni coordinata di ciascun vertice la quantità di spostamento desiderata. Anche per eseguire le rotazioni esistono altre modalità, alcune delle quali sono più semplici delle Matrici.

Perchè allora utilizzare proprio le Matrici?

Per prima cosa devi considerare che per ovvie questioni di praticità e di pulizia del codice conviene trovare un comune metodo per eseguire tutte e tre le operazioni di Traslazione, di Rotazione e di Scaling. Secondo, la trasformazione più complicata tra le tre è la Rotazione quindi andiamo ad analizzare quali metodologie esistono per ruotare un oggetto tridimensionale:

  1. Axis/Angle. Questa modalità permette di ruotare un oggetto impostando un vettore arbitrario ed un grado di rotazione. La sua implementazione è piuttosto semplice ma soffre di un gravissimo problema che si chiama Gimbal Lock. Questo effetto avviene quando le rotazioni su differenti assi sono concatenate in modo tale che viene perso un grado di libertà, ad esempio quando due assi di rotazione vengono a coincidere.
  2. Matrici. Le Matrici non soffrono di Gimbal Lock e permettono di effettuare qualunque trasformazione in successione e memorizzare lo stato spaziale completo dell'oggetto dentro un'unica struttura: la Matrice, appunto. L'unico lato negativo è che per implementare questa tecnica sarà necessario sviluppare una libreria apposita, niente di complicato, solo un pò di lavoro. Inoltre OpenGL supporta nativamente le Matrici.
  3. Quaternioni. Questa modalità è quella più complessa da implementare. Non soffre di problemi di Gimbal Lock e permette anche di effettuare rotazioni e traslazioni in maniera molto più morbida rispetto alle matrici. Viene perlopiù utilizzata per interpolare i vari keyframes relativi ad animazioni tridimensionali tramite tecnica SLERP (Spherical Linear Interpolation). Si tratta di una tecnica molto complessa che sarà analizzata in dettaglio durante la lezione riguardante il Loader 3DS Avanzato (Inserirò tale tutorial nel libro).

Quindi il motivo per cui utilizzeremo le Matrici è perchè queste ultime hanno il miglior compromesso tra facilità d'uso e funzionalità.

Ora è giunto il momento di passare alla teoria sulle Matrici e cercherò di fare del mio meglio esponendo i concetti fondamentali ed approfondendo solo quelli che saranno veramente utili al nostro Engine.

Prepara il tuo drink da programmatore, stavolta ti consiglio un ottimo succo di frutta energetico, e passiamo al sodo.

NOZIONI FONDAMENTALI SULLE MATRICI

Le Matrici sono diavolerie matematiche che sono state inventate per aiutare a risolvere sistemi di equazioni lineari. Ti ricordi cosa sono i sistemi di equazioni lineari? I sistemi di equazioni lineari sono insiemi di equazioni di grado unitario come il seguente:

Tut Matrices System of Equations.png

Per risolvere tale sistema dobbiamo trovare una x, una y ed una z che risolvono contemporaneamente tutte le equazioni.

Quando ci troviamo a risolvere un sistema di equazioni semplice, come quello appena esposto, possiamo utilizzare varie tecniche la più famosa delle quali è la "sostituzione". Ma per equazioni di 4 e più variabili risolvere questi sistemi diventa troppo complicato. Per nostra fortuna i matematici hanno inventato una tecnica di rappresentazione delle equazioni lineari che permette di risolvere queste ultime in maniera relativamente più semplice: le Matrici.

DEFINIZIONE DI MATRICE

A questo punto daremo una definizione più corretta alle Matrici:

Le Matrici sono degli insiemi di numeri disposti in modo rettangolare. Possiamo immaginare le matrici come delle tabelle con numero di righe e colonne arbitrario.

Possiamo rappresentare le matrici in due modalità, o una lettera maiuscola oppure una tabella con dei numeri.

Il sistema precedente, grazie a queste fantastiche Matrici, può essere rappresentato come:

AX=B

Molto meglio no? Ma cosa vuol dire? Cosa sono i termini A, X e B?

Te lo dico io: i termini A, X e B sono Matrici ciascuna delle quali rappresenta una componente del precedente sistema di equazioni. In particolare: A rappresenta i coefficienti numerici delle equazioni, X i termini da trovare, B il risultato delle equazioni.

Vediamo ora come rappresentare le Matrici corrispondenti:

Tut Matrices A.png       Tut Matrices X.png       Tut Matrices B.png

Bene, abbiamo suddiviso il sistema di equazioni lineari in tre blocchi numerici, come facciamo ora a risolvere il sistema?

Torniamo ad utilizzare le Matrici in base alla notazione di prima, per risolvere tale sistema sarà necessario fare il seguente passaggio:

AX=B

X=B/A

X=A-1*B

Dall'ultima riga si deduce che per trovare la X basterà ottenere l'inverso della matrice A e moltiplicare il risultato per B.

Ciò implica che occorrerà saper calcolare l'inverso di una Matrice e saper moltiplicare due Matrici. Il risultato di questa operazione saranno i valori da assegnare alla Matrice X.

Come calcolare l'inverso di una matrice? Come si moltiplicano due Matrici?

La buona notizia è che nel prossimo capitolo vedremo come effettuare la moltiplicazione tra due Matrici, la cattiva notizia è invece che calcolare l'inverso di una Matrice è fuori dallo scopo di questa lezione. Cosa dici? Sei forse scontento? In caso affermativo calma i tuoi bollenti spiriti! ;) Hai forse intenzione di risolvere i sistemi lineari? Francamente, spero di no! ;)

Per effettuare le trasformazioni tanto agognate basterà saper moltiplicare due matrici e conoscere altre poche nozioni teoriche. Lascio a te mio caro lettore la facoltà di documentarti ulteriormente sul calcolo Matriciale.

LE TIPOLOGIE DI MATRICI

Ora entriamo un po' più in dettaglio con la definizione di Matrice. Abbiamo detto che una Matrice è un insieme di numeri disposti in una tabella. E fino qui ci siamo direi. Ora un'altra cosa molto importante da capire è che le matrici possono avere diverse dimensioni, possono essere molto grandi o molto piccole, possono essere anche composte da una sola linea. Per indicare la dimensione di una matrice si scrive il suo numero di righe per il suo numero di colonne. Se il numero di colonne di una matrice corrisponde al suo numero di righe la matrice viene definita: matrice quadrata. Facciamo qualche esempio pratico:


Esempio di Matrice 3X3 (3 Righe e 3 Colonne, Matrice Quadrata):

Esempio di Matrice 3X3 (3 Righe e 3 Colonne, Matrice Quadrata)


Esempio di Matrice 3X2 (3 Righe e 2 Colonne):

Esempio di Matrice 3X2 (3 Righe e 2 Colonne)


Esempio di Matrice 3X1 (3 Righe e 1 Colonna):

sempio di Matrice 3X1 (3 Righe e 1 Colonna)


Esempio di Matrice 4X4 (4 Righe e 4 Colonne, Matrice Quadrata):

Esempio di Matrice 4X4 (4 Righe e 4 Colonne, Matrice Quadrata)


Esempio di Matrice 1x5 (1 Riga e 5 Colonne):

Esempio di Matrice 1x5 (1 Riga e 5 Colonne)


Ed ora che abbiamo un po' di dimestichezza con il concetto di Matrice veniamo al sodo.

LA LIBRERIA PER LE OPERAZIONI CON LE MATRICI

Esattamente come abbiamo fatto con la libreria per gestire i vettori creiamo due nuovi files dove andremo a collocare tutte le funzioni di utilità relative alla gestione delle Matrici: mat_matr.cpp e mat_matr.h

La tipologia di Matrici che utilizzeremo nel nostro Engine sarà di due tipi: una di dimensione 1x4 ed un'altra di dimensione 4x4. I motivi della scelta di queste particolari dimensioni ci saranno presto chiari.

Nel file mat_matr.h inseriamo quindi la definizione delle due tipologie di matrici. Le Matrici sono semplici tabelle di numeri in virgola mobile, definiremo quindi le nostre Matrici tramite array di float.

typedef float matrix_1x4_type [4];
typedef float matrix_4x4_type [4][4];

Finalmente abbiamo incominciato a scrivere codice! Non aspettavi altro vero? ;)

COPIA DI MATRICI

Le prime cose che inseriremo nella libreria saranno due utilissime funzioni per la copia di Matrici. L'utilità di queste funzioni ci sarà chiara in seguito.

La prima funzione copia la Matrice di dimensione 1X4 p_source alla Matrice 1X4 p_dest:

void MatrCopy_1x4 (matrix_1x4_type p_dest, matrix_1x4_type p_source)
{
   int i;

   for(i=0;i<4;i++)
   {
        p_dest[i]=p_source[i];
   }
}

La seconda funzione copia la Matrice di dimensione 4X4 p_source alla Matrice 4X4 p_dest:

void MatrCopy_4x4 (matrix_4x4_type p_dest, matrix_4x4_type p_source)
{
   int j,k;
   for(j=0;j<4;j++)
      for(k=0;k<4;k++)
        p_dest[j][k]=p_source[j][k];
}

Avete notato come vengono scansionati gli elementi di una Matrice Quadrata? Si, si tratta di due cicli for concatenati. Bene, tutte le funzioni relative alle Matrici che implementeremo nella nostra libreria avranno più o meno quest'aspetto.

LA MATRICE IDENTITA'

Ora è venuto il momento di definire una matrice molto speciale: la Matrice Identità. Questa Matrice ha una strana proprietà: se si moltiplica qualunque Matrice per la Matrice Identità si ha come risultato la Matrice di origine.

Fantastico! Ma a cosa serve?

La Matrice Identità si comporta come il numero 1 per la moltiplicazione, in pratica è l'unità di riferimento per le moltiplicazioni tra Matrici. Grazie ad essa possiamo inizializzare le Matrici dei nostri Oggetti. Ciò significa che impostando la Matrice Oggetto come Matrice Identità inizializzeremo di conseguenza lo stato spaziale del nostro Oggetto.

La Matrice Identità è una Matrice Quadrata in cui tutti gli elementi della Diagonale Principale sono costituiti dal numero 1, mentre i restanti elementi sono costituiti dal numero 0. Per Diagonale Principale di una Matrice Quadrata si intende la diagonale che parte dall'angolo superiore sinistro fino ad arrivare all'angolo inferiore destro della matrice.

Ecco un esempio di matrice identità di dimensione 4x4:

Tut Matrices IdentityMatrix.png

Creiamo ora una funzione che prende in ingresso una Matrice 4x4 e la inizializza come Matrice Identità:

void MatrIdentity_4x4 (matrix_4x4_type p_matrix)
{
	int j,k;

	for (j=0;j<4;j++)
	{
		for (k=0;k<4;k++)
		{
			if (j==k)
				p_matrix[j][k]=1;
			else
				p_matrix[j][k]=0;
		}
	}
}

La funzione è molto semplice, si tratta di due cicli concatenati con i quali si scansiona l'intera Matrice. Quando i termini j e k sono uguali significa che ci troviamo su uno degli elementi della Diagonale Principale che viene così impostato ad 1.

SOMMA E DIFFERENZA DI MATRICI

La somma e la differenza di matrici è un'operazione molto semplice. L'unica cosa a cui dobbiamo stare attenti è il fatto che le matrici coinvolte in operazioni di somma e differenza devono essere due matrici della stessa dimensione. Ad esempio possiamo sommare una matrice di dimensione 4x4 con un'altra di dimensione 4x4, oppure possiamo fare la differenza di due matrici di 3x2, non possiamo pero sommare una matrice di 1x4 con un'altra di 4x4 ad esempio.

Per sommare (o sottrarre) due matrici A e B basterà sommare (o sottrarre) ciascun componente della matrice A con il corrispettivo componente della matrice B. La Matrice risultato dell'operazione sarà ovviamente una matrice della stessa dimensione delle matrici A e B. Vediamo un esempio di somma tra due Matrici 2x2:

Semplice non credi?

Non implementeremo alcun codice riguardante la Somma e la Differenza tra Matrici poiché tali funzionalità non ci saranno necessarie. Lascio a te la facoltà di implementare le funzione in grado di effettuare la Somma e la Differenza tra Matrici, magari puoi realizzare questa funzionalità come esercizio.

MOLTIPLICAZIONE DI MATRICI

Ed ecco che siamo arrivati alla parte più importante di questa lezione: la Moltiplicazione di Matrici!

Per Moltiplicare due Matrici A e B dobbiamo anzitutto accertarci che le colonne di A siano dello stesso numero delle righe di B, ad es. sia A una matrice di dimensione ixj e B una matrice di dimensione kxm per moltiplicare A*B dobbiamo assicurarci che j=k. Ovviamente il risultato della Moltiplicazione sarà una nuova ;atrice con le dimensioni riga e colonna più grandi prese tra le due Matrici di partenza.

Sono perfettamente moltiplicabili tra di loro ad esempio due Matrici con uguale dimensione, ad esempio 4x4 con 4x4 il cui risultato corrisponde ad una nuova Matrice di 4x4. Oppure due matrici del tipo 1x4 e 4x4 sono anch'esse perfettamente moltiplicabili, il risultato di quest'altra moltiplicazione è una Matrice di 4x4. Non ho fatto questi esempi a caso, si tratta proprio delle tipologie di Matrici che noi andremo a moltiplicare!

Ora che sappiamo cosa possiamo moltiplicare vediamo in dettaglio come si fa questa famosa Moltiplicazione.

La definizione di Moltiplicazione di Matrici è la seguente:

Sia A una matrice m x n e B una matrice n x k, si definisce prodotto tra le matrici A e B la matrice C = A B il cui generico elemento ci,j è la somma dei prodotti degli elementi della i-esima riga di A per i corrispondenti elementi della j-esima colonna di B

Nel caso non ti risultasse chiara questa definizione puoi dare un occhiata all'esempio che segue:


Ok? Implementiamo ora una funzione della libreria che effettua la moltiplicazione tra due matrici p_matrix1 di dimensione 1x4 e p_matrix2 di dimensione 4x4 e restituisce il risultato in p_matrix_res 4x4:

void MatrMul_1x4_4x4 (matrix_1x4_type p_matrix1, matrix_4x4_type p_matrix2, matrix_1x4_type p_matrix_res)
{
    int j,k;
    float l_sum;

    for (j=0;j<4;j++)
    {
        l_sum=0;
        for(k=0;k<4;k++)
            l_sum+=p_matrix1[k]*p_matrix2[k][j];
        p_matrix_res[j]=l_sum;
    }
}

Di la verità, in fin dei conti è semplice vero?

Ora creiamo un'altra funzione per moltiplicare due matrici 4x4:

void MatrMul_4x4_4x4 (matrix_4x4_type p_matrix1, matrix_4x4_type p_matrix2, matrix_4x4_type p_matrix_res)
{
    int i,j,k;
    float l_sum;

    for (i=0;i<4;i++)
    {
        for (j=0;j<4;j++)
        {
            l_sum=0;
            for(k=0;k<4;k++)
                l_sum+=p_matrix1[i][k]*p_matrix2[k][j];
            p_matrix_res[i][j]=l_sum;
        }
    }
}

Si, lo riconosco, è praticamente simile alla funzione precedente, abbiamo solo aggiunto un livello di concatenazione in più per gestire le righe in più della matrice 1.

Bene, abbiamo quasi finito con la libreria per la gestione delle Matrici manca solo una piccola cosa.

LE TABELLE DI LOOK-UP

Ti starai certamente chiedendo: ora cosa sono queste tabelle? Altre complicazioni?

No, stai tranquillo, niente complicazioni! Le tabelle di Look-Up sono una cosa molto semplice, si tratta di un trucco che renderà più veloce il nostro Engine.

Come ti ho già raccontato nel Tutorial n.2, nei primordi della programmazione grafica, i poveri programmatori del tempo, lavorando su CPU lentissime, erano costretti a risparmiare quante più risorse di calcolo possibili. Fu allora che si inventarono uno stratagemma molto intelligente: le Tabelle di Look-Up!

Le Tabelle di Look-Up sono semplici array riempiti con dei risultati di calcolo. Riempiremo queste tabelle durante la fase di inizializzazione del motore per poi riutilizzarle quando avremo bisogno di eseguire alcuni calcoli.

Perché delle semplici tabelle possono rendere il nostro Engine più veloce? Semplice, perché accedere ad una zona di memoria RAM è più veloce per la CPU che eseguire un calcolo complesso! Ovviamente tali tabelle sono necessarie solamente quando si lavora con calcoli complessi, ed infatti noi le utilizzeremo durante le operazioni di Rotazione degli Oggetti, infatti in quello specifico caso utilizzeremo molto i Seni ed i Coseni che sono notoriamente molto dispendiosi per la CPU.

Inizializziamo quindi le due tabelle per gestire i calcoli sui seni ed i coseni:

float matr_sin_table[3600], matr_cos_table[3600]; // Look up tables

Il valore 3600 all'interno degli array corrisponde al valore 360 in gradi di rotazione. Le nostre Tabelle di Look-Up saranno quindi riempite con tutti i valori di seno e di coseno relativi ad una rotazione completa (con risoluzione un decimo di grado).

Una volta riempite queste tabelle se volessi sapere il valore del coseno relativo ad un angolo di 65° ti basterà controllare il valore della variabile matr_cos_table[650], geniale vero?

Ecco la funzione che si occupa di inizializzare le tabelle di Lookup:

void MatrGenerateLookupTab (void)
{
   int i;
   for(i=0; i<3600; i++)
   {
      matr_sin_table[i]=(float)sin(i*3.1415/1800);
      matr_cos_table[i]=(float)cos(i*3.1415/1800); 
   }    
}

Cosa fa questa funzione di preciso? Semplice, ad ogni incremento di i andiamo ad inserire nelle tabelle i valori di seno e coseno relativi al decimo di grado i-esimo. Non spaventatevi guardando la formula: i*3.1415/1800, si tratta di una semplice conversione da Gradi a Radianti. Le funzioni seno e coseno infatti vogliono il valore della rotazione in Radianti! Inutile dire che a noi ragionare in Radianti non piace e quindi abbiamo bisogno di fare questa conversione.

IMPLEMENTARE LE MATRICI

Fino ad ora abbiamo lasciato ad OpenGL il compito di muovere il nostro oggetto ad ogni iterazione del loop principale del programma utilizzando i comandi glTraslate e glRotate. In questa lezione rimuoveremo queste funzioni per utilizzare le nostre. Ma cosa facevano tali comandi di preciso? Se leggiamo la specifica dei comandi glTranslate e glRotate abbiamo le seguenti definizioni:

  • glTranslate: multiply the current matrix by a translation matrix.
  • glRotate: multiply the current matrix by a rotation matrix.

Queste definizioni fanno riferimento a tre matrici: la Matrice Corrente, la Matrice Traslazione e la Matrice Rotazione Come sono fatte e a cosa servono queste tre matrici? Presto spiegato:

  • La Matrice Corrente, che chiameremo Matrice Oggetto è una Matrice 4x4 che assegneremo al nostro oggetto. Ciascun oggetto avrà infatti una propria matrice che memorizzerà istante per istante la posizione, la rotazione e lo scaling dell'oggetto in questione. Non sarà necessario utilizzare altre variabili per la posizione e la rotazione del nostro oggetto, tutte queste informazioni saranno contenute nella Matrice Oggetto.
  • La Matrice di Traslazione è una Matrice 4x4 che utilizzeremo per traslare (muovere) un oggetto da una posizione nello spazio ad un'altra
  • La Matrice di Rotazione è una Matrice 4x4 in grado di far ruotare un oggetto
  • Infine c'è anche la Matrice di Scaling, che fino ad ora non abbiamo mai utilizzato. Si tratta di una Matrice 4x4 in grado di cambiare le dimensioni dell'oggetto

Bene ora che sai a cosa servono tutte queste Matrici ti starai probabilmente chiedendo: come fa l'oggetto a muoversi, ruotare o scalare grazie a queste Matrici?

La risposta è semplice, supponi che la Matrice Oggetto si chiami O, e le Matrici di Traslazione, Rotazione e Scaling si chiamino rispettivamente T, R e S.

Allora:

  • Per Traslare un oggetto basterà moltiplicare la matrice T per la matrice O ed assegnare il risultato ad O.
  • Per Ruotare un oggetto basterà moltiplicare la matrice R per la matrice O ed assegnare il risultato ad O.
  • Per Scalare un oggetto basterà moltiplicare la matrice S per la matrice O ed assegnare il risultato ad O.

Fermi, so già a cosa state pensando: la matrice oggetto non è l'oggetto, come fa l'oggetto a trasformarsi se abbiamo “trasformato” solo la sua matrice? Come facciamo ad eseguire quelle trasformazioni realmente sull'oggetto?

Potrei rispondervi: state tranquilli, basta passare alla Signora OpenGL la matrice dell'oggetto corrente e ci penserà Lei a fare il tutto, e cosi effettivamente sarà. E' molto gentile da parte di OpenGL fare queste operazioni al posto nostro e gliene siamo estremamente grati. Però, come è nostra consuetudine, noi non ci accontentiamo di questa spiegazione, vediamo quindi come sia possibile trasformare un oggetto tramite la sua matrice.

Credo ti sia chiaro che qualunque trasformazione vorrai fare all'oggetto ciò che veramente sarà trasformato saranno i vertici dell'oggetto stesso. Sia che tu voglia spostare l'oggetto, ruotarlo o scalarlo dovrai intervenire sulle sue fondamenta, su ciò che lo compone, ovvero i vertici. E cosa sono i vertici se non terne di numeri? Pensaci, ogni vertice corrisponde ad una terna di valori x,y,z. Ti dice niente? Si, ogni vertice è una piccola matrice di dimensione 1x3. Come trasformare allora l'oggetto? Semplicissimo: si moltiplica ciascun vertice dell'oggetto con la matrice dell'oggetto stesso. E' sempre una moltiplicazione tra matrici, solo che la prima matrice è una matrice con soli tre numeri. Il risultato di ciascuna moltiplicazione sarà il vertice trasformato.

Se sei stato attento fino ad ora noterai una piccola incongruenza, infatti abbiamo in precedenza detto che le nostre matrici avranno dimensione 1x4 e 4x4. Prima cosa da notare è che i nostri Vertici corrispondono a matrici di dimensione 3x1 e non 4x1, la seconda e che non sarebbe possibile moltiplicare una matrice di 1x3 con un altra di 4x4. La soluzione a questo enigma eccola qui:

Dobbiamo considerare i nostri vertici come matrici di 4x1 impostando ad 1 il valore aggiunto in questo modo: Supponendo che Vn sia il vertice n-esimo, Vn = [x y z 1].

Bene, ora possiamo moltiplicare il vertice preso in considerazione per la Matrice Oggetto: Vn=Vn*O

Bello vero? Attento però, in questa piccola formula c'è un errore, riesci a vederlo? Non riesci a trovarlo? Te lo dico io. Come puoi vedere i vertici dell'oggetto in questa formula vengono via via modificati perdendo per sempre la propria informazione originale. Perdendo l'informazione originale in una simulazione che evolve nel tempo si può incorrere in piccoli errori di risoluzione che vanno via via accumulandosi. E poiché ciascun vertice è un entità autonoma si corre il rischio di vedere i nostri oggetti deformarsi con il tempo. Ciò è assolutamente da evitare!

Da questa premessa è chiaro che l'unica cosa che deve evolvere nel tempo è la matrice di trasformazione dell'oggetto preso in considerazione. Continueremo comunque a moltiplicare i nostri vertici per la matrice di trasformazione ma assegneremo i valori ottenuti in variabili ausiliarie. La nuova formula sarà quindi.

Wn=Vn*O

Dove Wn è il vertice temporaneo dell'oggetto calcolato nel Loop principale del nostro programma istante per istante ed utilizzato da OpenGL per disegnare l'oggetto dove lo vogliamo.

Per applicare questa soluzione faremo una cosa molto semplice, ad ogni Loop del Programma invieremo ad OpenGL la nuova Matrice Oggetto, si occupera lei di applicare tutte le trasformazioni ai vertici originali dell'oggetto. Vedremo più avanti come fare, ora entriamo più in dettaglio con la definizione delle Matrici di cui abbiamo parlato fino ad ora.

LA MATRICE OGGETTO

Come ti ho già anticipato il nostro oggetto avrà una matrice tutta sua. Dichiariamo quindi questa matrice nel file object.h inserendo questa nuova riga nella struttura obj_type:

matrix_4x4_type matrix; // Object matrix

Inizializzeremo poi questa Matrice con la funzione MatrIdentity_4x4 durante la fase di caricamento dell'oggetto. Vedremo in dettaglio questa operazione quando ci occuperemo della modifica della funzione ObjLoad.

Ora incominciamo subito ad utilizzare questa Matrice! Creiamo una funzione in grado di posizionare un oggetto in un punto esatto dello spazio. Tale funzione andrà ovviamente a modificare la matrice oggetto. Apriamo nuovamente il file object.c e scriviamo:

void ObjPosition (obj_type_ptr p_object,float p_x,float p_y,float p_z)
{
	//The position fields in the object matrix are filled with the new values
    p_object->matrix[3][0]=p_x;
    p_object->matrix[3][1]=p_y;
    p_object->matrix[3][2]=p_z;    
}

Questa funzione ha tre parametri di ingresso: il puntatore all'oggetto e 3 variabili float indicanti la nuova posizione dell'oggetto tramite coordinate x,y,z.

La Matrice Oggetto risulterà quindi:

O=

Gli 1 presenti sulla diagonale principale sono il frutto dell'inizializzazione fatta con la funzione: MatrIdentity_4x4.

Hai notato dove sono assegnate le informazioni p_x,p_y,p_z riguardanti la posizione dell'oggetto? Tali valori corrispondono rispettivamente agli elementi 3,0 3,1 e 3,2 della matrice oggetto.

Vuoi sapere i motivi che sono alla base di questa disposizione? Te li spiego subito.

Prendi qualunque vertice a caso dell'oggetto, esso come visto in precedenza corrisponderà alla matrice 1x4:

Vn=[x y z 1].

Se moltiplichiamo questa matrice per la Matrice Oggetto appena creata, in base alla teoria riguardante la Moltiplicazione di Matrici, il vertice diventerà:

[(x+p_x) (y+p_y) (z+p_z) 1]

Interessante vero? Cosa dici? Non ti è chiaro? Vai subito a ristudiarti la Moltiplicazione di Matrici altrimenti formatto il tuo HD! ;P

MATRICE DI TRASLAZIONE

La Matrice di Traslazione è una speciale Matrice che serve per muovere un oggetto in base al sistema di coordinate relativo all'oggetto stesso. Questa è la Matrice di Traslazione:

Come puoi vedere i parametri di traslazione Tx, Ty, Tz si trovano sull'ultima riga. Per muovere un oggetto basterà impostare questi parametri in base al movimento x,y,z che vogliamo far fare al nostro oggetto. La differenza rispetto alla funzione precedente è che prima modificavamo i parametri direttamente nella Matrice Oggetto ed ora li modifichiamo su di una Matrice esterna all'oggetto.

Ed è proprio questa la differenza tra Posizionare e Traslare. Ora ti spiego meglio e ti prego di avere molta attenzione in questa fase poiché le due trasformazioni potrebbero generare confusione.

Quando posizioniamo un oggetto tramite la funzione ObjPosition lo posizioniamo in base a delle coordinate assolute, in pratica decidiamo che il nostro oggetto dovrà stare in uno specifico punto dello spazio, ad esempio x=10, y=20 e z=30.

La funzione sottostante ObjTranslate invece funziona diversamente: essa riposiziona l'oggetto in base al sistema di coordinate dell'oggetto stesso. Se ad esempio alla funzione sottostante passo i parametri x=10, y=20 e z=30 come nella funzione precedente il mio oggetto non sarà posizionato in quel preciso punto dello spazio, piuttosto verrà spostato di x=x+10, y=y+20 e z=z+30, in pratica questi valori vengono sommati ai valori di posizione che l'oggetto già aveva in precedenza.

Ma ti dirò di più, ObjTranslate considera anche la rotazione dell'oggetto per effettuare la traslazione. Che significa? Te lo spiego. Supponi ad esempio che il tuo oggetto sia un razzo e che tu intenda simulare un movimento in seguito all'accensione dei motori. Supponi anche che l'asse longitudinale del razzo sia parallelo all'asse z del tuo universo. Spostare il tuo razzo è semplice, basta incrementare il valore della posizione z del razzo ed il gioco è fatto.

I conti non tornano tuttavia se il tuo razzo ha effettuato una rotazione. In questo caso infatti il sistema di assi dell'universo e del razzo non corrispondono più e non puoi semplicemente cambiare il valore di z per spostare il razzo. ObjTranslate risolve questa problematica, qualunque traslazione tu faccia tramite questa funzione sarà sempre relativa agli assi relativi dell'oggetto preso in considerazione.

Qual'è il segreto? Vediamo la funzione in dettaglio:

void ObjTranslate (obj_type_ptr p_object,float p_x,float p_y,float p_z)
{
    int j,k;
    matrix_4x4_type l_matrix, l_res;

    MatrIdentity_4x4(l_matrix);
    l_matrix[3][0]=p_x;
    l_matrix[3][1]=p_y;
    l_matrix[3][2]=p_z;

	//The object matrix is multiplied by a translation matrix
    MatrMul_4x4_4x4(l_matrix,p_object->matrix,l_res);
    for(j=0;j<4;j++)
      for(k=0;k<4;k++)
        p_object->matrix[j][k]=l_res[j][k];
}

A prima vista questa funzione sembra molto simile a ObjPosition, osservandola più in dettaglio per notiamo queste differenze:

  1. Viene inizializzata una Matrice Ausiliaria.
  2. La Matrice Ausiliaria riceve i nuovi valori di posizione.
  3. La Matrice Ausiliaria viene moltiplicata per la Matrice Oggetto.

Cosa è questa Matrice Ausiliaria? Semplice: è la Matrice di Traslazione di cui parlavamo in precedenza!

E' stata in pratica effettuata una concatenazione di Matrici, come spiegheremo la concatenazione permette di effettuare movimenti successivi, ecco perché il questa funzione lavora sul sistema di riferimento locale all'oggetto, perché considera le trasformazioni effettuate in precedenza dallo stesso oggetto!

Fantastico non trovi?

MATRICE DI ROTAZIONE

Ed ora è arrivato il momento della famosa Matrice di Rotazione! In realtà esistono tre diverse Matrici di Rotazione, una per ogni asse cartesiano: Rx, Ry, Rz. Eccole qui:


I parametri di Rotazione si trovano sulle prime 3 righe/colonne della Matrice. Hai visto ora il motivo per cui usiamo matrici 4x4? Si, proprio cosi, per rendere compatibili le trasformazioni di rotazione e di traslazione!

Ti dimostrerò ora perché queste matrici sono fatte proprio cosi.

Per prima cosa vorrei che ti sia chiaro come funziona la rotazione? La rotazione è una trasformazione un pochino complicata poiché comporta l'utilizzo di Seni e Coseni.

Facciamo un esempio pratico testando una rotazione di un un punto x,y,z di un angolo A attraverso l'asse z, la formula corrispondente sarà:

x_new=x*cos(A) - y*sin(A)

y_new=x*cos(A) + y*sin(A)

z_new=z

Il nuovo punto ruotato si troverà a coordinate x_new, y_new, z_new. La trovi complicata? Niente di complicato solo un opportuno uso di Seni e Coseni.

Hai notato come l'asse z non sia influenzato dalla rotazione? Si, hai visto bene, infatti ruotando un oggetto attraverso uno specifico asse (che sia x, y o z) le coordinate di quell'asse relative a tutti i vertici rimarranno le stesse, pensaci, è proprio cosi!

Bene, ora facciamo la stessa cosa che abbiamo fatto per l'esempio sulla traslazione. Moltiplichiamo il vertice/vettore per la Matrice di Rotazione Rz ecco il nuovo vertice:

Vn=[(x*cos(A) - y*sin(A)) (x*cos(A) + y*sin(A)) z 1]

Che corrisponde proprio alla formula relativa alla rotazione sull'asse z.

Ti lascio come esercizio la dimostrazione delle Matrici riguardanti la Rotazione sugli assi x e y. Cosa dici? Non hai alcuna intenzione di farli? Ok, approvo!!!

Meglio scrivere codice:

void ObjRotate (obj_type_ptr p_object,int p_angle_x,int p_angle_y,int p_angle_z)
{
    matrix_4x4_type l_matrix, l_res;

	//Range control
	if (p_angle_x<0) p_angle_x=3600+p_angle_x;
    if (p_angle_y<0) p_angle_y=3600+p_angle_y;  
    if (p_angle_z<0) p_angle_z=3600+p_angle_z;
    if (p_angle_x<0 || p_angle_x>3600) p_angle_x=0;
    if (p_angle_y<0 || p_angle_y>3600) p_angle_y=0;  
    if (p_angle_z<0 || p_angle_z>3600) p_angle_z=0;

    if (p_angle_x)
    {
		//The object matrix is multiplied by the X rotation matrix
        MatrIdentity_4x4(l_matrix);   
        l_matrix[1][1]=(matr_cos_table[p_angle_x]);
        l_matrix[1][2]=(matr_sin_table[p_angle_x]);
        l_matrix[2][1]=(-matr_sin_table[p_angle_x]);
        l_matrix[2][2]=(matr_cos_table[p_angle_x]);
        MatrMul_4x4_4x4(l_matrix,p_object->matrix,l_res);
        MatrCopy_4x4(p_object->matrix,l_res);
    }
    if (p_angle_y)
    {
		// ...by the Y rotation matrix
        MatrIdentity_4x4(l_matrix);
        l_matrix[0][0]=(matr_cos_table[p_angle_y]);
        l_matrix[0][2]=(-matr_sin_table[p_angle_y]);
        l_matrix[2][0]=(matr_sin_table[p_angle_y]);
        l_matrix[2][2]=(matr_cos_table[p_angle_y]);
        MatrMul_4x4_4x4(l_matrix,p_object->matrix,l_res);
        MatrCopy_4x4(p_object->matrix,l_res);
    }
    if (p_angle_z)
    {
		// ...by the Z rotation matrix
        MatrIdentity_4x4(l_matrix);
        l_matrix[0][0]=(matr_cos_table[p_angle_z]);
        l_matrix[0][1]=(matr_sin_table[p_angle_z]);
        l_matrix[1][0]=(-matr_sin_table[p_angle_z]);
        l_matrix[1][1]=(matr_cos_table[p_angle_z]);
        MatrMul_4x4_4x4(l_matrix,p_object->matrix,l_res);
        MatrCopy_4x4(p_object->matrix,l_res);
    }
}

Il codice appena esposto dovrebbe esserti sufficientemente chiaro a questo punto. Puoi notare come abbiamo ampiamente utilizzato le tabelle di Look-Up per calcolare i Seni ed i Coseni.

Per ciascuna Matrice di Rotazione abbiamo svolto questi passaggi:

  1. Abbiamo controllato i valori di input per evitare overflow
  2. Abbiamo inizializzato la Matrice di Rotazione come l_matrix.
  3. Abbiamo assegnato i valori di rotazione ad l_matrix utilizzando le tabelle di Look-Up
  4. Abbiamo moltiplicato la nuova matrice per la Matrice Oggetto ed abbiamo restituito il risultato alla matrice ausiliaria l_res
  5. Abbiamo copiato l_res sulla Matrice Oggetto

L'ultima cosa da dire riguardo la rotazione è che questa trasformazione funzionerà correttamente solo per oggetti il cui centro corrisponda esattamente alle coordinate locali 0,0,0. Che significa? Significa semplicemente che quando creerai il tuo oggetto con un editor 3D (ad esempio 3D Studio, Blender ecc.) dovrai accertarti che quest'ultimo sia posizionato al centro del sistema di coordinate dell'Editor.

Semplice e chiaro! O, no?

MATRICE DI SCALING

Questa è la matrice di Scaling:

La sua dimostrazione è la seguente:

Preso il solito vertice Vn=[x y z 1] e moltiplicandolo per la Matrice S avremo:

[x*s_x y*s_y z*s_z 1]

Se applichiamo questa trasformazione a tutti i vertici di un oggetto ne provocheremo una trasformazione di scala. Ad esempio supponendo tu voglia raddoppiare le dimensioni di un oggetto basterà inserire s_x=2, s_y=2 e s_z=2 nella Matrice di Scaling e moltiplicarla per tutti i vertici dell'Oggetto. Questo ovviamente comporterà un raddoppio di tutte le coordinate dell'oggetto facendolo di conseguenza ingrandire. Come per le rotazioni questa trasformazione funzionerà correttamente solo per oggetti il cui centro corrisponda esattamente alle coordinate locali 0,0,0.

CONCATENARE LE MATRICI

Concatenare le Matrici vuol dire applicare diverse trasformazioni una dopo l'altra, è una cosa fantastica non trovi? Concatenando una rotazione con una traslazione abbiamo a tutti gli effetti effettuato una Rototraslazione.

Ora abbiamo la matrice oggetto O che include tutte le trasformazioni volute e, cosa molto importante, nella sequenza che gli abbiamo passato! E' importante stabilire la sequenza corretta al fine di far fare al nostro oggetto la trasformazione voluta, eseguire prima una rotazione e poi una traslazione non è uguale ad eseguire prima una traslazione e poi una rotazione quindi:

O*T*R <> O*R*T

Cosi come concateniamo rotazioni e traslazioni possiamo concatenare rotazioni (o traslazioni) successive:

O=O*R1*R2*R3

oppure

O=O*T1*T2*T3

Possiamo cosi ruotare e traslare l'oggetto a nostro piacimento, step per step, ad esempio con il nostro joystick o con la tastiera, cosi come avviene nei nostri amati videogiochi.

Ora implementiamo queste trasformazioni nel codice, so già che non stai più nella pelle! ;)

LE ULTIME MODIFICHE

Bene, ora occupiamoci delle funzionalità relative all'oggetto. Se ti ricordi avevo promesso che in questa lezione avremmo finalmente implementato la possibilità di gestire più oggetti, e sarà proprio cosi. Il nostro Engine per ora è sviluppato in stile C, non abbiamo implementato una classe per gestire l'oggetto ma una semplice struttura. Continueremo con questa filosofia e riserveremo un tutorial in futuro per convertire tutto il codice in vero C++ con relative classi e istanze.

GESTIRE PIU' OGGETTI

Apriamo ora il file object.cpp e modifichiamo la sezione di inizializzazione delle variabili globali in questo modo:

obj_type object[MAX_OBJECTS]; //Now the object is generic, the cube has annoyed us a little bit, or not?
int obj_qty=0; //Number of objects in our world
int obj_control=0; //Number of the object that we can control

La prima modifica che salta all'occhio è la conversione della variabile oggetto in un array di oggetti. Definiremo la costante MAX_OBJECTS in object.h e ripeteremo con dicitura extern l'inizializzazione delle stesse variabili appena scritte su object.cpp.

La variabile obj_qty conterrà la quantità di oggetti attualmente caricata. Attenzione a non confondere questo valore con la costante MAX_OBJECT che definisce invece il numero massimo di oggetti che è possibile caricare.

La variabile obj_control invece memorizzerà l'indice dell'oggetto selezionato attualmente. Utilizzeremo questo valore per decidere quale oggetto sarà la "vittima sacrificale" delle rotazioni e traslazioni effettuate da noi tramite tastiera! ;)


LA NUOVA FUNZIONE DI CARICAMENTO OGGETTO

In base a tutto quello che abbiamo studiato (e sudato!) fino ad ora, questa sarà la nuova funzione di caricamento oggetto:

char ObjLoad(char *p_object_name, char *p_texture_name, float p_pos_x, float p_pos_y, float p_pos_z, int p_rot_x, int p_rot_y, int p_rot_z)
{
	if (Load3DS (&object[obj_qty],p_object_name)==0) return(0); //Object loading
	object[obj_qty].id_texture=LoadBMP(p_texture_name); // The Function LoadBitmap() returns the current texture ID
	ObjCalcNormals(&object[obj_qty]); //Once we have all the object data we need to calculate all the normals of the object's vertices
	MatrIdentity_4x4(object[obj_qty].matrix); //Object matrix init
	ObjPosition(&object[obj_qty], p_pos_x, p_pos_y, p_pos_z); // Object initial position
	ObjRotate(&object[obj_qty], p_rot_x, p_rot_y, p_rot_z); // Object initial rotation
	obj_qty++; // Let's increase the object number and get ready to load another object!
	return (1); // If all is ok then return 1
}

La prima cosa che salta subito all'occhio è l'utilizzo dell'array di oggetti object[obj_qty] con la variabile obj_qty che in questa funzione viene incrementata come un contatore.

Il resto delle cose si commenta da se, puoi notare che abbiamo per prima cosa inizializzato la Matrice Oggetto e poi abbiamo posizionato e ruotato il nostro oggetto in base alla sua posizione definita grazie ai parametri in ingresso a questa funzione.

Ora rechiamoci nel file main.cpp ed aggiungiamo il caricamento di tre oggetti:

//Objects loading
ObjLoad ("fighter1.3ds","skull.bmp",             -10.0, 0.0, -30.0,    900,0,0);
ObjLoad ("fighter2.3ds",'\0',                     10.0, 0.0, -30.0,    900,0,0);
ObjLoad ("fighter3.3ds","spaceshiptexture.bmp",    0.0, 0.0, -30.0,    900,0,0);

Ti sarà facile notare che abbiamo disposto i tre oggetti (in questo caso astronavi) uno accanto all'altro e li abbiamo ruotati tutti di 90 gradi sull'asse x.

LA NUOVA FUNZIONE DI RENDERING

Ed ecco Signore e Signori la nuova funzione di rendering, ce la siamo sudata ma ora ce la godiamo:

void display(void)
{
    int i,j;

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // This clear the background color to dark blue
    glMatrixMode(GL_MODELVIEW); // Modeling transformation
    glLoadIdentity(); // Initialize the model matrix as identity
	
    for (i=0;i<obj_qty;i++)
	{
		glPushMatrix(); // We save the current matrix
		glMultMatrixf(&object[i].matrix[0][0]); // Now let's multiply the object matrix by the identity-first matrix

Abbiamo per prima cosa aggiunto il ciclo for per scansionare tutto l'array oggetti. Oh, ma cosa fanno le OpenGL riguardanti le Matrici? Non te le ho spiegate scusami. Come ti dicevo per il nostro Engine non utilizzeremo completamente le funzionalità OpenGL per quanto riguarda le Matrici, ci limiteremo a passare ad OpenGL la Matrice Oggetto già trasformata dalle nostre routine. Ho fatto questa scelta per avere il massimo controllo sulla gestione delle Matrici, in futuro ottimizzeremo anche questa fase poiché sarebbe opportuno far fare tutti i calcoli ad OpenGL che è meglio ottimizzata e permette di utilizzare anche l'accelerazione hardware relativa alle trasformazioni tridimensionali.

Ora esaminiamo le funzioni OpenGL per le Matrici utilizzate fino a qui:

  • void glMatrixMode(GLenum mode); Questa funzione l'abbiamo già vista nel secondo Tutorial. OpenGL utilizza due diverse matrici, una per gestire la trasformazione prospettica tramite la costante GL_PROJECTION, ed una per gestire gli oggetti tramite GL_MODELVIEW. Impostiamo la matrice corrente come GL_MODELVIEW perchè ora stiamo lavorando sugli oggetti.
  • void glLoadIdentity(void); Questa funzione è analoga alla nostra MatrIdentity_4x4, si occupa inizializza la matrice GL_MODELVIEW.
  • void glPushMatrix(void); Questa funzione è molto interessante. OpenGL lavora sulle Matrici tramite uno stack, cioè una lista di concatenazioni che può scorrere tramite le funzioni glPushMatrix e glPopMatrix. Noi utilizzeremo queste due funzioni rispettivamente all'inizio e alla fine del rendering di ogni oggetto.
  • void glMultMatrixf(const GLfloat *m); Questa funzione è la chiave di tutto. Grazie ad essa possiamo passare ad OpenGL la nostra Matrice Oggetto. Come facciamo? Facile, glMultMatrixf moltiplicherà la Matrice Oggetto con la sua Matrice MODELVIEW che era stata in precedenza inizializzata, in questo modo la MODELVIEW diventerà a tutti gli effetti la nostra Matrice Oggetto. OpenGL utilizzerà poi questa MODELVIEW per trasformare tutti i vertici dell'oggetto ed il bello è che lo farà in maniera completamente trasparente per noi.
	
		if (object[i].id_texture!=-1) 
		{
    glBindTexture(GL_TEXTURE_2D, object[i].id_texture); // We set the active texture 
		    glEnable(GL_TEXTURE_2D); // Texture mapping ON
		}
		else
		    glDisable(GL_TEXTURE_2D); // Texture mapping OFF

		glBegin(GL_TRIANGLES); // glBegin and glEnd delimit the vertices that define a primitive (in our case triangles)
		for (j=0;j<object[i].polygons_qty;j++)
		{
			//----------------- FIRST VERTEX -----------------
			//Normal coordinates of the first vertex
			glNormal3f( object[i].normal[ object[i].polygon[j].a ].x,
						object[i].normal[ object[i].polygon[j].a ].y,
						object[i].normal[ object[i].polygon[j].a ].z);
			// Texture coordinates of the first vertex
			glTexCoord2f( object[i].mapcoord[ object[i].polygon[j].a ].u,
						  object[i].mapcoord[ object[i].polygon[j].a ].v);
			// Coordinates of the first vertex
			glVertex3f( object[i].vertex[ object[i].polygon[j].a ].x,
						object[i].vertex[ object[i].polygon[j].a ].y,
						object[i].vertex[ object[i].polygon[j].a ].z);

			//----------------- SECOND VERTEX -----------------
			//Normal coordinates of the second vertex
			glNormal3f( object[i].normal[ object[i].polygon[j].b ].x,
						object[i].normal[ object[i].polygon[j].b ].y,
						object[i].normal[ object[i].polygon[j].b ].z);
			// Texture coordinates of the second vertex
			glTexCoord2f( object[i].mapcoord[ object[i].polygon[j].b ].u,
						  object[i].mapcoord[ object[i].polygon[j].b ].v);
			// Coordinates of the second vertex
			glVertex3f( object[i].vertex[ object[i].polygon[j].b ].x,
						object[i].vertex[ object[i].polygon[j].b ].y,
						object[i].vertex[ object[i].polygon[j].b ].z);
        
			//----------------- THIRD VERTEX -----------------
			//Normal coordinates of the third vertex
			glNormal3f( object[i].normal[ object[i].polygon[j].c ].x,
						object[i].normal[ object[i].polygon[j].c ].y,
						object[i].normal[ object[i].polygon[j].c ].z);
			// Texture coordinates of the third vertex
			glTexCoord2f( object[i].mapcoord[ object[i].polygon[j].c ].u,
						  object[i].mapcoord[ object[i].polygon[j].c ].v);
			// Coordinates of the Third vertex
			glVertex3f( object[i].vertex[ object[i].polygon[j].c ].x,
						object[i].vertex[ object[i].polygon[j].c ].y,
						object[i].vertex[ object[i].polygon[j].c ].z);

		}
		glEnd();
		glPopMatrix(); // Restore the previous matrix 
	}
    glFlush(); // This force the execution of OpenGL commands
    glutSwapBuffers(); // In double buffered mode we invert the positions of the visible buffer and the writing buffer
}

Avete notato che abbiamo passato tutte le coordinate dei vertici degli oggetti in base ai loro valori originali? Non abbiamo effettuato alcuna trasformazione sui vertici! Come già ti ho anticipato infatti OpenGL effettuerà queste trasformazioni internamente. Terminiamo il rendering dell'oggetto utilizzando la funzione:

  • void glPushMatrix(void); Tramite questa funzione ripristiniamo la Matrice MODELVIEW nel suo stato originale, pronta per essere riempita con la Matrice di un nuovo oggetto.

Ed anche questa funzione è terminata, ci siamo quasi...

APPLICARE LE TRASFORMAZIONI TRAMITE TASTIERA

Ancora un piccolo sforzo e ce l'abbiamo fatta. Ora dobbiamo modificare le due funzioni relative alla tastiera. I nuovi tasti da aggiungere alla funzione keyboard saranno i seguenti: j, m, k, r. Tramite tali tasti trasleremo sull'asse z e ruoteremo sull'asse y l'oggetto selezionato. In pratica tali tasti serviranno per simulare un movimento in avanti della astronave ed un movimento di rollio (virata).

Vediamo la nuova funzione in dettaglio:

void keyboard(unsigned char p_key, int p_x, int p_y)
{  
	switch (p_key)
    	{
		case 'j': case 'J':
			ObjTranslate(&object[obj_control],0,0,-1);
        	break;
		case 'm': case 'M':
			ObjTranslate(&object[obj_control],0,0,1);
        	break;
        	case 'k': case 'K':
			ObjRotate(&object[obj_control],0,20,0);
        	break;
        	case 'l': case 'L':
			ObjRotate(&object[obj_control],0,-20,0);
	        break;
		case 'r': case 'R':
		if (filling==0)
		{
			glPolygonMode (GL_FRONT_AND_BACK, GL_FILL); // Polygon rasterization mode (polygon filled)
			filling=1;
		}   
		else 
		{
			glPolygonMode (GL_FRONT_AND_BACK, GL_LINE); // Polygon rasterization mode (polygon outlined)
			filling=0;
		}
		break;
		case 27:
			exit(0);
		break;
	}
}

Bene, abbiamo finalmente utilizzato le funzioni ObjTranslate e ObjRotate. Ora non ci resta che modificare la nuova funzione keyboard_s per aggiungere i comandi di imbardata e beccheggio tramite i tasti direzione. Aggiungeremo anche i tasti PAGE UP e PAGE DOWN per cambiare l'oggetto selezionato. In questo modo avremo la possibilità di spostare tutte le tre astronavi a nostro piacimento.

void keyboard_s (int p_key, int p_x, int py)
{
	switch (p_key)
	{
        	case GLUT_KEY_UP:
			ObjRotate(&object[obj_control],-20,0,0);
        break;
        case GLUT_KEY_DOWN:
			ObjRotate(&object[obj_control],20,0,0);
        break;
        case GLUT_KEY_LEFT:
			ObjRotate(&object[obj_control],0,0,20);
        break;
        case GLUT_KEY_RIGHT:
			ObjRotate(&object[obj_control],0,0,-20);
        break;
        case GLUT_KEY_PAGE_UP:
			obj_control++;
			if (obj_control>=obj_qty) obj_control=0;
		break;
        case GLUT_KEY_PAGE_DOWN:
			obj_control--;
			if (obj_control<0) obj_control=obj_qty-1;
		break;
    }
}

E con questa ultima funzione mio caro lettore, finalmente abbiamo concluso!

CONCLUSIONE

Tra tutte le lezioni che ho fatto fino ad ora questa è stata la più lunga e laboriosa! Sono veramente soddisfatto di aver terminato questa lezione! Sei stanco anche tu vero? Bene, è un buon segno! =)

Hey ma hai già terminato il drink? Allora direi che è ora di stappare lo champagne! Però lasciamene un pò!

SOURCE CODE

The Source Code of this lesson can be downloaded from the Tutorials Main Page