Dessiner des objets géométriques
Pour rappel, les primitives supportées sous OpenGL sont les points, lignes, polygones, triangles, ...
Pour dessiner ces primitives, vous utilisiez glVertex, glColor, glNormal ou glTexCoord entre glBegin et glEnd.
Ces fonctions sont encore appelées des fonctions immédiates. L'avantage étant de pouvez manipuler rapidement et donc de comprendre rapidement comment fonctionne OpenGL. Mais la multiplication des appels pour dessiner un objet graphique en 3D ne permet pas vraiment une optimisation des traitements.
C'est fonctions
immédiates sont même carrément rendues indisponibles à
partir d'OpenGL 3.x dans le profil "core".
Il faut donc les remplacer par celles qui vont suivrent. Cela ne va pas simplifier
la tâche à ceux voulant apprendre OpenGL...
OpenGL 2.0
Une solution est apparu dans OpenGL 2.0 qui permettait de transmettre un tableau de vertice (glVertexPointer) et les informations associées (glTexCoordPointer, glNormalPointer, glColorPointer). L'avantage:une instruction pour passer l'ensemble des informations.
Mais une autre solution est préférée: glVertexAttribPointer.
OpenGL 3.0
Vous devez passer par glVertexAttribPointer.
C'est donc cette solution que je vais expliquer maintenant.
Buffer object
Une nouvelle solution consiste à utiliser un buffer object et de transmettre les vertice dans ce buffer. 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'avatange: 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 compter donc pas modifier la taille du bloc
mémoire. Il faudra supprimer ce bloc puis le recréer.
VBO
La solution qui est préconisée consiste à passer une fois pour toute, dans la mémoire de la carte graphique, les coordonnées des vertice mais aussi leur couleurs, texels, ... et autres qui seront traités plus tard.
Restons en aux coordonnées des vertice pour le moment.
Les cartes graphiques ont donc de la mémoire (si si !). Pour pouvoir transférer les informations de vertice, il faudra d'abord se réserver de la mémoire appelée buffer object. N'ayant pas accès directement à ces buffers objects (le programme tourne côté client càd CPU de votre machine, le buffer est sur la carte graphique côté GPU donc), il faudra passer par des noms ou identifiants de buffers:
void glGenBuffers(GLsizei n, GLuint * buffers);
En entrée:
GLsizei pour indiquer le nombre d'identifiant de buffer object à générer.
buffers: tableau qui va contenir les différents identifiants générés.
En sortie:
GL_INVALID_VALUE si n est < 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 mais pour les buffers:
void glBindBuffer(GLenum target, GLuint buffer);
En entrée:
target indique le type de cible souhaité pour le buffer: GL_ARRAY_BUFFER.
buffer est le nom/identifiant du buffer objet.
Il reste à demander le transfert des données vers la carte graphique:
void glBufferData(GLenum target, GLsizeiptr size, const GLvoid * data, GLenum usage);
En entrée:
target est toujours la cible pour laquelle le buffer sera utilisée donc toujours GL_ARRAY_BUFFER ici.
size est la taille en octet qui sera occupée en mémoire graphique
data buffer contenant les données (côté client donc là où il y a le CPU / votre code C++) ou null si vous ne voulez pas transmettre de data
usage GL_STATIC_DRAW car le contenu de la mémoire graphique ne va pas évoluer ensuite.
Des erreurs peuvent remonter:
GL_INVALID_ENUM si target n'est pas
valide ou si usage n'est pas une valeur suivante:
GL_STREAM_DRAW, GL_STREAM_READ, GL_STREAM_COPY,
GL_STATIC_DRAW, GL_STATIC_READ, GL_STATIC_COPY,
GL_DYNAMIC_DRAW, GL_DYNAMIC_READ ou GL_DYNAMIC_COPY.
GL_INVALID_VALUE si size est négatif
GL_INVALID_OPERATION si utilisation
du buffer particulier dont l'identifiant/nom est 0
GL_OUT_OF_MEMORY Pas suffisement de
mémoire.
Enfin indiquez que vous n'allez plus utiliser dans l'immédiat la ressource buffer. Pour cela, il suffit d'indiquer un nom/identifiant particulier: le 0.
void glBindBuffer(GLenum target, 0);
A mettre
impérativement, sinon vous allez avoir des surprises (désagréables)
!
Ensuite, tout se passe lors de la phase du rendu de l'image
Il faut refaire un bind sur le buffer
glBindBuffer(GL_ARRAY_BUFFER, bufferId);
Puis indiquer à OpenGL comment est construit le buffer pour pouvoir alimenter correctement vPosition dans le vertex shader.
void glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid * pointer);
void glVertexAttribIPointer(GLuint index, GLint size, GLenum type, GLsizei stride, const GLvoid * pointer);
En entrée
index correspond à l'index de l'attribut du vertex shader qui recevra
ici les positions des vertice
size Le nombre de composants (ici 3 ou 4 par coordonnées)
type: A priori, vous utiliserez du float, donc GL_FLOAT
normalized: laissons le à false pour le moment...
stride: un pas, à calculer en octet ! Permet à OpenGL de connaitre le déplacement à faire pour trouver les positions du vertice suivant.
pointer A voir plutôt comme un offset: le point de départ dans le buffer. A priori 0 si les coordonnées sont au début du buffer.
Vous commencez
à voir que les coordonnées pourraient être suivies d'autres
informations grace au stride et à l'offset...
Oui, les coordonnées
peuvent être composées de 3 voire 4 éléments (4
coordonnées).
Des codes erreurs éventuelles:
GL_INVALID_VALUE si l'index est supérieur ou égal à GL_MAX_VERTEX_ATTRIBS, si size n'est pas une valeur parmis 1, 2, 3, 4 or GL_BGRA. si le type est invalide ou enfin si stride est < 0
- si size est GL_BGRA et type n'est pas positionné à GL_UNSIGNED_BYTE, GL_INT_2_10_10_10_REV ou GL_UNSIGNED_INT_2_10_10_10_REV.
- si type est GL_INT_2_10_10_10_REV ou GL_UNSIGNED_INT_2_10_10_10_REV et que size n'est pas égal à 4 ou GL_BGRA.
- si size est positionné à GL_BGRA et noramlized vaut GL_FALSE.
- si le bind GL_ARRAY_BUFFER est sur un id null et que le pointeur n'est pas NULL.
Effacer les buffer objects
Cette fonction permet d'effacer les buffer objects à partir de leur nom/identifiant:
void glDeleteBuffers( GLsizei n, const GLuint * buffers);
Cette fonction
ignore l'identifiant 0 (réservé par le système) et tous
ceux qui ne correspondent pas à des buffer objects.
En entrée:
n spécifie le nombre de buffer objects à supprimer
buffers est un tableau contenant les idenfiants des buffer objects à supprimer
En sortie, les buffer objects sont supprimés ou GL_INVALID_VALUE est généré si n est négatif.
Compléments
glIsBuffer permet d'indiquer qu'un nom/identifiant correspond à un buffer object
GLboolean glIsBuffer(GLuint buffer);
Exemple de code
Le main dont le role est d'initialiser OpenGL, de permettre le rendu et de créer une instance Triangle qui va dessiner une objet.
D'où la classe Triangle (très basique et à compléter pour une meilleure gestions des erreurs):
La méthode la plus importante étant la méthode draw qui va transmettre à OpenGL la façon de construire le triangle qui passera par un VBO.
et son .h
La classe Triangle utilise donc un VBO dont voici une classe pour le gérer:
- L'allocation dans le constructeur
- Une méthode bind et unbind
- Une méthode initVertexAttribPointer
- Et de quoi libérer la mémoire sur la carte.
Et son include
Puis un zip contenant les codes déjà vus pour gérer les shaders et un fichier makefile.
Il suffit de taper make all pour compiler et linker le tout.