lunedì 28 gennaio 2013

OpenGL come macchina a stati

OpenGL è una macchina a stati. Essa viene impostata in vari stati o modi di funzionamento e resta in quello stato sino a quando non viene modificato. Come si è già visto, il colore corrente è una variabile di stato. Si può impostare il colore corrente in bianco, rosso, e qualsiasi altro colore, quindi ogni oggetto sarà disegnato in quel colore, sino a quando il colore impostato non verrà cambiato. Altri controllano cose quali l'impostazione della vista attuale, e le trasformazioni della proiezione ortogonale, le trame di linee e poligoni, il modo di disegnare i poligoni, i metodi per impacchettare i pixel, e le posizioni e caratteristiche delle luci, le proprietà dei materiali degli oggetti disegnati. Molti di queste variabili di stato hanno modalità che possono attivate o disattivate mediante i comandi glEnable() o glDisable(). Ogni variabile di stato ha una modalità di default, e in ogni punto del programma si può chiedere al sistema il valore corrente. Tipicamente, vengono usati uno dei seguenti 6 comandi per conoscerlo: glGetBooleanv(), glGetDoublev(), glGetInteger(),glGetPointer(), glIsEnabled(). La selezione di alcuni di questi comandi dipende dal tipo di dato da recuperare. Alcune variabili di stato possiedono comandi specifici (quali glGetLight*(), glGetError() o glGetPolygonStipple(). Inoltre si possono salvare un insieme di variabili di stato su uno stack di attributi, mediante glPushAttrib() o glPushClientAttrib() temporaneamente. Dopo averle modificate, sono recuperate mediante le istruzioni glPopAttrib(), glPopClientAttrib(). Per cambiamenti di stato temporanei è consigliato utilizzare queste ultime istruzioni, in quanto sono più efficienti.

sabato 26 gennaio 2013

Animare con OpenGL

Il programma elencato di seguito crea una semplice animazione realizzata utilizzando le funzioni OpenGL.

#include <GL/gl.h>
#include <GL/glu.h>
#include <GL/glut.h>
#include <stdlib.h>

static GLfloat spinta = 1.0;

void inizia(void)
{
   glClearColor (0.0, 0.0, 0.0, 0.0);
   glShadeModel (GL_FLAT);
}

void mostra(void)
{
   glClear(GL_COLOR_BUFFER_BIT);
   glPushMatrix();
   glRotatef(spinta, 0.0, 0.0, 1.0);
   glColor3f(0.0, 0.0, 1.0);
   glRectf(-25.0, -25.0, 25.0, 25.0);
   glPopMatrix();
   glutSwapBuffers();
}

void spinMostra(void)
{
   spinta = spinta + 2.0;
   if (spinta > 360.0)
      spinta = spinta - 360.0;
   glutPostRedisplay();
}

void ridisegna(int w, int h)
{
   glViewport (0, 0, (GLsizei) w, (GLsizei) h);
   glMatrixMode(GL_PROJECTION);
   glLoadIdentity();
   glOrtho(-50.0, 50.0, -50.0, 50.0, -1.0, 1.0);
   glMatrixMode(GL_MODELVIEW);
   glLoadIdentity();
}

void topo(int bottone, int stato, int x, int y)
{
   switch (bottone) {
      case GLUT_LEFT_BUTTON:
         if (stato == GLUT_DOWN)
            glutIdleFunc(spinMostra);
         break;
      case GLUT_MIDDLE_BUTTON:
         if (stato == GLUT_DOWN)
            glutIdleFunc(NULL);
         break;
      default:
         break;
   }
}

/*
 *  funzioni di doppio buffer immagine
 *  funzioni che registrano lo stato del mouse
 */
int main(int argc, char** argv)
{
   glutInit(&argc, argv);
   glutInitDisplayMode (GLUT_DOUBLE | GLUT_RGB);
   glutInitWindowSize (300, 300);
   glutInitWindowPosition (125, 125);
   glutCreateWindow (argv[0]);
   inizia ();
   glutDisplayFunc(mostra);
   glutReshapeFunc(ridisegna);
   glutMouseFunc(topo);
   glutMainLoop();
   return 0;
}

lunedì 21 gennaio 2013

Un assaggio di codice OpenGL II

La prima linea in main() inizializza una finestra sullo schermo. La funzione InizializzaFinestra() è pensata come il luogo ove le routine di sistema per la finestra vengono richiamate, esse non sono funzioni OpenGL. Le due linee seguenti sono istruzioni OpenGL per rendere lo schermo nero, glClearColor(), stabilisce il colore con cui la finestra deve essere pulita; glClear(), ripulisce la finestra. Il colore della finestra può essere cambiato nuovamente mediante glClearColor(). In modo simile glColor3f() stabilisce il colore con il quale gli oggetti devono essere disegnati, in questo caso il bianco. Tutti gli oggetti disegnati dopo questa istruzione saranno di quel colore, sino a quando non ne viene richiamato un altro. Il successivo comando OpenGL, usato dal programma glOrtho(), specifica le coordinate del systema OpenGL e come l'immagine debba essere disegnata sullo schermo. Le chiamate successive sono comprese tra glBegin() e glEnd(), definiscono l'oggetto da disegnare, in questo caso solo un poligono a quattro vertici. Gli angoli del poligono sono definiti da glVertex3f(). Infine glFlush() assicura che i comandi siano eseguiti piuttosto che immagazzianti in un buffer in attesa di ulteriori istruzioni OpenGL. La funzione AggiornaEventiFinestra() gestisce il contenuto della finestra e l'inizio della gestione dei processi.
Questo codice illustrato non è ben strutturato, in quanto non permette di cambiare le dimensioni della finestra o modificare il punto di vista. Successivamente saranno spiegate delle funzioni che possano rendere il codice più efficiente.

martedì 15 gennaio 2013

Un assaggio di codice OpenGL

Poiché si possono fare molte cose con OpenGL, un programma di questo tipo può essere molto complicato. Comunque la struttura di base di un programma utile è semplice; il suo compito è inizializzare certi stati che controllano come OpenGL calcola e specifica gli oggetti da calcolare. Prima di guardare del codice OpenGL, impariamo ad usare alcuni termini. Rendering, è il termine usato per il processo nel quale le immagini sono create dai modelli. Questi modelli sono costituiti da geometrie primitive (punti, linee, poligoni – dei quali sono specificati i vertici). L'immagine finale calcolata (rendered) consiste di un insieme di pixel sullo schermo, l'elemento visibile più piccolo che uno schermo hardware possa visualizzare. Le informazioni riguardo ai pixel (ad esempio il colore che devono avere) sono organizzati in memoria come piani di bit. Un piano di bit è un'area di memoria che conserva le informazioni di ogni pixel dello schermo; il bit potrebbe indicare come il pixel in particolare deve essere. I piani di bit sono a loro volta organizzati in blocchi di trama (framebuffer), il qual conserva le informazioni grafiche dello schermo e controlla l'intensità di tutti i pixel. Ora diamo un'occhiata al codice di un programma OpenGL. Il programma disegna un quadrato bianco su schermo nero.
#include<CosaServe.h>
main(){
InizializzaFinestra( );
glClearColor (0.0, 0.0, 0.0, 0.0);
glClear (GL_COLOR_BUFFER_BIT);
glColor3f (1.0, 1.0, 1.0);
glOrtho(0.0, 1.0, 0.0, 1.0, -1.0, 1.0);
glBegin(GL_POLYGON);
glVertex3f (0.3, 0.3, 0.0);
glVertex3f (0.9, 0.3, 0.0);
glVertex3f (0.9, 0.9, 0.0);
glVertex3f (0.3, 0.9, 0.0);
glEnd();
glFlush();
AggiornaEventiFinestra( );}

Immagine risultante dal programma



sabato 12 gennaio 2013

OpenGL

Introduzione alle OpenGL

OpenGL è una interfaccia software per hardware di grafica. Questa interfaccia consiste di circa 150 comandi distinti che possono essere usati per specifici oggetti e operazioni e produrre applicazione interattive in tre dimensioni. OpenGL è stato progettato per essere un interfaccia diretta e hardware indipendente la quale può essere utilizzata su molte piattaforme hardware. Per raggiungere queste qualità, non vi sono comandi che effettuano funzioni nelle finestre od ottengano input dall'utente sono incluse in OpenGL; invece è necessario integrarle con le istruzioni per il particolare sistema a finestre che si sta utilizzando o per definire l'input dell'utente a seconda dell'hardware utilizzato. In maniera simile OpenGL non fornisce comandi ad alto livello per descrivere i modelli degli oggetti in tre dimensioni. Questi comandi, che permettono di descrivere oggetti come parti del corpo, aeroplani o edifici; devono essere costruiti a partire da elementi semplici, un set di geometrie primitive (punti, linee, poligoni). Una sofisticata libreria che abbia queste funzioni può essere costruita inglobando OpenGL. La OpenGL Utility Library (GLU) fornisce molte caratteristiche di modellazione, come superfici quadratiche e curve e superfici NURBS. GLU è uno standard per ogni implementazione di OpenGL. Esiste inoltre uno strumento ad alto livello, Open Inventor, costruito sulla base di OpenGL, il quale rende accessibile molte funzioni separate da OpenGL.
Cosa è in grado di fare OpenGL:
  • Scena wireframe – Mostra tutti gli oggetti come se fossero costruiti in filo di ferro, ogni linea corrisponde al bordo di un poligono. Un superficie può essere costituita da centinaia di poligoni.
  • Scena depth-cued – La scena mostrata in wireframe visualizza linee più sottili per gli oggetti che si trovano più lontani dall'osservatore nella scena come si presenta nella realtà. Inoltre OpenGL permette di simulare effetti atmosferici per raggiungere i risultati della profondità di campo.
  • Scena antialiased - Questa tecnica permette di ridurre le linee seghettate sui bordi degli oggetti, creando con approssimazione dei bordi arrotondati mediante pixel, confinati in una griglia rettangolare. Le linee seghettate sono maggiormente visibili se quasi orizzontali o quasi verticali.
  • Scena flat shaded, unlit – Gli oggetti sulla scena sono mostrati come solidi. Ma essi appaiono piatti in quanto un solo colore viene usato per poligono, non sono morbidamente arrotondati. Inoltre non vi sono effetti di luce.
  • Scena smooth- shaded, lit - Gli oggetti sono molto più realistici in quanto il loro colore risponde agli effetti di luce presenti sulla scena, ombreggiatura morbida. La Texture mapping consente di applicare delle immagini di tessitura a due dimensioni sulla superficie dell'oggetto a tre dimensioni.
  • Effetto motion-blur – Gli oggetti presentano un effetto di leggera sfocatura e movimento che lascia una traccia in base alla direzione di movimento.
  • Effetto profondità di campo - simula l'inabilità di una lente di telecamera a mantenere tutti gli oggetti sulla scena messi a fuoco. La telecamera focalizza in un solo punto della scena, pertanto gli oggetti più vicini e lontani dalla telecamera sono in qualche modo sfocati.

sabato 5 gennaio 2013

Organizzare il proprio codice

Quando viene programmato un gioco, il codice è pieno di funzioni e classi, le quali possono diventare ingestibili in un singolo file. Inoltre è utile riutilizzare parte dei propri programmi, ad esempio alcune classi e funzioni, come base per successivi programmi in C++. Bisogna quindi spezzare il codice in pezzi maneggevoli e utili attraverso file multipli. Generalmente si separano le funzioni relative o le singole classi in un file proprio.

Il programma Roditore

Il programma Roditore non è composto da un solo file, è una collezione di tre files che lavorano insieme per creare una singola applicazione. Si tratta di un progetto molto semplice. Viene instanziato un solo roditore, il quale dice “ciao”. Questo esempio dimostra come sia possibile spezzare il programma in file multipli.

Usare un file

E' possibile creare questo semplice programma con un singolo file di codice, in quanto piuttosto corto, come mostrato sotto.
#include<iostream>
using namespace std;
class Roditore
{ public: void Saluto();
};
void Roditore::Saluto()
{ cout << “Ciao, Io sono un roditore.\n”; }
int main()
{ cout << “Creare un oggetto Roditore.\n”;
Roditore ctopo;
ctopo.Saluto();
return 0;
}

Comunque, è possibile spezzare questo file in file multipli che lavorano assieme come un progetto, come viene fatto di seguito. In questo progetto sono creati 3 files.
  • File intestazione. Contiene solo la definizione della classe Roditore.
  • File implementazione. Contiene l'implementazione delle funzioni membro della classe Roditore.
  • File applicazione. Contiene il programma, con la funzione main(), la quale usa la classe Roditore presente nei file implementazione e intestazione. Questo file viene eseguito dal compilatore.

Creare un file intestazione (Header)

I file header sono creati per essere inclusi in altri file. Si è già parlato di file intestazione in quanto ogni programma già presentato include almeno un file header proveniente dalla libreria standard, <iostream>. Quando si spezza il programma in file multipli, generalmente si scrive un proprio file intestazione e solitamente lo si fa per ogni classe. Questo file include solo la definizione della classe, non la sua implementazione.
//roditore.h
//file intestazione
#ifndef RODITORE_H
#define RODITORE_H
class Roditore
{ public: void Saluto();
};
#endif

La definizione della classe Roditore è molto semplice, in quanto dichiara una sola funzione pubblica, Saluto(). Vi sono alcuni concetti nuovi nel codice. Le tre linee che iniziano per # sono direttive del preprocessore, in pratica istruzioni per il compilatore. Insieme dicono al compilatore cosa non includere nella definizione di Roditore del progetto se sono già state incluse in esso. Viene presa questa precauzione per evitare che la stessa classe sia definita più di una volta nel codice, questo risulta in un errore. Cosa sta succedendo? La prima direttiva dice che se il simbolo RODITORE_H non è stato definito (in una lista di simboli che il compilatore conserva per compilare il codice), il programma deve andare avanti e processare tutto il codice che segue, sino al marcatore della terminazione.
#ifndef RODITORE_H
Se il simbolo è sulla lista, il programma deve saltare tutto il codice che segue, sino al marcatore di terminazione. Il marcatore di fine è l'ultima direttiva presente sul file.
#endif
La prima volta che roditore.h il file intestazione viene incluso nel processo, esso viene compilato, RODITORE_H non è nella lista dei simboli, e vien inclusa la definizione della classe Roditore. Inoltre il compilatore processa la direttiva, la quale chiede di includere il simbolo RODITORE_H nella sua lista di simboli.
#define RODITORE_H
Questo significa che se vien fatto un altro tentativo di includere questo file intestazione nel progetto, il compilatore dopo aver visto il simbolo RODITORE_H nella lista, salterà la definizione della classe Roditore, senza tentare di definirla un'altra volta. Comunque il simbolo RODITORE_H che viene scelto per il file d'intestazione nasce da una convenzione, per la quale il nome del file viene scritto in maiuscole seguite da _H. Questa convenzione ci permette di definire un simbolo unico per ogni file intestazione. Inoltre è quello che ogni altro programmatore si aspetta di trovare scritto.

Creare il file implementazione

Poiché il file intestazione contiene solo la definizione della classe, i dati di implementazione della classe devono essere immagazzinati in un altro file. Questo file deve avere lo stesso nome del file intestazione, ma la sua estensione è la familiare .cpp. L'implementazione di roditore.h viene chiamata roditore.cpp. Di seguito si riporta il codice.
//roditore.cpp
//file implementazione
#include<iostream>
#include “roditore.h”
using namespace std;
void Roditore::Saluto()
{ cout << “Ciao, Io sono un roditore.\n”; }

Il file contiene l'implementazione di Roditore::Saluto(). Inoltre include la definizione delle variabili e dei membri statici. Da notare l'istruzione con la quale viene incluso il file intestazione:
#include “roditore.h”
Quando si effettua un'inclusione, è come se una copia venga incollata direttamente dove si trova l'istruzione include. Includendo roditore.h si completa la definizione della classe. Questi due file insieme rappresentano un modo pulito per memorizzare una singola classe. Il passo successivo è usare la classe in un programma reale.

Creare file applicazione

Si possono includere i propri file intestazione nell'applicazione usando l'istruzione include. Viene incluso roditore.h in una semplice applicazione. Di seguito viene riportato il codice.
//roditore_app.cpp
//file applicazione
#include<iostream>
#include “roditore.h”
using namespace std;
int main()
{  cout << “Creare un oggetto Roditore.\n”;
    Roditore ctopo;
    ctopo.Saluto();
 return 0;
}

Quando viene compilato il programma, il compilatore legge la seguente linea di codice e la definizione della classe Roditore memorizzata nei file roditore.h e roditore.cpp.
#include “roditore.h”
Come se il codice di definizione fosse stato aggiunto al programma. Pertanto durante l'esecuzione, l'oggetto Roditore viene fatta istanza e la funzione dice ciao.

 

mercoledì 2 gennaio 2013

Usare classi astratte

A volte si vorrebbero definire delle classi che agiscano come base per altre classi, ma non ha senso istanziare oggetti da questa classe in quanto troppo generica. Per esempio, si supponga di avere un gioco nel quale un gruppo di creature si aggirano per l'area. Nonostante via sia un'ampia varietà di creature, esse hanno due cose in comune: Possiedono un valore di salute e possono offrire un saluto. Così possiamo definire una classe, Creatura, come base per derivare altre classi, ad esempio Fatina, Drago, Troll e così via. Nonostante Creatura ci aiuta, non ha senso istanziare un oggetto Creatura. Ma ha più senso se Creatura sia indicata solo come classe di base e non per istanziare oggetti. Bene in C++ è possibile definire questo tipo di classi, sono chiamate classi astratte.

Presentare il programma Creatura astratta

Il programma Creatura astratta dimostra le classi astratte. Nel programma viene definita una classe astratta, Creatura, la quale può essere usata come classe di base per uno specifico di classi creatura. Ne viene definita uno, Troll. Quando viene istanziato l'oggetto Troll e vi è una funzione che ottiene il saluto scorbutico del troll, inoltre un'altra funzione ne mostra la salute.
//Creatura astratta
//Dimostra le classi astratte
#include<iostream>
using namespace std;
class Creatura
{ public: Creatura(int salute = 100): m_Salute(salute){}
             virtual void Saluto() const =0 // funzione virtuale pura
             virtual void MostraSalute() const { cout << “Salute: ” << m_Salute << endl; }
  protected: int m_Salute;
};
class Troll: public Creatura
{
  public: Troll(int salute = 150): Creatura(salute){}
             virtual void Saluto() const
             { cout << “Il Troll sbruffa un ciao.\n”; }
};
int main()
{
     Creatura* pCreatura = new Troll();
     pCreatura - > Saluto();
     pCreatura - > MostraSalute();

  return 0;
}