Scarica qui i sorgenti.

OpenGL Tutorial

di Marco Frisan (aka Ender) basato sul codice sorgente ideato e scritto da GioFX



0 Capitolo Introduttivo


0.1 Requisiti di sistema


Mac OS X 10.0 o superiore

ProjectBuilder o Xcode



0.2 A chi è rivolto questo tutorial


Benché il tutorial sia abbastanza esaustivo è necessario avere almeno delle nozioni base di programmazione col linguaggio C. In alternativa una buona conoscenza di C++, Objective-C o Java può essere un buon punto di partenza, ma è necessario sapere almeno quali siano le differenze principali fra questi linguaggi e il C.

E anche importante conoscere abbastanza bene Xcode. Anche se quel poco che serve sapere di Xcode per eseguire questo tutorial verrà spiegato la conoscenza del programma IDE che si usa è sempre fondamentale. Non viene affrontata, in questo testo, la compilazione da linea di comando. Chi volesse usare un editor di testo e gcc deve trovare altrove le istruzioni per la compilazione.



0.3 Introduzione


A causa della diffusione sempre più massiccia dei personal computers anche in settori diversi da quelli amministrativi, l'industria informatica cominciò ad accorgersi che la CPU, da sola, non era più adatta a soddisfare le esigenze prestazionali che avrebbero potuto consentire ai computer di offrire migliori prestazioni grafiche. Sia l'industria scientifica che quella dell'intrattenimento avevano bisogno di qualcosa in più. All'inizio nacquero le prime GPU proprietarie con prestazioni grafiche avanzate e con set di istruzioni ottimizzate.

Finché cominciarono ad affermarsi le schede video che offrivano l'indubbio vantaggio di poter aumentare le prestazioni grafiche di un computer semplicemente sostituendo la scheda video con una più potente. Le GPU montate sulle schede video offrivano dei set di istruzioni specifiche per dialogare direttamente con il chip.

Alla fine, si sono affermati due grandi librerie. DirectX, disponibile solo per il sistema operativo di Microsoft, e OpenGL disponibile per tutte le altre piattaforme.


Per ovvie ragioni, quindi, Apple decise di sfruttare le possibilità offerte da OpenGL, e in particolar modo, ora, che Mac OS X è fondato su un'implementazione di Unix.


Tramite le istruzioni OpenGL possiamo dialogare direttamente con la scheda video e quindi sfruttare la sua potenza di calcolo e memorizzazione evitando di sovraccaricare di lavoro il processore e la RAM. Così facendo, ovvero suddividendo i compiti su hardware specifiche riusciamo a ottimizzare le prestazioni del computer nel suo complesso e allo stesso tempo ottenere ottime prestazioni grafiche.


Come primo tutorial eseguiremo qualcosa di davvero molto semplice. Creeremo una finestra, sulla quale disegneremo un quadrato rotante, colorato di rosso, verde, blu e giallo.


Nonostante lo scopo sia quello di introdurre OpenGL, questo tutorial, nella sua evoluzione, si è rivelato essere anche una valida introduzione a Carbon C. La prima parte, infatti tratta di come creare la finestra e impostare gli Event Handlers per interagire con l'applicazione.


Ma bando alle chance e diamoci da fare.



1 Preparazione


1.1 Creare il progetto in Xcode


L'applicazione sarà costituita da tre soli documenti. Un Header (o Interfaccia o Intestazione) e due documenti di Implementazione. main.h, l'Header, conterrà tutte le dichiarazioni che implementeremo negli altri due documenti: main.c e opengl.c.


Per prima cosa, quindi, dobbiamo creare un nuovo progetto in Xcode. Consiglio di scegliere il template Empty Project, perché tutto il codice che scriveremo sarà in puro Carbon C e non abbiamo bisogno di alcun documento NIB o cose simili, specifiche del ambiente di sviluppo Mac. Ho dato al progetto il nome "opengl_skeleton", ma voi potete scegliere il nome che preferite.


Dopo aver creato il progetto dovremo creare subito un nuovo Target. Clickate col tasto destro del mouse su Targets e scegliete Add, New Target..., dal menù contestuale. Nel Assistant, scegliete Carbon, Application.


Ci sarà bisogno di aggiungere anche frameworks di cui ha bisogno l'applicazione per funzionare. Dovremo aggiungerne tre: AGL.framework, OpenGL.framework e Carbon.framework.


Ora che il progetto è configurato possiamo creare il primo sorgente. Clickate col tasto destro sulla cartellina gialla sources (nella colonna di sinistra della finestra del progetto) e, dal menù contestuale, scegliete Add, New File.... Nel Assistant scegliete Carbon, Header File. Dategli il nome "main.h".


Così, come viene creato da Xcode, main.h contiene soltanto una direttiva del preprocessore, che include l'Header di Carbon.


#include <Carbon/Carbon.h>


Due parole su main.h. Fra poco inizieremo a scrivere del codice. Alla fine, main.h conterrà le dichiarazioni di tutte le procedure (anche dette funzioni) che implementeremo sucessivamente nei due documenti di Implementazione. Le funzioni di entrambi i documenti di Implementazione saranno dichiarate in main.h.



1.2 main.c, il cuore dell'applicazione


Per creare main.h, clickate col tasto destro sulla cartellina gialla sources e dal menù contestuale scegliete Add, New File.... Nel Assistant scegliete Carbon, C File e premete il bottone Next. Nella pagina successiva dell'Assistant digitate il nome "main.c", nel campo di testo File Name:, e disattivate il checkbox Also create "main.h", poiché noi l'abbiamo già creato prima. Lasciate il resto così com'è e procedete.


All'inizio main.c contiene soltanto la direttiva per il preprocessore:


#include "main.h"


Questo è pressappoco il risultato che otterrete.


0001.png


Ora sta a noi creare le funzioni che ci servono.



2 L'implementazione


2.1 La funzione main()


Come certamente saprete, la funzione main() è obbligatoria ed è il punto di partenza di ogni applicazione in C. main() è anche l'unica funzione che non necessita di essere dichiarata nell'Header.


Possiamo quindi inserire subito una funzione main() vuota all'interno di main.c, come appare nell'immagine.


0002.png



2.2 Breve descrizione delle funzioni di main.c


Dentro al documento main.c implementeremo altre cinque funzioni: InitHandlers(), DeleteHandlers(), appEventHandler(), windowEventHandler() e TimerAction().


Brevemente vi spiego quale sarà il loro scopo.


All'interno del corpo della funzione InitHandlers() inseriremo il codice per preparare tutti gli Event Handlers, siano essi gestori di messaggi, eventi di finestra, ...


La funzione DeleteHandlers() conterrà il codice per liberare la memoria dalle risorse impiegate dagli Event Handlers, quando l'applicazione viene chiusa.


La funzione appEventHandler() sarà il gestore degli eventi generati dal mouse (click, drag, ...).


La funzione windowEventHandler() gestirà altri tipi di eventi, prodotti dalla finestra dell'applicazione. Questa funzione verrà invocata, ad esempio, quando la finestra verrà chiusa, oppure se verrà ridimensionata. windowsEventHandler(), inoltre, intercetterà tutti gli eventi prodotti dalla pressione (anzi, per essere precisi, il rilascio) dei pulsanti della tastiera.


Infine faremo in modo che la funzione TimerAction() venga invocata con intervalli regolari di un centesimo di secondo (0,01 secondi). Grazie al fatto che viene eseguita periodicamente, TimerAction() sarà il luogo ideale per ridisegnare il contenuto della finestra (rendering) e a produrre delle animazioni.


Prima di implementare le cinque funzioni, però, dobbiamo dichiararle all'interno di main.h.


Aprite l'editor di Xcode e inserite le dichiarazioni. Così:


0003.png


Come potete osservare soltanto appEventHandler() e windowEventHandler() restituiscono qualcosa, per la precisione un tipo OSStatus. Al momento preferisco sorvolare su quale sia lo scopo di OSStatus e concentrare l'attenzione sul resto dell'applicazione.


La prima funzione che implementeremo è InitHandlers(). All suo interno inizializzeremo due Event Handlers, che associeremo alle variabili: gAppEventHandler e gWindowEventHandler. Per avere accesso a queste due variabili da tutte le funzioni di main.c, le inizializzeremo, con valore NULL, all'inizio del codice immediatamente dopo le direttive del preprocessore (#include) e prima di ogni altra istruzione.


0004.png


gAppEventHandler e gWindowEventHandler sono due variabili sono di tipo EventHandlerUPP. UPP è la sigla di Universal Procedure Pointer. Significa che queste due variabili sono dei puntatori a delle procedure (funzioni). Fra poco, infatti, punteremo queste due variabili a due procedure, che creeremo appositamente per gestire gli eventi prodotti dalle azioni dell'utente. Queste due procedure diventeranno, quindi, i nostri Event Handler personalizzati.


NOTA: per maggiori informazioni su EventHandlerUPP potete leggere Carbon Event Manager Reference e Carbon Event Manager Programming Guide.


Per far si che una funzione personalizzata diventi un Event Hadler Carbon mette a disposizione diverse funzioni. Data la semplicità dell'applicazione che stiamo sviluppando, fra quelle disponibili, ne useremo due che semplificano parecchio la vita: InstallApplicationEventHandler() e InstallWindowEventHandler().


Ad entrambe le funzioni dovremo passare i parametri che seguono:


  1. inHandler, il puntatore alla funzione, che vogliamo far diventare il nostro Event Handler personalizzato;
  2. inNumTypes, il numero di tipi di eventi che l'Event Handler intercetterà;
  3. inList, il puntatore a un array, costituito da elementi del tipo EventTypeSpec, ovvero la lista di tipi di eventi intercettati da questo Event Handler;
  4. inUserData, il valore passato in questo parametro viene rinviato alla funzione che diventerà il nostro Event Handler personalizzato;
  5. outRef, un riferimento all'Event Handler che potremo usare più tardi per rimuovere l'Event Handler dalla memoria. Questo parametro può assumere anche il valore NULL.


Siccome vogliamo mantenere un riferimento all'Event Handler una volta installato, dichiariamo subito una variabile EventHandlerRef all'inizio della funzione InitHandlers().


EventHandlerRef ref;


0005.png


Subito dopo inizializziamo l'array di tipo EventTypeSpec, per contenere la lista di tipi di eventi, che l'Event Handler dovrà gestire.


EventTypeSpec alist[] = { { kEventClassMouse, kEventMouseDown },

{ kEventClassMouse, kEventMouseUp },

{ kEventClassMouse, kEventMouseDragged },

{ kEventClassMouse, kEventMouseMoved },

{ kEventClassMouse, kEventMouseWheelMoved } };



Quindi, inizializziamo il puntatore di tipo EventHandlerUPP indirizzandolo alla funzione appEventHandler().


gAppEventHandler = NewEventHandlerUPP(appEventHandler);


Ora siamo pronti per installare l'Event Handler.


InstallApplicationEventHandler(gAppEventHandler, GetEventTypeCount(alist), alist, window, &ref);


Degli argomenti passati alla funzione InstallApplicationEventHandler(), quelli che potrebbero essere ambigui e necessitano di spiegazioni sono GetEventTypeCount(alist), &ref e window.

GetEventTypeCount(alist) non è altro che una funzione che restituisce il numero di elementi dell'array alist.

&ref è un puntatore al valore della variabile ref, che abbiamo dichiarato poche righe sopra. Passando il puntatore alla funzione InstallApplicationEventHandler() di fatto inizializziamo ref.

window è un reference che punta alla finestra dell'applicazione. Ma non l'abbiamo ancora dichiarata da nessuna parte. È utile che la finestra dell'applicazione sia accessibile a tutte le funzioni di main.c, quindi la dichiareremo immediatamente sotto ai due EventHandlerUPP prima della funzione main().


0006.png


Ora inizializzeremo l'Event Handler per gestire gli eventi propagati dalla finestra e dalla tastiera. Non mi dilungo in ulteriori spiegazioni. La procedura da seguire è identica.


0007.png



2.3 Eseguire un animazione: il Timer


Le animazioni sono costituite da una sequenza di fotogrammi che si susseguono velocemente sullo schermo. Come al cinema o in TV. In ogni fotogramma di un animazione gli oggetti rappresentati possono cambiare colore, posizione, orientamento, scala e tante altre cose. Questi cambiamenti di stato che si susseguono velocemente ci danno l'illusione di movimento.

Abbiamo pertanto bisogno di creare un'animazione che disegni e visualizzi sullo schermo dei fotogrammi in veloce sequenza. Pertanto, per crearla, dobbiamo riuscire a eseguire ripetutamente una qualche operazione ad intervalli più o meno regolari. Per fare questo, dobbiamo far entrare l'applicazione in un loop infinito, che aggiorni lo stato degli oggetti e li ridisegni sullo schermo periodicamente.

Normalmente si userebbe un ciclo while(); ma in questo caso abbiamo deciso di utilizzare un comodo strumento offerto da Carbon, un Timer. Questo genere di "oggetti", in Carbon, fa sempre parte delle librerie per la gestione degli eventi. Un Timer in Carbon, in pratica è un processo che propaga un evento ripetutamente e a intervalli prestabiliti.

Noi possiamo definire quale funzione debba gestire questo tipo di eventi con che frequenza il Timer debba propagarli.


Per prima cosa, quindi, inizializzeremo una variabile globale di tipo EventLoopTimerRef, che sarà un reference che punta al Timer.


EventLoopTimerRef theTimer = NULL;


0008.png


Dopodiché, all'interno di InitHandlers(), installeremo l'Event Handler.


InstallEventLoopTimer(GetCurrentEventLoop(), 0, 0.01, NewEventLoopTimerUPP(TimerAction), NULL, &theTimer);


0009.png


La funzione InstallEventLoopTimer() funziona in modo analogo alle le funzioni che abbiamo usato prima. Ma ci sono delle piccole differenze, pertanto è il caso di vedere un attimino quali parametri ammette.


  1. Il primo parametro della funzione deve essere di tipo EventLoopRef, che è un riferimento a un Event Loop. Per ottenerlo usiamo la funzione di convenienza GetCurrentEventLoop(), che restituisce l'Event Loop attualmente in esecuzione. Nel nostro caso, quindi, l'unico in esecuzione: l'Event Loop principale dell'applicazione.
  2. Il secondo parametro stabilisce il tempo che il Timer deve attendere prima di lanciare il primo evento. Vi abbiamo fatto impostare il valore 0 (zero), in questo modo il Timer partirà immediatamente.
  3. Il terzo argomento stabilisce il tempo che deve intercorrere fra un evento e quello sucessivo. Questo tempo si esprime in secondi, percui, nel nostro caso, la funzione associata all'evento verrà invocata ogni centesimo di secondo.
  4. Il quarto argomento è un puntatore alla procedura, ovvero alla funzione che vogliamo installare come Event Handler personalizzato.
  5. Il quinto argomento (opzionale) serve per passare dei dati alla funzione installata come Event Handler, ad ogni sucessiva invocazione. L'abbiamo impostata su NULL, poiché per il momento non siamo interessati a sfruttare questa funzionalità.
  6. E, infine, l'iltimo argomento è un riferimento all Timer installato, che abbiamo dichiarato sopra.


Come potete vedere non ci sono molte differenze rispetto all'installazione degli altri Event Handler solo che in questo caso, ad esempio, abbiamo inizializzato e passato l'UPP nella stessa istruzione, senza assegnarlo prima ad una variabile separata.



2.4 Fare qualcosa al verificarsi di un evento: gli Event Handlers


A questo punto non ci resta che definire gli Event Handlers: TimerAction(), windowEventHandler() e appEventHandler(). Avevamo già dichiarato le funzioni in main.h, percui possiamo collocarle in qualsiasi punto di main.c.


Cominciamo con la più semlice TimerAction().


void TimerAction(EventLoopTimerRef inTimer, void* userData) {

// ...

}


All'interno della funzione TimeAction(), inserite un'unica istruzione:


OpenGL_Render();


Essa invoca la funzione OpenGL_Render(), a cui assegneremo il compito di eseguire il rendering OpenGL vero e proprio. Ma la implementeremo più tardi, dentro all'altro documento di Implementazione. Prima dobbiamo completare il codice di main.c.


Inserite le altre due funzioni.


OSStatus appEventHandler(EventHandlerCallRef myHandler, EventRef event, void* userData) {

// ...

}


OSStatus windowEventHandler(EventHandlerCallRef myHandler, EventRef event, void* userData) {

// ...

}



2.5 La funzione appEventHandler()


Cominceremo con appEventHandler(). Innanzitutto definiamo tre variabili che ci serviranno in seguito: result, evtClass, evtKind.


OSStatus result = eventNotHandledErr;

UInt32 evtClass = GetEventClass(event);

UInt32 evtKind = GetEventKind(event);


Alla fine della sua esecuzione appEventHandler() restituisce result. Se osservate la [] il tipo restituito da appEventHandler() è proprio OSStatus.

result è inizializzata con il valore di un codice di errore: eventNotHadledErr. Il suo nome è sufficientemente descrittivo: è un codice di errore che identifica un evento non gestito o meglio, non gestibile. Nel caso in cui la funzione appEventHandler() non fosse in grado di gestire un dato evento, il valore di result rimarrebbe inalterato e perciò appEventHandler() restituirebbe il valore di eventNotHadledErr. In caso contrario, il valore di result verrebbe modificato con un nuovo codice, che identifica, invece, un'operazione completata con successo, e appEventHandler() restituirebbe il nuovo codice.

Le altre due variabili le abbiamo inizializzate con la classe (window, mouse, keyboard, ...) e il tipo di evento intercettato dalla funzione.


NOTA: per sapere cos'è la classe di un evento consultate Event Types, nel capitolo Carbon Event Handling Theory della guida Carbon Event Manager Programming Guide, disponibile nella documentazione di Xcode oppure online.


0010.png


Usando un'istruzione switch(), verificheremo a quale classe appartiene l'evento intercettato. In questo modo:


switch (evtClass) {

case kEventClassMouse:

// ...

}


appEventHandler(), per ora, gestisce una sola classe di eventi, identificata dalla costante kEventClassMouse. Il ciclo switch(), pertanto, verifica un solo caso. Il caso in cui la variabile evtClass assuma lo stesso valore di kEventClassMouse.


Non appena si verifica tale condizione inizializziamo due nuove variabili locali: button e location che servono rispettivamente a memorizzare quale pulsante sia stato premuto e la posizione del puntatore del mouse.


EventMouseButton button = 0;

HIPoint location = {0.0f, 0.0f};


Alcuni tipi di evento portano con se informazioni aggiuntive. Quando, ad esempio, facciamo click col mouse in un qualsiasi punto dello schermo, oltre alle informazioni legate all'evento stesso (classe, tipo, ...), Carbon registra anche altre informazioni, come la posizione del mouse, sottoforma di coordinate cartesiane X e Y. Analogamente, quando un evento viene generato da una finestra, Carbon registra quale finestra abbia generato l'evento.

Per ricavare queste informazioni aggiuntive Carbon ci offre la funzione GetEventParameter(). E così inseriremo l'istruzione GetEventParameter() in modo da memorizzare nella variabile globale window la finestra che ha generato l'evento catturato. Ricordate window? L'avevamo inizializzata all'inizio del codice di main.c, assegnandole il valore NULL.

Comunque la prossima istruzione è:


GetEventParameter(event, kEventParamWindowRef, typeWindowRef, NULL, sizeof(WindowRef), NULL, &window);


NOTA: Per una descrizione dettagliata della funzione GetEventParameter() leggete il documento Carbon Event Manager Reference, accessibile dal menù Help di Xcode, ma disponibile anche online.


Gli Event Handlers in Carbon sono organizzati per classi e tipi di eventi che gestiscono. Per ogni copia classe/tipo possiamo registrare quanti Event Handlers vogliamo. Essi vengono impilati in uno stack.


NOTA: Sapete, vero, cos'è uno stack? Ok, ok! Per quelli che non lo sapessero, lo stack è simile ad un array, ma funziona come una pila dalla quale possiamo togliere o aggiungere un elemento alla volta dalla cima della pila. L'elemento in cima alla pila è l'elemento corrente ed è l'unico visibile (=leggibile).


Quando viene generato un evento prodotto da un certo Target (una finestra, un'applicazione, ...), Carbon passa la gestione dell'evento al primo Event Handler in cima allo stack degli Event Handler, registrati per quel Target.

Ci sono, però, in Carbon degli Event Handler preinstallati. Essi fanno in modo che l'interfaccia risponda agli input dell'utente in modo standard. Per esempio, facendo minimizzare una finestra (effetto Genio), quando facciamo click sul bottone giallo della barra del titolo della finestra.


Dato che noi abbiamo messo appEventHandler() in cima allo stack, tutti questi comportamenti standard andrebbero persi. C'è però un modo per sfruttare comunque le funzionalità offerte dagli Event Handler sottostanti: la funzione CallNextEventHandler().


result = CallNextEventHandler(myHandler, event);


Se l'Event Handler successivo non sarà in grado di gestire questo evento, CallNextEventHandler() restituirà un errore di tipo eventNotHandledErr che noi memorizzeremo nella variabile result.

Quindi nell'istruzione successiva potremo verificare se l'Event Handler standard ha avuto successo o no. E, eventualmente, gestire l'evento in modo personalizzato.


if (eventNotHandledErr == result) {

// ...

}


Se mettiamo insieme tutto quello che abbiamo scritto fin ora otteniamo che appEventHandler() contiene il codice che segue.


OSStatus result = eventNotHandledErr;

UInt32 evtClass = GetEventClass(event);

UInt32 evtKind = GetEventKind(event);

switch (evtClass) {

case kEventClassMouse:

EventMouseButton button = 0;

HIPoint location = {0.0f, 0.0f};

GetEventParameter(event, kEventParamWindowRef, typeWindowRef, NULL, sizeof(WindowRef), NULL, &window);

result = CallNextEventHandler(myHandler, event);

if (eventNotHandledErr == result) {

// ...

}

break;

}


Resta da riempire la condizionale if() e come primo passo otterremo finalmente i valori che hanno assunto button e location, nel momento in cui è stato generato l'evento.


GetEventParameter(event, kEventParamMouseButton, typeMouseButton, NULL, sizeof(EventMouseButton), NULL, &button);

GetEventParameter(event, kEventParamWindowMouseLocation, typeHIPoint, NULL, sizeof(HIPoint), NULL, &location);


Come abbiamo fatto per ottenere window usiamo la funzione GetEventParameter(). E, quindi, con un altro ciclo switch() faremo decidere all'applicazione cosa fare a seconda del tipo di evento intercettato.


switch (evtKind) {

case kEventMouseDown:

case kEventMouseUp:

case kEventMouseDragged:

case kEventMouseMoved:

case kEventMouseWheelMoved:

OpenGL_OnMouse(evtKind,location.x,location.y);

break;

}


Ho riportato quest'ultima porzione di codice nella sua interezza perché è abbastanza banale.


Qui GioFX ha adoperato un trucchetto, sfruttando una caratteristica del ciclo switch(). Se non c'è nessuna istruzione break dentro ad un caso, switch() prosegue eseguendo il codice del caso successivo. I primi quattro casi sono tutti privi di istruzione break quindi kEventMouseWheelMoved è l'unico a venir eseguito, in ogni caso.

Questo perché GioFX ha preferito demandare tutta la gestione degli eventi alla funzione OpenGL_OnMouse(). In modo che nei prossimi tutorials, non toccheremo quasi più main.c.

OpenGL_OnMouse() è una funzione, che dichiareremo in main.h, e che implementeremo in seguito. Per ora, basta capire che essa verrà invocata ogni qual volta eseguiremo una delle seguenti azioni con il mouse: premere il pulsante, rilasciare il pulsante del mouse, muovere il mouse, trascinare il mouse (ovvero muoverlo mentre si tiene premuto il pulsante = Drag) e ruotare la rotellina del mouse.


Alla fine di tutto, la funzione appEventHandler() restituisce result. Come già spiegato in precedenza, result potrebbe assumere il valore di un tipo di evento, oppure, nel caso che la funzione non sia stata in grado di gestire l'evento, potrebbe essere rimasto immutato. Nel qual caso il valore restituito sarebbe eventNotHandleErr.


return result;


E con questo la nostra funzione appEventHandler() è conclusa.



2.6 Tener traccia degli errori


A questo punto, quindi, è il caso di aggiungere alla funzione InitHandlers() un piccolo pezzetto di codice, che ci potrebbe servire in futuro per tener traccia dello stato degli event handlers.


Immediatamente dopo alla dichiarazione della funzione, dichiarate la varibile ref e la varibile err.


EventHandlerRef ref;


OSErr err;


0011.png


E, quindi, modificate le istruzioni che invocano InstallApplicationEventHandler() e InstallWindowEventHandler() in questo modo.


err = InstallApplicationEventHandler(gAppEventHandler, GetEventTypeCount(alist), alist, window, &ref);

//...

err = InstallWindowEventHandler(window, gWindowEventHandler, GetEventTypeCount(wlist), wlist, window, &ref);


Ecco come appariranno nel codice.


0012.png



2.7 La funzione windowEventHandler()


Bene. È venuto il momento di implementare anche windowEventHandler().


L'approcio è pressapoco lo stesso usato in appEventHandler(). Innanzi tutto, inizializzeremo tre veriabili.


OSStatus result = eventNotHandledErr;

UInt32 evtClass = GetEventClass(event);

UInt32 evtKind = GetEventKind(event);


Per implementare il resto del codice della funzione windowEventHandler() partiremo da un'istruzione switch(), che verifica il valore di evtClass. L'istruzione switch() contiene due casi: il caso in cui il valore di evtClass è uguale al valore di kEventClassKeyboard, ovvero il caso in cui l'evento è generato dalla tastiera; e il caso in cui il valore di evtClass è uguale a kEventClassWindow, ovvero il caso in cui l'evento è generato dalla finestra attiva (l'applicazione che abbiamo creato contiene una sola finestra, percui a generare l'evento è sempre la stessa).


switch(evtClass) {

case kEventClassKeyboard:

// ...

case kEventClassWindow:

// ...

}

return result;


Non dimentichiamoci di collocare l'istruzione return result; alla fine della funzione, poiché questa funzione deve restituire un valore di tipo OSStatus.


Il codice, che abbiamo inserito fin qui, dovrebbe essere più o meno simile all'immagine che segue.


0013.png


Non è finita. I casi gestiti dall'istruzione switch(), per ora, non fanno nulla. Come prima cosa, è bene ricordarsi di collocare un istruzione break, come ultima istruzione all'interno di entrambi i casi, per evitare che il loop non esca mai.

Aggiungete anche un'altra istruzione switch(), all'interno di ciascuno dei due casi, che verifichi il valore della variabile evtKind.

Il loop switch(), annidato nel caso kEventClassKeyboard, contiente a sua volta un unico caso: kEventRawKeyUp.

Il loop switch(), annidato nel caso kEventClassWindow, invece, contiene tre casi: kEventWindowClose, kEventWindowCloseAll e kEventWindowBoundsChanged.


switch (evtClass) {

case kEventClassKeyboard:

switch (evtKind) {

case kEventRawKeyUp:

// ...

}

break;

case kEventClassWindow:

switch (evtKind) {

case kEventWindowClose:

// ...

case kEventWindowCloseAll:

// ...

case kEventWindowBoundsChanged:

// ...

}

break;

}


C'è ancora una cosetta da aggiungere, prima di implementare i quattro casi rimasti vuoti.


E si tratta di assegnare alla variabile globale window il riferimento alla finestra che ha generato l'evento, come avevamo fatto in appEventHandler().


GetEventParameter(event, kEventParamDirectObject, typeWindowRef, NULL, sizeof(WindowRef), NULL, &window);


La funzioneGetEventParameter(), a cui abbiamo passato il puntatore &window come ultimo argomento, memorizzerà nella variabile window l'informazione.

Il risultato, ottenuto fin ora, dovrebbe essere qualcosa di simile a questa immagine.


0014.png


Ora possiamo implementare i quattro casi degli switch() annidati.


All'inizio del caso kEventRawKeyUp, inserite l'istruzione:


result = CallNextEventHandler(myHandler, event);


Quest'istruzione invoca l'event handler, che si trova nella posizione immediatamente al di sotto di quello attuale (ovvero al di sotto di windowEventHandler()), nello stack degli event handlers registrati per questo target (ovvero la finestra). Il risultato (result) è un codice di errore. Se l'event handler sottostante non è in grado di gestire questo tipo di evento, a result verrà assegnato un codice di errore.


In questo modo windowEventHandler() demanda la gestione degli eventi da tastiera, legati alla finestra attuale, ad un Event Handler standard, in modo da non sovrascrivere il comportamento standard delle finestre di Mac OS X.


Nell'istruzione sucessiva verifichiamo il valore assunto da result. Se l'event handler standard non è in grado di gestire l'evento, result assumerà un valore uguale a eventNotHandledErr. Percui lo verifichiamo in questo modo:


if (eventNotHandledErr == result) {

// Codice che gestisce gli eventi non standard

}


E all'interno del if() collocheremo il codice che dovrà gestire gli eventi non standard. Per completare il corpo di if(), quindi, inserite il codice che segue:


UInt32 keyCode;

GetEventParameter(event, kEventParamKeyCode, typeUInt32, NULL, sizeof(UInt32), NULL, &keyCode);

OpenGL_OnKey(keyCode);


Quel che abbiamo fatto è molto semplice. Abbiamo dichiarato la variabile keyCode di tipo UInt32 per memorizzare il codice del tasto premuto dall'utente. Per ottenere questo codice abbiamo usato GetEventParameter() passandole &keyCode come ultimo parametro. E, infine, abbiamo invocato la funzione OpenGL_OnKey(), che fa parte delle funzioni OpenGL personalizzate, che dobbiamo ancora definire.

OpenGL_OnKey() servirà a eseguire delle operazioni ogni qual volta premiamo un tasto della tastiera che non fa parte dei comandi standard di Mac OS X. Ma non è ancora arrivato il momento di implementare OpenGL_OnKey().


Ecco il risultato dell'implementazione del caso kEventRawKeyUp.


0015.png


Non ci resta che implementare gli altri tre casi kEventWindowClose, kEventWindowCloseAll e kEventWindowBoundsChanged.


switch (evtKind) {

case kEventWindowClose:

// ...

//break;

case kEventWindowCloseAll:

QuitApplicationEventLoop();

break;

case kEventWindowBoundsChanged:

OpenGL_SetViewport();

OpenGL_Render();

break;

}


Come vedete nel primo caso abbiamo commentato tutto il contenuto. Come è successo in appEventHandler(), a causa dell'assenza di un'istruzione break, il ciclo eseguirà il caso immediatamente sucessivo. Il comportamento è chiaro: kEventWindowClose e kEventWindowCloseAll hanno lo stesso effetto, cioé la chiusura dell'applicazione. Inutile scrivere codice ridondante. Lasciamo che kEventWindowClose passi la palla al suo fratellino.


Le ultime due istruzioni, contenute nel caso kEventWindowBoundsChanged, invocano due funzioni, OpenGL_SetViewport() e OpenGL_Render(), che implementeremo più tardi. Spiego solo brevemente a cosa serviranno. In questo caso le funzioni vengono invocate se la finestra è stata ridimensionata: la prima funzione, quindi, resetterà l'area di disegno applicando le nuove dimensioni, la seconda aggiornerà il contenuto dell'area di disegno dando una passata di rendering "straordinaria" per adattare il contenuto della finestra alle nuove dimensioni.


windowEventHandler() ora è completa.



2.8 Libereare la memoria allocata alla chiusura dell'applicazione: la funzione DeleteHandlers()


Resta molto poco da implementare per completare "main.c". Innanzi tutto abbiamo bisogno di una funzione che liberi la memoria dagli event handlers, creati con InitHandlers(), quando chiudiamo l'applicazione. Questa funzione l'abbiamo già dichiarata in "main.h" e l'abbiamo chiamata DeleteHandlers(). Inseriremo la sua implementazione immediatamente dopo al corpo della funzione InitHandlers().


Al suo interno inseriremo tre condizionali if(), che verificano l'esistenza dei tre event handlers che abbiamo creato: theTimer, gAppEventHandler e gWindowEventHandler.


void DeleteHandlers() {

if (theTimer) {

}

if (gAppEventHandler) {

}

if (gWindowEventHandler) {

}

}


Nella prima condizionale inseriremo questo codice.


RemoveEventLoopTimer(theTimer);

theTimer = NULL;


Nella seconda inseriremo questo.


DisposeEventHandlerUPP(gAppEventHandler);

gAppEventHandler = NULL;


E nella terza...


DisposeEventHandlerUPP(gWindowEventHandler);

gWindowEventHandler = NULL;


Mi sembra sia del codice che non ha particolare bisogno di spiegazioni. In ogni caso, informazioni più dettagliate su queste funzioni sono reperibili nel solito "Carbon event manager reference".


Ecco come apparirà la nostra nuova funzione DeleteHandlers().


0016.png



2.9 Far partire l'applicazione: la funzione main()


Non ci resta che utilizzare le funzioni che abbiamo creato per implementare il corpo della funzione main() e far fare effettivamente qualcosa alla nostra applicazione.

Tenete, comunque, presente che dobbiamo ancora implementare la parte OpenGL, ovvero quelle funzioni, il cui nome inizia con OpenGL_, già usate in "main.c", che, però, non abbiamo ancora definito da nessuna parte.

Ma adesso concentriamoci su main(). Prima di ogni altra cosa, dovremo creare e visualizzare una finestra sullo schermo. Per fare ciò useremo il codice che segue:


Rect r = {50, 10, 480, 640};

CreateNewWindow(kDocumentWindowClass, kWindowOpaqueForEventsAttribute | kWindowStandardHandlerAttribute | kWindowInWindowMenuAttribute | kWindowStandardDocumentAttributes, &r, &window);

SetWindowTitleWithCFString(window, CFStringCreateWithCString(kCFAllocatorDefault, _TITLE, CFStringGetSystemEncoding()));

ShowWindow(window);


Per ora, preferisco sorvolare sui dettagli di questo codice. Quel che conta è sapere che usiamo un tipo Rect per definire la posizione e le dimensioni della finestra. Quindi inizializziamo la finestra usando la funzione CreateNewWindow(), alla quale passiamo &r ovvero il puntatore al tipo Rect, che abbiamo definito sopra, e un puntatore (&window) a window, la variabile globale per accedere alla finestra. Con SetWindowTitle(), invece scriviamo un titolo sulla barra del titolo della finestra usando la macro _TITLE, che possiamo andare a definire subito in "main.h". Immediatamente dopo a #include <Carbon/Carbon.h>, inserite il seguente codice:


#define _TITLE "OpenGL Tutorial - www.xcodeitalia.com"


Infine, invocando ShowWindow(), diciamo al sistema di visualizzare la finestra a monitor.

Da questo punto in poi la funzione main() non farà altro che invocare le funzioni, che avevamo creato nei paragrafi precedenti. Mi sembra inutile perdere tempo in chiacchiere perché i commenti che GioFX ha messo nel codice allegato sono già abbastanza esaustivi.


InitHandlers();


OpenGL_Init(window);

RunApplicationEventLoop();

OpenGL_Dispose();

DeleteHandlers();

return 0;


Noterete che sono state introdotte altre due funzioni, che non abbiamo ancora definito. Non sono le sole. Se ricordate, nel corso di questa prima parte del tutorial, ne abbiamo tralasciate diverse. Tutte con dei nomi che iniziano per OpenGL_.



3 Implementare il codice OpenGL


3.1 La parte OpenGL dell'applicazione: dichiarazione delle funzioni


Concluso il lavoro con "main.c" è tempo di implementare "opengl.c". "opengl.c" definisce 6 funzioni: OpenGL_Init, OpenGL_Dispose, OpenGL_Render, OpenGL_OnKey, OpenGL_OnMouse e OpenGL_SetViewport. Dovremo innanzitutto dichiararle nell'header, "main.h". Date le dimensioni ridottissime del codice, non c'è ragione di creare un nuovo header e, quindi, usiamo "main.h" per dichiarare, sia le funzioni che abbiamo già implementato in "main.c" sia quelle che implementeremo ora nel nuovo file "opengl.c".


Aggiungiamo quindi le dichiarazioni in "main.h".


void OpenGL_Init(WindowRef whatWindow);

void OpenGL_Dispose(void);

void OpenGL_Render(void);

void OpenGL_OnKey(int whatKey);

void OpenGL_OnMouse(int whatButton,int positionx,int positiony);

void OpenGL_SetViewport(void);


Siccome in "opengl.c" dovremo usare diverse funzioni OpenGL è bene includere gli headers di OpenGL in "main.h", in modo che le librerie OpenGL siano accessibili al nostro programma. Immediatamente dopo a #include <Carbon/Carbon.h> inseriamo:


#include <OpenGL/gl.h>

#include <OpenGL/glu.h>

#include <OpenGL/OpenGL.h>

#include <OpenGL/glext.h>

#include <AGL/agl.h>


Il risultato finale del nostro file "main.h" dovrebbe essere pressappoco questo qui.


0017.png



3.2 Il file di implementazione opengl.c


Ora dobbiamo creare un nuovo file C per implementare le nuove funzioni dichiarate in "main.h". Clickate, quindi, con il tasto destro sopra alla cartellina gialla sources e selezionate Add e, quindi, New File.... Nell'Assistant scegliete Carbon e, quindi, C File e clickate sul bottone Next.

Vi si presenterà la seguente schermata. Impostate il nome e il checkbox come ho fatto io; mentre Location, Add to Project e Targets, li lasciate così come sono.


0019.png


0018.png


Bene. Apriamo il nuovo file. Come potrete notare, Xcode presuppone ci sia un file "opengl.h" da includere ed ha, infatti, inserito automaticamente la macro #include "opengl.h". Nel nostro caso sarà sufficiente rinominare "opengl.h" in "main.h".

Includendo "main.h", automaticamente includiamo anche tutti gli headers inclusi da "main.h" e quindi anche gli headers di OpenGL, GLU, AGL e, naturalmente, Carbon.

Una volta incluso l'header corretto, potete passare a scrivere in "opengl.c" le definizioni delle 6 funzioni che abbiamo dichiarato ma, per ora, lasciatele vuote.


0020.png


Inizieremo a riempire il corpo delle nostre funzioni, ad una ad una. Ma prima è il caso di inserire un paio di variabili globali che potrebbero esserci utili in seguito. Immediatamente dopo #include "main.h" inseriremo, quindi:


WindowRef _window = NULL;


AGLContext _glContext = NULL;

CGrafPtr _grafPort = NULL;


float _angle = 0;


AGLContext è una struttura che AGL usa per memorizzare lo stato e altre informazioni associate con il rendering context di OpenGL. Se non sapete cos'è il rendering context vi posso dire che lo si potrebbe definire (con termini impropri) come la "tela" dove OpenGL disegna (rendering) i modelli 3D.


CGraphPtr è un puntatore alla graphic port della finestra. A causa delle origini di Carbon (presente già in Mac OS 8) CGraphPtr è definita nel framework QuickDraw. Se desiderate approfondire, nella guida "OpenGL programming guide for Mac OS X" troverete il capitolo "Drawing to a Carbon Window", che vi introdurrà a quello che stiamo facendo e vi spiegherà anche cosa sia CGraphPtr più chiaramente.


_angle è una banalissima variabile float che useremo per avere accesso globalmente all'angolo di rotazione dell'oggetto che disegneremo con OpenGL.



3.3 Preparare OpenGL: la funzione OpenGL_Init()


All'interno della funzione OpenGL_Init() prepareremo tutto quello che ci serve per consentire a OpenGL di disegnare effettivamente qualcosa sulla "tela". In particolare, daremo a OpenGL e a Carbon le informazioni necessarie per interagire e sapere come e dove disegnare.

Come prima cosa, inizializziamo le variabili globali _window e _grafPort.


_window = theWindow;

_grafPort = GetWindowPort(_window);


_window viene inizializzata usando il parametro theWindow, passato alla funzione OpenGL_Init() come parametro dal codice che la invoca (vedi main()). _grafPort viene invece inizializzata utilizzando la funzione GetWindowPort(), che prende come parametro il puntatore ad una finestra. Come specificato nella guida "Window Manager Reference", che fa parte di Carbon, GetWindowPort() « Gets the window’s color graphics port ». È un'espressione pressoché intraducibile in Italiano, ma tanto per completezza: GetWindowPort() ottiene la porta grafica a colori della finestra. Chiaro, no? ;-)


Immediatamente dopo abbiamo bisogno di definire il pixel format. AGL usa il tipo AGLPixelFormat per memoriazzare queste informazioni. Pertanto aggiungiamo la sequenza di istruzioni, che segue, per inizializzarlo.

NOTA PER ME: Forse è il caso di approfondire con almeno due paroline su che cos'è pixel format.


AGLPixelFormat pixelFormat = NULL;

GLint attrib[64];

int i=0;

attrib[i++] = AGL_RGBA;

attrib[i++] = AGL_DOUBLEBUFFER;

attrib[i++] = AGL_ACCELERATED;

attrib[i++] = AGL_PIXEL_SIZE;

attrib[i++] = 32;

attrib[i++] = AGL_DEPTH_SIZE;

attrib[i++] = 16;

attrib[i++] = AGL_NONE;

pixelFormat = aglChoosePixelFormat(NULL, 1, attrib);


Nell'array di interi GLint immagazziniamo gli attributi del pixel format che vorremmo. Quindi, invocando aglChoosePixelFormat() chiediamo al sistema di trovare il pixel format che più si avvicina alla nostra descrizione. Non starò a dilungarmi, basti sapere che il nostro pixel buffer supporterà RGBA, il double buffering e userà l'accelerazione hardware. I pixels del contesto saranno a 32 bits (8 bits per canale) e il depth buffer è impostato a 16 bits. AGL_NONE è richiesto da AGL per indicare che la lista degli attributi del pixel format è terminata.


Ora dobbiamo: 1) inizializzare il contesto grafico OpenGL; 2) dire ad AGL che il contesto in uso è quello appena inizializzato; 3) dire ad AGL che la graphic port della finestra, che abbiamo creato, è incaricata del disegno del contesto grafico OpenGL.

Nell'ordine queste operazioni si eseguono col codice che segue.


_glContext = aglCreateContext(pixelFormat, NULL);

aglSetCurrentContext(_glContext);

aglSetDrawable(_glContext, _grafPort);


Da questo momento in poi AGL non ci interesserà più. Poiché, avendo inizializzato tutto ciò di cui avevamo bisogno, possiamo iniziare ad usare le istruzioni OpenGL. Nell'ultima parte della funzione OpenGL_Init() ci occuperemo di definire lo sfondo della view port (o graphic port che dir si voglia). Per colore di sfondo useremo un grigio al 50%, un po' bruttino, ma estremamente utile per distingure per bene i contorni degli oggetti disegnati in primo piano. L'istruzione OpenGL per definire il colore è:


glClearColor(0.5, 0.5, 0.5, 0);


Dopodiché definiamo il modo in cui vengono disegnati i poligoni.


glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);


GL_FRONT_AND_BACK dice a OpenGL che vogliamo disegnare i poligoni fronte e retro. Immaginate un poligono come una tessera del bancomat, solo che il poligono non ha spessore. Noi vogliamo che OpenGL disegni sia la faccia di fronte che quella di retro. Questo perché è anche possibile chiedere ad OpenGL di disegnare solo la faccia di fronte (o quella di retro). Il risultato sarebbe che, se ruotassimo il poligono sul retro, esso risulterebbe come invisibile.

GL_FILL dice a OpenGL di colorare la superficie del poligono. Se avessimo impostato GL_LINE, OpenGL avrebbe disegnato solo i contorni del poligono.


Ora diciamo a OpenGL di disabilitare le textures. È un'impostazione che definiamo solo momentaneamente poiché, per ora, non abbiamo bisogno di gestire delle textures.


glDisable(GL_TEXTURE_2D);


OpenGL è in grado di decidere se disegnare o meno una faccia in base al suo orientamento rispetto al punto di osservazione (ovvero noi). Qual'ora un poligono fosse orientato in modo da non mostrarci la sua faccia frontale, oppure fosse dietro ad altri poligoni, disegnarlo comunque sarebbe uno spreco di risorse computazionali (ovvero di cicli di GPU). Ecco che quindi OpenGL non lo disegna. Ma, nel nostro caso, in cui stiamo per disegare un solo quadrato non vale la pena impostare questo parametro poiché desideriamo vedere sempre e comunque il quadrato disegnato. Diciamo quindi a OpenGL di ignorare il suo comportamento standard.


glDisable(GL_CULL_FACE);


In conclusione, diciamo a OpenGL di impostare la view port attiva ed eseguire il primo render.


OpenGL_SetViewport();

OpenGL_Render();


Entrambe le ultime due funzioni sono funzioni che abbiamo creato noi e, pertanto, le dobbiamo implementare. Cominciamo con OpenGL_SetViewport().



3.4 Adattare l'area di disegno in caso di ridimensionamento della finestra: la funzione OpenGL_SetViewport()


OpenGL_SetViewport() si occupa di inizializare l'area di rendering (=view port) all'inizio dell'applicazione, oppure quando la finestra viene ridimensionata. Come prima cosa la funzione si accerta che esista un contesto OpenGL. Altrimenti esce senza fare nulla.


if (!_glContext)

return;


Quindi invoca la seguente istruzione per dire ad AGL quale sia l'attuale contesto OpenGL.


aglSetCurrentContext(_glContext);


Ricava le dimensioni della finestra.


Rect r;

GetWindowPortBounds(_window, &r);

int width = r.right - r.left;

int height = r.bottom - r.top;


E imposta le dimensioni della view port in base alle dimensioni della finestra.


glViewport(0, 0, width, height);


In la gestione delle matrici in OpenGL è organizzata in 3 staks di matrici.


Il primo stack è il Model View stack, il secondo è il Projection stack e il terzo è il Texture stack. Essi rappresentano rispettivamente: le trasformazioni nello spazio 3D (ovvero le effettive trasformazioni dei modelli); il tipo di proiezione sulla view port, ovvero la trasformazione con cui i modelli vengono proiettati sull'area di disegno (che corrisponde al piano di proiezione); e, infine, le matrici di trasformazione nell'ambiente delle textures.

La funzione glMatrixMode() si usa per dire a OpenGL a quale stack di matrici verranno applicate le operazioni matriciali che seguono nel listato. glMatrixMode() ha un solo parametro e esso può assumere solo 3 valori, che corrispondono ai 3 stacks: GL_MODELVIEW, GL_PROJECTION e GL_TEXTURE.

A noi, per ora, serve il secondo. Perché intendiamo definire il tipo di proiezione che useremo per disegnare i modelli nella view port.


glMatrixMode(GL_PROJECTION);


La matrice corrente dello stack, con cui stiamo lavorando, è sempre quella in cima allo stack. Per evitare di concatenare più trasforamazioni e ottenere risultati indesiderati, oppure per inizializzare la matrice corrente, dobbiamo resettarla con il valore della matrice identità.


glLoadIdentity();


La funzione glLoadIdentity() assegna alla matrice corrente il valore della matrice identità.


NOTA: Se non sapete cos'è la matrice identità vi consiglio di trovare qualche manuale di geometria lineare. Su internet si trovano parecchie guide.


gluPerspective() è una comodissima funzione offerta dalla libreria OpenGL Utility (GLU, da non confondere con GLUT). Essa ci fornisce un modo semplice per creare una matrice di trasformazione, che rappresenti una proiezione prospetica, passandole alcuni parametri: GLdouble fovy, GLdouble aspect, GLdouble zNear e GLdouble zFar. Rispettivamente: il campo visivo (field of view), ovvero l'angolo di visuale; il rapporto delle dimensioni dell'area di disegno (aspect ratio), si usa anche per i monitor o i formati video, ad esempio 16/9 e o 4/3; la distanza del piano di taglio più vicino al punto di osservazione (near cliping plane), ovvero il piano prima del quale OpenGL non disegna; la distanza del piano di taglio più lontano dal punto di osservazione (far cliping plane), ovvero il piano oltre al quale OpenGL non disegna.


gluPerspective(50, (GLdouble)((float)width / (float)height), 1, 8000);


Infine OpenGL_SetViewport() ritorna allo stack di matrici per manipolare lo spazio 3D dei i modelli, inizializza la prima matrice col valore della matrice identità e, per concludere, notifica il contesto di rendering (rendering context) che la geometria della finestra è stata modificata.


glMatrixMode(GL_MODELVIEW);

glLoadIdentity();

aglUpdateContext(_glContext);


Ed ecco il risultato della nostra funzione.


0021.png



3.5 La funzione OpenGL_Render()


L'altra funzione invocata alla fine di OpenGL_Init() è OpenGL_Render(). Essa è una funzione molto importante ed è una delle funzioni di questo programma che subiranno maggiori modifiche nei prossimi tutorials allo scopo di aggiungere cose nuove.


Oltre ad essere invocata, per la prima volta, all'interno di OpenGL_Init(), essa viene invocata ad intervalli regolari dalla funzione TimerAction() in main.c. Essa è il luogo ideale per attuare l'animazione dei modelli OpenGL. Infatti, se per ogni chiamata di OpenGL_Render() noi alteriamo una o più proprietà di un modello (rotazione, traslazione, scalatura dell'intero modello, di singoli vertici, colore, ...) otteniamo un'animazione.


Così come abbiamo fatto con OpenGL_SetViewport(), verifichiamo l'esistenza di _glContext.


if (!_glContext)

return;


Prima di poter effettivamente disegnare qualcosa è necessario ripulire il buffer che non è a schermo (l'offscreen buffer, vedi double buffering), poiché è proprio quello che andremo a disegnare e che, poi, verrà sostituito al buffer a schermo per visualizzarlo veramente.


glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


glClear() accetta un solo parametro, una maschera che specifica quale bit buffer bisogna ripulire. È possibile usare l'operatore bitwise OR per concatenare più maschere con l'effetto di pulire tutti i bit buffers usati nella concatenazione.


Poi, dobbiamo assicurarci di inizializzare la matrice in cima allo stack Model View.


glLoadIdentity();


Ora disegneremo veramente qualcosa. Per poter disegnare abbiamo bisogno di definire le trasformazioni (rotazione e traslazione) che vogliamo applicare. Le funzioni che useremo per farlo vengono eseguite sulla matrice corrente dello stack con cui stiamo lavorando. Per chiarezza specifico che si tratta dello stack Model View.

Quindi si da inizio al disegno di un quadrilatero usando la funzione glBegin(), a cui passeremo il parametro GL_QUADS.


glTranslatef(0, 0, -30);

glRotatef(_angle, 1, 0, 0);

glRotatef(45, 0, 1, 0);

glRotatef(_angle, 0, 0, 1);

glBegin(GL_QUADS);

glColor4f(1, 0, 0, 1); glVertex3f(-10, -10, 0);

glColor4f(0, 1, 0, 1); glVertex3f(+10, -10, 0);

glColor4f(0, 0, 1, 1); glVertex3f(+10, +10, 0);

glColor4f(1, 1, 0, 1); glVertex3f(-10, +10, 0);

glEnd();


La procedura di disegno del quadrilatero si conclude con l'istruzione glEnd(). Per indicare che le istruzioni fra glBegin() e glEnd() costituiscono una specie di procedura OpenGL si usa rientrate le righe di una tabulazione.


Qualche parola sulle trasformazioni utilizzate. La traslazione passata a glTranslatef() significa che trasliamo la scena di 30 unità sull'asse Z, nel verso negativo, partendo dall'origine degli assi. L'asse Z, apparentemente, giace sul "pavimento" dello spazio 3D OpenGL e il suo verso negativo corrisponde alla direzione in cui l'osservatore sta guardando quando l'applicazione è appena avviata. Questo perché, di default, OpenGL orienta la telecamera in modo che guardi in quella direzione e con l'asse Y come asse verticale. L'asse X, invece, pur giacendo sempre sul "pavimento", è orientata parallelamente all'orizzonte. E, infine, l'asse Y è l'asse verticale.

Pertanto non sarà difficile dedurre cosa succede immediatamente dopo. Abbiamo applicato, infatti, 3 rotazioni, ciascuna su un asse diverso, usando la funzione glRotatef(). glRotatef() prende 4 parametri: l'angolo e 3 valori che costituiscono il vettore unitario che rappresenta asse di rotazione. Per definire i tre assi cartesiani X, Y e Z con dei vettori, si usano rispettivamente i tre vettori unitari: (1, 0, 0), (0, 1, 0) e (0, 0, 1).


NOTA: Volendo è possibile definire assi di rotazione arbitrari, che non corrispondono a X, Y e Z.


Come angolo di rotazione abbiamo usato la variabile globale _angle per gli assi X e Z; e il valore fisso 45 per l'asse Y.


Il quadrilatero, invece, viene disegnato passando a OpenGL i valori dei suoi vertici. Ad ogni vertice viene assegnato un colore e un vettore. Quest'ultimo rappresenta le coordinate locali del vertice (ossia le coordinate del vertice rispetto all'origine del quadrilatero) e viene definito utilizzando la funzione glVertex3f().

I colori di ciascun vertice sono, nell'ordine: rosso, verde, blu e giallo. Per ottenerli abbiamo usato la funzione glColor4f(), che prende quattro parametri. I quattro parametri corrispondono al canale del rosso, quello del verde, quello del blu e quello della trasparenza (il canale alpha): in altri termini RGBA. Tutti e quattro i parametri possono assumere un valore decimale (float) compreso fra 0 e 1. 0 indica la totale assenza di colore e 1 la massima quantità di colore. Per il canale alpha, 0 rappresenta la trasparenza al 100% e 1 l'opacità. Al contrario di quel che probabilmente vi sareste aspettati non si usa la classica rappresentazione con valori interi compresi fra 0 e 255.



3.6 Il Double Buffering


Non è ancora finita qui. Mancano ancora due istruzioni prima che OpenGL_Render() abbia completato il suo lavoro.


Forse avrete già sentito parlare di Double Buffering. Lo scopo di questa tecnica e di evitare un effetto/difetto dovuto alla frequenza di aggiornamento verticale del monitor. Il computer disegna l'immagine una riga alla volta. Se l'applicazione disegnasse direttamente sulla View Port a schermo, per quanto veloce possa essere il computer (e il monitor), l'occhio umano sarebbe comunque in grado di cogliere questa operazione di ridisegno, e l'utente osserverebbe degli scalini comparire e scomparire ininterrottamente sull'immagine in movimento, in particolare sui bordi del quadrilatero.

Per ovviare a questo problema, in realtà si lavora con due buffer che tengono in memoria le informazioni di due immagini, il fotogramma corrente e quello sucessivo. Il buffer che contiene le informazioni del fotogramma successivo non è visibile (si dice Offscreen Buffer) ed è quello su cui abbiamo disegnato fin ora. L'altro, il fotogramma corrente, è quello visibile (Onscreen Buffer), che non viene toccato e rimane tale fintanto che l'Offscreen Buffer non è stato completamente ridisegnato.

Una volta che l'Offscreen Buffer è pronto l'applicazione assegna al puntatore del Onscreen Buffer l'indirizzo di memoria dell'Offscreen Buffer e viceversa. In questo modo, il buffer, che prima era visibile, diventa invisibile e potremmo disegnarci sopra il prossimo fotogramma. Nel frattemo, viene reso visibile il buffer, prima era invisibile, in modo da mostrare il fotogramma appena disegnato.

Grazie a questa continua alternanza (che a seconda della potenza della vostra scheda video e del vostro computer può avvenire anche migliaia di volte in un secondo) i dati del fotogramma da passare al monitor vengono inviati in un colpo solo e si evitano effetti visivi indesiderati.


L'operazione di invertire i due buffer, in AGL, si fa con la funzione aglSwapBuffers(), che è la penultima istruzione che aggiungeremo nella funzione OpenGL_Render(). L'ultima istruzione serve a incrementare l'angolo di rotazione _angle facendo si che, ad ogni sucessiva chiamata, OpenGL_Render() ruoti il quadrangolo un po' di più e ci faccia vedere un animazione vera e propria.


aglSwapBuffers(_glContext);

_angle += 0.5f;


E così abbiamo completato anche OpenGL_Render(). Ecco come appare.


0022.png



3.7 La funzione OpenGL_Dispose()


OpenGL_Dispose() viene invocata da main(), solo un'istruzione prima di cancellare gli event handlers usando DeleteHandlers(), ed ha uno scopo analogo: invece di cancellare gli Event Handlers, però, si occupa di cancellare dalla memoria il contesto di rendering OpenGL e le informazioni ad esso legate. Prima di far questo però controlla se ci sia ancora un contesto di rendering OpenGL.


if (!_glContext)

return;


Quindi esegue queste operazioni nell'ordine: 1) AGL assegna il valore NULL al Constesto di Rendering corrente, questa operazione svincola _glContext da AGL; 2) assegna NULL anche alla View Port (o Graphic Port) legata a _glContext, con l'effetto quindi di svincolare _glContext anche dalla View Port stessa; 3) libera tutte le risorse usate da _glContext; 4) assegna il valore NULL a _glContext. Queste quattro azioni si traducono nelle istruzioni che seguono.


aglSetCurrentContext(NULL);

aglSetDrawable(_glContext, NULL);

aglDestroyContext(_glContext);

_glContext = NULL;


Con queste ultime istruzioni, abbiamo completato la nostra applicazione. Ecco come appare OpenGL_Dispose().


0023.png



4 Conclusioni


Ora potete testare la vostra prima applicazione OpenGL per Mac. Clickate sul bottone Build and Go e godetevi un simpatico quadrato colorato che ruota sullo schermo.


Ora qualcuno si farà avanti e dirà: « Come finito? Ma se non abbiamo ancora implementato OpenGL_OnKey() e OpenGL_OnMouse()! ». La risposta è che abbiamo tralasciato quelle due funzioni, per il momento, perché verranno implementate nei prossimi tutorials.


...