Aller au contenu

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);

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
class Foo
{
    GLuint m_VAO_id;

public:
    Foo()
    {
        glCreateVertexArrays (1, &m_VAO_id);
        glBindVertexArray (m_VAO_id);

        ...
        glBindVertexArray (0);  // par sécurité
    }

    ~Foo()
    {
        glDeleteVertexArrays (1, &m_VAO_id);
    }

    void draw()
    {
        ...
    }
}; // Foo

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
class Foo
{
    GLuint m_VAO_id, m_VBO_id;

public:
    Foo()
    {
        glCreateVertexArrays (1, &m_VAO_id);
        glBindVertexArray (m_VAO_id);

        glGenBuffers (1, &m_VBO_id);
        glBindBuffer (GL_ARRAY_BUFFER, m_VBO_id);

        ...
        glBindVertexArray (0);
    }

    ~Foo()
    {
        glDeleteBuffers (1, &m_VBO_id);
        glDeleteVertexArrays (1, &m_VAO_id);
    }

    void draw()
    {
        ...
    }
}; // Foo

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 sera GL_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 met GL_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éral GL_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 ; un 0 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
class Triangles
{
    GLuint m_VAO_id, m_PosVBO_id, m_ColVBO_id;
    GLint m_vPos_loc, m_vCol_loc;

public:
    Triangles (GLint vPos_loc, GLint vCol_loc)
        : m_vPos_loc {vPos_loc}, m_vCol_loc {vCol_loc}
    {
        // Données
        GLfloat positions[] = {
           -0.7, -0.5, -0.1,
            ...
        };
        GLfloat colors[] = {
            1.0, 0.6, 0.6,
            ...
        };

        // Création du VAO
        glCreateVertexArrays (1, &m_VAO_id);
        glBindVertexArray (m_VAO_id);

        // Création du VBO pour les positions
        glGenBuffers (1, &m_PosVBO_id);
        glBindBuffer (GL_ARRAY_BUFFER, m_PosVBO_id);

        // Copie le buffer dans la mémoire du serveur
        glBufferData (GL_ARRAY_BUFFER, sizeof(positions), positions, GL_STATIC_DRAW);

        // VAA associant les données à la variable vPos du shader, avec l'offset 0
        glVertexAttribPointer (m_vPos_loc, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0);
        glEnableVertexAttribArray (m_vPos_loc);  

        // Création du VBO pour les couleurs
        glGenBuffers (1, &m_ColVBO_id);
        glBindBuffer (GL_ARRAY_BUFFER, m_ColVBO_id);

        // Copie le buffer dans la mémoire du serveur
        glBufferData (GL_ARRAY_BUFFER, sizeof(colors), colors, GL_STATIC_DRAW);

        // VAA associant les données à la variable vCol du shader, avec l'offset 0
        glVertexAttribPointer (m_vCol_loc, 3, GL_FLOAT, GL_FALSE, 0, (void*) 0);
        glEnableVertexAttribArray (m_vCol_loc);

        glBindVertexArray (0);  // désactive le VAO courant m_VAO_id
    }

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
    void draw()
    {
        glBindVertexArray (m_VAO_id);
        glDrawArrays (GL_TRIANGLES, 0, 6);
        glBindVertexArray (0);
    }

Dans le destructeur, on libère les noms du VAO et du VBO :

1
2
3
4
5
6
    ~Triangles()
    {
        glDeleteBuffers (1, &m_ColVBO_id);
        glDeleteBuffers (1, &m_PosVBO_id);
        glDeleteVertexArrays (1, &m_VAO_id);
    }

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
class MyApp
{
    ...
    Triangles* m_triangles = nullptr;

    void initGL()
    {
        ...
        // Création des objets graphiques
        m_triangles = new Triangles {m_vPos_loc, m_vCol_loc};
    }

    void tearGL()
    {
        // Destruction des objets graphiques
        delete m_triangles;
    }

    void displayGL()
    {
        ...
        // Dessins
        m_triangles->draw();
    }


public:
    MyApp()
    {
        ...
        initGL();
    }
    ...
    ~MyApp()
    {
        tearGL();
        ...
    }

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
class Triangles
{
    GLuint m_VAO_id, m_VBO_id;
    GLint m_vPos_loc, m_vCol_loc;

public:
    Triangles (GLint vPos_loc, GLint vCol_loc)
        : m_vPos_loc {vPos_loc}, m_vCol_loc {vCol_loc}
    {
        // Données
        GLfloat positions[] = {
           -0.7, -0.5, -0.1,
            ...
        };
        GLfloat colors[] = {
            1.0, 0.6, 0.6,
            ...
        };

        // Création d'une structure de données à plat
        std::vector<GLfloat> vertices;
        for (int i = 0; i < 6; i++) {
            for (int j = 0; j < 3; j++)
                vertices.push_back (positions[i*3+j]);
            for (int j = 0; j < 3; j++)
                vertices.push_back (colors[i*3+j]);
        }

        // Création du VAO
        glCreateVertexArrays (1, &m_VAO_id);
        glBindVertexArray (m_VAO_id);

        // Création du VBO pour les positions et couleurs
        glGenBuffers (1, &m_VBO_id);
        glBindBuffer (GL_ARRAY_BUFFER, m_VBO_id);

        // Copie le buffer dans la mémoire du serveur
        glBufferData (GL_ARRAY_BUFFER, vertices.size()*sizeof(GLfloat), 
            vertices.data(), GL_STATIC_DRAW);

        // VAA associant les données à la variable vPos du shader, avec l'offset 0
        glVertexAttribPointer (m_vPos_loc, 3, GL_FLOAT, GL_FALSE, 
            6*sizeof(GLfloat), reinterpret_cast<void*>(0*sizeof(GLfloat)));
        glEnableVertexAttribArray (m_vPos_loc);  

        // idem, pour la variable vCol du shader, avec offset 3
        glVertexAttribPointer (m_vCol_loc, 3, GL_FLOAT, GL_FALSE, 
            6*sizeof(GLfloat), reinterpret_cast<void*>(3*sizeof(GLfloat)));
        glEnableVertexAttribArray (m_vCol_loc); 

        glBindVertexArray (0);  // désactive le VAO courant m_VAO_id
    }

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
class WireCube
{
    bool m_is_white;
    GLfloat m_radius;
    GLuint m_VAO_id, m_VBO_id;
    GLint m_vPos_loc, m_vCol_loc;

public:
    WireCube (bool is_white, GLfloat radius, GLint vPos_loc, GLint vCol_loc)
        : m_is_white {is_white}, m_radius {radius},
          m_vPos_loc {vPos_loc}, m_vCol_loc {vCol_loc}
    {
        GLfloat r = m_radius;

        // Positions
        GLfloat positions[] = {
           -r, -r, -r,  // P0                 6 ------- 7
            r, -r, -r,  // P1               / |       / |
           -r,  r, -r,  // P2             /   |     /   |
            r,  r, -r,  // P3           2 ------- 3     |
           -r, -r,  r,  // P4           |     4 --|---- 5
            r, -r,  r,  // P5           |   /     |   /
           -r,  r,  r,  // P6           | /       | /
            r,  r,  r,  // P7           0 ------- 1
        };

        // Indices : positions, couleurs   |   // Couleurs               C1  C2
        GLint indexes[] = {                |   //                        | /
            0, 1,   0, 0,                  |   //                        + --C0
            2, 3,   0, 0,                  |   GLfloat colors_rgb[] = {
            4, 5,   0, 0,                  |       1.0, 0.0, 0.0,  // C0
            6, 7,   0, 0,                  |       0.0, 1.0, 0.0,  // C1
            0, 2,   1, 1,                  |       0.0, 0.0, 1.0   // C2
            1, 3,   1, 1,                  |   };
            4, 6,   1, 1,                  |   GLfloat colors_white[] = {
            5, 7,   1, 1,                  |       1.0, 1.0, 1.0,  // C0
            0, 4,   2, 2,                  |       1.0, 1.0, 1.0,  // C1
            1, 5,   2, 2,                  |       1.0, 1.0, 1.0   // C2
            2, 6,   2, 2,                  |   };
            3, 7,   2, 2                   |   GLfloat *colors = (is_white) ?
        };                                 |       colors_white : colors_rgb;

        // Création d'une structure de données à plat
        std::vector<GLfloat> vertices;
        for (int i = 0; i < 12*4; i+=4) {
            for (int j = 0; j < 3; j++)
                vertices.push_back (positions[indexes[i+0]*3+j]);
            for (int j = 0; j < 3; j++)
                vertices.push_back (colors[indexes[i+2]*3+j]);
            for (int j = 0; j < 3; j++)
                vertices.push_back (positions[indexes[i+1]*3+j]);
            for (int j = 0; j < 3; j++)
                vertices.push_back (colors[indexes[i+3]*3+j]);
        }

        ...

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 :