|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
TUTORIAL 2: RENDERING PIPELINE, OPENGL AND GLUT |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
INTRODUZIONE Nella lezione precedente abbiamo acquisito i dati degli oggetti e li abbiamo memorizzati in una struttura. Nella lezione di oggi introdurremo la libreria grafica OpenGL e la libreria di utilità GLUT che ci permetteranno di disegnare il nostro oggetto sullo schermo. Prima però dobbiamo completare la nostra analisi sulle principali sezioni di un motore 3d erano infatti rimasti incompleti i punti 2 e 3:
TRASFORMAZIONI PER POSIZIONARE GLI OGGETTI NEL MONDO (TRASFORMAZIONI OGGETTO E CAMERA) Quello che ci accingeremo a fare sarà nient'altro che prendere i punti che compongono l'oggetto, i vertici, ed applicare ad essi delle opportune trasformazioni che ci porteranno a visualizzarlo su schermo, utilizzando le librerie grafiche. Noi vogliamo creare un mondo, o per essere più esatti (essendo fanatici dei simulatori spaziali) un intero universo. Per questo la prima trasformazione che andremo ad applicare al nostro oggetto sarà la MODELING TRANSFORMATION. Poichè la nostra intenzione è soprattutto quella di creare delle astronavi che fino a prova contraria non sono oggetti statici, ma si muovono nello spazio circostante, dobbiamo trasformare le coordinare dell' oggetto da locali (cioè relative alla posizione centrale dell'oggetto stesso) ad assolute. Dobbiamo quindi traslare l'oggetto, sommando algebricamente la posizione locale dei vertici alla posizione attuale dell'oggetto nel mondo (che può variare continuamente se è in movimento). Lo stesso procedimento dovrà poi essere applicato per le rotazioni dell'oggetto. In seguito dobbiamo applicare all'oggetto posizionato nel mondo una ulteriore trasformazione: la VIEWING TRANSFORMATION. La nostra più grande felicità nel creare un mondo è quella di poterlo esplorare muovendoci a nostro piacimento, rendendo il nostro monitor una telecamera. Come effettuare questo tipo di trasformazione? La risposta è relativamente semplice, infatti basterà considerare sempre la nostra telecamera posizionata alle coordinate 0,0 e rotazioni nulle. E poi? Semplice! Qualunque movimento effettuerà la telecamera dovremmo applicare la trasformazione inversa al nostro oggetto anziché muovere la telecamera. Supponiamo ad esempio di muoverci verso il nostro oggetto di +10 punti sull' asse Z e di guardare poi in alto roteando la testa sull'asse x di 40 gradi. In realtà in tutti i motori grafici ciò che realmente accade è che l'oggetto effettuerà un movimento di -10 su Z ed in seguito una rotazione di -40 gradi sull'asse x. Questo semplifica moltissimo la gestione del motore in quanto la telecamera sarà sempre posizionata all'origine. Quindi la prima trasformazione che subiranno i vertici del nostro oggetto sarà quella di "essere traslati" in base ai valori inversi alla nostra posizione nello spazio ed "essere ruotati" in base ai valori inversi della nostra rotazione.
DISEGNARE LA SCENA SULLO SCHERMO 2D (trasformazioni di proiezione e di viewport, rimozione facce nascoste, buffer colore e profondità) Infine l'ultima trasformazione da effettuare sarà la PROJECTION TRANSFORMATION. Infatti noi abbiamo bisogno di visualizzare uno spazio tridimensionale su di uno schermo bidimensionale. Dobbiamo simulare l'asse Z perchè il nostro povero monitor ha solo due assi X e Y. Se ci pensate bene l'unico modo per effettuare questo tipo di transformazione è quello di suddividere tutte le coordinate X e Y in base alla loro componente Z. L'effetto di questo procedimento è quello di comprimere i punti distanti avvicinandoli al punto centrale x=0 e y=0. Ed è quello che accade in realtà e che molti di voi avranno studiato a scuola con il termine di proiezione prospettica.
TRASFORMAZIONE DI VIEWPORT Dopo aver effettuato tutti questi calcoli sulla nuova posizione dei triangoli relativa al punto di vista e' necessario eseguire delle operazioni di BACK FACE CULLING. Cioè bisogna escludere tutti i triangoli non visibili nella scena, in modo tale che le successive operazioni non siano appesantite inutilmente. Quali sono i triangoli non visibili? Tutti i triangoli presenti al di fuori della nostra viewport e tutti quelli che si trovano a comporre facce attualmente non visibili degli oggetti, come ad esempio le facce posteriori. Per buffer si intende una zona di memoria nella quale possiamo salvare o leggere dei dati. I nostri buffer sono delle zone grandi esattamente quanto la nostra viewport. Ad esempio se abbiamo aperto una finestra di 640 x 480 abbiamo allocato un buffer grande 640 x 480 = 307200 pixel, che significa, nel caso stiamo usando 16 bit di profondità di colore: 307200*16 = 4915200 bit. Che corrispondono a circa 614 kbyte di memoria video! OpenGL usa due buffer principali per poter disegnare: il COLOR BUFFER (che può essere singolo o doppio) ed il DEPTH BUFFER. Il primo è ciò che noi vediamo su schermo, dove vanno a finire tutte le operazioni di disegno. Dopo che sono stati effettuati tutti i calcoli geometrici OpenGL inizia a riempire questo buffer pixel per pixel riempiendo tutti i nostri poligoni e mostrandoceli su schermo una volta che l'operazione è stata completata. Se la scena è animata il color buffer viene disegnato e cancellato ogni frame. Spesso questo buffer viene utilizzato in modalita duale, è la tecnica del DOUBLE BUFFER, che noi useremo. Questa consiste nel visualizzare un buffer mentre l'altro buffer si pulisce e si riempie con il frame successivo, una volta che questa operazione è stata completata si scambiano i buffer. In questo modo l'animazione risultante è praticamente priva di sfarfallii e si ottimizzano le attese. Supponiamo adesso che nella nostra scena ci siano due triangoli uno dietro l'altro, entrambi rivolti verso di noi e quindi visibili. In questo caso l'ordine con cui si disegnano i triangoli è molto importante, se noi disegnamo prima il triangolo più vicino e poi quello più lontano i pixel che compongono quest'ultimo si sovrapporrebbero al triangolo più vicino creando effetti non realistici. Una tecnica per evitare questo inconveniente è quella di ordinare tutti i triangoli visibili e poi disegnarli in ordine dai più lontani ai più vicini, questa si chiama "tecnica del pittore". Noi non utilizzeremo questa tecnica perchè OpenGL ci fornisce uno strumento ben più potente: il depth buffer. Questo buffer ha le stesse dimensioni del color buffer ma invece di contenere i colori dei pixel è utilizzato come contenitore di informazioni che riguardano la profondità sull'asse Z di ogni pixel. Salvare queste informazioni è molto importante per un semplice motivo, quando andiamo a disegnare i nostri triangoli pixel per pixel sullo schermo facciamo un test per vedere se il pixel che andiamo a stampare è più vicino del pixel che già si trova su quel punto, se è più vicino allora aggiorniamo il depth buffer con il nuovo valore ed abilitiamo anche la scrittura sul color buffer altrimenti scartiamo il pixel. Questo produce risultati ottimi senza tuttavia pesare troppo sulla velocità di esecuzione nelle moderne schede video Noi non ci dovremmo preoccupare di effettuare tutte queste operazioni a mano in quanto la nostra libreria OpenGL si occuperà di svolgere gran parte di questo lavoro e si occuperà anche della parte di stampa su schermo, disegnando i nostri triangoli ed applicando ad essi gli effetti di colorazione, di luce e di mappatura. Voi non dovrete fare proprio niente! Quindi che cosa stiamo facendo qui? Stiamo perdendo tempo? No! Il nostro compito infatti è quello di fornire ad OpenGL tutte le informazioni di cui ha bisogno per svolgere il suo lavoro a basso livello interfacciandosi anche con la nostra scheda video, sfruttando se possibile l'accelerazione hardware.
FINALMENTE OPENGL! OpenGL è una libreria grafica che ci consente di interfacciarsi all'hardware grafico, Ci mette a disposizione una serie di funzioni per disegnare punti, linee e poligoni, effettua tutti i calcoli necessari per l'illuminazione, lo shading, la trasformazione dei vertici ecc. Glut invece e' una libreria di utilita' usata per interfacciarsi con il window system, per questo indipendente dalla piattaforma utilizzata (Windows o Linux), ci permette di creare una finestra, cosi' da lasciarci disegnare, e si occupa anche di acquisire lo stato della tastiera. La struttura di un programma OpenGL si divide in varie sezioni, che spiegheremo in dettaglio: -Funzione di inizializzazione, usata per inizializzare OpenGL e le matrici di modeling/viewing e di projection e per effettuare tutte le inizializzazioni di cui necessitiamo. -Funzione di resize, richiamata ogni volta che l'utente avvia il programma o cambia le dimensioni della finestra di output, ogni volta che accadono queste cose infatti occorre comunicare ad OpenGL la nuova viewport. -Funzione di acquisizione da tastiera, richiamata ogni volta che l'utente preme un tasto -Funzione di disegno della scena, nella quale si puliscono i buffer di colore e di profondita, si effettuano tutte le trasformazioni di modeling e di viewing, si disegna la scena tramite i comandi sulle primitive grafiche e si applicano le trasformazioni di projection, infine si invertono i 2 buffer di colore, nel caso si stia usando il double buffering -Ciclo principale, praticamente un loop infinito racchiuso nel main, il quale richiama ad ogni frame tutte le funzioni necessarie. Una tipica funzione OpenGL ha questa sintassi: glFunctionName(GL_TYPE arguments). Le funzioni Glut invece hanno questa sintassi glutFunctionName(arguments). OpenGL ha anche tipi di dati proprietari per aiutare la portabilità del codice. Questi tipi iniziano con il prefisso "GL" a sono seguiti da "u" (per i tipi senza segno) e dal tipo (float, int ecc.). Per esempio possiamo utilizzare GLfloat oppure GLuint per definire variabili compatibili rispettivamente con i tipi float e int.
HEADERS La prima cosa da fare e' di includere gli header necessari, windows.h (per gli utilizzatori di windows) e glut.h. #include <windows.h>
Dopo aver definito l'oggetto tramite il codice della scorsa lezione dichiariamo una funzione che nella quale inizializzeremo OpenGL tramite alcune funzioni.
FUNZIONE DI INIZIALIZZAZIONE void init(void) Cominciamo ora ad analizzare il codice: -void glClearColor(
GLfloat red, GLfloat green, GLfloat blue,
GLfloat alpha)
questa funzione specifica le componenti rossa, verde, blu ed alpha usate da glClear
per pulire il color buffer (lo schermo). Come
colore di sfondo abbiamo scelto un blu scuro, avendo assegnato
alla componente blu il valore 0.2. Dimenticavo di ricordarvi
che di solito in OpenGL il range valido per effettuare
qualunque tipo di parametrazione va da 0 a 1.
FUNZIONE DI RESIZE Questa funzione è molto simile alla "init": pulisce i buffer ridefinisce la viewport e disegna nuovamente la scena. void resize (int width,
int height) Queste sono le funzioni che ancora non abbiamo descritto: -void glClear(
GLbitfield mask ); pulisce i buffer specificati nel campo
"mask". Possiamo inserire più di un buffer separando i campi con
l'operatore logico OR "|". Nel nostro caso abbiamo cancellato il
color buffer ed il depth buffer.
FUNZIONI PER LA TASTIERA Definiremo ora due funzioni per la tastiera: una per gestire gli eventi corrispondenti alla pressione di tasti ASCII ("r" and "R" and a blank character ' ') ed un'altra per gestire i tasti direzione: void keyboard (unsigned
char key, int x, int y) Utilizziamo tre variabili per far ruotare il nostro oggetto intorno all'asse desiderato: rotation_x_increment, rotation_y_increment and rotation_z_increment. Possiamo resettare queste variabili utilizzando la barra spaziatrice per fermare l'oggetto nella sua posizione corrente. Possiamo anche cambiare la modalità di disegno per i poligoni (wireframe o solidi) utilizzando i tasti "r" o "R".
Questa funzione è molto simile alla precedente ma gestisce i tasti direzione. Vi prego di notare le costanti GLUT: GLUT_KEY_UP, GLUT_KEY_DOWN, GLUT_KEY_LEFT e GLUT_KEY_RIGHT che aumentano e diminuiscono le velocità di rotazione dell'oggetto secondo gli assi che identificano.
FUNZIONE DI DISEGNO Signore e signori ecco ciò che stavate attendendo con ansia: la funzione di disegno! void display(void) La prima parte di questa funzione pulisce i buffer colore e profondità ed applica le trasformazioni oggetto e vista. Per prima cosa selezioniamo come matrice correntemente attiva la modelview matrix tramite la funzione glMatrixMode (GL_MODELVIEW). Poi, inizializziamo questa matrice ogni frame tramite una chiamata a glLoadIdentity. -void glTranslatef(
GLfloat x, GLfloat y, GLfloat z );
muove il nostro oggetto nello spazio 3D. Questa funzione moltiplica la matrice
oggetto per la matrice di traslazione definita utilizzando i parametri x,y,z.
La lettera "f" dopo glTranslate indica che stiamo utilizzando valori
float, utilizzando invece la lettera "d" avremmo utilizzato valori
double. Utilizziamo glTranslate per muovere l'oggetto di 50 punti in avanti.
Vi ricordate la videocamera e la viewing transformation? Bene, possiamo
considerare questa operazione come una traslazione di -50 applicata alla
videocamera. Questo movimento è necessario poichè ci dobbiamo spostare di
una piccola distanza lontano dall'oggetto in modo tale da vederlo pienamente.
Potete provare a variare a piacimento il valore Z in modo da notare l'infuenza
che esso ha sulla distanza. glBegin(GL_TRIANGLES); La seconda parte della funzione display utilizza le funzioni glBegin e glEnd. Questi due comandi delimitano i vertici che definiscono una primitiva grafica. -void glBegin(
GLenum mode ); indica l'inizio di una lista di dati di
vertici che definiscono una primitiva grafica. Nel parametro "mode" dobbiamo
inserire il tipo di primitiva che vogliamo disegnare. Ci sono 10 differenti
tipologie di primitive che OpenGL ci permette di specificare (GL_TRIANGLES, GL_POYLGON, GL_LINES etc.).
Noi utilizzeremo GL_TRIANGLES per disegnare il nostro cubo utilizzando
12 triangoli. Per disegnare un singolo triangolo utilizzeremo tre chiamate a glVertex3f
e glColor3f.
LA FUNZIONE PRINCIPALE int main(int argc, char
**argv) Le seguenti quattro funzioni della libreria glut ci permettono di creare una finestra grafica senza troppi problemi. glutInit(&argc,
argv); -void glutInit(&argc,
argv); inizializza la libreria glut. Dobbiamo
chiamare questa funzione prima di chiamare qualunque altra funzione glut. Per definire le funzioni di callback utilizziamo:
glutDisplayFunc(display); -void
glutDisplayFunc(void (*func) (void)); specifica la funzione
da chiamare quando la finestra deve essere disegnata, cioè ad esempio quando
c'è una chiamata a glutPostRedisplay.
CONCLUSIONI Ed ora? Si, abbiamo finalmente terminato! Lo so, è stato un lavoro duro ma guardate un pò... ora siamo capaci di creare un vero oggetto rotante 3d! Questo significa che abbiamo creato il nostro primo motore 3d! Nella prossima lezione studieremo come realizzare il texture mapping. Ora è il momento di farmi sapere le vostre opinioni. Scrivetemi a info[at]spacesimulator.net.
CODICE SORGENTE Per compilare ed eseguire il progetto è necessaria la libreria GLUT che può essere trovata su: www.opengl.org/developers/documentation/glut.html Tramite
questi link è possibile scaricare il codice sorgente C/C++ e l'eseguibile in
formato compresso zip:
|
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
©2000-2003 Damiano Vitulli. All Rights Reserved. No Portion Of This Site May Be Reproduced Without Permission. Best viewed at 1024x768. |
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||