Programmation Graphique : CM séance 03
6. Core profile
Le profile core impose de déclarer toutes les données dans des buffer objects qui seront stockés dans le serveur (le GPU). Par exemple
- des Vertex Buffer Objects (VBO) pour stocker des sommets (positions, couleurs, etc) ;
- des Element Buffer Objects (EBO) pour stocker des indices de sommets.
De plus, ces buffer objects doivent être liés à une structure qui s'appelle un Vertex-Array Object (VAO), qui doit être créée en premier.
En réalité, ce qui distingue les profiles compatibility et core est que
- dans le profile compatibility il y a déjà un VAO par défaut ;
- dans le profile core, on doit déclarer explicitement au moins un VAO.
Pour utiliser le core profile, on commence par indiquer via des hints la version et le profile que l'on souhaite utiliser :
MyApp()
{
...
// On demande une version spécifique d'OpenGL
glfwWindowHint (GLFW_CONTEXT_VERSION_MAJOR, 3);
glfwWindowHint (GLFW_CONTEXT_VERSION_MINOR, 3);
//glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_COMPAT_PROFILE);
glfwWindowHint (GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
// Création de la fenêtre
m_window = glfwCreateWindow (...);
...
Si on rajoute ces lignes aux exemples du cours précédent (basés sur le profile compatibility) on obtient ... une fenêtre noire.
6.1. VAO
La première étape consiste donc à créer un ou plusieurs VAO.
Un VAO peut être vu comme un espace de noms (un nom OpenL étant un identifiant), c'est-à-dire une liste de noms d'objets à utiliser pour réaliser un dessin.
Typiquement, en C++ avec des classes de dessin Cube
, Cylindre
, etc, chaque
classe aura son propre VAO, qui sera créé dans le constructeur de l'objet, et
libéré dans le destructeur.
Pour créer un ou plusieurs VAOs il convient de réserver un ou
plusieurs noms (des ids) avec glCreateVertexArrays
:
void glCreateVertexArrays (GLsizei n, GLuint *vao_ids);
- on demande de réserver des noms pour
n
VAOs, - qui seront mémorisés dans le tableau
vao_ids
.
Par exemple, pour créer un seul VAO myVAO_id
, on écrira :
GLuint myVAO_id;
glCreateVertexArrays (1, &myVAO_id);
Note
Une autre façon de procéder (utilisée dans le Red Book), qui facilite l'ajout ultérieur de VAOs, est d'écrire :
enum VAO_ids { Foo_id, Bar_id, NumVAOs };
GLuint VAOs[NumVAOs];
glCreateVertexArrays (NumVAOs, VAOs);
L'identifiant du premier VAO sera VAOs[Foo_id]
.
Ensuite il faut rendre un VAO actif, en le reliant au contexte courant
avec glBindVertexArray
:
void glBindVertexArray (GLuint id);
où index
est l'identifiant du VAO. Par exemple :
glBindVertexArray (myVAO_id);
Note
Un seul VAO peut être actif à la fois. Le VAO actif "capturera" tous les buffers objects qui seront ensuite liés. Par la suite, il suffira de rendre actif le VAO pour que tous les buffers objects associés soient également utilisés.
Le fait de rendre actif un VAO, rend automatiquement inactif le VAO précédemment actif. On peut aussi rendre le VAO courant inactif, en invoquant simplement :
glBindVertexArray (0);
Enfin, lorsqu'on n'a plus besoin des VAOs on peut libérer leurs identifiants
en invoquant glDeleteVertexArrays
:
void glDeleteVertexArrays (GLsizei n, const GLuint *ids);
Par exemple :
GLuint myVAO_id;
glDeleteVertexArrays (1, &myVAO_id);
Pour compléter, il existe aussi la fonction glIsVertexArray
pour
tester si un nom est un identifiant de VAO.
Pour résumer, la structure d'une classe de dessin en C++ aura cette allure dans nos exemples :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
6.2. VBO
La seconde étape consiste à mémoriser des sommets (positions, couleurs, etc) dans un Vertex Buffer Object (VBO), qui sera stocké dans la mémoire du serveur une seule fois, puis réutilisé à chaque rendu.
De la même manière que dans la section précédente, on va réserver un ou
plusieurs identifiants de buffers objects (des VBOs, mais ça peut être autre
chose) avec la fonction glGenBuffers
:
void glGenBuffers (GLsizei n, GLuint *buffer_ids);
- on demande de réserver des noms pour
n
buffer objects, - dont les ids seront mémorisés dans le tableau
buffer_ids
.
Par exemple, pour créer un seul VBO myVBO_id
, on écrira :
GLuint myVBO_id;
glGenBuffers (1, &myVBO_id);
On lie ensuite le VBO au VAO courant en invoquant glBindBuffer
:
void glBindBuffer (GLenum target, GLuint buffer_id);
buffer_id
est l'identifiant du buffer object,target
est le type du buffer object demandé.
Il existe une quinzaine de types de buffer objects, dont les plus connus sont :
GL_ARRAY_BUFFER
pour les Vertex Buffer Objects (VBOs), destinés à stocker les données des sommets (position, couleur, etc) ;GL_ELEMENT_ARRAY_BUFFER
pour les Element Buffer Objets (EBOs ou parfois IBOs), qui stockent des indices de sommets, ce qui permet d'utiliser plusieurs fois des sommets dans les dessins.
Note
Dans le VAO courant, il peut y avoir un buffer objet différent lié pour chaque target ; par défaut il n'y en a aucun.
Lorsqu'on rappelle glBindBuffer
sur une target, le précédent buffer object
est délié. Pour délier le buffer object courant on peut appeler la fonction
avec buffer_id
égal à 0
.
Pour compléter, la fonction glIsBuffer
permet de savoir si un nom
est bien un nom de buffer object, et la fonction glDeleteBuffers
permet
de libérer un tableau de noms.
À ce stade, la structure d'une classe de dessin en C++ devient :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
6.3. Copie, VAA et rendu
On peut maintenant copier les données dans la zone mémoire (data store) du
serveur associée au buffer object avec la fonction glBufferData
:
void glBufferData (GLenum target,
GLsizeiptr size,
const void* data,
GLenum usage);
target
est le type du buffer object, pour un VBO ce seraGL_ARRAY_BUFFER
;size
est la taille en octets de la zone mémoire ;data
est l'adresse de base des données qui sont copiées ;usage
est une constante symbolique pour préciser le type de stockage demandé (statique, modifiable, à usage unique ou permanent, en lecture ou écriture). Dans cas général on metGL_STATIC_DRAW
.
Il existe aussi une variante glNamedBufferData
où le premier paramètre
est l'identifiant du buffer object au lieu du target.
Il faut ensuite préciser à l'aide d'un Vertex Attribute Array (VAA)
comment ces données sont organisées, et dans quelle variable de shader elles
seront accessibles, avec la fonction
glVertexAttribPointer
qui prend les paramètres suivants :
GLuint index
: la location de la variable dans le shader, qui recevra les coordonnées ou la couleur d'un sommet ;GLint size
: le nombre de valeurs par sommet ou couleur (entre 1 et 4) dans le tableau ;GLenum type
: le type d'une valeur (en généralGL_FLOAT
) dans le tableau ;GLboolean normalized
:GL_TRUE
s'il faut normaliser les données entre 0.0 et 1.0 ;GLsizei stride
: le nombre d'octets séparant 2 sommets ou couleurs dans le tableau ; un0
indique qu'elles sont consécutives en mémoire ;const void* pointer
: l'adresse de base du tableau ou l'offset mémoire.
Offset
La différence avec l'utilisation du VAA en mode compatibility vue au cours
précédent, est qu'on ne donne plus l'adresse de base des données dans pointer
,
car celle-ci a déjà été passée à dans glBufferData
; à la place on donne
un offset par rapport à l'adresse de base, castée en void*
.
La dernière étape est d'activer le VAA en invoquant glEnableVertexAttribArray
,
puis de désactiver le VAO.
On peut maintenant effectuer un ou plusieurs rendus, à partir des buffer objects liés au VAO :
-
on active le VAO :
glBindVertexArray (VAO_id);
-
on choisit les dessins à effectuer :
glDrawArrays (GL_TRIANGLES, 0, n);
-
enfin on désactive le VAO (par sécurité) :
glBindVertexArray (0);
6.4. Classe Triangles
Voyons ce que cela donne pour afficher deux triangles.
Dans l'exemple fw31-core.cpp
,
on utilise 2 VBO pour stocker des positions et des couleurs :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
|
Les deux derniers paramètres de glVertexAttribPointer
sont :
un stride de 0
(les données sont consécutives) et
un offset à (void*) 0
(les données commencent au début).
La méthode draw
n'a plus qu'à activer le VAO puis appeler glDrawArrays
:
1 2 3 4 5 6 |
|
Dans le destructeur, on libère les noms du VAO et du VBO :
1 2 3 4 5 6 |
|
Au niveau de l'architecture du programme, contrairement au cours précédent
avec le profile compatibility, on n'a plus besoin d'instancier la
classe de dessin Triangles
à chaque rendu. Désormais, on instancie
dynamiquement la classe de dessin à la fin de la méthode initGL
, et on
libère la mémoire dans la méthode tearGL
appelée à la destruction de
MyApp
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
6.5. Données combinées
Dans l'exemple fw32-pos-col.cpp
,
on montre comment combiner les positions et couleurs dans une structure
de données à plat lignes 22 à 27, de manière à utiliser un seul VBO :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
Ce qui change ici est au niveau de l'appel des VAA, lignes 43 et 48,
où l'on donne le stride de 6*sizeof(GLfloat)
, et un offset de 0 pour
les positions et de 3 pour les couleurs.
La méthode draw
est inchangée.
6.6. Classe WireCube
L'exemple fw33-cube.cpp
montre la création
d'une structure de données à plat plus élaborée, à partir d'une liste
de positions et de couleurs uniques :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
|
La suite des opérations (création du VAO, du VBO puis des 2 VAA) est identique à celle de la section précédente.
Pour approfondir :