3.8 Mise à jour des données

Transférer les données dans la carte graphique c'est bien, mais les mettre à jour quand un objet bouge c'est quand même mieux.

Notre caisse n'en a pas vraiment besoin car elle ne bouge pas. Mais lorsque nous animerons des personnages, il faudra bien mettre à jour leurs vertices pour voir leurs déplacements.

La modification de données dans la mémoire vidéo se fait un peu comme l'écriture de texte dans un fichier. Avec les fichiers, on commence par en ouvrir un avec un mode d'accès (lecture, écriture ou les deux), puis on écrit les données, et une fois qu'on a fini on ferme le fichier.

Avec les VBO c'est un peu la même chose, on va commencer par récupérer l'espace mémoire qu'on a allouée, puis on écrira les données à l'intérieur et enfin on fermera l'accès à la zone mémoire. La seule véritable différence avec les fichiers c'est que les VBO il faut les verrouiller.

Pour accéder à la mémoire vidéo, nous devons utiliser une fonction qui permet de retourner un pointeur un peu spéciale. Celui-ci forme en quelque sorte une passerelle entre le VBO et la RAM. Grâce à lui, nous pourrons accéder aux données de la mémoire vidéo comme s'il s'agissait de données présentes dans la RAM.

La fonction en question s’appelle glMapBuffer().

1
void* glMapBuffer(GLenum target, GLenum access);
  • target : Toujours le même, on lui donne la valeur GL_ARRAY_BUFFER
  • mode : Mode d'accès aux données du VBO (lecture, écriture ou les deux)

Contrairement à ce qu'on pourrait penser, la fonction renvoie bien quelque chose. L'utilisation du type void* permet de spécifier un pointeur. Un pointeur qui, ici, forme la passerelle dont nous avons parlée à l'instant.

Le paramètre mode peut prendre 3 valeurs :

  • GL_READ_ONLY : Lecture seulement
  • GL_WRITE_ONLY : Écriture seulement
  • GL_READ_WRITE : Lecture et écriture

Dans la plupart des cas, nous utiliserons la deuxième valeur car on ne fera que transférer des données, pas besoin de lecture dans ce cas

Ainsi, pour mettre à jour les informations contenues dans un VBO on commence par le verrouiller puis on récupère l'adresse de sa zone mémoire :

1
2
3
4
5
6
// Verrouillage du VBO qu'on suppose bien alloué et initialisé comme il faut
glBindBuffer(GL_ARRAY_BUFFER, vboID);

...

void *adresseVBO = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

Il peut arriver que la récupération de l'adresse échoue, ce n'est pas très courant mais ça peut arriver.

Si c'est le cas alors la fonction glMapBuffer() renvoie un pointeur NULL, il faut donc vérifier sa valeur avant de continuer. Si elle est nulle, alors on déverrouille le VBO et on annule le transfert. S'il n'y a aucune erreur on peut continuer :

1
2
3
4
5
6
7
8
9
10
11
12
// Verrouillage du VBO qu'on suppose bien alloué et initialisé comme il faut
glBindBuffer(GL_ARRAY_BUFFER, vboID);

...

void *adresseVBO = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);

if(adresseVBO == NULL){
	std::cout << "Erreur au niveau de la récupération du VBO" << std::endl;
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	return; 
}

Bien sûr, vous pouvez broder le message d'erreur si vous en avez envie.

Une fois notre adresse trouvée et validée nous pouvons commencer le transfert, et comme d'habitude avec les VBO les transferts se font en bytes. Le seul problème ici, c'est qu'on n'a pas de fonction OpenGL pour mettre à jour nos données, il va falloir le faire par nous-même avec comme seul outil l'adresse de destination dans la mémoire vidéo.

Heureusement pour nous, nous n'aurons pas à faire ça à la main. Il existe une fonction C (oui oui C pas C++) qui permet de copier tout un tas de bytes d'un coup. Cette fonction s'appelle memcpy() :

1
void* memcpy(void *destination, const void *source, size_t num);
  • destination : L'adresse où on écrira les données, ici ce sera l'adresse mémoire que l'on a récupérée
  • source : La source de données, ici ce sera les tableaux de vertices et de coordonnées de texture
  • num : La taille des données à copier (en bytes), c'est la même chose que le paramètre size de la fonction glBufferSubData()

Le seul problème avec cette fonction, c'est qu'elle ne prend pas en compte le décalage dans une zone mémoire, elle n'est capable d'écrire qu'à partir du début.

Pour contourner ceci, nous allons reprendre la macro BUFFER_OFFSET() que nous utilisons pour l'affichage :

1
2
3
4
// Macro utile au VBO
#ifndef BUFFER_OFFSET
    #define BUFFER_OFFSET(offset) ((char*)NULL + (offset))
#endif

Le code qui nous intéresse ici c'est la définition de la macro :

1
(char*)NULL + (offset)

Ce code permet de spécifier un décalage offset à partir de l'adresse NULL. Ce que nous voulons nous, c'est spécifier un décalage à partir de l'adresse adresseVBO.

Pour ce faire, nous devons simplement remplacer le mot-clef NULL par le pointeur adresseVBO et l'offset par la variable decalage :

1
(char*)adresseVBO + decalage

Simple n'est-ce pas ?

Alors bon, nous n'allons pas recréer une macro pour ça, ce ne serait pas utile et ça alourdirait le header (mais comme je sens que ça va me gonfler dans pas longtemps, on fera sûrement une macro, car un header de 4 lignes au lieu d'un header de 3 lignes, ça ne change pas grand chose). A la place, nous allons juste donner ce code au paramètre destination de la fonction memcpy(). Quant aux autres, nous leur donnerons en valeur la taille des données à copier et le pointeur source.

L'appel à la fonction ressemblera au final à ceci :

1
2
// Mise à jour des données
memcpy((char*)adresseVBO + decalage, donnees, tailleBytes);

Avec ça, nous pouvons mettre à jour n'importe quel VBO.

Il ne manque plus qu'une seule chose à faire. Pour sécuriser le VBO, il faut invalider le pointeur retourné par glMapBuffer(). Si on ne le fait pas il y a un risque d'écraser les données présentes à l'intérieur, on peut se retrouver avec un magnifique bug d'affichage avec ça. :lol:

La fonction permettant d'invalider ce pointeur s'appelle glUnmapBuffer() :

1
GLboolean glUnmapBuffer(GLenum target);
  • target : on lui donnera comme d'habitude la valeur GL_ARRAY_BUFFER

La fonction renvoie un GLboolean pour savoir si tout s'est bien passé.

On l'appelle juste après les transferts précédents. On en profite au passage pour affecter la valeur NULL (ou 0, mais je préfère NULL, ça permet de se rappeler que c'est bien un pointeur) au pointeur pour une double sécurisation :

1
2
3
// Annulation du pointeur
glUnmapBuffer(GL_ARRAY_BUFFER);
adresseVBO = NULL;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
glBindBuffer(GL_ARRAY_BUFFER, vboID);
	// Récupération de l'adresse du VBO
	void *adresseVBO = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);
	// Si l'adresse retournée est nulle alors on arrête le transfert
	if(adresseVBO == NULL){
		std::cout << "Erreur au niveau de la récupération du VBO" << std::endl;
		glBindBuffer(GL_ARRAY_BUFFER, 0);
		return; 
	}
	// Mise à jour des données
	memcpy((char*)adresseVBO + decalage, donnees, tailleBytes);
	// Annulation du pointeur
	glUnmapBuffer(GL_ARRAY_BUFFER);
	adresseVBO = NULL;
// Déverrouillage du VBO
glBindBuffer(GL_ARRAY_BUFFER, 0);