Utilisation des VBO avec OpenGL 3.x

Michaël Gallego

Avec l'introduction du système de dépréciation depuis OpenGL 3.0, de nombreuses fonctions sont devenues obsolètes, notamment sur l'utilisation des vertex buffer objects (VBO). Ce tutoriel a donc pour objectif de vous montrer comment utiliser les VBO dans un OpenGL moderne.

- Titre : Utilisation des VBO avec OpenGL 3.x

- Auteur : Michaël Gallego

- Parution : 25 juin 2009

- Licence :

Sommaire

- 0. Introduction

- I. Utilisation des VBO avec OpenGL 2.x

- II. Utilisation des VBO avec OpenGL 3.x

- II-A. Activation d'un tableau générique

- II-B. Envoi des données

- II-C. Envoi aux shaders

- II-D. On dessine !

- II-E. On récapitule

- III. Conclusion

- IV. Sources du programme

- V. Remerciements

0 - Introduction

Avant de commencer à lire ce tutoriel, je vous invite à lire l'excellent tutoriel de raptor sur les VBO, disponible à cette adresse : https://raptor.developpez.com/tutorial/opengl/vbo/

Une bonne compréhension des VBO s'avère en effet indispensable, puisque ce tutoriel n'a pas pour objectif de vous expliquer comment tout ceci fonctionne, mais plutôt comment les utiliser en évitant les fonctions dépréciées.

I - Utilisation des VBO avec OpenGL 2.x

Notez tout d'abord que ce tutoriel assume que l'envoi de la géométrie se fait à des shaders (puisque l'utilisation du pipeline fixe est dépréciée). Auparavant, et une fois les VBO correctement remplis avec des données, il était courant d'utiliser certaines fonctions définies par OpenGL afin d'envoyer le tout à un shader.

Ces fonctions sont : glColorPointer(), glEdgeFlagPointer(), glFogCoordPointer(), glIndexPointer(), glNormalPointer(), glSecondaryColorPointer(), glTexCoordPointer(), glVertexPointer(), et enfin les deux fonctions glEnableClientState() et glDisableClientState() qui permettaient d'activer et de désactiver le ou les types de tableaux à envoyer.

Ces différentes fonctions avaient pour conséquences de faire correspondre, dans un shader, une variable built-in avec des données envoyées par l'utilisateur. Par exemple, les données envoyées avec la fonction glVertexPointer() pouvaient être exploitées par la variable built-in gl_Vertex, tandis que les données envoyées avec la fonction glNormalPointer() étaient exploitées par la variable built-in gl_Normal, et ainsi de suite.

Voici un exemple d'utilisation, pris du tutoriel de raptor sur les VBO :

Comme nous le voyons ici, une fois le buffer activé via la fonction glBindBuffer, les variables buit-in à remplir sont spécifiées grâce aux fonctions glVertexPointer et glColorPointer, et les tableaux correspondants activés par la fonction glEnableClientState et désactivés avec glDisableClientState.

II - Utilisation des VBO avec OpenGL 3.x

OpenGL 3.x introduit la notion de tableaux génériques (en fait, ils étaient déjà disponibles avec OpenGL 2.1, mais leur usage n'était pas imposé). Un tableau générique fonctionne d'une manière relativement similaire à ce que nous connaissions sous OpenGL 2.x, si ce n'est que les fonctions permettant d'envoyer les données à une variable built-in spécifique (glVertexPointer() pour gl_Vertex.) ont disparues, et que c'est maintenant le programmeur qui doit se charger d'envoyer les bonnes données aux bons attributs dans le shader.

II-A - Activation d'un tableau générique

Avant toute chose, il convient d'activer un tableau générique. Ceci se fait par la fonction glEnableVertexAttribArray, qui prend comme unique paramètre un entier non signé représentant l'indice du tableau à activer.

// Activation du tableau générique n°2
glEnableVertexAttribArray (2);

Puis, une fois qu'on en a fini, il convient de les désactiver, avec la fonction glDisableVertexAttribArray, qui prend logiquement un entier non signé représentant l'indice du tableau à désactiver.

// Désactivation du tableau générique n°2
glDisableVertexAttribArray (2);

A noter que l'erreur GL_INVALID_VALUE est générée si l'indice envoyé à l'une de ces fonctions est supérieur à la constante GL_MAX_VERTEX_ATTRIBS.

II-B - Envoi des données

Nous avons vu comment remplacer les fonctions glEnableClientState/glDisableClientState, mais quid de toutes ces fonctions glColorPointer, glVertexPointer. ? Très simplement, tout cela devient beaucoup plus générique grâce à une unique fonction (en fait, deux) :

void glVertexAttribPointer (uint index, int size, enum type, boolean normalized, sizei stride, const void * pointer);

Le premier paramètre, index, correspond à l'indice du tableau générique auquel nous souhaitons faire correspondre les données (de la même manière que tout à l'heure, l'erreur GL_INVALID_VALUE est générée si l'indice est supérieur à la constante GL_MAX_VERTEX_ATTRIBS), size indique le nombre de valeurs stockées par sommet (1, 2, 3 ou 4), type correspond au... type de données stockées dans le tableau (GL_FLOAT, GL_UNSIGNED_BYTE, GL_INT.), le booléen normalized, s'il vaut « vrai », va normaliser les données entre 0 et 1. stride indique le nombre d'octets d'espacement entre deux sommets (l'erreur GL_INVALID_VALUE est générée si stride est négatif), et enfin pointer correspond aux données à envoyer.

A noter que toutes les données envoyées par cette fonction seront automatiquement converties en float, même si le booléen normalized vaut « vrai ». Si vous souhaitez faire en sorte que vos données soient conservées en tant qu'entiers « purs », vous devez utiliser une autre fonction :

void glVertexAttribIPointer (uint index, int size, enum type, sizei stride, const void * pointer);

Son utilisation reste similaire, si ce n'est la disparition du paramètre « normalized ».

II-C - Envoi aux shaders

Nous avons vu dans le tutoriel précédent consacré aux shaders l'apparition des variables in dans le vertex shader, et je vous ai dit qu'il s'agissait des attributs des sommets, mais sans vous dire comment on envoyait les données à ces variables. En effet, la chose pratique avec les fonctions glVertexPointer/glNormalPointer... était que, lorsque l'on appelait ces fonctions, l'utilisation dans le shader était très aisée. Pour utiliser les données envoyées avec glVertexPointer, il suffisait de se servir de la variable gl_Vertex. Hélas, la dépréciation de ces fonctions au profit des tableaux génériques (bien plus flexibles au demeurant) a également entraîné la déprécation de toutes ces variables built-in dans les shaders (il n'est donc plus correct d'utiliser les variables gl_Vertex ou gl_Normal dans un shader).

A la place, le programmeur doit spécifier lui-même ces propres variables dans le shader, comme ceci :

in vec3 VertexPosition; // Remplace gl_Vertex
in vec3 VertexNormal; // Remplace gl_Normal

Ici, nous spécifions nous-mêmes les variables. Avouez que ceci offre encore plus de flexibilité qu'auparavant (même si cela rend l'utilisation bien plus complexe). Bref, la question que l'on peut se poser est : comment dire à OpenGL que le tableau générique n°i, que j'ai activé grâce à la fonction glEnableVertexAttribArray, à qui j'ai envoyé mes données via la fonction glVertexAttribPointer, correspond, par exemple, à la variable VertexPosition ?

C'est finalement relativement simple et très logique, et il existe deux méthodes pour ceci. A noter que, pour que les fonctions qui suivent fonctionnent, il faut que le shader ait été correctement linké (via la fonction glLinkProgram). Une fois votre shader lié, il existe deux fonctions particulièrement utiles. La première a le prototype suivant :

int glGetAttribLocation (uint program, const char * name);

Le premier paramètre correspond à l'identifiant de votre programme de shader, et le deuxième correspond au nom de l'attribut de sommet dans le programme (par exemple, pour reprendre mon exemple plus haut, « VertexPosition » ou « VertexNormal »). La fonction renvoie un entier (notez, au passage, une petite incohérence dans les spécifications : la fonction glEnableVertexAttribArray prend un entier non signé, tandis que cette fonction renvoie un entier signé ; ce sont de petites incohérences dont les développeurs d'OpenGL sont au courant et devraient corriger au fil du temps).

Ainsi, cet indice peut dorénavant être utilisé pour activer les tableaux génériques et envoyer les données.

La deuxième méthode consiste à spécifier soi-même dans quel tableau générique nous souhaitons faire correspondre un attribut de vertice :

void glBindAttribLocation (uint program, uint index, const char * name);

Le second paramètre (notez ici qu'il s'agit bien d'un entier non-signé, youpi !) correspond donc à l'indice que vous souhaitez. Comme tout le temps, l'erreur GL_INVALID_VALUE est générée si index est supérieur à la constante GL_MAX_VERTEX_ATTRIBS.

A noter que, lorsque le programme de shader est lié, OpenGL génère automatiquement des points d'attache pour chaque attribut du shader actif (que l'on peut donc récupérer par glGetAttribLocation, donc).

Quelle méthode préférer ? A vrai dire, cela dépend clairement de l'architecture de vos programmes. Personnellement, je trouve que le fait de spécifier soi-même les indices des tableaux génériques offre bien plus de flexibilité que la première méthode, mais cela dépend clairement de votre utilisation.

II-D - On dessine !

Reste à savoir comment dessiner toutes ces données. Ici, pas de grande différence avec l'utilisation précédente des VBO, cela passe par les différentes fonctions que nous connaissons déjà : glArrayElement(), glDrawElements(), glMultiDrawElements(), glDrawArrays(), glMultiDrawArrays().

II-E - On récapitule

Récapitulons tout ça :

- A l'initialisation

- Création du programme de shader

- Liage du programme de shader

- Récupération ou spécification des indices des tableaux génériques associés à chaque attribut de vertice.

- Création et chargement des VBO (buffers). Cette étape peut évidemment se situer avant les précédentes, cela dépend de votre programme.

- A chaque boucle

- Activation du tableau générique

- Envoi des données à un tableau générique

- On dessine le tout

- Désactivation du tableau générique

Dans le tutoriel suivant, nous verrons comment simplifier l'étape « A chaque boucle », grâce à l'extension GL_ARB_vertex_array_object, introduite par OpenGL 3.0, et qui nous permet d'éviter d'activer les tableaux/envoyer les données/désactiver les tableaux à chaque tour.

Voici donc, en GLSL moderne, la traduction du code de raptor concernant les VBO. On assume également qu'un shader est correctement lié, et pour faire simple, nous allons reprendre la même structure que le tutoriel de raptor, et définir un shader contenant deux attributs (au passage, je précise que les noms peuvent être quelconques - c'est l'avantage de cette flexibilité gagnée -, vous pouvez très bien appeler vos positions « Coucou ») :

in vec3 VertexPosition; // Position, 3 floats
in vec3 VertexColor; // Couleur, 3 floats

Voici donc l'initialisation du VBO (à noter également que les indices que j'ai spécifiés sont arbitraires, j'ai mis 4 pour les couleurs, 6 pour les positions, mais on aurait très bien pu mettre 3 et 8 ou 1 et 2) :

GLfloat CubeArray[48] = {
1.0f, 0.0f, 0.0f, -1.0f, 1.0f, -1.0f,
1.0f, 0.0f, 1.0f, -1.0f, -1.0f, -1.0f,
1.0f, 1.0f, 1.0f, -1.0f, 1.0f, 1.0f,
0.0f, 0.0f, 1.0f, -1.0f, -1.0f, 1.0f,
0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 1.0f,
0.0f, 1.0f, 1.0f, 1.0f, -1.0f, 1.0f,
1.0f, 1.0f, 0.0f, 1.0f, 1.0f, -1.0f,
1.0f, 1.0f, 1.0f, 1.0f, -1.0f, -1.0f
};

GLuint IndiceArray[36] = {
0,1,2,2,1,3,
4,5,6,6,5,7,
3,1,5,5,1,7,
0,2,6,6,2,4,
6,7,0,0,7,1,
2,3,4,4,3,5
};

// Génération des buffers
glGenBuffers( 2, CubeBuffers );

// Buffer d'informations de vertex
glBindBuffer(GL_ARRAY_BUFFER, CubeBuffers[0]);
glBufferData(GL_ARRAY_BUFFER, sizeof(CubeArray), CubeArray, GL_STATIC_DRAW);

// Buffer d'indices
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, CubeBuffers[1]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(IndiceArray), IndiceArray, GL_STATIC_DRAW);

// Méthode 1 : Récupération des indices de tableaux prédéfinis
unsigned int colorIndex = glGetAttribLocation (programID, "VertexColor");
unsigned int positionIndex = glGetAttribLocation (programID, "VertexPosition");
// Méthode 2: Spécification des indices de tableaux
glBindAttribLocation (programID, 4, "VertexColor");
glBindAttribLocation (programID, 6, "VertexPosition");

Puis le rendu :

// Utilisation des données des buffers
glBindBuffer(GL_ARRAY_BUFFER, CubeBuffers[0]);
// Activation des tableaux génériques
glEnableVertexAttribArray (4); // Ou glEnableVertexAttribArray (colorIndex);
glEnableVertexAttribArray (6; // Ou glEnableVertexAttribArray (positionIndex);

// Envoi des données (4 et 6 peuvent être remplacés par colorIndex et positionIndex)
glVertexAttribPointer (4, 3, GL_FLOAT, GL_FALSE, 0, 0);
glVertexAttribPointer (6, 3, GL_FLOAT, GL_FALSE, sizeof (float) * 3, 0);

glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, CubeBuffers[1]);

// Rendu de notre géométrie
glDrawElements(GL_TRIANGLES, 36, GL_UNSIGNED_INT, 0);

// Désactivation des tableaux génériques
glDisableVertexAttribArray (4); // Ou glDisableVertexAttribArray (colorIndex)
glDisableVertexAttribArray (6); // Ou glDisableVertexAttribArray (positionIndex)

III - Conclusion

Dans ce tutoriel, nous avons vu comment utiliser les VBO avec OpenGL 3.x. Dans le tutoriel suivant, nous verrons comment encore simplifier leur utilisation grâce à une nouvelle extension introduite par OpenGL 3.0.

Résultat

IV - Sources du programme

Les sources du programme sont disponibles à cette adresse : ftp://ftp-developpez.com/bakura/tutoriels/jeux/VBO.zip

Le code a volontairement été simplifié au maximum afin de ne faire apparaître dans le main.cpp que les fonctions nécessaires à la compréhension du point abordé par cet article. A noter également que je me suis aperçu, trop tard, que je n'avais pas écrit de fonction Clean() pour nettoyer tous les buffers et libérer la mémoire. Ceci est corrigé dans les sources du tutoriel suivant.

V - Remerciements

Merci à IrmatDen pour sa relecture.