1. INPUT E INTERAZIONI
  2. Grafica Interattiva

Líinterazione tra uomo e macchina è una componente fondamentale di molte applicazioni. Nel caso delle applicazioni grafiche, líinterazione avviene attraverso il display del computer: líutente vede uníimmagine sul display, reagisce allíimmagine attraverso un dispositivo di input interattivo, ad esempio un mouse, quindi líimmagine è modificata in risposta allíinput dellíutente, e così via.

I dispositivi di input rappresentano la componente hardware maggiormente coinvolta nei processi di interazione tra uomo e macchina. Essi consentono infatti allíutente di trasferire informazioni al sistema di calcolo. Le tecniche utilizzate dai dispositivi di input per trasferire informazioni al calcolatore sono chiamate tecniche di interazione, o input mode. Il termine misura è invece utilizzato per classificare il tipo fondamentale di informazione trasferita al programma applicativo tramite le tecniche di interazione.

La libreria grafica OpenGL non consente di gestire le interazioni direttamente. La ragione principale di questa omissione è da ricercare nel tentativo di aumentare la portabilità della libreria, permettendole di funzionare su uníampia varietà di ambienti. Di conseguenza, windowing e funzioni di input sono state lasciate fuori. Questo rende la libreria portatile, ma introduce naturalmente delle difficoltà. Ogni programma applicativo deve avere almeno uníinterfaccia minima con líambiente di windowing e non è possibile trascurare del tutto questo aspetto se si vogliono scrivere programmi completi e significativi. Se líinterazione è omessa dalla libreria, è il programmatore a doversi preoccupare dei dettagli relativi al particolare ambiente in cui si trova ad operare.

Queste difficoltà potenziali possono essere risolte e/o evitate affiancando alla libreria grafica un toolkit di tecniche di interazione, ovvero una libreria che rende disponibili al programma applicativo una collezione di tecniche di interazione in grado di assicurare la funzionalità minima aspettata da tutti i sistemi, come ad esempio líapertura di finestre, líuso della tastiera e del mouse, la creazione di menu pop-up. Il toolkit collegato alla libreria OpenGL è costituito dalla libreria GLUT (dallíinglese GL utility toolkit) che si occupa della gestione delle interfacce con i sistemi di windowing (utilizziamo il termine sistema di windowing per fare riferimento allíambiente globale fornito dai sistemi come X Window, Microsoft Window e dal sistema operativo Macintosh).

Tratteremo quindi le interazioni tra uomo e macchina facendo ricorso al toolkit GLUT, in modo indipendente da un particolare sistema operativo o sistema window. Líuso del toolkit GLUT ci consentirà inoltre di evitare la complessità inerente nellíinterazione tra il sistema windowing, il window manager e il sistema grafico.

  1. Tecniche di Interazione

Due caratteristiche fondamentali descrivono il comportamento logico di un dispositivo di input: (1) le misure che il dispositivo restituisce al programma utente, e (2) il momento in cui il dispositivo restituisce queste misure. Inoltre, le tecniche con cui i dispositivi di input forniscono un input al programma applicativo possono essere descritte in termini di due entità: un processo di misura e un dispositivo di trigger. La misura di un dispositivo è ciò che il dispositivo restituisce al programma utente. Il trigger è invece un dispositivo fisico usato dallíutente per inviare segnali al computer. Misura e trigger dipendono dal particolare dispositivo di input utilizzato. Ad esempio, se consideriamo una tastiera, la misura è costituita da una stringa di caratteri, mentre il trigger può essere il tasto enter. Nei dispositivi di tipo locator, la misura include la posizione, e il trigger associato può essere un pulsante sul dispositivo di puntamento (se il dispositivo è un mouse, il trigger è uno dei suoi pulsanti). La misura può contenere anche altre informazioni, quali lo stato. Un dispositivo di tipo pick restituisce, ad esempio, anche líidentificatore dellíoggetto cui líutente sta puntando.

Vediamo in modo più preciso come sia possibile classificare le misure in base al loro contenuto.

Una misura di posizione coinvolge la specificazione di una posizione (xy) o (xyz) al programma applicativo. Ci sono due tipi di misure di posizione: spaziale e linguistica. Nel primo caso líutente non conosce i valori numerici delle coordinate, ma conosce la posizione da trasmettere al programma applicativo solo in relazione alla posizione degli oggetti vicini. In questo caso è molto importante fornire un feedback allíutente per mostrare la posizione sullo schermi, ad esempio visualizzando un cursore. Nel secondo caso, invece, líutente conosce il valore numerico delle coordinate della posizione.

Una misura di selezione consiste nella scelta di un elemento da un certo insieme di scelte possibili (ad esempio da un insieme di comandi, o di attributi). La scelta può avvenire indicando il nome dellíelemento da selezionare, quando líutente ne è a conoscenza, oppure puntando direttamente líoggetto desiderato, tramite un dispositivo di puntamento, e quindi effettuando il trigger. Se líinsieme delle scelte possibili da cui effettuare la selezione ha dimensione finita, risulta moltoconveniente líutilizzo dei cosiddetti menu. I menu possono essere visualizzati sullo schermo in modo statico, e quindi risultare permanentemente visibili, oppure dinamico. In questo secondo caso essi sono visualizzati solo su richiesta dellíutente (ad esempio attraverso la pressione di uno dei pulsanti del mouse).

Una misura di testo è costituita da una stringa di caratteri, che può non avere un particolare significato. Ad esempio, il nome di un comando battuto su una tastiera non è considerato una misura di testo.

Una misura di quantità, infine, consiste nella specificazione di un valore numerico compreso tra un valore massimo e un valore minimo. Come per le misure di posizione, anche le misure di quantità possono essere lingustiche o spaziali. Nel primo caso, líutente conosce esattamente il valore numerico da trasmettere, e líunico tipo di feedback di cui può avere bisogno consiste nel feedback numerico del valore selezionato. Nel secondo caso, líutente cerca di diminuire o aumentare un valore di un certo ammontare, e in questo caso è necessario un feedback grafico in grado di dare uníidea del valore specificato (ad esempio tramite un potenziometro).

La misura di un dispositivo di input si può ottenere secondo tre tecniche distinte, ciascuna definita dalle relazioni tra il processo di misura e il trigger. Normalmente, líinizializzazione del dispositivo di input avvia un processo di misura. Líinizializzazione può richiedere la chiamata di una funzione esplicita della libreria grafica, oppure può avvenire in modo automatico. In ogni caso, una volta che il processo di misura è stato avviato, la misura è presa e conservata in un buffer, anche se il contenuto del buffer può non ancora essere disponibile al programma. Ad esempio, la posizione di un mouse è tenuta continuamente sotto controllo dal sistema window, indipendentemente dal fatto che il programma applicativo abbia bisogno dellíinput del mouse.

Nel request mode, la misura del dispositivo non è disponibile al programma fino a quando non si utilizza il dispositivo di trigger. Questa tecnica di interazione con il computer è uno standard nelle applicazioni non grafiche, quali un tipico programma C che richiede dei caratteri in input. Quando il programma incontra una funzione quale scanf, si arresta e attende che líutente invii dei caratteri tramite tastiera. Eí possibile cancellare e modificare líinput, e impiegare tutto il tempo che desideriamo. I dati sono conservati in un buffer di tastiera il cui contenuto viene trasferito al programma solo in seguito alla pressione di un tasto particolare, come il tasto enter. Con un dispositivo di tipo locator, possiamo portare il dispositivo di puntamento alla locazione desiderata, e quindi, effettuando il trigger del dispositivo con líapposito pulsante, trasferire la locazione al programma applicativo. Le relazioni tra misura e trigger per il request mode sono illustrate in figura 6.1.

Figura 7.1 Request mode.

Nel sample mode, líinput è invece immediatamente disponibile: la misura viene restituita subito, e il trigger non è necessario (figura 6.2).

Figura 7.2 Sample mode.

Nel sample mode, líutente deve avere posizionato il dispositivo di puntamento oppure deve avere inserito i dati via tastiera prima della chiamata della funzione, poiché la misura è estratta immediatamente dal buffer.

Una caratteristica sia del request che del sample mode è che líutente deve identificare quale dispositivo fornirà líinput. Líinterfaccia con i dispositivi avviene per mezzo delle funzioni
request_locator(device_id, &measure);sample_locator(device_id, &measure);

Di conseguenza, qualsiasi altra informazione resa disponibile da un qualsiasi altro dispositivo di input, diverso da quello specificato nellíapposita funzione, viene ignorato. Entrambe le tecniche sono utili nelle situazioni in cui il programma guida líutente, ma non sono utili nelle applicazioni in cui líutente controlla il flusso del programma.

Le tecniche di input request e sample non sono tuttavia sufficienti per gestire la varietà di interazioni uomo-macchina oggi disponibili negli ambienti di calcolo moderni. Una tecnica di input più completa è líevent mode. Supponiamo di lavorare in un ambiente con dispositivi di input multipli, che eseguono un proprio processo di misura e che sono dotati di un proprio trigger. Ogni volta che si effettua il trigger su un dispositivo, viene generato un evento. generato. La misura, insieme allíidentificatore del dispositivo che líha eseguita, viene memorizzata in una event queue (coda di eventi), indipendentemente da ciò che il programma applicativo farà con questi eventi. Un modo con cui il programma applicativo può lavorare con gli eventi è quello mostrato in figura 6.3.

Il programma utente può esaminare líevento di testa nella coda, oppure può attendere che un evento si verifichi. Se cíè un evento nella coda, il programma può guardare il tipo di evento e decidere cosa fare.

Figura 7.3 Event mode input.

Un altro approccio consiste nellíassociare una funzione chiamata callback ad ogni specifico tipo di evento. Seguiremo questo approccio, in quanto si tratta di quello utilizzato nei principali sistemi window.

  1. Client e Server

Vediamo ora come integrare la computer graphics negli ambienti di calcolo distribuito e nelle reti. In questi ambienti, i nostri mattoni di partenza sono entità chiamate server, che possono eseguire degli incarichi per conto dei client. Client e server possono essere distribuiti su una rete (figura 6.4), oppure possono essere interamente contenuti in una singola unità computazionale.

Esempi familiari di server includono i print server, che consentono la condivisione stampanti tra gli utenti; e i computer server, come supercomputer remoti accessibili dai programmi utenti. Gli utenti e i programmi applicativi che fanno uso di questi servizi sono detti client o programmi client.

Figura 7.4 Network.

Una workstation in rete può essere sia un client che un server, o, più precisamente, può eseguire contemporaneamente programmi client e server. Una workstation con un display raster, una stampante ed un dispositivo puntatore, ad esempio un mouse, costituisce un server grafico che può fornire servizi di output sul suo display, e servizi di input attraverso la tastiera e il dispositivo puntatore.

  1. Display List

Attraverso líuso delle display list e possibile seguire líapproccio client e server su una rete e migliorare le prestazioni grafiche.

Molti sistemi grafici affiancano al computer host, general-purpose, un computer special-purpose, il processore di display, dotato di un insieme di istruzioni limitato, e con la maggior parte di istruzioni orientata alla visualizzazione delle primitive grafiche sullo schermo del CRT. Il programma applicativo viene eseguito dal computer host, mentre una opportuna lista di istruzioni viene inviata al processore di display, e le istruzioni di questa lista sono memorizzate in una memoria di display come una display list. Per le applicazioni più semplici, e non interattive, una volta che la display list è stata inviata al processore di display, líhost è libero e può dedicarsi ad altri incarichi, mentre il processore di display esegue la display list ripetutamente, ad un tasso tale da impedire il flicker delle immagini.

Il processore di display può in realtà essere visto come un server grafico, e il programma utente sul computer host come il client. E il maggiore intasamento del sistema non è dovuto al tasso a cui occorre effettuare il refresh del display (anche se questo continua a rappresentare un problema significativo), ma allíammontare del traffico che passa tra il client ed il server.

Ci sono due tecniche per inviare le entità grafiche al display. Possiamo inviare la descrizione completa del nostro oggetto al server grafico. Per una primitiva geometrica tipica, questo consiste nellíinviare vertici, attributi, tipo di primitiva e le informazioni relative alla visualizzazione. Nella tecnica operativa fondamentale, líimmediate mode, non appena il programma esegue una istruzione che definisce una primitiva, la primitiva viene inviata al server per la visualizzazione, e non è conservata nella memoria di sistema. Per ridisegnare la primitiva dopo una pulitura dello schermo, oppure per disegnare la stessa primitiva in un altro punto dello schermo dopo una interazione, il programma deve ridefinirla e quindi rimandarla al display. Per oggetti complessi, questo modo di procedere può causare una notevole flusso di dati tra il client e il server.

Un metodo più efficiente di invio di entità grafiche al display è il retained mode, che è basato sullíuso delle display list. Gli oggetti sono descritti una volta per tutte, e la descrizione è conservata in una display list. La display list può essere memorizzata nel server, e rivisualizzata tramite una semplice chiamata di funzione, emessa dal client verso il server. Questo modo di operare presenta il vantaggio di ridurre il traffico sulla rete, e permette inoltre al client di avvantaggiarsi dellíhardware grafico special-purpose reso eventualmente disponibile dal particolare server grafico. Ci sono naturalmente anche degli inconvenienti legati allíuso delle display list, che richiedono memoria sul server, e la cui creazione introduce inevitabilmente un sovraccarico di lavoro.

  1. Definizione delle display list

La libreria grafica OpenGL contiene un insieme limitato di istruzioni per manipolare le display list. Le display list vengono definite in modo simile alle primitive geometriche. Si utilizzano i comandi glNewList e glEndList allíinizio e alla fine della lista, mentre il suo contenuto viene indicato allíinterno dei due comandi. Ogni display list deve inoltre avere un identificatore. Ad esempio, il seguente codice definisce un box di colore rosso:
glNewList(BOX, GL_COMPILE);glBegin(GL_POLYGON);glColor3f(1.0, 0.0, 0.0);glVertex2f(-1.0, -1.0);glVertex2f(1.0, -1.0);glVertex2f(1.0, 1.0);glVertex2f(-1.0, 1.0);glEnd();glEndList();

Il flag GL_COMPILE comunica al sistema di inviare la lista al server, ma non di visualizzarne il contenuto. Se vogliamo una visualizzazione immediata del suo contenuto, possiamo usare il flag GL_COMPILE_AND_EXECUTE. Ogni volta che vogliamo disegnare il box sul server, eseguiamo la funzione
glCallList(BOX);

Si noti che, come nelle altre applicazioni, lo stato determina quali trasformazioni sono applicate alle primitive nella display list. Quindi, se cambiamo la matrice model-view o la matrice di proiezione tra le esecuzioni della display list, il box apparirà in posizioni differenti sul display, oppure potrebbe anche non apparire del tutto.

Poiché possiamo modificare lo stato dallíinterno di una display list, dobbiamo essere attenti per evitare che questi cambiamenti abbiano effetti che in seguito potrebbero risultare non desiderabili e inaspettati. Ad esempio, la display list BOX modifica il colore di drawing. Ogni volta che la display list viene eseguita, il colore di drawing viene posto al colore rosso e, a meno che esso non venga modificato, le primitive definite successivamente nel programma saranno colorate di rosso. Per salvaguardarsi da eventuali inconvenienti è opportuno utilizzare la matrice e la pila di attributi rese disponibili da OpenGL. Una pila è una struttura dati basata sulla disciplina LIFO: líultimo articolo inserito nella struttura è il primo ad essere rimosso. Il valore degli attributi e delle matrici viene memorizzato in testa alla propria pile (pushing); e può essere recuperato successivamente rimuovendolo dalla pila (popping). Una procedura standard, e sicura, consiste nellíeffettuare il pushing degli attributi e delle matrici nelle relative pile ogni volta che si utilizza una display list, e di rimetterli in vigore quando si esce dalla lista, tramite i comandi
glPushAttributes(GL_ALL_ATTRIB_BITS);glPushMatrix();

posti allíinizio della display list, e i comandi
glPopAttributes();glPopMatrix();

alla fine.

Ci sono altri comandi resi disponibili da OpenGL, che rendono ancora più semplice lavorare con le display list. Spesso occorre lavorare con display list multiple. La creazione di display list multiple, con identificatori consecutivi, può essere realizzata attraverso la funzione glGenLists(number); che restituisce il primo intero di number interi consecutivi che sono etichette non ancora utilizzate. La funzione glCallLists ci permette di eseguire display list multiple attraverso una sola chiamata.

  1. Esempio: generazione di testi con le display list

La generazione di stringhe di testo rappresenta un esempio semplice ma importante dellíuso delle display list in OpenGL.

Indipendentemente dal tipo di testo utilizzato (raster o stroke) è necessario un notevole ammontare di codice per descrivere un insieme di caratteri. Ad esempio, supponiamo di utilizzare un font raster in cui ciascun carattere è memorizzato come una matrice di 10 12 bit, che richiede 15 byte. Il metodo più semplice di visualizzare una stringa consiste nellíinviare un carattere al server ogni volta che lo vogliamo visualizzare. Questo trasferimento richiede un movimento di almeno 15 byte per ciascun carattere. Inoltre, se si utilizzano testi stroke, sono necessari anche più di 15 byte per carattere. Per le applicazioni che richiedono la visualizzazione di una grassa quantità di testo, inviare i singoli caratteri al display ogni volta che è necessario può comportare un peso notevole per il sistema grafico.

Una strategia più efficiente consiste nel definire una volta per tutte il font utilizzando una display list per ciascun carattere, e quindi nel memorizzare il font sul server per mezzo delle display list. Possiamo definire tanti font quanto consentito dalla memoria di display.

Vediamo allora come definire e visualizzare una stringa di caratteri (1 byte per carattere) utilizzando un font stroke e le display list. La procedura è essenzialmente la stessa per i font di tipo raster.

Per prima cosa si definisce una funzione OurFont(char c), che disegnerà ciascun carattere ASCII, indicato con c, che può apparire nella stringa. La funzione può avere la forma seguente:
void OurFont(char c) { switch(c) { case ëaí: Ö break;case ëAí:Öbreak; Ö}}

Allíinterno dei case, è necessario gestire con attenzione le spaziature: ogni carattere nella stringa deve essere visualizzato sulla destra del carattere precedente. Possiamo a questo scopo usare la funzione di traslazione glTranslate per ottenere la spaziatura desiderata. Supponiamo di dover definire la lettera ìOî e di volerla visualizzare allíinterno di un quadrato di lato unitario. La parte corrispondente nella funzione OurFont sarà:
case ëOí:glTranslatef(0.5, 0.5, 0.0); /* move to center */glBegin(GL_QUAD_STRIP);for (i=0; i <=12; i++) /* 12 vertices */{angle=3.14159 / 6.0 * i; /* 30 degrees in radians */glVertex2f(0.5*cos(angle); 0.5*sin(angle));glVertex2f(0.7*cos(angle); 0.7*sin(angle));}glEnd();glTranslate(0.5, -0.5, 0.0); /* move to lower right */break;

Questo codice approssima il cerchio con 12 quadrilateri. Si osservi che stiamo lavorando con caratteri bidimensionali, ciascuno definito nel piano z = 0. Assumiamo inoltre che ciascun carattere sia contenuto in un box. La strategia usuale è di partire allíangolo in basso a sinistra del primo carattere nella stringa, e quindi di disegnare un carattere alla volta, disegnandolo in modo di terminare nellíangolo in basso a destra del box di quel carattere, che costituisce líangolo in basso a sinistra del box successivo.

La prima traslazione ci porta al centro del box del carattere ìOî, definito da un quadrato unitario. Quindi, si definiscono i vertici usando due cerchi concentrici centrati su questo. Dopo che sono stati definiti i 12 quadrilateri, ci muoviamo nellíangolo in basso a destra del box. Il risultato delle due traslazioni è proprio quello di portarci nella posizione corretta da cui partire per visualizzare il carattere successivo.

Il vantaggio dellíapproccio descritto consiste nel fatto che i caratteri sono definiti una sola volta, e quindi inviati al server grafico come display list compilate.

Supponiamo di voler generare un font composto da un insieme di 256 caratteri. Il codice richiesto, usando la funzione OurFont è il seguente:
base = glGenLists(256); /* return index of first of 256 consecutive available ids */for(i=0, i<256, i++){glNewList(base + i, GL_COMPILE);OurFont(i);glEndList();}

Infine, la visualizzazione di una stringa viene effettuate chiamando la funzione
char *text_string;glCallLists( (Glint) strlen(text_string), GL_BYTE, text_string);

che utilizza una funzione standard di UNIX, strlen, per determinare la lunghezza della stringa text_string. Il primo argomento nella funzione glCallLists è il numero di liste da eseguire. Il terzo è un puntatore ad un vettore del tipo definito nel secondo argomento.

  1. Font nella libreria GLUT

Generalmente è preferibile utilizzare i font già esistenti, piuttosto che definirne di nuovi. Il toolkit GLUT fornisce alcuni font raster e stroke. Eí inoltre possibile accedere ai font messi a disposizione dal sistema windowing.

I font di GLUT non utilizzano le display list. Si può accedere ad un singolo carattere attraverso la funzione
glutStrokeCharacter(GLUT_STROKE_MONO_ROMAN, int character)

Questi caratteri devono essere utilizzati con una certa cautela. La loro dimensione (circa 120 unità al massimo) può avere poco a che fare con le unità del resto del programma; quindi i caratteri devono essere opportunamente scalati. La posizione può essere controllata usando una traslazione prima di chiamare la funzione. Inoltre, ciascuna invocazione di glutStrokeCharacter include una traslazione allíangolo in basso a destra del box del carattere per preparare il carattere successivo. Scalature e traslazioni influenzano lo stato di OpenGL, quindi in questo caso è necessario utilizzare i comandi glPushMatrix e glPopMatrix per prevenire posizionamenti non desiderati degli oggetti definiti successivamente dal programma.

I caratteri di tipo raster sono prodotti in modo analogo. Ad esempio, un singolo carattere 8 13 si può generare con il comando
glutBitmapCharacter(GLUT_BITMAP_8_BY_13, int character);

Il posizionamento dei caratteri raster è più semplice rispetto a quello dei caratteri stroke. poiché i caratteri sono disegnati direttamente nel frame buffer e non sono soggetti alle trasformazioni geometriche. OpenGL mantiene, nel suo stato, una posizione raster che identifica dove sarà piazzata la primitiva raster successiva. La posizione raster, definita attraverso il comando glRasterPos*(), viene mossa di un carattere verso destra ogni volta che viene invocata la funzione glutBitmapCharacter, senza che ciò influenzi il rendering delle primitive geometriche successive.

  1. Programmazione degli Input Event-Driven

Esamineremo ora alcuni eventi che sono riconosciuti dal sistema window, e scriveremo le funzioni che gestiscono le risposte del programma applicativo a questi eventi. Discuteremo solo gli eventi riconoscibili dal toolkit GLUT, che sono eventi comuni alla maggioranza dei sistemi window.

Per prima cosa vediamo come sia possibile utilizzare un dispositivo di input puntatore (prenderemo in considerazione il mouse) per decidere la terminazione di un programma. Faremo in modo che il programma chiami una funzione standard di terminazione, exit, quando un particolare pulsante del mouse viene premuto.

Ci sono due tipi di eventi associati ai dispositivi puntatore. Un move event si genera quando il mouse viene mosso tenendo premuto uno dei suoi pulsanti. Se il mouse viene mosso senza premere alcun pulsante, líevento è invece classificato come passive move event. Dopo un evento di tipo move, la posizione del mouse è disponibile al programma applicativo. Un mouse event si verifica quando uno dei pulsanti del mouse viene premuto o rilasciato (si osservi che quando un pulsante viene tenuto premuto, ciò non genera un evento fino a quando il pulsante non viene rilasciato). Líinformazione restituita, chiamata misura, include il pulsante che ha generato líevento, lo stato del pulsante dopo líevento (su o giù), e la posizione del cursore espressa in coordinate dello schermo. La funzione callback associata ad un mouse event, è posta di solito nella funzione main, attraverso la funzione GLUT
glutMouseFunc(mouse_callback_func);

La callback del mouse deve avere la forma seguente
void mouse_callback_func(int button, int state, int x, int y)

Allíinterno della funzione callback si specificano le azioni che vogliamo avvengano quando líevento specificato si verifica. Se vogliamo ad esempio che la pressione del pulsante di sinistra del mouse provochi la terminazione del programma dovremo utilizzare la seguente funzione callback:
void mouse_callback_function(int button, int state, int x, int y){if(button == GLUT_LEFT_BUTTON & state == GLUT_DOWN) exit( );}

In questo caso, il verificarsi di qualsiasi altro mouse event (pressione di un altro pulsante, etc.) non provoca nessuna azione, poiché non sono state definite callback corrispondenti ad altri eventi.

Vediamo un altro esempio. Scriviamo un programma che disegna un piccolo box nella locazione sullo schermo dove viene posto il cursore controllato dal mouse, quando viene premuto il suo pulsante di sinistra. Inoltre, faremo in modo che la pressione del pulsante intermedio provochi la terminazione del programma.

Consideriamo per prima cosa il programma main.
int main(int argc, char **argv){glutInit(&argc, argv);glutInitDisplayMode(GLUT_SINGLE | GLUT_RGB);glutCreateWindow("square");myinit();glutReshapeFunc(myReshape);glutMouseFunc(mouse);glutMainLoop();}

Líevento reshape è generato tutte le volte che viene ridimensionata la finestra di visualizzazione, ad esempio in seguito ad una interazione dellíutente. Si osservi che le primitive vengono generate solo quando si verifica un mouse event. La callback corrispondente al mouse event è posta nella funzione mouse:
void mouse(int btn, int state, int x, int y){if(btn==GLUT_LEFT_BUTTON&state==GLUT_DOWN) drawSquare(x,y);if(btn==GLUT_MIDDLE_BUTTON&state==GLUT_DOWN) exit( );}

Poiché il comando drawSquare genera solo le primitive, gli attributi devono essere specificati da qualche altra parte, ad esempio nella funzione myinit.

Abbiamo inoltre bisogno di tre variabili globali. La dimensione della finestra, che può variare dinamicamente, dovrebbe essere disponibile, sia per la reshape callback che per la funzione drawSquare. Se vogliamo modificare la dimensione del quadrato che stiamo disegnando, è utile rendere globale il parametro di square-size. La routine di inizializzazione seleziona una finestra di clipping che ha la stessa dimensione della finestra creata in main, e seleziona il viewport in modo che corrisponda allíintera finestra.
/* globals */
GLsizei wh = 500, ww = 500; /* initial window size */Glfloat size = 3.0; /* one--half of side length of square */
void myinit(void){
glViewport(0,0,ww,wh);glMatrixMode(GL_PROJECTION);glLoadIdentity();gluOrtho2(0.0, (Gldouble) ww , 0.0, (Gldouble) wh);glMatrixMode(GL_MODELVIEW);
/* set clear color to black, and clear window */
glClearColor(0.0, 0.0, 0.0, 0.0);glClear(GL_COLOR_BUFFER_BIT);glFlush();
}

La routine per disegnare il quadrato deve tenere conto che la posizione restituita dal mouse event è espressa nel sistema di coordinate del sistema window, che ha origine nellíangolo in alto a sinistra della finestra. Dovremo quindi modificare in modo opportuno il valore delle ordinate. Infine, scegliamo a caso un colore usando il generatore standard di numeri casuali random().
void drawSquare(int x, int y){
y=wh-y;glColor3ub( (char) random()%256, (char) random()%256, (char) random()%256);glBegin(GL_POLYGON);glVertex2f(x+size, y+size);glVertex2f(x-size, y+size);glVertex2f(x-size, y-size);glVertex2f(x+size, y-size);glEnd();glFlush();}

Il programma si completa inserendo i comandi include:
#include <GL/gl.h>#include<GL/glut.h>

Molti sistemi window danno allíutente la possibilità di modificare le dimensioni della finestra, utilizzando il mouse per muovere líangolo della funestra nella posizione desiderata. Ciò costituisce un esempio di window event.

Se la dimensione della finestra viene modificata, ci sono tre questioni che dobbiamo considerare, ciascuna priva di una risposta univoca: (1) stabilire se tutti gli oggetti allíinterno della finestra devono essere ridisegnati dopo la variazione di dimensione; (2) decidere come comportarsi se líaspect-ratio della nuova finestra è diverso da quello della finestra originale; (3) infine, decidere se modificare dimensioni e attributi delle primitive nel caso in cui la dimensione della nuova finestra è diversa da quella della finestra usata in precedenza.

Nellíesempio che stiamo esaminando, faremo in modo che vengano disegnati quadrati della medesima dimensione, indipendentemente dalla dimensione e dalla forma della finestra. Puliamo lo schermo ogni volta che la finestra viene ridimensionata, e utilizziamo interamente la nuova finestra come finestra di drawing. Líevento reshape restituisce líaltezza e líampiezza della nuova finestra, e questi valori vengono utilizzati per creare una nuova finestra di clipping con il comando gluOrtho2, ed un nuovo viewport con lo stesso aspect-ratio. Infine, utilizziamo il colore nero per pulire la finestra. Abbiamo così la seguente callback:
void myReshape(GLsizei w, Glsizei h){
/* adjust clipping box */
glMatrixMode(GL_PROJECTION);glLoadIdentity();gluOrtho2(0.0, (Gldouble)w, 0.0, (Gldouble)h);glMatrixMode(GL_MODELVIEW);glLoadIdentity();
/* adjust viewport and clear */
glViewport(0,0,w,h);glClearColor(0.0, 0.0, 0.0, 0.0);glClear(GL_COLOR_BUFFER_BIT);glFlush();}

Ci sono anche altre possibilità: ad esempio, potremmo modificare la dimensione del quadrato a seconda che la dimensione della finestra sia aumentata o diminuita. Uníaltra semplice modifica che potremmo includere consiste nel fare in modo che venga visualizzato un nuovo quadrato mentre un pulsante del mouse è tenuto premuto. La funzione callback rilevante è la motion callback, che viene predisposto attraverso la funzione.
glutMotionFunc(drawSquare);

Possiamo usare anche la tastiera come dispositivo di input. I keyboard event sono generati dalla pressione dei tasti, e tutte le keyboard callback sono registrate in una singola funzione callback:
void keyboard(unsigned char key, int x, int y){if(key == ëqí | key == ëQí) exit( );}

Delle callback rimanenti, due meritano uníattenzione speciale. La display callback
glutDisplayFunc(display);

viene invocata quando il toolkit GLUT stabilisce che la finestra deve essere ridisegnata. Una situazione di questo genere si verifica quando la finestra viene inizialmente aperta.

idle callback viene invocata quando non ci sono altri eventi. Per default è la funzione nulla. Un uso tipico dellíidle callback è quello di continuare a generare primitive grafiche attraverso una funzione display quando niente altro sta avvenendo.

  1. Menu

Il toolkit GLUT fornisce uníaltra caratteristica addizionale: la possibilità di realizzare pop-up menu, che possiamo usare con il mouse per creare applicazioni interattive sofisticate.

Per utilizzare i menu occorre definire le entrate, legare il menu ad un particolare pulsante del mouse, e definire infine una funzione corrispondente ad ogni entrata del menu.

Vediamo allora come realizzare un menu con tre entrate relativo allíesempio considerato nella sezione precedente. Vogliamo che la prima selezione ci consentirà di uscire dal programma; mentre la seconda e la terza di modificare la dimensione del quadrato nella funzione drawSquare. Le funzioni da chiamare per predisporre il menu e legarlo al pulsante destro del mouse devono essere poste nella funzione main. Si tratta delle seguenti funzioni:
glutCreateMenu(demo_menu);glutAddMenuEntry("quit", 1);glutAddMenuEntry("increase square size", 2);glutAddMenuEntry("increase square size", 3);glutAttachMenu(GLUT_RIGHT_BUTTON);

Il secondo argomento in ciascuna definizione delle entrate del menu è líidentificatore passato alla callback quando líentrata viene selezionata. La funzione callback è data da
void demo_menu(int id){if(id == 1) exit( );else if (id == 2) size = 2 * size;else if (size > 1) size = size/2;glutPostRedisplay( );}

Il comando glutPostRedisplay fa si che lo schermo sia ridisegnato senza il menu.

Il toolkit GLUT consente di realizzare anche i menu gerarchici. Ad esempio si può realizzare un menu principale con due sole entrate, la prima relativa alla terminazione del programma, e la seconda costituita da un sottomenu contenente le due entrate relative alla variazione di dimensione del quadrato, attraverso il codice:
sub_menu = gluCreateMenu(size_menu);glutAddMenuEntry("increase square size", 2);glutAddMenuEntry("increase square size", 3);glutCreateMenu(top_menu);glutAddMenuEntry("quit", 1);glutAddSubMenu("Resize", sub_menu);glutAttachMenu(GLUT_RIGHT_BUTTON);

  1. Picking

Il picking è uníoperazione di input che permette allíutente di identificare un oggetto sul display. Líazione di picking utilizza un dispositivo puntatore, ma líinformazione che si vuole restituire al programma applicativo in questo caso non è solo una posizione.

Un dispositivo pick è molto più difficile da implementare rispetto ai dispositivi locator, perlomeno nei sistemi moderni. Una delle ragioni di questa difficoltà è la seguente. Le primitive sono definite nel programma applicativo e vengono sottoposte ad una sequenza di trasformazioni, e alle operazioni di clipping e scan conversion, prima di essere memorizzate nel frame buffer. Questo processo è in gran parte reversibile da un punto di vista matematico, tuttavia líhardware non è reversibile. Quindi la conversione di una locazione sul display nella primitiva corrispondente non è un calcolo diretto.

Ci sono due modi principali per trattare questa difficoltà. Un processo, conosciuto come selezione, richiede líaggiustamento delle regioni di clipping e viewport in modo da poter tenere traccia di quali primitive in una piccola regione siano da visualizzare nella regione intorno al cursore.

Un approccio alternativo è basato sullíuso dei bounding rectangle, o extent, per gli oggetti di interesse. Líextent di un oggetto è il più piccolo rettangolo, allineato con gli assi del sistema di coordinate, che lo contiene.

Il picking comporta altre difficoltà: se un oggetto è definito in modo gerarchico, allora esso è parte di un insieme di oggetti. Quando líoggetto viene indicato dal dispositivo di puntamento, una lista di tutti gli oggetti di cui esso fa parte dovrebbe essere restituita al programma utente.

Le librerie grafiche, infine, non specificano di quanto sia necessario avvicinarsi allíoggetto da identificare con il dispositivo di puntamento. Tra le ragioni di tale omissione vi è sia quella di permettere diverse modalità di picking, che il fatto che, nonostante il display possa essere ad alta risoluzione, può risultare difficile per líutente indicare in modo preciso, tramite il dispositivo di puntamento, la locazione sullo schermo che corrisponde ad un certo pixel.

  1. Progettazione di programmi interattivi

» difficile definire cosa caratterizza un buon programma interattivo; tuttavia riconoscere ed apprezzare un buon programma interattivo è facile. Un tale programma dovrebbe includere caratteristiche quali

  1. Un display omogeneo, senza flicker né altri inconvenienti dovuti al processo di refresh delle immagini.
  2. Una varietà di dispositivi interattivi sul display.
  3. Una varietà di metodi per accedere e per visualizzare le informazioni.
  4. Uníinterfaccia facile da usare, che non richieda sforzi eccessivi di apprendimento.
  5. Un feedback allíutente.
  6. Un design che tenga conto delle proprietà visuali e motorie dellíutente.

Líimportanza di questa caratteristiche, e la difficoltà di progettare un buon programma interattivo non dovrebbe mai essere sottostimata.

Ci sono alcune caratteristiche comuni alla computer graphics ed al campo delle interazioni uomo macchina che possono essere perseguite al fine di migliorare i nostri programmi interattivi.

Il display del nostro CRT deve essere rinfrescato ad un tasso compreso tra le 50 e le 75 volte al secondo. In un sistema grafico, questa richiesta implica che il contenuto del frame buffer deve essere continuamente ridisegnato.

Fino a quando il contenuto del frame buffer rimane immutato e viene ridisegnato ad una frequenza di 50-75 Hz, non si avverte che il refresh delle immagini sta avvenendo. Se invece modifichiamo il contenuto del frame buffer durante líoperazione di refresh, possono risultare visualizzati alcuni effetti indesiderabili. Ad esempio, se líoggetto è complesso e non può essere disegnato in un solo ciclo, vedremo parti successive dellíoggetto in refresh successivi; se líoggetto è in movimento, líimmagine sul display risulta quindi distorta.

Il double buffering è una tecnica che consente di risolvere questi problemi. Supponiamo di avere a disposizione due frame buffer, che chiameremo front buffer e back buffer. Il front buffer è quello che verrà sempre visualizzato, mentre il back buffer è quello in cui disegniamo. Possiamo scambiare i buffer come vogliamo, dallíinterno del programma applicativo. Tutte le volte che i buffer sono scambiati si invoca una display callback. Quindi, se mettiamo il nostro rendering nella display callback, líeffetto sarà quello di aggiornare automaticamente il back buffer. Possiamo predisporre il double buffering usando líopzione GLUT_DOUBLE, al posto di GLUT_SINGLE, nel comando glutInitDisplayMode. La funzione di scambio dei buffer, usando GLUT, è
glutSwapBuffer();

Se dobbiamo generare un display complesso, possiamo disegnare nel back buffer usando cicli di refresh multipli, e scambiare i buffer alla fine.

Un uso più comune del double buffering avviene nelle animazioni, dove primitive, attributi, e condizioni di visualizzazione cambiano continuamente. Possiamo usare un double buffering e mettere le funzioni di drawing in una display callback. Allíinterno di questa callback, il primo passo consiste nel pulire il front buffer usando il comando glClear, e il passo finale consiste nellíinvocare il comando glutSwapBuffers. Nonostante disegnare tutti gli oggetti possa richiedere cicli multipli di refresh, líosservatore non vedrà mai un quadro incompleto.

Vediamo ora due esempi che illustrano le limitazioni del rendering geometrico e mostrano perché, a volte, è necessario lavorare direttamente nel frame buffer. Consideriamo un menu pop-up. Quando invochiamo una menu callback, il menu appare sul display, sovrapponendosi a quanto era già visualizzato. Dopo aver effettuato la selezione, il menu scompare e lo schermo viene riportato nello stato in cui si trovava prima dellíapparizione del menu. Non possiamo implementare questa sequenza di operazioni usando solo gli strumenti che abbiamo presentato fino ad adesso. Una tecnica per implementare queste operazioni è quella di conservare la parte del display sottostante il menu, e di ricopiarla quando il menu non è più necessario. Sfortunatamente, questa soluzione potenziale non coinvolge le primitive nel programma applicativo, ma le loro immagini dopo la scan conversion. Di conseguenza è necessario eseguire un certo insieme di operazioni che devono essere descritte in termini del contenuto del frame buffer.

Un secondo esempio è costituito dal cosiddetto rubberbanding, una tecnica per visualizzare i segmenti (e altre primitive) in modo interattivo. Quando líutente preme un pulsante (ad esempio del mouse) viene stabilita la locazione del primo estremo del segmento in corrispondenza della posizione del cursore. Il movimento successivo del cursore, effettuato mantenendo premuto il pulsante, determina la posizione dellíaltro estremo, che verrà fissato in modo definitivo solo quando il pulsante viene rilasciato. Il nome dato a questo processo deriva dal fatto che il segmento che vediamo sul display sembra una banda elastica, con un estremo fissato nella prima locazione, e il secondo estremo allungato e accorciato a seconda dei movimenti del cursore. Quando il pulsante viene rilasciato, il segmento finale appare sullo schermo.

Il rubberbanding è uníoperazione che comporta problemi in qualche modo simili a quelli legati alla visualizzazione dei menu. Ogni volta che il cursore viene portato in una nuova locazione, dobbiamo riportare il display al suo stato originale, prima di poter disegnare un nuovo segmento. Altri oggetti che possono essere disegnati interattivamente usando il rubberbanding includono rettangoli e cerchi. Menu e rubberbanding sono spesso inclusi in toolkit come GLUT, in modo che líutente si possa avvantaggiare di queste caratteristiche, anche senza essere in grado di programmarle.