• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Les Vertex Buffer Objects

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

On attaque cette deuxième partie du tutoriel avec des objets OpenGL qui vont nous être très utiles par la suite : les Vertex Buffer Objects. Ils sont très liés à la carte graphique car ils permettent d'héberger des données directement dessus (vertex, coordonnées de texture, ...).

Vu que nous avons déjà eu l'occasion de manipuler les objets OpenGL avec les textures, vous retrouverez pas mal de notions similaires, et tant mieux parce que ça nous facilite la vie. :p

C'est quoi un VBO ?

Définition

Je vous ai déjà donné une rapide définition des Vertex Buffer Objects (ou VBO) dans l'introduction mais faisons comme si vous n'avez rien vu. :p

Un Vertex Buffer Object qu'est ce c'est ?

C'est un objet OpenGL qui contient des données relatives à un modèle 3D comme les vertices, les coordonnées de texture, les normales (pour les lumières), ... Sa particularité vient du fait que les données qu'il contient se trouvent non pas dans la RAM mais directement dans la carte graphique.

Lorsque vous achetez une carte graphique, je suis sûr que la majorité d'entre vous ne regarde qu'une seule caractéristique : la quantité de mémoire RAM (ou plutôt VRAM pour Vidéo Random Access Memory). Cette mémoire se comporte exactement comme la RAM classique, elle stocke des informations qui seront utilisées plus tard par un programme.

Un Vertex Buffer Object est donc une zone mémoire (buffer) appartenant à la carte graphique dans laquelle on peut stocker des données.

Utilité

Vous vous posez peut-être la question de savoir quel intérêt peut-on avoir à stocker des données directement dans la carte graphique ?

L'intérêt n'est peut-être pas évident pour le moment car nous n'avons pas de projets d'envergure pour le moment, mais sachez que les VBO permettent de gagner beaucoup de temps calculs en évitant à OpenGL des aller et retours inutiles.

En effet, lorsque vous affichez un modèle 3D (comme une maison par exemple), vous devez spécifier ses vertices, ses coordonnées de texture, ... Toutes ces données se trouvent automatiquement dans la RAM. Or, à chaque fois que vous appelez la fonction glDrawArrays() ou glDrawElements() pour afficher votre modèle, OpenGL va chercher toutes les données correspondantes dans la RAM pour les transférer dans la mémoire graphique pour qu'ensuite seulement il puisse travailler avec :

Image utilisateur

Je pense que vous aurez compris que ces transferts ralentissent pas mal le temps de calcul, surtout quand vous transférez des centaines de milliers de données 60 fois par seconde !

Pour éviter de perdre du temps avec ces transferts, OpenGL nous propose d'utiliser les Vertex Buffer Objects pour accéder directement à la mémoire vidéo. Au moment du chargement d'un modèle, on envoie toutes les données au VBO qui va se contenter de les stocker dans la carte graphique. Il n'y a donc plus qu'un seul transfert :

Image utilisateur
Image utilisateur

On économise ainsi le transfert de centaines de milliers de données à chaque affichage, la carte graphique peut travailler plus rapidement.

Attention cependant, on conserve tout de même une copie des données dans la RAM. Elles ne sont plus copiées à chaque affichage mais elles sont quand même là. On doit les converser pour pouvoir effectuer des calculs dessus, comme un déplacement de personnage par exemple.

D'ailleurs, lorsqu'on les mettra à jour il faudra les re-transférer dans le VBO, mais ça ce sera pour la fin du chapitre. :p

Création

Le header

Introduction

On sait maintenant ce que sont les Vertex Buffer Objects, on peut donc passer à leur implémentation. Mais avant tout je voudrais faire une petite comparaison avec les textures.

Souvenez-vous que les textures sont des objets OpenGL eux-aussi et que les fonctions qui permettent de les gérer sont quasiment les mêmes pour tous les objets. Ainsi, on retrouvera des fonctions identiques aux textures pour gérer les VBO comme glGenXXX(), glBindXXX() et glDeleteXXX(). De plus, on retrouvera également les variables de type GLuint qui les représentera et qu'on utilisera notamment dans le verrouillage.

Dans ça chapitre, nous travaillerons sur la classe Cube en intégrant un Vertex Buffer Objet pour l'afficher. Nous ferons de même par la suite pour la classe Caisse en prenant en compte l'héritage C++. ;)

Nouvel attribut

Pour commencer, nous allons ajouter à notre liste d'attributs une nouvelle variable de type GLuint que nous appellerons m_vboID et qui représentera l'identifiant du VBO. Nous en aurons besoin pour sa configuration et son utilisation.

#ifndef DEF_CUBE
#define DEF_CUBE


// Includes 

....


// Classe Cube

class Cube
{
    public:

    Cube(float taille, std::string const vertexShader, std::string const fragmentShader);
    ~Cube();

    void afficher(glm::mat4 &projection, glm::mat4 &modelview);


    protected:

    Shader m_shader;
    float m_vertices[108];
    float m_couleurs[108];

    GLuint m_vboID;
};

#endif

Comme tout nouvel attribut, il faut penser à l'initialiser dans le constructeur. Nous lui donnerons la valeur 0 vu qu'il est vide pour le moment :

Cube::Cube(float taille, std::string const vertexShader, std::string const fragmentShader) : m_shader(vertexShader, fragmentShader), m_vboID(0)
{
    // Initialisation

    ....
}

La méthode charger()

Génération de l'identifiant

Nous n'allons pas gérer la création du VBO dans le constructeur car cela poserait problème avec l'héritage dans les classes filles. A la place, nous allons créer une méthode à part que nous appellerons simplement charger() comme pour les textures et le shader. Son prototype ne sera pas compliqué vu qu'il n'y aura aucun paramètre à gérer :

void charger();

Ne l'appelez pas chargerVBO() car nous mettrons autre chose à l'intérieur dans le chapitre suivant. ;)

Pour en revenir à notre nouvel attribut, rappelez-vous que les identifiants (ou ID) sont créés grâce à la fonction glGenXXX() et représentent l'objet OpenGL auquel ils sont rattachés. Pour créer celui des textures par exemple, nous utilisions la fonction glGenTextures().

Pour les VBO, nous utiliserons la même fonction sauf que l'on remplacera le mot Texture par Buffer (ou zone mémoire). La fonction à utiliser devient donc glGenBuffers() :

GLuint glGenBuffers(GLsizei number, GLuint *buffers);
  • number : Le nombre d'ID à initialiser. Nous lui donnerons toujours la valeur 1 comme pour les textures

  • buffers : Un tableau de type GLuint. On peut également mettre l'adresse d'une variable GLuint pour n'initialiser qu'un seul ID

Donc pour générer un nouvel ID, nous devons appeler cette fonction en donnant en paramètre 1 (pour n'en créer qu'un seul) ainsi que l'adresse de l'attribut m_vboID pour récupérer la valeur retournée :

void Cube::charger()
{
    // Génération de l'ID

    glGenBuffers(1, &m_vboID);
}
Le verrouillage

Si je vous parle de la notion de verrouillage, vous vous souvenez de ce que ce que ça signifie ? ;)

A chaque fois que l'on veut configurer ou utiliser un objet OpenGL il faut le verrouiller car OpenGL a justement besoin de savoir sur quelle chose il doit travailler.
Pour configurer les textures par exemple, il fallait les verrouiller avant de leur donner les pixels d'une image. Si nous ne l'avions pas fait, nous aurions envoyé les pixels on ne sait où dans la mémoire.

Avec les VBO c'est la même chose, avant de les configurer ou de les utiliser il faut les verrouiller.

La fonction permettant de faire cette opération s'appelle glBindXXX(), avec les VBO ce sera glBindBuffer().

void glBindBuffer(GLenum target, GLuint buffer);
  • target : type de l'objet que l'on veut verrouiller. Comme d'habitude, à chaque fois que l'on verra ce paramètre on lui donnera toujours la même valeur, et dans notre cas ce sera GL_ARRAY_BUFFER

  • buffer : ID qui représente le VBO. On lui donnera la valeur de l'ID et pas son adresse cette fois. ;)

On appelle donc cette fonction en donnant en paramètre GL_ARRAY_BUFFER pour target et l'attribut m_vboID pour buffer :

void Cube::charger()
{
    // Génération de l'ID

    glGenBuffers(1, &m_vboID);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);
}

Je profite également de cette partie pour vous montrer le déverrouillage d'objet. Vous savez qu'une fois que nous n'utilisons plus nos objets il faut les déverrouiller, pour éviter d'écrire par erreur dedans par exemple.

Pour déverrouiller un VBO, il suffit d'appeler la fonction glBindbuffer() avec un ID de 0 (valeur nulle).

void Cube::charger()
{
    // Génération de l'ID

    glGenBuffers(1, &m_vboID);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);



        // Configuration 

        ....



    // Déverrouillage de l'objet

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}
Allocation de la mémoire vidéo (Partie 1/2)

Maintenant que nous avons un VBO créé et verrouillé, nous allons pouvoir lui transférer les données stockées dans la RAM. Les transferts seront un peu déroutants car nous ne transférons pas directement des float ou des unsigned int mais des bytes.

Mais avant cela, il va falloir définir une zone mémoire à l'intérieur-même de la carte graphique. Les VBO se comportent un peu comme l'allocation dynamique, il faut allouer dynamiquement de la place dans la mémoire en fonction des données à transférer.

Pour allouer de la mémoire normale (RAM), nous devons utiliser le mot-clef new[] avec la taille désirée. Dans notre cas, il va falloir passer par une fonction dédiée qui s'appelle glBufferData() :

glBufferData(GLenum target, GLsizeiptr size, const GLvoid *data, GLenum  usage)
  • target : On ne le présente plus, on lui donnera la valeur GL_BUFFER_DATA

  • size : C'est la taille mémoire à allouer (en bytes), il faut prendre en compte TOUTES les données à envoyer (vertices, coordonnées de texture, ...) correspondant à un modèle 3D

  • data : Ce sont les données à transférer. On n'enverra rien du tout pour le moment car on ne peut pas envoyer plusieurs tableaux en un seul paramètre, nous lui affecterons la valeur 0

  • usage : Permet de définir la fréquence de mise à jour des données

Le deuxième et le quatrième paramètres sont les plus importants, nous allons passer un peu plus de temps dessus.

Le paramètre size est un peu particulier, il correspond à la taille de la mémoire à allouer en bytes (ou octets). Pourquoi en bytes ? Parce que les variables primitives ne font pas forcément la même taille dans la RAM que dans la VRAM, une variable de type float par exemple peut prendre plus de place dans l'une et moins dans l'autre.

Pour transférer des variables correctement, il faut trouver un moyen de communication commun entre les deux mémoires, et ce moyen c'est le byte.
Il nous faut donc demander à la carte graphique une zone mémoire exprimée en bytes, elle devra être assez grande pour pouvoir contenir toutes les données. Dans la classe Cube, nous avons un tableau pour les vertices et un autre pour les couleurs. La zone devra donc faire la taille de ces deux-la.

Pour avoir la taille d'un tableau, il suffit de multiplier le nombre de ses cases par la taille en bytes du type de variable utilisée. Je vous rappelle que la fonction permettant d'avoir la taille d'un type de variable s'appelle sizeof(type).

Pour calculer la taille des vertices par exemple, nous savons que nous en avons 108 donc nous faisons 108 * sizeof(float) :

// Taille du tableau de vertices

int tailleVerticesBytes = 108 * sizeof(float);

Pour le tableau de couleurs, on fait exactement le même calcul. Nous avons 108 cases donc nous faisons 108 * sizeof(float) :

// Taille du tableau de couleurs

int tailleCouleursBytes = 108 * sizeof(float);

La taille de la zone mémoire à allouer dans la carte graphique vaudra donc tailleVerticesBytes + tailleCouleursBytes. :)

De nouveaux attributs

Je profite de cette partie pour faire une petite parenthèse. Nous aurons besoin des attributs précédents plusieurs fois dans la classe Cube (et ses filles). Il vaut donc mieux créer des attributs les représentant plutôt que de les redéclarer à la main à chaque fois.

Rajoutons donc deux nouvelles variables m_tailleVerticesBytes et m_tailleCouleursBytes dans le header :

#ifndef DEF_CUBE
#define DEF_CUBE


// Includes 

....


// Classe Cube

class Cube
{
    public:

    Cube(float taille, std::string const vertexShader, std::string const fragmentShader);
    ~Cube();

    void afficher(glm::mat4 &projection, glm::mat4 &modelview);


    protected:

    Shader m_shader;
    float m_vertices[108];
    float m_couleurs[108];
 
    GLuint m_vboID;
    int m_tailleVerticesBytes;
    int m_tailleCouleursBytes;
};

#endif

Initialisons-les ensuite dans le constructeur en leur donnant la taille des vertices et des couleurs en bytes :

Cube::Cube(float taille, std::string const vertexShader, std::string const fragmentShader) : m_shader(vertexShader, fragmentShader), m_vboID(0),
                                                                                             m_tailleVerticesBytes(108 * sizeof(float)),
                                                                                             m_tailleCouleursBytes(108 * sizeof(float))
{
    // Initialisation

    ....
}

Le paramètre size de la fonction glBufferData() prendra donc la valeur m_tailleVerticesBytes + m_tailleCouleursBytes.

Allocation de la mémoire vidéo (Partie 2/2)

On ferme la parenthèse et on revient à l'étude des autres paramètres. Le prochain sur la liste est usage et il sera plus rapide à comprendre. :p

Il permet à OpenGL de savoir si les données que nous stockerons dans le VBO seront mises à jour rarement, fréquemment ou tout le temps.

Par exemple, un personnage qui bouge devra quasiment tout le temps mettre à jour ses vertices, une caisse en revanche ne devra jamais le faire. OpenGL à besoin de connaitre cette fréquence de mise à jour, c'est pour ça qu'il nous donne le paramètre usage.

En théorie, il existe 9 valeurs possibles pour ce paramètre mais nous n'en retiendrons que 3 :

  • GL_STATIC_DRAW : pour les données très peu mises à jour

  • GL_DYNAMIC_DRAW : pour les données mises à jour fréquemment (plusieurs fois par seconde mais pas à chaque frame)

  • GL_STREAM_DRAW : pour les données mises à jour tout le temps (A chaque frame cette fois-ci)

Vu que le cube ne bouge pas, nous mettrons la valeur GL_STATIC_DRAW. ;)

En définitif, avec tous les paramètres que l'on vient de voir, nous appellerons la fonction glBufferData() de cette façon :

void Cube::charger()
{
    // Génération de l'ID

    glGenBuffers(1, &m_vboID);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


        // Allocation de la mémoire

        glBufferData(GL_ARRAY_BUFFER, m_tailleVerticesBytes + m_tailleCouleursBytes, 0, GL_STATIC_DRAW);


    // Déverrouillage de l'objet

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

Avec ça, on vient de communiquer directement avec la carte graphique pour lui demander si elle pouvait nous réserver un petit espace mémoire. ;)

Transfert des données

La fonction glBufferData() nous a permis d'allouer un espace mémoire pour y stocker nos données, et vous avez vu que nous n'avons transféré aucune donnée même si elle nous le proposait. Nous ne l'avons pas fait car il est impossible d'envoyer plusieurs tableaux dans un seul paramètre.

Pour transférer les données, nous allons utiliser une autre fonction OpenGL dont le nom ressemble étonnamment à celui de la fonction précédente : glBufferSubData().

glBufferSubData(GLenum target, GLintptr offset, GLsizeiptr size, const GLvoid *data);
  • target : Toujours le même, on lui donnera la valeur GL_BUFFER_DATA

  • offset : Case en mémoire où on va commencer le transfert dans la VRAM

  • size : La taille des données à copier (en bytes)

  • data : Les données à copier, par exemple le tableau de vertices

Les paramètres importants ici sont l'offset et le size.

Je commence par le paramètre size, il correspond à la taille (en bytes) des données à envoyer. Si nous envoyons le tableau de vertices, nous donnerons la taille du tableau que nous avons calculée juste avant. Même chose pour le tableau de couleurs.

Le paramètre offset correspond quant à lui à la case mémoire dans laquelle va commencer la copie. Pour le transfert des vertices, ce paramètre sera de 0 vu que l'on commence à copier au début de la zone mémoire :

Image utilisateur

En revanche pour le transfert des couleurs, on ne va pas commencer la copie à la case 0 sinon on va écraser les valeurs transférées juste avant :

Image utilisateur

Il faudra commencer la copie à la fin du tableau de vertices, soit à la case tailleVerticesBytes :

Image utilisateur

A partir de cette case, on n'écrase plus rien, on copie au bon endroit.
D'ailleurs, offset signifie décalage en français, on renseigne un décalage dans la plage mémoire. ;)

Au final, pour envoyer toutes nos données à la carte graphique nous devons appeler la fonction glBindSubData() deux fois en faisant attention à copier le bon volume de données au bon endroit.
Le premier transfert s'occupera de copier le tableau de vertices dans la mémoire vidéo à partir de la case 0 :

// Transfert des vertices

glBufferSubData(GL_ARRAY_BUFFER, 0, m_tailleVerticesBytes, m_vertices);

Le second transfert s'occupera de copier le tableau de couleurs dans la mémoire vidéo juste après les vertices, soit à partir de la case tailleVerticesBytes :

// Transfert des couleurs

glBufferSubData(GL_ARRAY_BUFFER, m_tailleVerticesBytes, m_tailleCouleursBytes, m_couleurs);

On fait un petit récapitulatif de la création d'un VBO :

void Cube::charger()
{
    // Génération de l'ID

    glGenBuffers(1, &m_vboID);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


        // Allocation de la mémoire vidéo

        glBufferData(GL_ARRAY_BUFFER, m_tailleVerticesBytes + m_tailleCouleursBytes, 0, GL_STATIC_DRAW);


        // Transfert des données

        glBufferSubData(GL_ARRAY_BUFFER, 0, m_tailleVerticesBytes, m_vertices);
        glBufferSubData(GL_ARRAY_BUFFER, m_tailleVerticesBytes, m_tailleCouleursBytes, m_couleurs);


    // Déverrouillage de l'objet

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}
Éviter les fuites de mémoire

Si vous vous souvenez de ce que l'on a vu sur le changement de texture, vous vous souviendrez que nous avons ajouté une petite ligne de code au début de la méthode charger(). En effet, je vous avais parlé de ce qui se passait si on appelait deux fois cette méthode sur un même objet : cela entrainait une fuite de mémoire car le premier chargement était perdu dans la carte graphique.

Avec les VBO, nous avons le même problème : si on en charge un deux fois alors le premier chargement effectué sera perdu et les ressources seront considérées comme étant "toujours utilisées". Pour éviter cette fuite de mémoire, nous allons faire la même chose que les textures en vérifiant d'une part si le VBO a déjà été chargé, puis en le en détruisant si c'est le cas. Le tout au début de la méthode charger() avant l'initialisation.

La fonction permettant de savoir si un VBO a déjà été chargé s'appelle glIsBuffer() :

GLboolean glIsBuffer(GLuint buffer);

Elle ne prend en paramètre que l'identifiant à vérifier et renvoie la valeur GL_TRUE si le VBO a déjà été chargé ou GL_FALSE s'il ne l'a pas été.

La fonction de destruction quant à elle s'appelle glDeleteBuffers() :

void glDeleteBuffers(GLsizei number, const GLuint *buffers);
  • number : Le nombre d'ID à initialiser. Nous lui donnerons la valeur 1

  • buffers : Un tableau de type GLuint ou l'adresse d'une variable GLuint. Nous lui donnerons l'ID du VBO à détruire.

Nous appellerons ces deux fonctions dans un bloc if au début de la méthode charger() :

void Cube::charger()
{
    // Destruction d'un éventuel ancien VBO

    if(glIsBuffer(m_vboID) == GL_TRUE)
        glDeleteBuffers(1, &m_vboID);


    // Génération de l'ID

    glGenBuffers(1, &m_vboID);

    
    ....
}

Ce qui donne le code source définitif :

void Cube::charger()
{
    // Destruction d'un éventuel ancien VBO

    if(glIsBuffer(m_vboID) == GL_TRUE)
        glDeleteBuffers(1, &m_vboID);


    // Génération de l'ID

    glGenBuffers(1, &m_vboID);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


        // Allocation de la mémoire vidéo

        glBufferData(GL_ARRAY_BUFFER, m_tailleVerticesBytes + m_tailleCouleursBytes, 0, GL_STATIC_DRAW);


        // Transfert des données

        glBufferSubData(GL_ARRAY_BUFFER, 0, m_tailleVerticesBytes, m_vertices);
        glBufferSubData(GL_ARRAY_BUFFER, m_tailleVerticesBytes, m_tailleCouleursBytes, m_couleurs);


    // Déverrouillage de l'objet

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

Notre premier VBO est prêt à l'emploi. :D

Le destructeur

Encore une fois, tout comme les textures, les VBO doivent être détruits lorsque nous n'en avons plus besoin. Vu que nous sommes dans une classe, nous devons gérer cette destruction dans la méthode prévue pour ça : le destructeur.

Cette tâche se fait avec la fonction glDeleteBuffers() que nous avons vue précédemment. Nous l’appellerons dans le destructeur en lui donnant l'adresse de l'attribut m_vboID :

Cube::~Cube()
{
    // Destruction du VBO

    glDeleteBuffers(1, &m_vboID);
}

Utilisation

Utilisation d'un VBO

Maintenant que nous avons un VBO créé et initialisé, on ne va pas se priver du plaisir de l'utiliser dans nos programmes. :)

Pour utiliser un VBO on fait exactement la même chose qu'avec les textures. D'ailleurs, vous vous souvenez de ce qu'on doit faire pour envoyer une texture à OpenGL ?

....

On la verrouille !

On va faire exactement la même chose avec les VBO, pour les utiliser on va les verrouiller. Comme tous les objets OpenGL j'ai envie de dire. :p

Ok mais euh ... On le verrouille où ?

Ah très bonne question. ^^

Jusqu'à ce chapitre, pour afficher un modèle 3D nous devions envoyer les données à l'aide de la fonction glVertexAttribPointer() puis nous affichions le tout avec la fonction glDrawArrays(). Nous continuerons à procéder ainsi sauf que nous ajouterons le verrouillage du VBO juste avant la fonction glVertexAttribPointer().

En effet même si les données se trouvent dans la mémoire vidéo, OpenGL ne sait pas où elles se situent exactement, c'est à nous de lui dire. Pour ça, on va toujours utiliser la fonction glVertexAttribPointer() sauf qu'on va modifier son dernier paramètre. Je vous redonne son prototype pour que vous visualisiez le paramètre en question :

void glVertexAttribPointer(GLuint index, GLuint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);

Le paramètre pointer permet à OpenGL de savoir où se trouvent nos données (vertices et toute la clique je le rappelle). Avant nous donnions directement les tableaux à ce paramètre, ils étaient stockés dans la RAM ça ne posait pas de problème. Mais maintenant nous devons lui donner l'adresse des tableaux à l'intérieur de la mémoire vidéo.

Image utilisateur

Lorsque le VBO est verrouillé, OpenGL sait qu'il doit aller chercher les données dans la plage mémoire qui lui est consacrée. A partir de maintenant, le paramètre pointer n'attend plus un tableau mais un offset.

Offset ça ne vous rappelle pas un paramètre ? C'est celui de la fonction glBufferSubData(), il représente la case (le décalage en mémoire) où le transfert doit commencer.
Et bien c'est justement ce qu'attend OpenGL maintenant. Nous lui donnerons donc l'offset correspondant aux tableaux de données dans la mémoire vidéo.

Pour les vertices, cet offset sera de 0 car ils se situent au début de la zone mémoire.
Pour les couleurs, cet offset sera égal à l'attribut m_tailleVerticesBytes car elles sont situées juste après les vertices dans la zone mémoire.

L'appel à la fonction glVertexAttribPointer() devient donc :

// Verrouillage du VBO

glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


    // Accès aux vertices dans la mémoire vidéo

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
    glEnableVertexAttribArray(0);


    // Accès aux couleurs dans la mémoire vidéo

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, m_tailleVerticesBytes);
    glEnableVertexAttribArray(1);


// Déverrouillage du VBO

glBindBuffer(GL_ARRAY_BUFFER, 0);

Au niveau de la méthode afficher(), ça donnerait ceci :

void Cube::afficher(glm::mat4 &projection, glm::mat4 &modelview)
{
    // Activation du shader

    glUseProgram(m_shader.getProgramID());


        // Verrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


            // Accès aux vertices dans la mémoire vidéo

            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, 0);
            glEnableVertexAttribArray(0);


            // Accès aux couleurs dans la mémoire vidéo

            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, m_tailleVerticesBytes);
            glEnableVertexAttribArray(1);


        // Déverrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, 0);


        ....


    // Désactivation du shader

    glUseProgram(0);
}

Techniquement, on ne peut pas compiler ce code car le paramètre pointer attend un décalage (représenté par une adresse et non une variable). Or nous, nous lui donnons une variable de type integer. Alors bon, pour le 0 ça passe encore mais pour tailleVerticesBytes notre compilateur va nous râler dessus. :lol:

Pour utiliser les variables tailleXXX en décalage, OpenGL nous demande de les encadrer avec la macro suivante :

// Macro utile au VBO

#ifndef BUFFER_OFFSET

    #define BUFFER_OFFSET(offset) ((char*)NULL + (offset))

#endif

Ce code permet de spécifier un décalage offset en partant de l'adresse NULL (ou 0) qui indique le début du VBO. Le mot-clef char permet simplement de faire le lien avec les bytes, je vous rappelle que les variables de type char sont codées sur 1 byte. ;)

En utilisant cette macro, le compilateur comprend bien qu'on veut lui envoyer une adresse à l'aide d'une variable. Nous encadrons donc le paramètre pointer par BUFFER_OFFSET() :

// Verrouillage du VBO

glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


    // Accès aux vertices dans la mémoire vidéo

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
    glEnableVertexAttribArray(0);


    // Accès aux couleurs dans la mémoire vidéo

    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(m_tailleVerticesBytes));
    glEnableVertexAttribArray(1);


// Déverrouillage du VBO

glBindBuffer(GL_ARRAY_BUFFER, 0);

Petit point important avant de compiler tout ça, faites attention à bien appeler la méthode charger() au moment de créer votre cube. Si vous ne le faites pas, votre application va joliment planter devant vos yeux ébahies. ^^

// Déclaration d'un objet Cube

Cube cube(2.0, "Shaders/couleur3D.vert", "Shaders/couleur3D.frag");
cube.charger();

Cette fois, vous pouvez compiler tranquillement.

Image utilisateur

Vous avez le même rendu qu'avant sauf que maintenant OpenGL ne fait plus d'aller-retours pour chercher les données dans la RAM. Tout se trouve déjà dans la carte graphique et ça lui fait gagner du temps. :)

Utilisation de plusieurs VBO

On termine cette sous-partie avec une petite problématique ? Comment je fais si je veux utiliser plusieurs VBO pour afficher quelque chose ?

En temps normal, je vous dirais qu'il est inutile d'en utiliser plusieurs pour afficher un unique modèle, un seul fait parfaitement l'affaire. Cependant, il existe des cas où il est utile de séparer les données dans plusieurs VBO, en général ce sera pour faire de l'optimisation.

Si ça vous arrive un jour, sachez qu'il suffit simplement de verrouiller le premier VBO avant votre premier envoi de données, puis de verrouiller le deuxième avec le deuxième envoi, et ainsi de suite :

// Accès au premier VBO

glBindBuffer(GL_ARRAY_BUFFER, premierVBO);

    // Accès à la mémoire vidéo

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
    glEnableVertexAttribArray(0);


// Déverrouillage du VBO

glBindBuffer(GL_ARRAY_BUFFER, 0);



// Accès au deuxième VBO

glBindBuffer(GL_ARRAY_BUFFER, deuxiemeVBO);

    // Accès à la mémoire vidéo

    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(m_tailleVerticesBytes));
    glEnableVertexAttribArray(2);


// Déverrouillage du VBO

glBindBuffer(GL_ARRAY_BUFFER, 0);

Petit info au passage, si vous enchainez plusieurs verrouillages sur des objets OpenGL de même type (et j'insiste bien dessus) alors vous n'êtes pas obligés de tous les déverrouiller à chaque fois, ils le seront automatiquement. Seul le dernier déverrouillage est important.

On peut donc enlever le déverrouillage du premier objet dans ce cas :

// Accès au premier VBO

glBindBuffer(GL_ARRAY_BUFFER, premierVBO);

    // Accès à la mémoire vidéo

    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
    glEnableVertexAttribArray(0);


// Accès au deuxième VBO

glBindBuffer(GL_ARRAY_BUFFER, deuxiemeVBO);

    // Accès à la mémoire vidéo

    glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(m_tailleVerticesBytes));
    glEnableVertexAttribArray(2);


// Déverrouillage du VBO

glBindBuffer(GL_ARRAY_BUFFER, 0);

Mise à jour des données

La méthode updateVBO()

Pourquoi mettre à jour les 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. :p

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 concrétiser ce qu'on va voir sur ça, nous allons coder une méthode qui nous permettra de mettre à jour nos données. Celle-ci s’appellera updateVBO() :

void updateVBO(void *donnees, int tailleBytes, int decalage);
  • donnees : Un pointeur sur les données à envoyer, comme un tableau par exemple. Il est de type void* car on ne connaitra pas forcement le type de donnée que l'on enverra

  • tailleByes : La taille en bytes des données

  • decalage : Le décalage en mémoire où commencer la copie, nous allons voir ça un peu plus bas

Le code

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().

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 :

void Cube::updateVBO(void *donnees, int tailleBytes, int decalage)
{
    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


        // Récupération de l'adresse du VBO

        void *adresseVBO = glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY);


    // Déverrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

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 :

void Cube::updateVBO(void *donnees, int tailleBytes, int decalage)
{
    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_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; 
        }


    // Déverrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

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() :

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 :

// 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 :

(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 :

(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. 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 :

// 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() :

GLboolean glUnmapBuffer(GLenum target);
  • target : Encore celui-là. :p On lui donnera comme d'hab 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 0 au pointeur pour une double sécurisation :

// Annulation du pointeur

glUnmapBuffer(GL_ARRAY_BUFFER);
adresseVBO = 0;

Si on récapitule tout ça, on a un beau code de mise à jour de VBO :

void Cube::updateVBO(void *donnees, int tailleBytes, int decalage)
{
    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_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 = 0;


    // Déverrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

L'avantage avec cette méthode, c'est qu'elle n'aura pas besoin d'être réécrite dans les classes filles, elle est universelle. :)

D'ailleurs, comme promis au début du chapitre, nous allons maintenant passer à la gestion du VBO dans la classe fille Caisse.

Un autre exemple

Le header

Comme promis, nous nous occupons maintenant de l'implémentation des VBO dans la classe Caisse. Il n'y aura rien de nouveau à apprendre, nous connaissons déjà tout. :p Le seul point auquel il faudra faire attention est le fait qu'elle hérite déjà des attributs de sa classe mère.

Ainsi, nous ne devrons pas redéclarer une variable GLuint pour le VBO car nous héritons naturellement de celui de la classe Cube. Même chose pour la taille du tableau de vertices en bytes.

En revanche, nous devrons déclarer une variable m_tailleCoordTextureBytes car nous en aurons besoin pour définir la taille de la mémoire à allouer. En effet, nous n'enverrons pas des couleurs mais des coordonnées de texture, il faudra donc utiliser une autre variable. Nous devrons par ailleurs redéfinir la méthode charger() pour les envoyer à la place des couleurs.

Au final, on commence par déclarer, dans le header de la classe Caisse, une variable m_tailleCoordTextureBytes de type int :

#ifndef DEF_CAISSE
#define DEF_CAISSE


// Includes

....


// Classe Caisse

class Caisse : public Cube
{
    public:

    Caisse(float taille, std::string const vertexShader, std::string const fragmentShader, std::string const texture);
    ~Caisse();

    void afficher(glm::mat4 &projection, glm::mat4 &modelview);


    private:

    Texture m_texture;
    float m_coordTexture[72];
    int m_tailleCoordTextureBytes;
};

#endif

Une fois déclarée, nous l'initialisons dans le constructeur en lui donnant la taille du tableau de coordonnées de texture. Celui-ci fait 72 cases donc sa taille bytes fera 72 * sizeof(float) :

Caisse::Caisse(float taille, std::string const vertexShader,std::string const fragmentShader, std::string const texture) : Cube(taille, vertexShader, fragmentShader), 
                                                                                                                           m_texture(texture), 
                                                                                                                           m_tailleCoordTextureBytes(72 * sizeof(float))
{
    // Initialisation

    ....
}

La méthode charger()

La méthode charger() ne va pas beaucoup changer par rapport à son parent. En fait, nous avons juste à modifier les attributs utilisés pour définir la taille du VBO. Son prototype sera aussi le même :

void charger();

Pour son implémentation, nous n'allons pas nous prendre la tête et recopier simplement tout le contenu de la méthode charger() précédente. :p Il faudra évidemment faire des petites modifications ensuite mais au moins, nous aurons déjà le plus gros du code.

En copiant ce contenu, on se retrouve avec la méthode suivante :

void Caisse::charger()
{
    // Destruction d'un éventuel ancien VBO

    if(glIsBuffer(m_vboID) == GL_TRUE)
        glDeleteBuffers(1, &m_vboID);


    // Génération de l'ID

    glGenBuffers(1, &m_vboID);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


        // Allocation de la mémoire vidéo

        glBufferData(GL_ARRAY_BUFFER, m_tailleVerticesBytes + m_tailleCouleursBytes, 0, GL_STATIC_DRAW);


        // Transfert des données

        glBufferSubData(GL_ARRAY_BUFFER, 0, m_tailleVerticesBytes, m_vertices);
        glBufferSubData(GL_ARRAY_BUFFER, m_tailleVerticesBytes, m_tailleCouleursBytes, m_couleurs);


    // Déverrouillage de l'objet

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

Ce qu'il faut modifier ici ce sont les appels aux fonctions glBufferData() et glBufferSubData().

Nous savons que la première des deux permet d'allouer une zone mémoire dans la carte graphique pour le VBO. Elle a besoin de connaitre la taille de la mémoire qu'elle doit allouer. Nous devons, ici, lui donner le résultat de l'addition de la taille des vertices et des coordonnées de texture. Nous utiliserons pour cela les attributs :

  • m_tailleVerticesBytes (qui est héritée)

  • m_tailleCoordTextureBytes (que nous avons créée il y a quelques instants)

Les autres paramètres eux ne changent pas :

// Allocation de la mémoire vidéo

glBufferData(GL_ARRAY_BUFFER, m_tailleVerticesBytes + m_tailleCoordTextureBytes, 0, GL_STATIC_DRAW);

La fonction glBufferSubData(), quant à elle, permet de les remplir le VBO avec les données que l'on veut envoyer. Nous l'appelons deux de façon à envoyer les vertices d'une part et les coordonnées de texture de l'autre.

// Transfert des coordonnées de texture

glBufferSubData(GL_ARRAY_BUFFER, m_tailleVerticesBytes, m_tailleCoordTextureBytes, m_coordTexture);

Vu que les coordonnées de texture se trouvent juste après les vertices dans la mémoire, nous devons donc laisser le paramètre offset (le deuxième de la fonction) à la case m_tailleVerticesBytes.

La méthode charger() ressemble au final à ceci :

void Caisse::charger()
{
    // Destruction d'un éventuel ancien VBO

    if(glIsBuffer(m_vboID) == GL_TRUE)
        glDeleteBuffers(1, &m_vboID);


    // Génération de l'ID

    glGenBuffers(1, &m_vboID);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


        // Allocation de la mémoire vidéo

        glBufferData(GL_ARRAY_BUFFER, m_tailleVerticesBytes + m_tailleCoordTextureBytes, 0, GL_STATIC_DRAW);


        // Transfert des données

        glBufferSubData(GL_ARRAY_BUFFER, 0, m_tailleVerticesBytes, m_vertices);
        glBufferSubData(GL_ARRAY_BUFFER, m_tailleVerticesBytes, m_tailleCoordTextureBytes, m_coordTexture);


    // Déverrouillage de l'objet

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

N'oubliez pas d'appeler cette méthode lorsque vous souhaitez créer une caisse :

// Objet Caisse

Caisse caisse(2.0, "Shaders/texture.vert", "Shaders/texture.frag", "Textures/Caisse2.jpg");
caisse.charger();

La méthode afficher()

La méthode afficher() est plus simple à modifier que la précédente car il suffit juste de verrouiller le VBO et utiliser la macro BUFFER_OFFSET().

On commence donc par encadrer le code relatif aux tableaux Vertex Attrib par la fonction de verrouillage glBindBuffer() :

void Caisse::afficher(glm::mat4 &projection, glm::mat4 &modelview)
{
    // Activation du shader

    glUseProgram(m_shader.getProgramID());


        // Verrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


            // Accès aux vertices dans la mémoire vidéo

            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, m_vertices);
            glEnableVertexAttribArray(0);


            // Envoi des coordonnées de texture

            glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, m_coordTexture);
            glEnableVertexAttribArray(2);


        // Déverrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, 0);


        ....


    // Désactivation du shader

    glUseProgram(0);
}

Ensuite, on modifie le paramètre pointer pour utiliser la macro avec le bon décalage. Pour l'envoi des vertices, on lui donnera la valeur 0 vu qu'ils se trouvent au début de la zone mémoire :

// Accès aux vertices dans la mémoire vidéo

glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
glEnableVertexAttribArray(0);

Les coordonnées de texture, quant à elles, sont situées après les vertices dans la mémoire. On peut représenter cette situation par un schéma :

Image utilisateur

Il faut donc utiliser la case "tailleVerticesBytes" représentée par l'attribut m_tailleVerticesBytes afin d'accéder aux coordonnées de texture. C'est donc cet attribut qu'il faut donner à la macro pour trouver le début des coordonnées de texture :

// Accès aux coordonnées de texture dans la mémoire vidéo

glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(m_tailleVerticesBytes));
glEnableVertexAttribArray(2);

Avec ces modifications, la méthode afficher() devient :

void Caisse::afficher(glm::mat4 &projection, glm::mat4 &modelview)
{
    // Activation du shader

    glUseProgram(m_shader.getProgramID());


        // Verrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, m_vboID);


            // Envoi des vertices

            glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(0));
            glEnableVertexAttribArray(0);


            // Envoi des coordonnées de texture

            glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(m_tailleVerticesBytes));
            glEnableVertexAttribArray(2);


        // Déverrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, 0);


        // Envoi des matrices

        glUniformMatrix4fv(glGetUniformLocation(m_shader.getProgramID(), "projection"), 1, GL_FALSE, value_ptr(projection));
        glUniformMatrix4fv(glGetUniformLocation(m_shader.getProgramID(), "modelview"), 1, GL_FALSE, value_ptr(modelview));


        // Verrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, m_texture.getID());


        // Rendu

        glDrawArrays(GL_TRIANGLES, 0, 36);


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


        // Désactivation des tableaux

        glDisableVertexAttribArray(2);
        glDisableVertexAttribArray(0);


    // Désactivation du shader

    glUseProgram(0);
}

Vous pouvez compiler pour admirer votre caisse chargée directement depuis votre carte graphique. ;)

Image utilisateur

Télécharger : Code Source C++ des Vertex Buffer Objects

Dans ce chapitre, nous avons vu ce que sont les VBO et la manière dont ils fonctionnent. Ils permettent d'augmenter la vitesse de nos applications en évitant à OpenGL des transferts de données inutiles. Ils sont très utilisés dans toutes les applications un tant soit peu développées, ce qui en font donc des incontournables dans la programmation 3D. ;)

A partir de maintenant, nous utiliserons toujours les VBO dans nos codes sources. Après tout, la deuxième partie du tuto a pour but de nous aider à maitriser notre carte graphique. L'utilisation directe de la mémoire vidéo en est la première étape. ^^

Exemple de certificat de réussite
Exemple de certificat de réussite