INTRODUZIONE

Original Author: Damiano Vitulli

Translation by: Click here

In queste lezioni spiegherò come realizzare un motore 3d in linguaggio C usando le librerie OpenGL. Queste lezioni sono rivolte a chi già possiede una conoscenza base del linguaggio C. In caso contrario vi consiglio di studiare prima il linguaggio C (sulla rete ci sono un infinità di corsi).

Il motore che andremo a realizzare sarà strutturato per gestire ambienti aperti, caratteristica adatta per i simulatori spaziali (dopotutto ci troviamo a spacesimulator.net, no?) e non è escluso che in futuro aggiungerò anche sezioni per visualizzare terreni e tutorials più avanzati nei quali svelerò anche alcune soluzioni che ho implementato nel mio stesso motore. State certi però che procederò gradualmente dai concetti più elementari ai più complessi. E naturalmente per cominciare è necessario spiegare il concetto base... cos'è un motore 3d?

Un motore 3d è un insieme di strutture, di funzioni e di algoritmi che consentono di visualizzare, dopo avere effettuato opportuni calcoli e trasformazioni, degli oggetti tridimensionali su uno schermo 2d.

Le principali sezioni di un motore 3d sono:

  1. Acquisizione delle informazioni sulla struttura degli oggetti e successiva memorizzazione in strutture.
  2. Trasformazioni per posizionarli nel mondo.
  3. Visualizzazione su schermo.

Noi siamo qui per questo e quindi mettiamoci subito a lavoro!

In questa lezione vedremo come definire le strutture principali per disegnare un oggetto 3d.

I VERTICI

Supponiamo di avere un oggetto e di volerlo rappresentare in uno schermo 2D. A tale scopo occorre ottenere informazioni riguardanti la sua struttura. Come facciamo? Definiamo dei punti chiave: i vertici, i quali identificano, appunto, i vertici dell'oggetto. Ogni vertice 3d è composto da tre coordinate cartesiane x,y,z. Ogni coordinata deve essere espressa tramite valori a virgola mobile a singola o a doppia precisione (float o double), questo perchè è necessario avere sempre la massima risoluzione disponibile per effettuare i calcoli sulla posizione e rotazione e per disegnare l'oggetto.

Per definire un vertice in linguaggio C usiamo una struttura vertex composta da tre variabili x,y,z.

typedef struct {
   float x,y,z;
} vertex_type;

E' molto importante tenere a mente che tutti i calcoli che riguardano il posizionamento e la rotazione di un oggetto si applicano ai vertici, essendo la struttura base. La seconda struttura in ordine di importanza sono i poligoni.

I POLIGONI

I poligoni sono le facce dell'oggetto e sono composti da N vertici. In quasi tutti i motori i poligoni sono composti da 3 vertici e quindi anche noi useremo questa convenzione.

A questo punto possiamo pensare di definire una struttura poligono la quale conterrà 3 vertici:

typedef struct {
   vertex_type a,b,c;
} polygon_type;

ed in seguito dichiariamo la variabile array polygons che conterrà tutti i poligoni che costituiranno l'oggetto:

#define MAX_POLYGONS 2000
polygon_type polygon[MAX_POLYGONS];

Ma attenzione! La nostra definizione di poligono assegna ad ogni poligono 3 vertici unici e non condivisi con altri poligoni. In realtà se ci riflettete un attimo ogni poligono condivide i suoi lati, e quindi anche i suoi vertici, con altri poligoni. Quindi abbiamo commesso un errore. Beh, non è proprio un errore, ma abbiamo comunque aumentato in modo considerevole il numero di vertici presenti sulla scena, ben oltre il necessario. Come abbiamo già detto il motore userà i vertici per effettuare la maggior parte dei suoi calcoli e quindi dobbiamo assolutamente trovare un altro modo per definire i poligoni.

A pensarci bene possiamo per prima cosa creare una lista di vertici che conterrà tutti i vertici dell'oggetto e poi... per definire i poligoni useremo una sorta di "referenziamento" per "puntare" ai vertici di quella lista.

Dichiariamo adesso una variabile array di tipo vertex_type che conterrà MAX_VERTEX vertici.

#define MAX_VERTICES 2000
vertex_type vertex[MAX_VERTICES];

La struttura poligono non conterrà più i vertici ma soltanto 3 numeri che identificheranno dei vertici corrispondenti alla lista dei vertici. In questo modo più poligoni possono puntare allo stesso vertice e ciò ottimizza notevolmente il design dell'engine.

typedef struct {
   int a,b,c;
} polygon_type;

L'OGGETTO

Facciamo un pò d'ordine ed organizziamo le precedenti definizioni in una struttura che chiameremo obj_type.

typedef struct {
   vertex_type vertex[MAX_VERTICES];
   polygon_type polygon[MAX_POLYGONS];
} obj_type,*obj_type_ptr;

Si tratta solo di una definizione di base, in futuro arricchiremo questa struttura con nuovi campi che identificheranno la posizione, la rotazione e lo stato dell'oggetto.

Per definire un cubo occorrerà quindi riempire l'array di vertici in questo modo (coordinate da 0 a +1):

obj_type obj;
obj.vertex[0].x=0;  obj.vertex[0].y=0;  obj.vertex[0].z=0; //vertex v0
obj.vertex[1].x=1;  obj.vertex[1].y=0;  obj.vertex[1].z=0; //vertex v1
obj.vertex[2].x=1;  obj.vertex[2].y=0;  obj.vertex[2].z=1; //vertex v2
obj.vertex[3].x=0;  obj.vertex[3].y=0;  obj.vertex[3].z=1; //vertex v3
obj.vertex[4].x=0;  obj.vertex[4].y=1;  obj.vertex[4].z=0; //vertex v4
obj.vertex[5].x=1;  obj.vertex[5].y=1;  obj.vertex[5].z=0; //vertex v5
obj.vertex[6].x=1;  obj.vertex[6].y=1;  obj.vertex[6].z=1; //vertex v6
obj.vertex[7].x=0;  obj.vertex[7].y=1;  obj.vertex[7].z=1; //vertex v7

Il problema adesso è come suddividere il nostro cubo in triangoli. La risposta è semplice, ogni faccia del cubo è un quadrato che è composto da due triangoli adiacenti. Il nostro oggetto sarà composto quindi da 12 poligoni (triangoli) e da 8 vertici.

Tut 3dengine cube.png

Riempiamo la lista di poligoni:

obj.polygon[0].a=0;  obj.polygon[0].b=1;  obj.polygon[0].c=4;  //polygon v0,v1,v4
obj.polygon[1].a=1;  obj.polygon[1].b=5;  obj.polygon[1].c=4;  //polygon v1,v5,v4
obj.polygon[2].a=1;  obj.polygon[2].b=2;  obj.polygon[2].c=5;  //polygon v1,v2,v5
obj.polygon[3].a=2;  obj.polygon[3].b=6;  obj.polygon[3].c=5;  //polygon v2,v6,v5
obj.polygon[4].a=2;  obj.polygon[4].b=3;  obj.polygon[4].c=6;  //polygon v2,v3,v6
obj.polygon[5].a=3;  obj.polygon[5].b=7;  obj.polygon[5].c=6;  //polygon v3,v7,v6
obj.polygon[6].a=3;  obj.polygon[6].b=0;  obj.polygon[6].c=7;  //polygon v3,v0,v7
obj.polygon[7].a=0;  obj.polygon[7].b=4;  obj.polygon[7].c=7;  //polygon v0,v4,v7
obj.polygon[8].a=4;  obj.polygon[8].b=5;  obj.polygon[8].c=7;  //polygon v4,v5,v7
obj.polygon[9].a=5;  obj.polygon[9].b=6;  obj.polygon[9].c=7;  //polygon v5,v6,v7
obj.polygon[10].a=3; obj.polygon[10].b=2; obj.polygon[10].c=0; //polygon v3,v2,v0
obj.polygon[11].a=2; obj.polygon[11].b=1; obj.polygon[11].c=0; //polygon v2,v1,v0

Una cosa molto importante da considerare è che per definire i poligoni bisogna usare sempre lo stesso verso orario o antiorario per tutti i poligoni sulla scena. Come vedremo più avanti il verso è usato per vedere se il poligono è visibile o meno. Quindi attenzione a non sbagliare altrimenti alcuni poligoni non saranno visibili. Noi useremo la convenzione antioraria (ad esempio per il primo poligono potremmo avere 0,1,4 oppure 1,4,0 oppure 4,0,1 e non 1,0,4 e nemmeno 0,4,1 e nemmeno 4,1,0).

UN ALTRO MODO PIU' ELEGANTE DI DEFINIRE L'OGGETTO

In C/C++ possiamo riempire la struttura obj_type utilizzando questa modalità più elegante:

obj_type cube =
{
    {
        -10,-10, 10, //vertex v0
         10,-10, 10, //vertex v1
         10,-10,-10, //vertex v2
        -10,-10,-10, //vertex v3
        -10, 10, 10, //vertex v4
         10, 10, 10, //vertex v5
         10, 10,-10, //vertex v6
        -10, 10,-10  //vertex v7
    },
    {
        0, 1, 4, //polygon v0,v1,v4
        1, 5, 4, //polygon v1,v5,v4
        1, 2, 5, //polygon v1,v2,v5
        2, 6, 5, //polygon v2,v6,v5
        2, 3, 6, //polygon v2,v3,v6
        3, 7, 6, //polygon v3,v7,v6
        3, 0, 7, //polygon v3,v0,v7
        0, 4, 7, //polygon v0,v4,v7
        4, 5, 7, //polygon v4,v5,v7
        5, 6, 7, //polygon v5,v6,v7
        3, 2, 0, //polygon v3,v2,v0
        2, 1, 0, //polygon v2,v1,v0
    }
};

Potete notare che abbiamo cambiato le coordinate dei vertici da 0,1 a 10,-10. Questo è intenzionale, infatti è di fondamentale importanza che il centro dell'oggetto sia disposto alle coordinate 0,0 (approfondiremo questo concetto in un altro tutorial). Il nostro oggetto sarà anche 20 volte più grande in modo da facilitare la gestione delle coordinate.

CONCLUSIONE

Per questa lezione è tutto, nella prossima inizieremo ad usare OpenGL ed integreremo quanto visto finora per disegnare il cubo sullo schermo.

SOURCE CODE

The Source Code of this lesson can be downloaded from the Tutorials Main Page