Buffer object et VBO
A partir de
l'API level 8
Buffer object
La solution "vertex array" était déjà une belle avancée dans le monde OpenGL, en effet, initialement les vertice étaient envoyés un par un (mode immédiat via une fonction nommée glVertex() éventuellement épaulé par les display lists). Ce tableau a donc permis une amélioration des performances puisqu'il ne fallait plus qu"une commande pour envoyer l'ensemble des vertice.
Cependant, cette solution a encore un défaut: il faut envoyer toutes les données lors de chaque affichage à l'écran: perte de temps si l'objet 3D n'a pas changé et consommation mémoire inutile puisque présent en mémoire à la fois côté CPU et en mémoire sur la carte graphique (côté GPU).
D'où une nouvelle solution qui consiste à utiliser un buffer object. Ce buffer object est une zone mémoire ou tampon que vous réservez directement dans la mémoire de la carte graphique. Vous en faîtes ensuite ce que vous voulez. L'avantage: vous ne transférez l'information qu'une fois à la carte graphique, vous pouvez donc supprimer ces informations de la mémoire côté CPU (en laissant le carbage collector oeuvrer pour vous). D'où un gain de temps dans les traitements.
L'allocation
mémoire est très basique: Allocation et libération de
la mémoire allouée. Ne comptez donc pas modifier la taille
du bloc mémoire. Il faudra supprimer ce bloc puis le recréer.
Le VBO
VBO signifie Vertex Buffer Object. Il s'agit simplement d'un buffer object (vu ci-dessus) "spécialisé" dans les vertice. Cette zone mémoire sera donc composée des coordonnées de vertice, mais aussi éventuellement d'autres informations utiles aux vertice comme la couleur, la position des texel, ...
Le fonctionnement
Pour pouvoir utiliser un (des) buffer object, il faut demander un nom ou mieux un identifiant buffer à OpenGL.
Aucune zone
mémoire n'est encore réservée lors de cette phase du traitement.
Alimenter le VBO
public static void glGenBuffers(int n, IntBuffer buffers)
ou
public static void glGenBuffers(int n, int[] buffers, int offset)
n: nombre d'identifiants de buffers voulus.
buffers: Tableau d'entiers qui recevra les noms (identifiants) des buffers.
offset: Offset dans le tableau buffers où doit commencer l'alimentation des nouveaux identifiants. A priori, ce sera depuis le début du tableau, donc 0...
Exemple
Pour réserver 4 noms de buffer depuis le début du tableau
final int buffers[] = new int[4];
GLES20.glGenBuffers(4, buffers, 0);
Les identifiants des buffers étant connus, il sera possible de réserver une zone mémoire et d'alimenter ceux-ci avec les données que vous voulez:
Pour cela, il faut indiquez sur quel buffer vous voulez travailler grâce à la maintenant traditionnelle commande bind
public static void glBindBuffer (int target, int buffer)
En entrée:
Le type de zone mémoire utilisé: il sera positionné à GLES20.GL_ARRAY_BUFFER
Le nom/identifiant du buffer à utiliser parmi ceux préalablement générés
Exemple:
// Maintenant, nous voulons travailller sur ce buffer
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
Transférer les données dans la zone mémoire:
public static void glBufferData (int target, int size, Buffer data, int usage)
En entrée:
target toujours positionné à GLES20.GL_ARRAY_BUFFER
size: la taille en octet du buffer contenant les informations à transmettre dans le buffer object
et pour rappel,
un Float est d'une taille de 4 octets. Donc un buffer de 12 éléments
occupe et a une taille de 12 * 4 octets !
data: la zone mémoire qui contient les données des vertice
usage: Positionnée GLES20.GL_STATIC_DRAW pour indiquer que le buffer ne sera pas maj dynamiquement.
Exemple:
// Ok, on peut transférer les informations
GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexBuffer.capacity() * BYTES_PER_FLOAT,
vertexBuffer, GLES20.GL_STATIC_DRAW);
Les informations
auront été préalablement stockées
en mémoire native, d'autant plus qu'ici, il s'agit de transmettre
des données vers une zone mémoire de la carte graphique encore
moins accessible depuis votre programme !
Lorsque vous avez terminé le transfert, il faut éviter que les
opérations suivantes ne soient affectées au buffer:
// Fini de travailler avec ce buffer !
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
A mettre
impérativement, sinon vous allez avoir des surprises (désagréables)
!
Utiliser le vbo lors du traitement de l'image
Reste la phase indiquant à OpenGL qu'il faut utiliser le buffer object pour affichage:
Les shaders ne changent pas, ils seront toujours codés de la même manière qu'avec les vertex array et la gestion des couleurs.
Le début du code sera toujours le même, déclaration est uilisation des attributs pour indiquer au vertex shader la position et la couleur d'un vertex
// Indiquer les programmes shader à utiliser...
GLES20.glUseProgram(mProgram);
// Création d'un attribut aPosition à l'index 0
GLES20.glBindAttribLocation(mProgram, 0, "aPosition");
GLES20.glBindAttribLocation(mProgram, 1, "aColor");
// Récupération de l'index, c'est juste pour l'exemple, puisqu'ici,
il est déjà connu !
indexVPosition = GLES20.glGetAttribLocation(mProgram, "aPosition");
indexAColor = GLES20.glGetAttribLocation(mProgram, "aColor");
// Indiquer que l'attribut va pointer sur un buffer
GLES20.glEnableVertexAttribArray(indexVPosition);
GLES20.glEnableVertexAttribArray(indexAColor);
Par contre, il y a quelques changements par rapports au vertex array:
- il n'est pas nécessaire de renvoyer les coordonnées des vertice dans le buffer.
- il suffit d'indiquer le buffer object à utiliser !
Je vais traiter le cas le plus compliqué, mais en même temps le plus intéressant pour le système: le buffer est composé des coordonnées et des couleurs d'un vertex ...
// Et utilisation du buffer OpenGL
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, buffers[0]);
Il faut donc expliquer à OpenGL comment est construit le buffer pour pouvoir alimenter correctement vPosition dans le vertex shader.
Nous réutilisons la méthode GLES20.glVertexAttribPointer, mais sous la forme suivante:
public static void glVertexAttribPointer (int indx, int size, int type, boolean normalized, int stride, int offset)
En entrée:
indx: index de l'attribut du shader qui sera alimenté lors de l'exécution. Par exemple indexVPosition...
size: Nombre d'éléménts, par exemple 4 si le vertex est composé de 4 coordonnées
type: A priori, vous utiliserez du float, donc GLES20.GL_FLOAT
normalized: laissons le à false pour le moment...
stride: toujours le pas, à calculer en octet !
offset: le point de départ dans le buffer.
offset en
octet ! Donc un float = 4 octets, l'offset sera deplacement à faire x
4 !
D'où le code suivant:
En partant du principe que le tableau est composé de 4
coordonnées, suivies de 4 composantes
de couleurs:
// L'offset est à 0, car les coordonnées commencent à
0 dans le tableau
GLES20.glVertexAttribPointer(indexVPosition, NB_COORDS_PAR_VERTEX, GLES20.GL_FLOAT,
false, (NB_COORDS_PAR_VERTEX + NB_COORDS_PAR_COULEUR) * BYTES_PER_FLOAT, 0);
GLES20.glVertexAttribPointer(indexAColor, // Index de l'attribut
NB_COORDS_PAR_COULEUR, // 4 informations par couleur (correspondant à
r, v, b et alpha)
GLES20.GL_FLOAT, // De type float
false,
(NB_COORDS_PAR_VERTEX + NB_COORDS_PAR_COULEUR) * BYTES_PER_FLOAT, // La taille
occupée en byte par un vertex est donc 4 * taille d'un float
// L'offset est à 4x4, car les composantes de la couleur commencent
après les 4 floats des coordonnées d'un vertex dans le tableau
NB_COORDS_PAR_VERTEX * BYTES_PER_FLOAT); // Et l'index de départ * taille
d'un float
// Ok, on peut dessiner maintenant !
GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, NB_VERTEX);
// Disable du vertex buffer pour ne plus l'utiliser par la suite...
GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
GLES20.glDisableVertexAttribArray(indexVPosition);
GLES20.glDisableVertexAttribArray(indexAColor);
Comme pour les
vertex array, rien ne vous empêchera d'utiliser deux buffers objects,
un pour les vertice, et un autre pour les couleurs...
Effacer les buffer objects
De la mémoire a été réservée sur la carte vidéo. Or le carbage collector ne fait pas de nettoyage aussi "loin". Donc tout ce que vous réservez ne se libérera pas tout seul !
Il faut donc faire ce travail:
La méthode GLES20.glDeleteBuffers permettra de libérer les blocs mémoires:
public static void glDeleteBuffers (int n, int[] buffers, int offset)
public static void glDeleteBuffers (int n, IntBuffer buffers)
Exemple
final int[] buffersToDelete = new int[] { buffers[0], buffers[1]};
GLES20.glDeleteBuffers(buffersToDelete.length, buffersToDelete, 0);
Compléments
public static boolean glIsBuffer (int buffer) va permettre de vérifier qu'un buffer est valide.
Exemple
Voici les codes principaux qui vont gérer les buffer objects
Tout d'abord une nouvelle classe: VBO
Et la classe Triangle réadaptée pour utiliser une instance de VBO
Pour le reste, quelques changements, secondaires...
Externalisation des shaders dans le repertoire asset, mais pas de changement particulier:
Après tout, je n'ai pas fait une classe helper de chargement de fichiers shader pour rien !
Et tout ce qui permet de compiler et de linker les shaders toujours inchangés
Et enfin l'activité, la surface et le renderer légèrement modifiés pour charger les shaders, détruire les buffer objects, ...
Le AndroidManifest.xml n'a pas changé...