Chaque sous partie de la partie pratique est associée à une classe dans le programme accompagnant cet article.
Deux autres exemples, non décrits dans ce document, montrent l’utilisation de GLSL et Cg avec les VBOs. L’implémentation
avec des classes a pour but unique de rendre interchangeables les différentes méthodes dans le programme d’exemple.
2.1. Utilisation de base façon Vertex Array (classe CTest1)
Pour faciliter la compréhension, commençons par un exemple correspondant parfaitement au fonctionnement de base
des vertex arrays. Les VBOs utilisent une API similaire aux objets de textures pour leur gestion.
GLvoid glGenBuffers(GLsizei n, GLuint* buffers);
GLvoid glDeleteBuffers(GLsizei n, const GLuint* buffers);
buffers est un tableau créé par l’utilisateur dans lequel sont stockés les identifiants des VBOs. n objets sont
créés ou détruits, attention donc à la taille de buffers.
Admettons que nous souhaitions afficher un carré à l’écran au moyen de deux triangles. Nos sources sont par exemple :
static const GLsizeiptr PositionSize = 6 * 2 * sizeof(GLfloat);
static const GLfloat PositionData[] =
{
-1.0f,-1.0f,
1.0f,-1.0f,
1.0f, 1.0f,
1.0f, 1.0f,
-1.0f, 1.0f,
-1.0f,-1.0f,
};
static const GLsizeiptr ColorSize = 6 * 3 * sizeof(GLubyte);
static const GLubyte ColorData[] =
{
255, 0, 0,
255, 255, 0,
0, 255, 0,
0, 255, 0,
0, 0, 255,
255, 0, 0
};
Nous allons utiliser deux VBOs pour afficher les six sommets décrits par les tableaux précédents. Les tableaux sont
identifiés par GL_POSITION_OBJECT et GL_COLOR_OBJECT simplement par commodité. La création ou la destruction des
VBOs s’effectue avec les fonctions glGenBuffers et glDeleteBuffers. La fonction glBindBuffer permet de sélectionner
le VBO actif.
static const int BufferSize = 2;
static GLuint BufferName[BufferSize];
static const GLsizei VertexCount = 6;
enum
{
POSITION_OBJECT = 0,
COLOR_OBJECT = 1
};
Le code C++ pour afficher ce carré est alors :
glBindBuffer(GL_ARRAY_BUFFER, BufferName[COLOR_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, ColorSize, ColorData, GL_STREAM_DRAW);
glColorPointer(3, GL_UNSIGNED_BYTE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, PositionSize, PositionData, GL_STREAM_DRAW);
glVertexPointer(2, GL_FLOAT, 0, 0);
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glDrawArrays(GL_TRIANGLES, 0, VertexCount);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
glBufferData initialise le stockage de données du VBO. Le dernier paramètre spécifie l’usage du VBO tel qu’il est
décrit en section 1.2 et 1.3. La liste de tous les usages est disponible en section 3.1. Les fonctions glColorPointer
et glVertexPointer permettent de spécifier l’emplacement pour trouver respectivement les couleurs et les coordonnées
spatiales des sommets.
L’ordre de ces trois appels de fonctions est particulièrement important. Il est intuitif qu’il faut sélectionner
le VBO actif en premier pour le paramétrer. Cependant, l’ordre de glBufferData et gl*Pointer est également important.
En effet, gl*Pointer réfère à la source des données du VBO actif, cette source étant initialisée par glBufferData.
Remarques :
Parfois, on préférera séparer la tâche de chargement des données et la tâche de description des données pour
des problèmes de flexibilité d’utilisation. Ainsi, la solution suivante est tout à fait viable:
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, PositionSize, PositionData, GL_STREAM_DRAW);
...
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glVertexPointer(3, GL_FLOAT, 0, 0);
Dès lors que la fonction glBindBuffer est appelée avec un nom de VBOs valide, OpenGL bascule en mode VBO.
Pour revenir en mode Vertex Array, il faut utiliser glBindBuffer avec la valeur 0 comme nom d’objet.
Le rendu s’effectue avec l’une des fonctions dédiées aux rendus de tableaux : glDrawArrays ou
glMultiDrawArrays dans cet exemple.
Dans le cas plutôt rare où la taille totale de la mémoire de la carte graphique est inférieure à la taille que
l’on demande de réserver pour un unique VBO, une erreur de type GL_OUT_OF_MEMORY est émise et récupérable comme
habituellement avec glGetError.
2.2. Utilisation avec index buffer (classe CTest2)
Lors du premier exemple nous avons utilisé la cible GL_ARRAY_BUFFER. Elle est utilisée pour tout type de données,
excepté les tableaux d’indices qui utilisent la cible GL_ELEMENT_ARRAY_BUFFER.
L’initialisation d’un tableau d’indexes s’effectue ainsi :
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, BufferName[INDEX_OBJECT]);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, IndexSize, IndexData, GL_STREAM_DRAW);
C’est la partie qui diffère le plus des vertex arrays. Si la fonction de rendu par élément est utilisée avec la
valeur nulle à la place d’un tableau d’indices, alors le VBO actif ayant pour cible GL_ELEMENT_ARRAY_BUFFER est utilisé.
Dans cet exemple, le rendu s’effectue avec l’une des fonctions dédiées aux tableaux indexés : glDrawElements,
glDrawRangeElements ou glMultiDrawElements.
2.3. Utilisation des tableaux entrelacés (classe CTest3)
Il n’y a pas eu de mise à jour de la fonction glInterleavedArrays [3.4.2] depuis son arrivée avec OpenGL 1.1. Cette
fonction fut largement utilisée pour le rendu avec tableau entrelacé. Elle peut encore être utilisée avec les VBOs.
mais il y a une alternative bien plus intéressante basée sur les fonctions gl*Pointer. L’idée est de spécifier
pour chaque attribut du tableau entrelacé la source des données en utilisant le paramètre de stride.
#pragma pack(push, 1)
struct SVertex
{
GLubyte r;
GLubyte g;
GLubyte b;
GLfloat x;
GLfloat y;
};
#pragma pack(pop)
glBindBuffer(GL_ARRAY_BUFFER, BufferName);
glBufferData(GL_ARRAY_BUFFER, VertexSize, VertexData, GL_STREAM_DRAW);
glColorPointer(3, GL_UNSIGNED_BYTE, sizeof(SVertex), BUFFER_OFFSET(ColorOffset));
glVertexPointer(2, GL_FLOAT, sizeof(SVertex), BUFFER_OFFSET(VertexOffset));
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glDrawArrays(GL_TRIANGLES, 0, VertexCount);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
Pour cet exemple, nous utilisons une structure qui nous permet d’entrelacer les données. Notons que la définition de
la structure est entourée des instructions du pré-processeur standard #pragma pack. En effet, si l’on recherche
la taille de la structure avec l’instruction sizeof, il y a toutes les chances pour que la valeur retournée soit 12, voir 16
octets au lieu de 11 dans le cas présent. GLfloat fait usuellement 4 octets, et GLubyte fait usuellement 1 octet donc
une taille requise de 11 octets. Cependant, les compilateurs alignent les données en mémoire car les processeurs
sont optimisés pour récupérer des données de la taille de leurs registres. 4 octets pour les CPU 32 bits et 8 octets
pour les CPU 64 bits. Bonne initiative, mais quand l’espace mémoire coûte cher, nous allons oublier cette optimisation.
En effet, dans notre cas, non seulement les structures coûtent 1/12 de mémoire supplémentaire, mais c’est
aussi 1/12 de données à transférer jusqu’à la carte graphique. Enfin, il se peut que des difficultés apparaissent
quant à la gestion des octets supplémentaires, particulièrement dans le cas présent. Où se trouvent les octets
supplémentaires?
Les fonctions glColorPointer et glVertexPointer doivent toujours indiquer les sources et les types des données
stockées dans le VBO. Pour cela, les spécifications proposent une macro nommée BUFFER_OFFSET :
#define BUFFER_OFFSET(i) ((char*)NULL + (i))
Avec les VBOs, il ne s’agit plus de donner l’adresse de la source de donnée, car la source est stockée par le VBO
quelque part, mais plutôt un offset qui indique où l’on doit commencer à lire les données dans la mémoire
du VBOs. sizeof(SVertex) est dans cet exemple la valeur de stride, c'est-à-dire qu’elle indique le nombre
d’octets entre deux sommets pour un même attribut. Habituellement cette valeur est nulle pour simplifier
l’API OpenGL. Ceci signifie que les valeurs sont contiguës, c'est-à-dire que le VBO ne contient qu’un seul attribut
par vertex et aucun espace vide. En conséquence, si nous créons un tableau ne contenant que les
positions spatiales 3D des sommets, alors les deux appels suivant sont équivalents :
glVertexPointer(3, GL_FLOAT, 0, 0);
glVertexPointer(3, GL_FLOAT, sizeof(float) * 3, 0);
La macro BUFFER_OFFSET permet également d’éviter un warning de conversion d’un entier en pointer.
2.4. Utilisation des tableaux sérialisés (classe CTest4)
Pour de nombreux cas, les données utilisées pour décrire les primitives géométriques n’ont aucune raison d’être entrelacées,
ce choix pouvant même être nuisible. Il est tout à fait possible que nous ne voulions mettre à jour qu’une partie
des donnés. Par exemple, dans le cas de l’animation d’un maillage tel qu’un personnage, les coordonnées de textures n’ont
pas besoin d’être mises à jour, au contraire des positions et des normales des sommets.
Pour cela, nous n’utilisons qu’un seul VBO dans lequel nous insérons plusieurs types de données au moyen de la
fonction glBufferSubData.
En premier lieu, nous réservons l’espace mémoire pour l’intégralité des données avec la fonction glBufferData. Au
lieu de passer la source des données dans le troisième paramètre nous utilisons la valeur 0.
Ensuite, nous utilisons la fonction glBufferSubData pour remplir le tableau. Le second paramètre correspond à
l’offset dans les données du VBO. Le troisième indique la taille des données sources à ajouter et le dernier est
la source des données.
glBindBuffer(GL_ARRAY_BUFFER, BufferName);
glBufferData(GL_ARRAY_BUFFER, ColorSize + PositionSize, 0, GL_STREAM_DRAW);
glBufferSubData(GL_ARRAY_BUFFER, 0, ColorSize, ColorData);
glBufferSubData(GL_ARRAY_BUFFER, ColorSize, PositionSize, PositionData);
glColorPointer(3, GL_UNSIGNED_BYTE, 0, 0);
glVertexPointer(2, GL_FLOAT, 0, BUFFER_OFFSET(ColorSize));
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_COLOR_ARRAY);
glDrawArrays(GL_TRIANGLES, 0, VertexCount);
glDisableClientState(GL_COLOR_ARRAY);
glDisableClientState(GL_VERTEX_ARRAY);
La fonction glBufferSubData peut être utilisée pour mettre à jour seulement une partie des données, comme par exemple
dans le cas de modèles partiellement animés ou lorsque plusieurs modèles sont stockés dans un seul VBO, ce qui
peut-être très efficace.
2.5. Vertex mapping (classe CTest5)
Dans certains cas nous voudrions nous passer d’un tableau intermédiaire pour stocker les données de la
géométrie. Ceci peut accélérer le rendu en évitant une copie de données inutile. Le vertex mapping utilise la
fonction glMapBuffer pour accéder via un pointeur à la mémoire réservée par le VBO.
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, PositionSize, NULL, GL_STREAM_DRAW);
GLvoid* PositionBuffer = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
memcpy(PositionBuffer, PositionData, PositionSize);
glUnmapBuffer(GL_ARRAY_BUFFER);
glVertexPointer(2, GL_FLOAT, 0, 0);
Une fois de plus, la fonction glBufferData n’est utilisée que pour réserver l’espace mémoire, l’initialisation est
effectuée à la main par l’utilisateur au moyen de la fonction glMapBuffer. Il existe trois types d’accès aux
données du VBO : GL_WRITE_ONLY, GL_READ_ONLY et GL_READ_WRITE. Les noms sont particulièrement explicites. Les
modes autorisant la lecture sont également très utiles car ils évitent la duplication de données pour des
utilisations non graphique. La fonction glUnmapBuffer invalide le pointeur. Il est préférable d’appeler
glUnmapBuffer le plus tôt possible car le vertex mapping implique des synchronisations du CPU et du GPU.
Lorsque plusieurs VBOs sont utilisés, une bonne optimisation consiste à les initialiser en parallèle car ce
procédé diminue le nombre de synchronisation CPU/GPU. Voici une solution :
glBindBuffer(GL_ARRAY_BUFFER, BufferName[COLOR_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, ColorSize, NULL, GL_STREAM_DRAW);
GLvoid* ColorBuffer = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glBufferData(GL_ARRAY_BUFFER, PositionSize, NULL, GL_STREAM_DRAW);
GLvoid* PositionBuffer = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
memcpy(ColorBuffer, ColorData, ColorSize);
memcpy(PositionBuffer, PositionData, PositionSize);
glBindBuffer(GL_ARRAY_BUFFER, BufferName[COLOR_OBJECT]);
glUnmapBuffer(GL_ARRAY_BUFFER);
glColorPointer(3, GL_UNSIGNED_BYTE, 0, 0);
glBindBuffer(GL_ARRAY_BUFFER, BufferName[POSITION_OBJECT]);
glUnmapBuffer(GL_ARRAY_BUFFER);
glVertexPointer(2, GL_FLOAT, 0, 0);
Ceci n’est valable qu'à titre d’exemple, car l’idée même du Vertex Mapping est d’éviter une copie de
données... ce que memcpy fait ici.
2.6. Démo avec Animation par Vertex Shader en GLSL
Cette démo utilise le shader de déformation présenté dans le tutoriel
suivant:
Mesh Deformers - Twister.
La démo montre l'utilisation des VBOs en mode GL_STATIC_DRAW et la déformation de la boîte est assurée par
un shader GLSL.
A des fins de comparaison, la démo est livrée en deux versions: une utilisant les VBOs (XPGL_Demo_vbo.exe) pour
le rendu et l'autre les Vertex Arrays classiques (XPGL_Demo_va.exe).
Le tableau suivant nous montre la différence de performance entre les VBO et les Vertex Arrays (VA):
| Graphic Card | XPGL_Demo_vbo.exe | XPGL_Demo_va.exe |
| ATI X1950XTX | 760 fps | 145 fps |
La démo utilise une petite librairie spécialement développée pour des experimentations OpenGL: XPGL (eXPerimental Graphics Library)
.