Progetto1: Real-time rendering con le OpenGL
Shinto Shrine
di Carlo Mangani


shintomain



Index


1. Scopo del progetto (su)

Lo scopo di questo progetto e' realizzare una scena 3d navigabile in tempo reale utilizzando le librerie OpenGL. Inoltre, deve essere realizzato un percorso che la telecamera seguira' durante l'esecuzione del programma. Tale percorso puo' essere modificato opportunamente dall'utente.
Durante lo sviluppo del codice sono state effettuate sostanziali modifiche strutturali. Dapprima e' stato utilizzato un glx enviroment "puro", creando una finestra opengl ed un glxcontext manualmente, per poi passare ad un approccio con le librerie glut per la gestione dell' I/O. La prima scelta e' sicuramente piu' efficente e customizzabile ma anche meno portabile e stabile. Il linguaggio adottato e' il C++, poiche' un approccio object oriented ha semplificato di molto lo sviluppo del codice e l'inclusione di classi esterne.
Le parti principali che compongono il lavoro sono le seguenti:

2. La scena (su)

Le specifiche non richiedevano espressamente una scena esteticamente accetabile o particolarmente complessa, ma ho voluto comunque portare avanti una mia personale ricerca sulle OpenGL e sulle sue possibilita'. Per questo motivo la produzione della scena e' avvenuta utilizzando un software opensource per la modellazione 3d di nome Blender ed esportando il modello finito in un formato standard (obj) ed infine caricando le mesh poligonali nel programma dal file. Questo mi ha permesso di aggiungere complessita' alla scena, geometrie e texture, semplicemente utilizzando il programma. Sono stati aggiunti alcuni effetti di ambiente, quali luci e nebbia, utilizzando le seguenti procedure GL:

2.1. Composizione della scena

La scelta del soggetto e' stata guidata dalla mia passione per la cultura e la tradizione del Giappone, antico e moderno, e in generale per il mondo orientale, in particolare si tratta di un tempio shintoista situato su una collina e protetto da tre cancelli (Tori) che impediscono agli spiriti maligni di invadere il luogo sacro. ai piedi della collina sulla destra ho aggiunto altre due costruzioni tradizionali per riempire gli spazi vuoti della scena. La prima e' una postazione di avvistamento mentre la seconda e' una rustica abitazione contadina corredada da un pozzo e da un carretto. Ai piedi della collina, vicino al primo tori, e' presente inoltre una targa con un'iscrizione giapponese che recita "Vento, Legno, Fuoco, Montagna".

2.2. La scena in numeri

In totale la scena e' composta da: da cio' si comprende come una tale scena necessiti di requisiti hardware non banali per essere caricata ed eseguita decentemente.

3. Loader .OBJ (su)

Come accennato piu' su' nella pagina, ho utilizzato una classe, ModelType, che permette di caricare un file .obj, uno dei formati piu' famosi per i modelli 3D, utilizzato da Maya, celebre programma di modellazione e animazione della Alias WaveFront. In principio pensavo di scrivere il mio modulo per gestire tali file, ma visto che in rete ho trovato molti, ottimi, esempi, ho preferito usare uno di questi, scelto in base alla completezza e alle funzionalita'.
Per utilizzare tale classe nel codice e' bastato includere l'header ModelType.h, dichiarare un'istanza della classe e lanciare prima il metodo LoadObj(), e poi il metodo Draw(). Vista l'imponente mole del file .obj (ASCII: 12MB) incluso nel progetto l'operazione di caricamento del file e generazione di ponti, facce e normali potrebbe richiedere qualche minuto su alcuni computer. Da notare che i frammenti di codice racchiusi fra "//" sono parti aggiunte e *NON* sono opera dell'autore della classe.

3.1. Metodi della classe ModelType

La classe ModelType e' corredata da alcune strutture dati, quali TextureImage, OBJVertex, OBJTexCoord, OBJFace, etc, per memorizzare gli elementi geometrici ed ornamentali del modello caricato. Andiamo ad analizzare il funzionamento della classe passo passo, seguendo i suoi metodi ed analizzando le principali funzioni OpenGL utilizzate in essi:

3.2. Struttura dei file .OBJ e .MTL

Un file .obj e' formata da alcuni tag che differenziano le varie informazioni si vertici le normali le coordinate texture e le facce dell'oggetto.
  • mtllib file: indica quale file .mtl usare come libreria di materiali (stringa)
  • o oggetto: inizio delle informazioni per un oggetto (stringa)
  • v x y z: coordinate XYZ per un vertice dell'oggetto (3 float)
  • vt u v w: coordinate UVW per le texture (3 float)
  • vn x y z: vettore normale, ignorato dal parser, il programma ricalcola da se le normali dell'oggetto (3 float)
  • usemtl material: indica quale materiale dalla libreria usare per l'oggetto corrente (stringa)
  • f v/n/n v/n/n v/n/n: descrive una faccia triangolare nella forma vertice/normale/normale
    da notare che il valore della normale e' ripetuto (9 interi)
  • f v/n/n v/n/n v/n/n v/n/n: descrive una faccia quadrata nella forma vertice/normale/normale (12 interi)
Il file .mtl e' la libreria contenente le informazioni sui materiali utilizzati nel modello. Ecco com'e' strutturato:
  • newmtl material: setta il nome per un nuovo materiale (stringa)
  • Ns reflect: valore di riflessione del materiale(float)
  • Kd r g b: colore diffuso (3 float)
  • Ka r g b: colore ambiente (3 float)
  • Ks r g b: colore speculare (3 float)
  • Tr opacity: setta il livello di trasparenza (float)
  • map_Kd file: setta la texture da usare come valore diffuso (stringa)
  • map_Ka file: setta la texture da usare come valore ambiente (stringa)
  • map_Ks file: setta la texture da usare come valore speculare (stringa)
  • Smooth s: indica se usare o meno il fastSmooth per questo materiale (intero)
  • Normal n: indica se usare o meno le normali per questo materiale (intero)
Da notare che vi sono molti altri tipi di informazioni includibili nei file .obj e .mtl ma che non vengono parsate dalla ModelType quindi di scarso interesse ai fini di questo progetto.

4. Camera motion (su)

La specifica principale di questo progetto e' l'implementazione del movimento della telecamera, sia interattivamente utilizzando la tastiera, che automaticamente dando luogo ad un'animazione. Prima di andare avanti introduciamo alcune strutture dati che sono servite nell'implementazione.
La prima e' la struttura keyFrame:

typedef struct keyFrame
{
GLfloat position[3];
GLfloat rotation[3];
GLfloat TD[3];
GLfloat TS[3];
int num;
keyFrame *next;
keyFrame *prev;
};

che rappresenta un keyframe all'interno dell'animazione. La demo inclusa con il codice comprende 7 keyframe (KEYNUM), fra un keyframe ed il seguente vengono interpolati 250 frame intermedi (STEPS) per un totale di 1750 complessivi per l'animazione, le coordinate di tali frames (posizione e rotazione) vengono contenute in questi array:

float framePos[TOTALFRAME][3];
float frameRot[TOTALFRAME][3];

La struttura dati e' definita come una double-linked list, in questo modo il primo keyFrame sara' il successivo dell'ultimo ed il percorso della telecamera sara' ciclico. I valori STEPS e KEYNUM inoltre possono essere modificati nel header Core.h e nel file che contiene le le coordinate per i keyframe Data/keyframes, in modo da ottenere un diverso percorso o un'animazione piu' lenta/veloce.

4.1. L'interpolazione dei keyFrames

Per ottenere un'animzione "morbida", e' stato adottato un metodo di interpolazione fra punti detto "di curve di Hermite". Tale metodo e' semplice da calcolare ma allo stesso tempo molto potente. La curva prodotta e' anche conosciuta come KB-spline, curva controllabile tramite i tre parametri di tensione, continuita' e bias, che analizzeremo in seguito, in'oltre il codice comprende la possibilita' di utilizzare le funzioni di bezier per produrre un diverso tipo di spline.
Per calcolare la curva di Hermite abbiamo bisogno dei seguenti vettori:
  • P1: il punto di partenza della curva
  • T1: la tangente (eg direzione e velocita') ovvero come la curva lascia il punto di partenza
  • P2: il punto di arrivo della curva
  • T2: la tangente (eg direzione e velocita') ovvero come la curva incontra il punto di arrivo

hermite


Questi quattro vettori sono semplicemente moltiplicati con le quattro funzioni base di Hermite e sommati assieme:

h1(s) = 2s^3 - 3s^2 + 1
h2(s) = -2s^3 + 3s^2
h3(s) = s^3 - 2s^2 + s
h4(s) = s^3 - s^2

ecco i grafici delle quattro funzioni (da sinistra a destra: h1, h2, h3, h4), tutti i grafici, ad eccezione di h4, sono stati tracciati da (0,0) a (1,1).
hermite1 hermite2 hermite3 hermite4

Esprimendo tutto in forma di matrice otteremmo la seguente formula:
Il vettore S: il punto di interpolazione e la sua potenza fino alla terza
il vettore C: i parametri della curva di Hermite
la matrice h: la forma matriciale della quattro polinomiali di Hermite
	| s^3 |		| P1 |		|  2  -2   1   1 |
S =	| s^2 |	C =	| P2 |	h =	| -3   3  -2  -1 |
	| s^1 |		| T1 |		|  0   0   1   0 |
	| 1   |		| T2 |		|  1   0   0   0 |
Per calcolare un punto sulla curva si costruisce il vettore S, lo si moltiplica con la matrice h e quindi si moltiplica per C.

P = S * h * C

Se invece volessimo creare una curva di Bezier ci basterebbe sostituire la matrice h con la seguente matrice di coefficenti
     | -1   3  -3   1 |
b =  |  3  -6   3   0 |
     | -3   3   0   0 |
     |  1   0   0   0 |
Controllare delle tangenti per rendere la curva puo' essere difficile, cosi' come sara' difficile stabilire che forma avra' una curva se dobbiamo definirla. In piu' per ottenere delle curve strette bisogna trascinare i punti della tangente molto lontano dalla curva stessa. Per questo motivo in realta' e' stato utilizzato un tipo particolare di curve di Hermite, i Kochanek-Bartels Splines (detti anche TCB-Splines).
La differenza sostanziale con le Hermite semplici sta' nella presenza di alcune funzioni molto utili per calcolare l'equazione delle tangenti utilizzando tre parametri di cui ho accennato sopra:
  • Tensione: quanto strettamente la curva piega
  • Continuita': Quant'e' rapido il cambiamento di direzione e di velocita'
  • Bias: qual'e' la direzione della curva non appena passa il punto chiave
L'equazione della tangente "entrante":

          (1-t)*(1-c)*(1+b)
TS    =   -----------------  * ( P   -  P    )
  i              2                i      i-1

          (1-t)*(1+c)*(1-b)
      +   -----------------  * ( P   -  P    )
                 2                i+1    i

L'equazione della tangente "uscente":

          (1-t)*(1+c)*(1+b)
TD    =   -----------------  * ( P   -  P    )
  i              2                i      i-1

          (1-t)*(1-c)*(1-b)
      +   -----------------  * ( P   -  P    )
                 2                i+1    i

Quando bisogna interpolare la curva si deve usare questo vettore:

    |  P(i)    |
C = |  P(i+1)  |
    |  TD(i)   |
    |  TS(i+1) |

4.2. Le funzioni che creano l'animazione

Nel codice del programma il lavoro appena spiegato viene svolto da due funzioni:
  • bool loadKeyFrames(char *filename);
  • void keyFrameInterpolation(keyFrame *cur, float t, float c, float b, bool hermite);
loadKeyFrames(): carica da un file di testo le coordinate di posizione e di rotazione per ogni keyFrame. Ogni riga del file deve avere 3 coordinate per la posizione e 3 coordinate per la rotazione in questa forma:

p X Y Z r X Y Z

ed il numero di keyframe definiti dev'essere uguale a KEYNUM.
keyFrameInterpolation(): calcola i punti intermedi tra un keyframe ed il successivo, memorizzandoli negli array framePos[][] e frameRot[][]. Utilizza come parametri tre float t, c e b, che sono la tensione, continuita' e bias, ed in piu' un booleano hermite, che stabilisce quale matrice usare per il calcolo della curva, Hermite o Bezier.
Durante l'esecuzione del programma un indice currentFrame stara' ad indicare l'attuale frame da disegnare.

5. Altre classi e funzioni ausiliarie (su)

Oltre agli elementi gia' discussi, sono stati implementati altre classi e funzioni ausiliarie che verranno brevemente analizzate qui di seguito:
  • bool dumpFrame(char *filename);
  • bool writePPM(char *filename, GLubyte *buffer, int mode);
  • class BitmapFont{};
  • class MainApp{};
dumpFrame(): utilizza la glReadPixels() per fare il dump dell buffer della finestra per poi passarlo come argomento a writePPM().
writePPM(): crea un file immagine in formato PPM RAW con il buffer catturato da dumpFrame().
BitmapFont(): e' la classe adibita alla proiezione su schermo di messaggi di testo, utilizza i caratteri caricati da un'immagine BMP e non fa altro che creare dei GL_QUADS, su cui sono stati mappati tutti i caratteri, appropiati al testo che si desidera scrivere.
MainApp(): E' la classe che contiene la funzione main(), in particolare definisce un metodo main() virtuale che viene ereditato dalla classe principale del progetto: Core.

6. Conclusioni (su)

Quando ho iniziato a progettare il lavoro mi ero preposto diversi obbiettivi, i piu' importanti dei quali sono stati portati a compimento. Principalmente, volevo creare qualcosa di abbastanza scalabile, riutilizzabile e in definitiva gradevole dal punto di vista estetico. Penso di aver raggiunto, nelle tre settimane di stesura del codice, almeno due dei precedenti risultati. Rimane in ogni caso un'opera legata ad un ambito di personale ricerca e studio sulle librerie OpenGL e sulle loro potenzialita', nonche' sull'applicazione pratica di alcune proprieta' numeriche e geometriche studiate in passato, e quindi saranno graditissimi consigli, commenti e critiche.
L'intero lavoro viene rilasciato sotto la licenza GPL fatta eccezzione per la classe ModelType (ModelType.cpp, ModelType.h) che invece viene rilasciata dall'autore, Karl Berg, sotto la licenza ZLIB.
La galleria include un filmato mpeg, per la sua realizzazione sono stati prima dumpati i frame dell'animazione ed in seguito e' stato utilizzato un tool opensource, ppmtompeg, per creare il filmato dai vari fotogrammi.
Come per l'animazione, anche gli altri screenshot sono stati salvati con la funzione dumpFrame().

7. Galleria (su)

shrineKanji.png shrineDark.png shrineHeavyFog.png shrineTotal.png
shrineNofog.png shrineWFcolor.png shrineWFblack.png shrineBlend.png
(ScreenShot di alcune parti della scena con diversi tipi di effetti)
shrine.mpeg
(Filmato in MPEG dei primi 1500 frame dell'animazione {3.5mb})

8. Download (su)


!!!Importante!!!
leggere il file README contenuto nel pacchetto prima di lanciare il programma.

shinto_shrine.tgz(3.6mb): pacchetto contenente i sorgenti, le texture, i file .obj, .mtl e blend.

Per scompattare ed eseguire digitare:

tar xvzf shinto_shrine.tgz
cd shinto_shrine
make run

doc_shinto.tgz(2.4mb): contiene la presente pagina e le immagini.

Have fun!


Torna all'indice dei progetti