mercoledì 21 dicembre 2011

OpenGL in C/C++ - Impostazione progetto

Per lavorare con OpenGL abbiamo bisogno di un elemento grafico come la finestra, per cui il primo passo da fare è aprire Dev C++ e creare un nuovo progetto di tipo Windows Application, e scegliere come linguaggio di programmazione C++, perché così possiamo utilizzare le funzionalità sia di C che di C++. L'ambiente di sviluppo genererà in automatico il codice per la creazione della finestra e della procedura principale per la gestione dei messaggi della finestra.

Per avere un progetto più ordinato possiamo suddividerlo in più file che conterranno un certo gruppo di funzioni, perciò ci serve un header file per contenere le definizioni delle funzioni, mentre creeremo dei file di estensione *.cpp, che conterranno i costruttori delle funzioni. Ai file da aggiungere ho deciso di dare i seguenti nomi:

functions.h - sarà il nostro file header e conterrà i nomi delle funzioni da noi definite
init.cpp - il file che conterrà i costruttori delle funzioni che ci serviranno per l'inizializzazione di OpenGL
draw.cpp - conterrà i costruttori delle funzioni necessarie per il disegno nella finestra

Per aggiungere un nuovo file al progetto basta andare in Project->New file.
A questo punto dobbiamo modificare le #include del nostro progetto sostituendo con quelli elencati in basso, nei file indicati nel codice:

// main.cpp
#include "functions.h"

// functions.h 
#include <windows.h>
#include <gl/gl.h>
#include <gl/glu.h>

// init.cpp
#include "functions.h"

// draw.cpp
#include "functions.h"

Adesso dobbiamo dire al compilatore di importare le librerie che ci servono per utilizzare le funzioni definite nei header di OpenGL. Per questo dobbiamo andare in Project->Project Options e selezionare la scheda Parameters. Nella terza colonna sotto nome Linker aggiungere le seguenti righe:

../../../../../../../Dev-Cpp/lib/libopengl32.a
../../../../../../../Dev-Cpp/lib/libglaux.a
../../../../../../../Dev-Cpp/lib/libglu32.a
../../../../../../../Dev-Cpp/lib/libglut.a
../../../../../../../Dev-Cpp/lib/libglut32.a

Come risultato dovreste avere la seguente schermata:


Dopodiché dovete premere su ok per terminare la procedura. Adesso il compilatore sa dove andare a prendere il corpo delle funzioni di OpenGL che utilizzeremo nei nostri progetti.
Il passo successivo è quello di andare a impostare la finestra per rendere possibile il disegno su di essa da parte di OpenGL, per questo dobbiamo creare alcune variabili globali, che metteremo subito prima della funzione WinMain:

bool fullscreen = false; /* indica se vogliamo una finestra a schermo intero, oppure una finestra normale */
int width = 800; /* la larghezza della finestra */
int height = 600; /* l'altezza della finestra */
int bits = 32; /* il numero di bit per pixel che determinera` anche il numero di colori a disposizione e quindi la qualita` dell'immagine */ 
bool keys[256]; /* l'array di booleani ci permettera` di rilevare i tasti premuti, anche se contemporaneamente */

bool left_down = false; /* indica se è premuto il pulsante sinistro del mouse */
int x = 400; /* la posizione orizzontale del mouse */
int y = 300; /* la posizione verticale del mouse */
HDC hDC; /* l'HDC della finestra sulla quale disegneremo */

Le prime quattro righe servono sostanzialmente per impostare la modalità di visualizzazione della vostra applicazione. Le tre variavili width, height e bits ci serviranno per impostare la risoluzione dello schermo nel caso in cui andremo nella modalità fullscreen, mentre la variabile booleana fullscreen indicherà se vogliamo andare nella modalità fullscreen, oppure visualizzare l'applicazione nella modalità windowed.
Le successive tre variabili ci serviranno, invece, per determinare i tasti premuti e la posizione del mouse, visto che la procedura del disegno verrà effettuata in un'altra funzione, per non fare confusione. Quindi l'array keys verrà inizializzato a false per indicare che nessun tasto della tastiera è stato premuto e nel momento in cui viene premuto un tasto metteremo a true l'elemento il cui indice corrisponde al codice del tasto premuto. In questo modo ci assicuriamo di rilievare la pressione su più tasti. E come si è potuto capire le variabili x e y memorizzerano la posizione del mouse, che ci servirà nei casi in cui vorremo effettuare delle operazioni con il mouse. Mentre, invece, la variabile booleana left_down ci indicherà se è premuto il bottone sinistro del mouse ed non è stato rilasciato.
L'ultima variabile è molto importante, perché rappresenterà il handle al device context, ovvero l'accesso al dispositivo grafico che gestirà il processo di disegno della nostra finestra.
Per semplificare il codice del programma aggiungiamo altre tre variabili, però questa volta nel corpo della funzione WinMain:

RECT wndrect; /* necessaria per impostare la modalità di visualizzazione */
DWORD dwExStyle; /* conterrà il parametro ExStyle alla creazione della finestra principale */
DWORD dwStyle; /* conterrà lo stile della finestra principale */

La prima variabile ci servirà per impostare la dimensione della finestra nel caso in cui visualizzeremo l'applicazione della modalità windowed. dwExStyle ci servirà per memorizzare lo stile esteso della finestra a seconda della modalità di visualizzazione della finestra. Mentre dwStyle avrà il compito di contenere lo stile della finestra, anch'esso determinato in base alla modalità di visualizzazione.
Per quanto riguardo la classe della finestra, possiamo lasciare il codice generato da Dev C++.
Il passo successivo è quello di registrare la classe creata, che viene effettuata mediante la funzione RegisterClassEx(WNDCLASSEX). La funzione la mettiamo come condizione di un costrutto if, per avere immediatamente il risultato della registrazione e fermare il programma nel caso di fallimento. Per fare ciò inseriamo le seguenti righe di codice:

if (!RegisterClassEx (&wincl)){ /* in caso di fallimento */
    MessageBox(NULL,
        TEXT("Errore registrazione classe finestra"),
        TEXT("Errore"),MB_OK); /* visualizza messaggio di errore */
    return 0; /* esce dal programma */ 
}

Una volta verificato che la classe della finestra sia stata registrata procediamo con il riempimento della variabile wndrect, che è una struttura di tipo RECT e ci servirà per determinare l'area della finestra, nella quale potremo disegnare dopo l'impostazione della modalità di visualizzazione di essa. Il codice per il riempimento di questa struttura lo trovate di seguito:

wndrect.left = (long)0
wndrect.top = (long)0;
wndrect.right = width;
wndrect.bottom = height;

In questo modo abbiamo definito la dimensione dell'area interna della finestra sulla quale andremo a disegnare, che nel caso della modalità fullscreen rappresenta la dimensione della finestra stessa, mentre nel caso in cui siamo nella modalità windowed rappresenta soltanto la parte interna, escludendo i bordi e l'intestazione. 
Fatto ciò procediamo con l'impostazione della modalità di visualizzazione, quindi dobbiamo vedere se vogliamo andare nella modalità fullscreen e nel caso affermativo riempiamo una struttura di tipo DEVMODE, dopodiché con la funzione ChangeDisplaySettings(*DEVMODE,int) cambiamo la risoluzione dello schermo a quella da noi richiesta. Per fare ciò creiamo una nuova funzione nel file init.cpp:

bool set_fullscreen(int width, int height, int bits){
    DEVMODE dmScreenSettings;
    memset(&dmScreenSettings,0,sizeof(dmScreenSettings));
    dmScreenSettings.dmSize = sizeof(dmScreenSettings); 
    dmScreenSettings.dmPelsWidth = width;
    dmScreenSettings.dmPelsHeight = height;
    dmScreenSettings.dmBitsPerPel = bits;
    dmScreenSettings.dmFields = DM_PELSWIDTH | DM_PELSHEIGHT | DM_BITSPERPEL;
    if (ChangeDisplaySettings(&dmScreenSettings,CDS_FULLSCREEN)!=
                DISP_CHANGE_SUCCESSFUL){
        MessageBox(NULL,
            TEXT("Impossibile passare alla modalitа fullscreen"),
            TEXT("Errore"),MB_OK | MB_ICONERROR);
        return false;
    }
    return true;
}

In questo modo quando chiamiamo la funzione set_fullscreen appena definita riempiamo una struttura di tipo DEVMODE, per poi utilizzarla nella funzione ChangeDisplaySettings, che cambierà la risoluzione dello schermo. Ma nel caso in cui la risoluzione scelta non sia disponibile sul computer usato, viene visualizzato un messaggio di errore e la funzione termina restituendo false, che determina il relativo comportamento del programma, altrimenti esce restituendo true. Naturalmente la definizione della funzione deve essere fatta anche nel file functions.h, che viene fatta semplicemente aggiungendo la seguente riga di codice:

bool set_fullscreen(int width, int height, int bits);

A questo punto nel file main.cpp scriviamo:

if (fullscreen){ /* se è richiesta la modalità fullscreen */
    /* richiamiamo la funzione definita in init.cpp */ 
    fullscreen = set_fullscreen(width,height,bits);            

if (fullscreen){ /* se il dispositivo supporta il fullscreen */
    /* Visualizza la finestra sopra la barra delle applicazioni */
    dwExStyle = WS_EX_APPWINDOW; 
    dwStyle = WS_POPUP; /* la finestra è una popup */
    ShowCursor(false); /* nasconde il cursore quando è fullscreen */               
}else{ /* se siamo nella modalità windowed */
    dwExStyle = WS_EX_WINDOWEDGE | WS_EX_APPWINDOW; 
    dwStyle = WS_OVERLAPPEDWINDOW; /* la finestra ha i bordi */      
}
/* adatta la dimensione della finestra alla dimensione indicata dalla struttura wndrect e a seconda dello stile della finestra */
AdjustWindowRectEx(&wndrect,dwStyle,false,dwExStyle);

E adesso possiamo procedere con la creazione della finestra principale, sulla quale andremo a disegnare. Nel codice generato abbiamo già la chiamata alla funzione che crea la finestra, però dobbiamo applicare qualche modifica per crearla in base ai valori delle variabili globali definite all'inizio del programma:

hwnd = CreateWindowEx(dwExStyle, /* lo stile esteso */
           szClassName,  /* nome della classe della finestra */
           TEXT("Model Editor"), /* titolo della finestra */
           dwStyle | WS_CLIPSIBLINGS | WS_CLIPCHILDREN, /* stile */
           CW_USEDEFAULT, /* posizione orizzontale definito da windows */
           CW_USEDEFAULT, /* posizione verticale definito da windows */ 
           wndrect.right-wndrect.left, /* larghezza finestra */
           wndrect.bottom-wndrect.top, /* altezza finestra */
           HWND_DESKTOP, /* la finestra padre */
           NULL, /* nessun menu */
           hThisInstance, /* istanza del programma */
           NULL); 

Con questa istruzione abbiamo creato la nostra finestra principale, che ha le impostazioni da noi richieste, quindi dobbiamo procedere con l'inizializzazione di OpenGL. Il primo passo da fare in questa direzione è quello di impostare le caratteristiche dei pixel di cui è formata la nostra area di disegno. Quindi andremo a definire una nuova funzione che fa questo lavoro e visto che anche questa parte si riferisce all'inizializzazione, la scriveremo dentro init.cpp:

GLvoid SetGLPixelFormat(HWND hwnd, int bits){
    GLuint pxFormat;
    HDC hDC;
    HGLRC hRC;
    /* definisce il formato dei pixel */
    static  PIXELFORMATDESCRIPTOR pfd = {                 
        sizeof(PIXELFORMATDESCRIPTOR),  /* dimensione struttura*/                
        1, /* numero della versione */                              
        PFD_DRAW_TO_WINDOW | /* supporta le finestre */                        
        PFD_SUPPORT_OPENGL | /* supporta opengl */                       
        PFD_DOUBLEBUFFER, /* supporta la doppia bufferizzazione */
        PFD_TYPE_RGBA, /* richiesto un fromato RGBA */                          
        bits, /* numero di bit per pixel */                             
        0, 0, 0, 0, 0, 0, /* bit di colore ignorati */                       
        0, /* nessun buffer per la trasparenza */                             
        0, /* shift bit ignorato */                             
        0, /* nessun buffer di accumulazione */                             
        0, 0, 0, 0, /* bit di accumulazione ignorati */                         
        16, /* buffer a 16 bit per la profondità */                            
        0, /* nessun buffer per la matrice */                              
        0, /* nessun buffer ausiliario */                             
        PFD_MAIN_PLANE, /* disegno sul livello principale */                         
        0, /* riservato */                              
        0, 0, 0 /* maschere di livello ignorate */                            
    }; 
    hDC = GetDC(hwnd); /* cattura l'HDC della finestra principale */
    pxFormat = ChoosePixelFormat(hDC,&pfd); /* sceglie il formato dei pixel */
    SetPixelFormat(hDC,pxFormat,&pfd); /* imposta il formato dei pixel */
    hRC = wglCreateContext(hDC); /* crea un contesto grafico */
    wglMakeCurrent(hDC,hRC); /* lo imposta come corrente */ 
}

Creata la funzione, dobbiamo aggiungere la sua definizione al file functions.h, in modo che sia visibile a tutti i file del progetto, quindi aggiungiamo la seguente riga:

GLvoid SetGLPixelFormat(HWND hwnd, int bits);

A questo punto basta richiamarla nel file main.cpp:

SetGLPixelFormat(hwnd,bits); /* imposta il formato dei pixel */
ShowWindow(hwnd,nCmdShow); /* !!! generata dal ambiente di sviluppo */

Dopo aver visualizzato la finestra procediamo con l'inizializzazione di opengl e del modo in cui verranno visualizzate le figure disegnate con questa libreria. E anche questa volta creiamo una funzione esterna per fare questo lavoro, inoltre, definiamo un'altra funzione, che si occuperà di impostare la prospettiva a seconda della dimensione della finestra. Entrambe le funzioni andranno scritte in init.cpp:

int InitGL(GLvoid){
    /* imposta il colore di pulizia dello schermo */
    glClearColor(0.0f,0.0f,0.0f,0.5f);
    glClearDepth(1.0f); /* la profondità della pulizia */
    /* abilita la scrittura sul buffer di profondità */
    glEnable(GL_DEPTH_TEST); 
    return 0; /* se tutto va bene termina con lo 0 */    
}

GLvoid ResizeGLScene(GLsizei width, GLsizei height){
    height = height==0?1:height; /* evita divisione per zero */
    glViewport(0,0,width,height); /* imposta la vista */
    /* seleziona la matrice della progettazione */
    glMatrixMode(GL_PROJECTION); 
    glLoadIdentity(); /* resetta la matrice */
    /* calcola la prospettiva con la profondità massima a 100 */
    gluPerspective(45.0f,(GLfloat)width/(GLfloat)height,
                   1.0f,100.0f);
    /* seleziona la matrice di visualizzazione delle figure */
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity(); /* resetta la matrice */
}

Ovviamente al file functions.h dovremo aggiungere:

int InitGL(GLvoid);
GLvoid ResizeGLScene(GLsizei width, GLsizei height);

Come è possibile notare, in queste due funzioni per la prima volta abbiamo usato delle funzioni che appartengono alla libreria OpenGL. Vediamo in breve cosa rappresenta ognuna di queste funzioni:

glClearColor - specifica il colore che verrà utilizzato per cancellare l'immagine disegnata precedentemente sullo schermo. I quattro parametri di tipo GLfloat possono assumere valori da 0.0f a 1.0f e rappresentano il colore espresso in RGBA, un formato che permette di definire anche la trasparenza di un colore. 

glClearDepth - indica la profondità alla quale viene effettuata la pulizia dello schermo, cancellando tutto ciò che sta oltre a quella profondità. Bisogna tenere conto del fatto che OpenGL si prende la briga di occuparsi della visualizzazione delle figure nello spazio, per cui il programmatore deve soltanto indicare le coordinate di ogni figura e OpenGL deciderà quale parte della figura visualizzare e quale no, a seconda se viene coperta da un'altra figura che ci copre la vista. Inoltre, si deve tenere a mente il fatto che se vogliamo indicare una profondità di 10.0f, dobbiamo indicarla come -10.0f, perché tutto ciò che vogliamo visualizzare si trova oltre lo schermo, cioè sulla parte negativa dell'asse z. 

glEnable(GL_DEPTH_TEST) - abilita l'utilizzo del buffer di profondità. In genere la funzione glEnable viene utilizzata per abilitare diverse funzionalità della libreria, come, ad esempio, l'abilitazione della luminosità o della visualizzazione delle texture.

glViewport - viene utilizzata per impostare i parametri utilizzati per la trasformazione delle immagini di un ambiente tridimensionale in uno bidimensionale, per produrre la sensazione di avere un mondo 3D dietro lo schermo.

glMatrixMode() - seleziona una delle matrici utilizzate per la generazione dell'immagine 3D desiderata. Le due matrici finora utilizzate sono la matrice di visualizzazione e la matrice della gestione delle figure dell'ambiente tridimensionale. La prima, chiamata projection matrix viene utilizzata per produrre le immagini 2D partendo dall'ambiente 3D, in modo da poterle visualizzare sullo schermo, che rappresenta, appunto,  un ambiente bidimensionale. La seconda, invece, chiamata modelview matrix che serve per definire il posizionamento delle figure in un ambiente 3D. 

glLoadIdentity() - utilizzata molto spesso, perché permette di reimpostare la matrice sulla quale si sta lavorando ad una matrice identica, che rappresenta la situazione iniziale.


gluPerspective - imposta i parametri della prospettiva da usare, il primo argomento indica il tipo di prospettiva a 45°, il secondo indica il rapporto larghezza/altezza, il terzo e il quarto indicano il minimo e il massimo della profondità, oltre la quale la figura sparisce.

Chiusa la parentesi, procediamo con il nostro programma. Sempre nel file main.cpp subito dopo la visualizzazione della finestra facciamo una chiamata alla funzione InitGL, seguita dalla chiamata alla funzione ResizeGLScene, dopodiché scriviamo il codice generato da Dev C++, leggermente modificato, per integrarsi con OpenGL. Le modifiche da efettuare stanno nel ciclo di elaborazione dei messaggi. Vediamo il codice:

InitGL();
ResizeGLScene(width,height);
hDC = GetDC(hwnd); /* ottiene l'HDC della finestra */
bool done = false; /* tiene aperto il ciclo */
while (!done){
    /* cattura gli eventi della finestra */
    if (PeekMessage(&messages,NULL,0,0,PM_REMOVE)){
        /* se arriva il messaggio di chiusura */
        if (messages.message==WM_QUIT){
            done = true;  /* esce dal ciclo e chiude il programma */
        }
        TranslateMessage(&messages); /* decodifica il messaggio */
        DispatchMessage(&messages); /* lo invia a WindowProcedure */
    }else{ /* se non ci sono messaggi */
        /* disegna la scena da visualizzare */
        if (DrawGLScene(keys,x,y,left_down)){  
            SwapBuffers(hDC); /* scambia il buffer */               
        }
    }
}

Con questo abbiamo concluso la definizione della funzione WinMain, però ciclo di elaborazione dei messaggi abbiamo utilizzato la funzione DrawGLScene, che non abbiamo ancora definito, per cui andiamo subito a definirla. Visto che questa funzione ci serve per disegnare nella finestra andremo a definirla nel file draw.cpp, finora non utilizzato:

bool DrawGLScene(bool keys[], int x, int y, bool left_down){      
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); 
    glLoadIdentity(); /* reimposta la matrice */
    glTranslatef(0.0f,0.0f,-5.0f);
    glBegin(GL_QUADS);
        glVertex3f(-1.0f,1.0f,0.0f);
        glVertex3f(1.0f,1.0f,0.0f);
        glVertex3f(1.0f,-1.0f,0.0f);
        glVertex3f(-1.0f,-1.0f,0.0f);
    glEnd();
    return true;
}

Come per le altre funzioni definite esternamente anche questa deve essere dichiarata in functions.h:

bool DrawGLScene(bool keys[], int x, int y, bool left_down);

Abbiamo creato un'altra funzione, ma vediamo un attimo come è fatta:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) - la seguente funzione della libreria OpenGL esegue la pulizia dello schermo alla profondità indicata. Viene fatta subito prima di disegnare un'altra immagine.

glTranslatef - trasla l'origine delle coordinate al punto indicato nelle coordinate, per cui ogni figura disegnata dopo questa istruzione verrà posizionata rispetto al nuovo origine. La funzione prende in input 3 parametri che indicano le coordinate x, y e z rispettivamente, che devono essere di tipo float. Esistono varie versioni di questa funzione: glTranslateiglTranslated, che prendono in ingresso interi.

glBegin(GL_QUADS) - indica l'inizio della definizione delle coordinate dei vertici di una figura, che in questo caso è una quadrica, ma possono essere linee, triangoli, poligoni, a seconda del valore passato alla funzione. In base alla figura che si vuole disegnare cambia anche il numero di vertici che dobbiamo definire.

glVertex3f - definisce un vertice della figura che viene disegnata. La funzione, analogamente a glTranslatef, prende in ingresso 3 parametri di tipo float, che rappresentano le coordinate del vertice rispetto all'origine corrente. Anch'essa può prendere in input più tipi di dati, a seconda dell'ultima lettera del nome, che indica, appunto, il tipo dei dati.

glEnd() - indica la fine della definizione della figura.

Ormai siamo quasi arrivati alla fine, perché ci resta soltanto aggiungere qualche ramo allo switch di WindowProcedure, che si trova in main.cpp, per gestire gli eventi di movimento del mouse, della pressione dei tasti e del ridimensionamento della finestra. Quindi aggiungiamo allo switch il seguente codice:

case WM_SIZE:
    /* ricalcola la prospettiva e la dimensione dell'ambiente a seconda della dimensione della finestra */
    ResizeGLScene(LOWORD(lParam),HIWORD(lParam));
    break
case WM_MOUSEMOVE:
    x = LOWORD(lParam); /* memorizza la posizione orizzontale del mouse */
    y = HIWORD(lParam); /* memorizza la posizione verticale del mouse */
    break
case WM_KEYDOWN:
    /* imposta a true l'elemento con il codice del tasto premuto */
    keys[wParam]=true
    break;   
case WM_KEYUP:
    /* imposta a false l'elemento con il codice del tasto premuto */
    keys[wParam]=false
    break
case WM_LBUTTONDOWN:
    /* mette a true alla pressione del bottone sinistro del mouse */
    left_down = true
    break;
case WM_LBUTTONUP:
    /* mette a false al rilascio del bottone sinistro del mouse */
    left_down = false
    break;

Così abbiamo completato il programma, che deve soltanto disegnare una quadratica (quadrato nel nostro caso), e quindi possiamo eseguirlo, ottenendo il seguente risultato:
Qualcuno di voi si sarà chiesto perché fare tutto questo lavoro per avere soltanto un quadrato, quando è possibile disegnare direttamente sulla finestra? Ma la risposta è molto semplice, perché una volta inizializzata la libreria OpenGL possiamo disegnare facilmente diverse figure, avendo uno spazio tridimensionale e avendo a disposizione le funzioni necessarie per muovere, ruotare etc. le figure create, oltre che ad una notevole velocità del programma, dato che OpenGL ha accesso diretto all'interfaccia hardware che si occupa della grafica.

Download progetto Dev C++: impostazione_opengl.zip

Nessun commento:

Posta un commento