OpenGL Tutorial 2 – Hello Dot!

26 novembre

<- Prev | Indice | Next ->

Codice di esempio

Background

Questo è il nostro primo incontro con GLEW, l’OpenGL Extension Wrangler Library. GLEW ci aiuta a gestire le estensione in OpenGL. Una volta inizializzata domanda tutte le estensioni disponibili sulla tua piattaforma, le carica dinamicamente e fornisce un facile accesso per mezzo di un solo file header.
In questo tutorial vedremo per la prima volta l’uso dei vertex buffer objects (VBOs). Come implica il nome, sono utilizzati per memorizzare i vertici. Gli oggetti che esistono nel mondo 3D che stai cercando di visualizzare, siano essi mostri, castelli o un semplice cubo rotante, saranno sempre costruit collegando insieme un gruppo di vertici. I VBO sono il modo più efficiente di caricare i vertici nella GPU. Sono buffers che possono essere memorizzati nella memoria video e offrono il minor tempo di accesso alla GPU.
Questo e il prossimo tutorial saranno i soli nella serie ad usare la fixed function pipeline invece di quella programmabile. Effettivamente, non ci sarà alcuna trasformazione in nessuno dei due tutorial. Semplicemente ci fideremo di come i dati verranno gestiti dalla pipeline. Uno studio approfondito della pipeline seguirà nei prossimi tutorial ma per adesso è abbastanza capire che prima di arrivare al rasterizer (che è ciò che disegna i punti, le linee e i triangoli usando le screen coordinates) i vertici visibili hanno le loro coordinate X, Y, Z nell’intervallo [-1.0, +1.0]. Il rasterizer mappa queste coordinate nello screen space (p.es. se la larghezza dello schermo fosse 1024 allora la coordinata -1.0 sarebbe mappata a 0 e +1.0 sarebbe mappata a 1023). Finalmente, il rasterizer disegna le primitive secondo la topologia definita nella draw call (vedi sotto nell’analisi del codice). Poiché non abbiamo assegnato alcuno shader alla pipeline i nostri vertici non subiranno alcuna trasformazione. Questo significa che dovremo giusto assegnare dei valori nel sopra citato intervallo per renderli visbili. In effetti, un valore 0 sia per X che per Y posiziona il vertice nell’esatto centro di entrambi gli assi – in altre parole, il centro dello schermo.

Installare GLEW: GLEW è disponibile dal sito principale a http://glew.sourceforge.net/. Quasi tutte le distribuzioni Linux provvedono dei pacchetti precompilati. Su Ubuntu si può installare da terminale con il seguente comando:

>_ Comando
apt-get install libglew1.6 libglew1.6-dev

Analisi del Codice

#include <GL/glew.h>

Qui includiamo il singolo header per GLEW. Se includete altri headers OpenGL dovrete stare attenti ad includere questo file prima degli altri altrimenti GLEW si lamenterà. Per linkare il programma con GLEW si deve aggiungere “-lGLEW” al makefile.

#include "math_3d.h"

In questo tutorial iniziamo ad usare delle strutture d’aiuto come i vettori. Amplieremo questo header andando avanti.

GLenum res = glewInit();
if (res != GLEW_OK)
{
fprintf(stderr, "Error: '%s'\n", glewGetErrorString(res));
return 1;
}

Qui inizializziamo GLEW e controlliamo se ci sono stati erroi. Questo va fatto dopo che GLUT è stato inizializzato (ovvero successivamente alla creazione del contesto OpenGL, ndr).

Vector3f Vertices[1];
Vertices[0] = Vector3f(0.0f, 0.0f, 0.0f);

Creiamo un array di un Vector3f (questo tipo è definito in math_3d.h) e inizializziamo XYZ a 0. Ciò farà apparire il punto al centro dello schemo.

GLuint VBO;

Allochiamo un Gluint nella parte globale del programma per memorizzare l’handle del vertex buffer object. Vedrai dopo che per accedere agli oggetti OenGL si dovranno utilizzare quasi sempre (se non ogni volta) variabili di tipo GLuint.

glGenBuffers(1, &VBO);

OpenGL definisce diverse funzioni glGen* per generare oggetti di vario tipo. Spesso prendono 2 parametri – il primo specifica il numero di oggetti che vuoi creare e il secondo è l’indirizzo di un array di Gluints per memorizzare gli handles che il driver alloca (assicurati che l’array sia abbastanza grande per gestire la tua richiesta!). Le chiamate future a questa funzione non genereranno gli stessi handles a meno che tu non li cancelli prima con glDeleteBuffers. Nota che a questo punto non definisci ancora cosa intendi fare con i buffers ed essi possono essere considerati generici. Ciò è compito della prossima funzione.

glBindBuffer(GL_ARRAY_BUFFER, VBO);

OpenGL ha un modo unico di usare gli handles. in molte API sono semplicemente passati alla funzione e ciò che dev’esser fatto su quell’handle viene fatto. In OpenGL invece assegniamo l’handle ad un target name ed eseguiamo i comandi su quel target. Questi comandi influiscono sull’handle vincolato fino a quando un altro handle sarà legato al suo posto o la chiamata sopra riceverà 0 (zero) come handle. Il target GL_ARRAY_BUFFER implica che il buffer conterrà un array di vertici. Un altro target utile è GL_ELEMENT_ARRAY_BUFFER il quale significa che il buffer conterrà gli indici dei vertici di un altro buffer. Sono disponibili altri buffer e li vedremo nei prossimi tutorial.

glBufferData(GL_ARRAY_BUFFER, sizeof(Vertices), Vertices, GL_STATIC_DRAW);

Dopo aver legato il nostro oggetto lo riempiremo con i dati. La chiamata di sopra prende come argomenti il target name (lo stesso usato per il binding (vincolo, ndt) ), la dimensione dei dati in byte, l’indirizzo dell’array di vertici e un flag che indica il modello di utilizzo per questi dati. Siccome non cambieremo il contenuto del buffer scriviamo GL_STATIC_DRAW. L’opposto  è GL_DYNAMIC_DRAW. Nonostante questo sia solo un suggerimento per OpenGL è consigliato decidere bene che flag utilizzare. Il driver si appoggia ad esso per un ottimizzazione euristica (p.es. il miglior posto in memoria per memorizzare il buffer.

glEnableVertexAttribArray(0);

Nel tutorial sugli shaders vedrai che gli attributi dei vertici uati nello shader (posizioni, normali, etc) hanno un indice che ti permette di legare i dati nel programma C/C++ al nome degli attributi nello shader. In più ogni indice di atributi dei vertici deve venire abilitato. In questo tutorial non usiamo ancora alcuno shader, ma nella fixed function pipeline (che viene attivata in mancanza di shaders) le posizioni dei vertici che abbiamo caricato nel buffer è trattato come vertex attribute index uguale a 0 (zero). Devi abilitare ogni vertex attribute o altrimenti la pipeline non potrà accedere ai dati.

glBindBuffer(GL_ARRAY_BUFFER, VBO);

Qui ri-vincoliamo il nostro buffer perché ci prepariamo a fare la draw call. In questo piccolo prgramma abbiamo un solo vertex buffer sicché fare questa chiamata ad ogni frame risulta ridondante, ma in progetti più complessi ci sono molti buffers per memorizzare i tuoi vari modelli e devi aggiornare lo stato della pipeline con il buffer che vuoi usare.

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);

Questa chiamata dice alla pipeline come interpretare i dati nel buffer. I primi parametri specificano l’indice dell’attributo. Nel nostro caso sappiamo che è 0 (zero) per default ma quando inizieremo a usare gli shaders dovremo stabilire esplicitamente l’indice nello shader o richiederlo. Il secondo parametro è il numero di componenti per l’attributo (3 per X, Y e Z). Il terzo è il tipo di ogni componente. Il quarto parametro indica se vogliamo che il nostro attributo venga normalizzato prima che venga utilizzato nella pipeline. Nel nostro caso vogliamo che i dati non vengano modificati. Il quinto parametro (chiamato stride) è il numero di byte tra due istanze di quell’attributo nel buffer. Quando c’è un solo attributo (p.es. il buffer contiene solo le posizioni dei vertici) e tra le istanze non c’è spazio passiamo il valore 0 (zero). Se avessimo un’array di strutture contenenti una posizione e una normale (entrambe sono vettori di 3 float) passeremmo la dimensione della struttura in byte (6*4=24). L’ultimo parametro è utile nel caso del precedente esempio. semplicemente dobbiamo specificare l’offset nela struttura dove la pipeline troverà il nostro attributo. Nel caso della struttura con le posizioni e le normali l’offset delle posizioni sarebbe zero mentre quello delle normali sarebbe 12.

glDrawArrays(GL_POINTS, 0, 1);

Finalmente effettuiamo la chiamata per disegnare la geometria. Tutti i comandi che abbiamo visto fino ad ora sono importanti ma preparano solamente il palco per il comando che disegnerà qualcosa. Ora la GPU inizierà a lavorare. Combinerà i parametri della draw call con lo stato costruito e mostrerà il render a schermo.
OpenGL prevede vari tipi di draw call e ognuna è ottimizzata per un certo caso. In generale si possono suddividere in due categorie – ordered draw calls e indexed draw calls. Le ordered draw sono più semplici. La GPU attraversa il vertex buffer, passando per ogni vertice, e li interpreta secondo la topologia specificata nella draw call. Per esempio, se specifichi GL_TRIANGLES allora i vertici 0-2 diventano il primo triangolo, 3-5 il secondo, etc. Se vuoi che uno stesso vertice appaia in più di un triangolo lo dovrai definire più volte, il che è uno spreco di spazio.
Le indexed draw sono più complesse e prevedono un buffer in più chiamato index buffer. L’index buffer contiene degli indici dei vertici nel vertex buffer. La GPU legge l’index buffer e similmente alla descrizione di cui sopra gli indici 0-2 diventano il primo triangolo e così via. Se però vuoi uno stesso vertice in due triangoli devi semplicemente mettere due volte il suo indice nell’index buffer. Il vertex buffer necessita di una sola copia. Le index draw sono più comuni nei giochi perché la maggior parte dei modelli sono creati da triangoli che rappresentano delle superfici (la pelle delle persone, i muri dei castelli, etc) e un sacco di vertici sono in comune.
In questo tutorial usiamo la draw call più semplice – glDrawArrays. Questa è un’ordered call, quindi nessuna index buffer. La nostra topologia è fatta di punti e quindi ogni vertice è un punto. Il prossimo parametro è l’indice del primo vertice da disegnare. Nel nostro caso vogliamo incominciare dall’inizio del buffer e così mettiamo zero ma questo meccanismo ci permette di memorizzare più modelli nello stesso buffer e scegliere quello da disegnare in base all’offset nel buffer. L’ultimo parametro è il numero di vertici da disegnare.

glDisableVertexAttribArray(0);

È buona pratica disabilitare ogni vertex attribute quando non viene utilizzato. Lasciarlo abilitato senza che uno shader lo usi è un buon modo per cercarsi guai.

<- Prev | Indice | Next ->

Tags: