Ce cours est visible gratuitement en ligne.

J'ai tout compris !
Développez vos applications 3D avec OpenGL 3.3

Développez vos applications 3D avec OpenGL 3.3

Mis à jour le vendredi 21 juin 2013
  • Moyen

La troisième dimension (Partie 2/2)

Dans cette deuxième partie, nous allons enfin faire ce que vous attendez tous : de la 3D. :D Cette partie sera plus facile que la première, il y aura de nouvelles notions à apprendre mais rien de bien compliqué. Encore une fois, si vous n'avez pas tout compris jusque là, je vous conseille de relire à tête reposée les chapitres précédents.

La caméra

Introduction

Dans le chapitre précédent, nous avons appris à faire interagir les matrices avec OpenGL et nous avons par la même occasion créé la matrice de projection. La bonne nouvelle, c'est que l'on connait déjà presque tout pour faire de la 3D. En effet, la troisième dimension est déjà implémentée dans nos programmes, nous l'avons vu en ajoutant la coordonnée Z à nos vertices. Seulement, nous sommes mal placés pour voir ce rendu en relief. Pour régler ce problème, il va falloir placer une chose indispensable à OpenGL : la caméra.

Eh oui, :p OpenGL fonctionne avec une caméra, exactement comme les films. Il faut donc placer une caméra qui va fixer un point pour que le spectateur (ou le joueur dans notre cas) puisse voir la scène.

Dans les versions précédentes d'OpenGL, la caméra était gérée par la même librairie que celle qui gérait la projection : la librairie GLU. Mais comme vous vous en douter, nous allons utiliser GLM pour la remplacer. :)

La méthode lookAt

L'ancienne fonction

Avant d'utiliser la librairie GLM, nous allons étudier l'ancienne fonction GLU qui gérait la caméra. Celle-ci permettait de placer la caméra au niveau d'un point dans l'espace. Ce point de vue nous permet de voir une scène en 3 dimensions au lieu des 2 dimensions dont nous avons l'habitude depuis le début du tuto.

La fonction en question s'appelle gluLookAt() et voici son prototype :

void gluLookAt(double eyeX, double eyeY, double eyeZ, double centerX, double centerY, double centerZ, double upX, double upY, double upZ);

Hola y'a trop de paramètres. :o

Si on les prend à part oui, mais vous remarquerez que les noms se ressemblent plus ou moins. En fait, il n'y a que 3 paramètres. Si on les prend par groupe de 3, on se retrouve avec 3 vecteurs bien distincts :

  • Le vecteur eye (œil) qui est un vecteur permetant de placer la caméra.

  • La vecteur center (centre) qui est le point que la caméra doit fixer. Il y a 3 coordonnées, le point se trouve donc dans un espace 3D.

  • Le vecteur axe qui est la verticale du repère.

Petite précision pour le dernier vecteur. En théorie, l'axe vertical est l'axe Y mais dans pas mal de jeux-vidéo on prend souvent l'axe Z. Personnellement, je fais de la résistance et je préfère utiliser l'axe Y comme axe vertical comme on nous l'a toujours appris depuis le collège. ;)

La nouvelle méthode

Pour remplacer cette ancienne fonction, nous allons utiliser une méthode de la librairie GLM qui s'appelle lookAt(). Son prototype est un peu plus compact que la fonction GLU :

mat4 lookAt(vec3 eye, vec3 center, vec3 up);
  • eye : vecteur permettant de placer la caméra

  • center : vecteur permettant d'indiquer le point fixé

  • up : vecteur représentant la verticale du repère

Vous remarquez que la méthode prend bien les 9 paramètres de gluLookAt(), elle les place juste dans 3 objets de type vec3. ;)

Par ailleurs, elle renvoie une matrice toute neuve, elle ne modifie donc pas le contenu d'une matrice existante comme le font les méthodes de transformations.

Utilisation

Grâce à cette méthode, nous pouvons tout de même utiliser notre caméra.

On va d'ailleurs faire un petit test dès maintenant. Vous vous souvenez des deux triangles du chapitre précédent ? On va voir ce que ça donne si on adopte un "point de vue" en 3 dimensions. Bon je vous préviens, avec des triangles 2D ça va être moche mais ce sera déjà notre premier pas dans la 3D. ;)

Nous allons placer notre caméra au point de coordonnées (1, 1, 1) et celle-ci fixera le centre du repère, donc le point de coordonnées (0, 0, 0). Enfin, nous utiliserons l'axe Y comme axe vertical (vous pouvez utiliser celui que vous voulez). L'appel à la méthode lookAt() sera donc :

// Placement de la caméra

modelview = lookAt(vec3(1, 1, 1), vec3(0, 0, 0), vec3(0, 1, 0));

Nous ajoutons cette ligne de code juste après avoir ré-initialisé la matrice modelview :

// Boucle principale

while(!terminer)
{
    /* Gestion des évènements .... */


    // Nettoyage de la fenêtre

    glClear(GL_COLOR_BUFFER_BIT);


    // Ré-initialisation de la matrice et placement de la caméra

    modelview = mat4(1.0);
    modelview = lookAt(vec3(1, 1, 1), vec3(0, 0, 0), vec3(0, 1, 0));


    /* Rendu ... */
}

D'ailleurs, vous pouvez maintenant supprimer la ligne qui ré-initialise la matrice modelview avec les valeurs d'une matrice d'identité car la méthode lookAt() écrase complétement son ancien contenu et fait donc office de ré-initialisation. La ligne est donc inutile. :)

// Boucle principale

while(!terminer)
{
    /* Gestion des évènements .... */


    // Nettoyage de la fenêtre

    glClear(GL_COLOR_BUFFER_BIT);


    // Placement de la caméra

    modelview = lookAt(vec3(1, 1, 1), vec3(0, 0, 0), vec3(0, 1, 0));


    /* Rendu ... */
}

Voici ce que vous devriez obtenir :

Image utilisateur

Bon je vous avais prévenu c'est laid. :p Et pourtant on est bien en 3D :

Image utilisateur

Avec des triangles 2D on n'ira pas très loin mais ça va vite changer. Commençons enfin la partie la plus intéressante. :diable:

Affichage d'un Cube

Introduction

Cette fois ça y est, enfin de la 3D ! :D

La première bonne nouvelle est que l'on va se débarrasser enfin du repère que l'on a utilisé jusqu'à maintenant :

Image utilisateur

Nous allons désormais utiliser celui-ci :

Image utilisateur

Nos polygones ne seront plus limités à des coordonnées comprises entre 0 et 1.

Concrètement, qu'est-ce qu'il faut pour passer à la 3D ?

Vous ne vous en êtes peut-être pas rendu compte, mais depuis le début du tutoriel, vous avez déjà appris pas mal de choses. Petit à petit, vous avez appris tout ce qui est nécessaire pour commencer la programmation 3D :

  • Un shader

  • Des matrices

  • La projection

  • Une caméra

En réunissant intelligemment tout ça, on peut intégrer une troisième dimension à nos programmes. ;)

Affichage d'un cube

La partie théorique

On va commencer par un exercice simple qui réunira tout ce que l'on connait déjà ainsi que ce que l'on va voir maintenant. A la fin, on sera en mesure d'afficher notre premier modèle 3D : un cube (en couleur s'il vous plaît ;) ).

Avant de s'attaquer à la programmation, il est essentiel de faire un peu de théorie en voyant de quoi est composé un cube. Il suffit d'ouvrir un manuel de géométrie pour y lire une des propriétés principales du cube : "un cube est composé de 8 sommets".

Image utilisateur

Les chiffres en parenthèses représentent les coordonnées de chaque sommet. Une arrête mesure donc 2 unités.

Hum intéressant, on retrouve le mot sommet ( = vertex). Il y a 8 sommets, nous aurons donc besoin de 8 vertices :

Image utilisateur

Bien évidemment, il faudra les dédoubler pour afficher toutes nos faces comme nous le faisions avec le losange par exemple. Nous allons les étudier ensemble une par une en expliquant bien les étapes nécessaires.

D'ailleurs en parlant de ça, dans les anciennes versions d'OpenGL, on utilisait une primitive spécifique pour afficher un carré (comme GL_TRIANGLES pour les triangles) que l'on utilisait avec la fonction glDrawArrays(). Cependant, cette primitive a également été supprimée au même titre que les fonctions lentes vu que les cartes graphiques ne savent gérer nativement que des triangles.

Il existe heureusement une petite combine pour afficher des carrés sans cette primitive : il suffit de coller deux triangles rectangles entre eux :

Image utilisateur

Ne vous inquiétez pas, ce n'est pas plus lent à l'affichage même si on affiche deux choses au lieu d'une. Pour vous dire, que ce soit des sphères, des cubes ou des personnages, absolument tous les modèles 3D ne sont composés uniquement que de triangles. ;)

La première face

Allez on attaque la partie programmation. :D

Pour le moment, on va tout coder dans la boucle principale, ce sera plus simple à comprendre. Ensuite, nous migrerons proprement le code dans une classe dédiée.

On va commencer notre cube en affichant sa première face (celle du fond) :

Image utilisateur

Pour cela, nous aurons besoin de 6 sommets vu que nous avons besoin de deux triangles pour faire un carré. Si on regarde le schéma ci-dessus, on remarque que l'on peut faire un triangle avec les vertices 0, 1 et 2, et un autre avec les vertices 2, 3 et 0. Le tableau dont nous avons besoin ressemblera donc à ceci :

void SceneOpenGL::bouclePrincipale()
{
    // Vertices

    float vertices[] = {-1.0, -1.0, -1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,     // Triangle 1
                        -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0};    // Triangle 2
}

Avant d'intégrer ce tableau, nous allons reprendre ensemble la boucle principale pour voir ce que donnerait le nouvel affichage. On commence par évidemment par déclarer les matrices projection et modelview :

void SceneOpenGL::bouclePrincipale()
{
    // Booléen terminer

    bool terminer(false);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Boucle principale

    while(!terminer)
    {
        /* Rendu */
    }
}

Nous pouvons maintenant intégrer le tableau de vertices que l'on a trouvé juste avant. On le place après la déclaration des matrices :

void SceneOpenGL::bouclePrincipale()
{
    // Booléen terminer

    bool terminer(false);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Vertices

    float vertices[] = {-1.0, -1.0, -1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,     // Triangle 1
                        -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0};    // Triangle 2


    // Boucle principale

    while(!terminer)
    {
        /* Rendu */
    }
}

Pour admirer le rendu final il ne manque plus qu'à colorier les deux triangles. Nous utiliserons le rouge vu qu'elle est présente dans le schéma un peu plus haut.

Le tableau à utiliser pour cela doit permettre d'affecter une couleur pour chaque vertex. Nous en avons 6 pour le moment donc nous aurons besoin de 6 couleurs :

// Couleurs

float couleurs[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Triangle 1
                    1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0};          // Triangle 2

On en profite au passage pour déclarer le shader qui permettra de colorier nos faces :

// Couleurs

float couleurs[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Triangle 1
                    1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0};          // Triangle 2


// Shader

Shader shaderCouleur("Shaders/couleur3D.vert", "Shaders/couleur3D.frag");
shaderCouleur.charger();

Une fois toutes ces déclarations faites, nous devrons nous occuper de la caméra. Nous devons la déclarer juste avant la boucle principale et la placer à chaque tour de boucle au point de coordonnées (0, 0, 1). Elle sera en mode 'affichage 2D' temporairement pour nous permettre de voir le carré correctement :

// Boucle principale

while(!terminer)
{   
    // Gestion des évènements

    SDL_WaitEvent(&m_evenements);

    if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        terminer = true;


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT);


    // Placement de la caméra

    modelview = lookAt(vec3(0, 0, 1), vec3(0, 0, 0), vec3(0, 1, 0));


    // Rendu

    ....


    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);
}

Pour le rendu en lui-même, il n'y a pas de grand changement à faire. On commence par activer le shader puis on envoie nos données aux tableaux Vertex Attrib, on sait le faire depuis un moment grâce à la fonction glVertexAttribPointer() :

// Activation du shader

glUseProgram(shaderCouleur.getProgramID());


    // Envoi des vertices

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


    // Envoi de la couleur

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


    ....


// Désactivation du shader

glUseProgram(0);

Il ne nous reste plus qu'à envoyer les matrices projection et modelview au shader à l'aide des grosses fonctions. On pensera également à afficher le rendu grâce à la fonction glDrawArrays() et à désactiver les tableaux Vertex Attrib :

// Envoi des matrices

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


// Rendu

glDrawArrays(GL_TRIANGLES, 0, 6);


// Désactivation des tableaux

glDisableVertexAttribArray(1);
glDisableVertexAttribArray(0);

Si on résume tout ça :

void SceneOpenGL::bouclePrincipale()
{
    // Booléen terminer

    bool terminer(false);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Vertices

    float vertices[] = {-1.0, -1.0, -1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,     // Triangle 1
                        -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0};    // Triangle 2


    // Couleurs

    float couleurs[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Triangle 1
                        1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0};          // Triangle 2


    // Shader

    Shader shaderCouleur("Shaders/couleur3D.vert", "Shaders/couleur3D.frag");
    shaderCouleur.charger();


    // Boucle principale

    while(!terminer)
    {
        // Gestion des évènements

        SDL_WaitEvent(&m_evenements);

        if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
            terminer = true;


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT);


        // Placement de la caméra

        modelview = lookAt(vec3(0, 0, 1), vec3(0, 0, 0), vec3(0, 1, 0));


        // Activation du shader

        glUseProgram(shaderCouleur.getProgramID());


            // Envoi des vertices

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


            // Envoi de la couleur

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


            // Envoi des matrices

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


            // Rendu

            glDrawArrays(GL_TRIANGLES, 0, 6);


            // Désactivation des tableaux

            glDisableVertexAttribArray(1);
            glDisableVertexAttribArray(0);



        // Désactivation du shader

        glUseProgram(0);


        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(m_fenetre);
    }
}

Si vous compilez ce code, vous devriez obtenir votre premier carré avec OpenGL :

Image utilisateur

Notre première face du cube est maintenant prête. :D

La deuxième face

Passons maintenant à la deuxième face du cube.

Pour commencer, on va placer la caméra un peu différemment de façon à voir la scène en 3 dimensions. Nous la positionnerons au point de coordonnées (3, 3, 3) et la ferons fixer l'origine du repère (0, 0, 0) :

// Placement de la caméra

modelview = lookAt(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0));

La deuxième face que nous devons afficher doit ressembler à ceci :

Image utilisateur

On remarque qu'il faut prendre les vertices 5, 1 et 2 pour afficher le premier triangle, puis les vertices 2, 6 et 5 pour le second. Si on fait correspondre leurs coordonnées avec le schéma du début, on trouve les deux triangles suivants :

// Vertices

float vertices[] = {-1.0, -1.0, -1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1
                    -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1

                    1.0, -1.0, 1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,       // Face 2
                    1.0, -1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, -1.0};        // Face 2

Évidemment, si nous utilisons de nouveaux vertices il faut leur associer une couleur. On ajoute donc 6 triplets au tableau couleurs spécialement pour eux. Nous utiliserons le vert pour différencier les deux faces :

// Couleurs

float couleurs[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1
                    1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1

                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2
                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0};          // Face 2

Pour afficher la nouvelle face, il suffit de modifier le fameux paramètre count pour qu'il prenne en compte les 6 nouveaux sommets. On lui donne donc la valeur 6 + 6 = 12 :

// Affichage des triangles

glDrawArrays(GL_TRIANGLES, 0, 12);

Si vous compilez tout ça, vous devriez obtenir :

Image utilisateur

On a maintenant la deuxième face, et en 3D s'il vous plaît. ;)

La troisième face

Allez, on continue avec la troisième face. On commence par définir les sommets dont nous aurons besoin :

Image utilisateur

Comme d'habitude, on fait correspondre ces sommets avec leurs coordonnées pour trouver le tableau suivant :

// Vertices

float vertices[] = {-1.0, -1.0, -1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1
                    -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1

                    1.0, -1.0, 1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,       // Face 2
                    1.0, -1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, -1.0,         // Face 2

                    -1.0, -1.0, 1.0,   1.0, -1.0, 1.0,   1.0, -1.0, -1.0,      // Face 3
                    -1.0, -1.0, 1.0,   -1.0, -1.0, -1.0,   1.0, -1.0, -1.0};   // Face 3

Ensuite, on fait correspondre une nouvelle couleur aux deux nouveaux triangles formés par les vertices. On utilisera le bleu comme sur le schéma :

// Couleurs

float couleurs[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1
                    1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1

                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2
                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2

                    0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 3
                    0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0};          // Face 3

Pour finir, on doit modifier une fois de plus le paramètre count pour prendre en compte les nouveaux vertices. Sa valeur passe de 12 à 18 :

// Affichage des triangles

glDrawArrays(GL_TRIANGLES, 0, 18);

En compilant le nouveau code, on obtient :

Image utilisateur

Pourquoi ça s'affiche comme ça ? Y'a un bug ? o_O

Non, ce n'est pas un bug, et vous allez vite comprendre pourquoi la face ne s'affiche pas correctement. ^^

Le Depth Buffer

C'est la première fois que nous avons un problème d'affichage, et c'est tout à fait normal puisqu'avant nous n'avions jamais eu de formes superposées l'une sur l'autre. Ici, la face bleue et la face verte se superposent, et pour OpenGL c'est un problème car il ne sait pas quelle forme doit être visible et quelle forme doit être cachée. N'oubliez pas qu'un ordinateur est très bête, il ne sait rien faire à part calculer.

Le Depth Buffer (ou Tampon de profondeur) est ce qui va permettre à OpenGL de comprendre ce qu'il doit afficher et ce qu'il doit masquer. Si un pixel de modèle se trouve derrière un autre alors le Depth Buffer indiquera à OpenGL : "N'affiche pas ce pixel, mais affiche celui-ci car il est devant".

Je vous avais déjà parlé brièvement de cette notion dans le chapitre sur les shaders, notamment avec ce schéma :

Image utilisateur

La dernière étape du pipeline 3D était le Test de profondeur. C'est justement là qu'intervient le Depth Buffer. Heureusement pour nous, il ne faudra pas gérer ce tampon par nous-même, cette fonctionnalité n'a pas été supprimée avec la nouvelle version d'OpenGL. :) Avant que l'API puisse se servir de ce tampon, il faut l'activer grâce à la fonction glEnable().

void glEnable(GLenum cap);

Nous reverrons plusieurs fois cette fonction qui permet d'activer certaines fonctionnalités d'OpenGL. Le paramètre cap est justement la fonctionnalité à activer. Pour le Depth Buffer, on lui donne le paramètre GL_DEPTH_TEST. On va donc appeler la fonction comme ceci dans la méthode initGL() juste après l'initialisation de la librairie GLEW :

bool SceneOpenGL::initGL()
{
    #ifdef WIN32

        /* ***** Initialisation de la librairie GLEW ***** */

    #endif


    // Activation du Depth Buffer

    glEnable(GL_DEPTH_TEST);


    // Tout s'est bien passé, on retourne true

    return true;

A chaque tour de boucle, il faudra (comme avec les couleurs et la matrice modelview) ré-initialiser le Depth Buffer afin de vider toute traces de l'affichage précédent. Pour ça, il suffit d'ajouter un paramètre à une fonction que l'on utilise déjà : glClear(). Rappelez-vous que cette fonction permet de vider les buffers qu'on lui donne en paramètre.

Pour le moment, on ne lui donne que le paramètre GL_COLOR_BUFFER_BIT pour effacer ce qui se trouve à l'écran. Maintenant, on va ajouter le paramètre GL_DEPTH_BUFFER_BIT pour effacer le Depth Buffer :

// Nettoyage de la fenêtre et du Depth Buffer

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

Maintenant que l'on sait ce qu'est le Depth Buffer, on peut régler notre problème d'affichage. Après avoir placé la fonction glEnable(), vous devriez obtenir ceci :

Image utilisateur
Les trois dernières faces

Courage nous sommes presque au bout, il ne reste plus qu'à afficher les 3 dernières faces. Et je fais bien de vous dire courage car je vais vous demander de terminer les trois dernières faces tous seuls. :p

Je vais vous donner les schémas contenant les vertices et la couleur dont vous aurez besoin pour chaque face, pour le reste ce sera à vous de le faire. Il n'y a rien de compliqué en plus, nous avons déjà fait la moité du cube ensemble et vous n'aurez pas de surprise d'affichage car nous savons maintenant gérer le Depth Buffer.

Les tableaux finaux peuvent vous paraitre gros et moches, c'est tout à fait normal. Pour vous dire, les vertices de personnages ou de décors sont infiniment plus moches au vu de leur nombre de données. :lol: Mais l'avantage avec eux c'est que nous n'avons pas à les coder à la main, nous ne les voyons même pas d'ailleurs. Mais bon, ça ça sera pour plus tard. Pour le moment, je vous demande de finir notre fameux cube à l'aide des schémas suivants :

Image utilisateur
Image utilisateur
Image utilisateur

Allez hop à votre clavier !

.....

On passe à la correction. Le principe reste le même, il faut juste faire correspondre les vertices et les couleurs. Voici ce que donne les tableaux finaux :

Vos vertices peuvent parfaitement être déclarés dans un ordre différent de celui que je donne. Ce n'est pas grave du moment que vous affichez des carrés correctement :

float vertices[] = {-1.0, -1.0, -1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1
                    -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1

                    1.0, -1.0, 1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,       // Face 2
                    1.0, -1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, -1.0,         // Face 2

                    -1.0, -1.0, 1.0,   1.0, -1.0, 1.0,   1.0, -1.0, -1.0,      // Face 3
                    -1.0, -1.0, 1.0,   -1.0, -1.0, -1.0,   1.0, -1.0, -1.0,    // Face 3

                    -1.0, -1.0, 1.0,   1.0, -1.0, 1.0,   1.0, 1.0, 1.0,        // Face 4
                    -1.0, -1.0, 1.0,   -1.0, 1.0, 1.0,   1.0, 1.0, 1.0,        // Face 4

                    -1.0, -1.0, -1.0,   -1.0, -1.0, 1.0,   -1.0, 1.0, 1.0,     // Face 5
                    -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   -1.0, 1.0, 1.0,     // Face 5

                    -1.0, 1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, -1.0,         // Face 6
                    -1.0, 1.0, 1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0};      // Face 6

L'ordre des couleurs peut, lui aussi, être différent. Ce qui compte, c'est que les lignes de couleur correspondent à leurs lignes de vertex :

// Couleurs

float couleurs[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1
                    1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1

                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2
                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2

                    0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 3
                    0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 3

                    1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 4
                    1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 4

                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 5
                    0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 5

                    0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 6
                    0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0};          // Face 6

Au niveau de la fonction glDrawArrays(), vous devriez avoir la valeur du paramètre count à 36 :

// Affichage des triangles

glDrawArrays(GL_TRIANGLES, 0, 36);

A la fin, vous devriez avoir votre premier modèle 3D !

Image utilisateur

Magnifique n'est-ce pas ? ^^

La classe Cube

La classe Cube

Le header

Après tous les efforts que nous avons fournis dans la partie précédente, nous avons enfin pu afficher notre premier modèle 3D. :D
Vous avez remarqué que, mises à part les matrices, le processus était le même que pour les modèles 2D. C'est-dire-à qu'il nous a suffi d'activer un shader, d'envoyer les données aux tableaux Vertex Attrib, puis d'afficher le tout avec glDrawArrays().

Ce que nous allons faire maintenant va nous permettre de nettoyer un peu la boucle principale. En effet, comme je vous l'ai précisé dans la correction du cube, les tableaux de vertices et de couleurs sont assez indigestes. Il serait donc judicieux de créer une classe dédiée au cube de façon à enfermer ces lignes de code à l'intérieur. Nous gagnerions en lisibilité et de plus, nous pourrions créer des cubes à l'infini en seulement quelques lignes de code !

Notre nouvel objectif va donc être la création d'une classe Cube dont on se servira pour la suite du tutoriel.

Et nous allons commencer tout de suite par le header. Celui-ci sera placé dans un fichier que nous appellerons Cube.h et devra contenir la déclaration de la classe ainsi que les inclusions nécessaires pour OpenGL, les shaders et les matrices :

#ifndef DEF_CUBE
#define DEF_CUBE


// Includes OpenGL

#ifdef WIN32
#include <GL/glew.h>

#else
#define GL3_PROTOTYPES 1
#include <GL3/gl3.h>

#endif


// Includes GLM

#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>


// Includes

#include "Shader.h"


// Classe Cube

class Cube
{
    public:


    private:
};

#endif

Au niveau des attributs, cette classe devra contenir tous les éléments dont nous avons eu besoin pour afficher notre modèle, à savoir :

  • Un objet de type Shader

  • Un tableau flottant de vertices

  • Un tableau flottant de couleurs

Les matrices ne font pas partie de cette liste car nous n'en avons besoin qu'au moment de l'affichage, inutile donc de créer des attributs pour elles. Nous les enverrons en tant que paramètres dans une méthode.

Si on rassemble tout ça, on trouve :

// Attributs

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

La taille 108 des deux tableaux vient de la multiplication du nombre de vertices nécessaires pour un cube (36) par leur nombre de coordonnées (3), ce qui fait 36 vertices x 3 coordonnées = 108 cases.

Passons maintenant au constructeur, celui-ci aura besoin de trois paramètres : la taille du cube que l'on veut afficher ainsi que les deux codes sources du shader à utiliser.

Nous n'avons pas intégré la possibilité de choisir les dimensions avant afin d'éviter d’alourdir le code qui était déjà assez dense. Mais vu qu'à présent nous codons une classe, il serait quand même plus agréable de pouvoir créer des cubes de n'importe quelle taille en modifiant simplement une seule valeur. :)

Nous prendrons une variable de type float pour gérer cette taille. Quant aux autres paramètres, vous savez déjà que ce seront des string :

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

On profite de ce passage pour déclarer le destructeur de la classe :

~Cube();

Après tous ces petits ajouts, nous nous retrouvons devant le header complet de la classe Cube :

#ifndef DEF_CUBE
#define DEF_CUBE


// Includes OpenGL

#ifdef WIN32
#include <GL/glew.h>

#else
#define GL3_PROTOTYPES 1
#include <GL3/gl3.h>

#endif


// Includes GLM

#include <glm/glm.hpp>
#include <glm/gtx/transform.hpp>
#include <glm/gtc/type_ptr.hpp>


// Includes

#include "Shader.h"


// Classe Cube

class Cube
{
    public:

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


    private:

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

#endif
Le constructeur

Passons maintenant à l'implémentation de la classe avec en premier lieu le constructeur.

Celui-ci débute avec l'initialisation des attributs. Nous en avons 3 mais seul le shader peut vraiment être initialisé ici car les vertices et les couleurs sont des tableaux, nous ne pouvons donc pas le faire directement après les deux points ":". Nous lui donnons les deux codes sources reçus en paramètres :

Cube::Cube(float taille, std::string const vertexShader, std::string const fragmentShader) : m_shader(vertexShader, fragmentShader)
{

}

Ce qui se trouve entre les accolades commence également par le shader car nous devons appeler sa méthode charger() de façon à le charger complétement :

Cube::Cube(float taille, std::string const vertexShader, std::string const fragmentShader) : m_shader(vertexShader, fragmentShader)
{
    // Chargement du shader

    m_shader.charger();
}

Le shader est maintenant initialisé et prêt à l'emploi.

On passe maintenant au plus délicat : l'initialisation des tableaux de vertices et de couleurs. Il y a deux manières de faire en C++ :

  • Soit on initialise leurs valeurs une par une (donc on initialise séparément les 24 valeurs d'un tableau de 24 cases par exemple).

  • Soit on déclare des tableaux temporaires contenant les valeurs désirées, puis on utilise des boucles pour les affecter aux vrais tableaux.

Si on utilise la première méthode, il nous faudrait 108 lignes de code juste pour initialiser les vertices, et le double si on s'occupe aussi des couleurs. Avouez tout de même que c'est méchamment fastidieux, surtout si on doit le faire deux fois. Je pense donc que vous serez d'accord pour utiliser la seconde méthode. :lol:

Nous devons donc utiliser un tableau temporaire qui va contenir tous les vertices du cube, nous l'appellerons verticesTmp[] :

// Vertices temporaires

float verticesTmp[] = {-1.0, -1.0, -1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1
                       -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0,     // Face 1

                       1.0, -1.0, 1.0,   1.0, -1.0, -1.0,   1.0, 1.0, -1.0,       // Face 2
                       1.0, -1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, -1.0,         // Face 2

                       -1.0, -1.0, 1.0,   1.0, -1.0, 1.0,   1.0, -1.0, -1.0,      // Face 3
                       -1.0, -1.0, 1.0,   -1.0, -1.0, -1.0,   1.0, -1.0, -1.0,    // Face 3

                       -1.0, -1.0, 1.0,   1.0, -1.0, 1.0,   1.0, 1.0, 1.0,        // Face 4
                       -1.0, -1.0, 1.0,   -1.0, 1.0, 1.0,   1.0, 1.0, 1.0,        // Face 4

                       -1.0, -1.0, -1.0,   -1.0, -1.0, 1.0,   -1.0, 1.0, 1.0,     // Face 5
                       -1.0, -1.0, -1.0,   -1.0, 1.0, -1.0,   -1.0, 1.0, 1.0,     // Face 5

                       -1.0, 1.0, 1.0,   1.0, 1.0, 1.0,   1.0, 1.0, -1.0,         // Face 6
                       -1.0, 1.0, 1.0,   -1.0, 1.0, -1.0,   1.0, 1.0, -1.0};      // Face 6

Les vertices en l'état n'ont que bien peu d'intérêt car ils ne prennent pas en compte le paramètre taille du constructeur. Pour régler ce problème, nous allons simplement remplacer toutes les occurrences de la valeur 1.0 par le paramètre taille lui-même. C'est un peu long à faire mais la fonctionnalité "Find and Replace" de votre IDE devrait vous faciliter un peu la tâche. ;)

Le tableau remanié devrait ressembler à celui-ci :

// Vertices temporaires

float verticesTmp[] = {-taille, -taille, -taille,   taille, -taille, -taille,   taille, taille, -taille,     // Face 1
                       -taille, -taille, -taille,   -taille, taille, -taille,   taille, taille, -taille,     // Face 1

                       taille, -taille, taille,   taille, -taille, -taille,   taille, taille, -taille,       // Face 2
                       taille, -taille, taille,   taille, taille, taille,   taille, taille, -taille,         // Face 2

                       -taille, -taille, taille,   taille, -taille, taille,   taille, -taille, -taille,      // Face 3
                       -taille, -taille, taille,   -taille, -taille, -taille,   taille, -taille, -taille,    // Face 3

                       -taille, -taille, taille,   taille, -taille, taille,   taille, taille, taille,        // Face 4
                       -taille, -taille, taille,   -taille, taille, taille,   taille, taille, taille,        // Face 4

                       -taille, -taille, -taille,   -taille, -taille, taille,   -taille, taille, taille,     // Face 5
                       -taille, -taille, -taille,   -taille, taille, -taille,   -taille, taille, taille,     // Face 5

                       -taille, taille, taille,   taille, taille, taille,   taille, taille, -taille,         // Face 6
                       -taille, taille, taille,   -taille, taille, -taille,   taille, taille, -taille};      // Face 6

Il reste encore une petite modification à faire. Si on regarde de plus près nos données, on remarque que les vertices vont de -taille à +taille. Cet intervalle fait que notre cube est multiplié par 2. :(

Pour éviter cela, il faut diviser le paramètre taille par 2 avant de remplir le tableau. Ainsi, notre cube qui devait être multiplié par 2 ne le sera plus :

// Division du paramètre taille

taille /= 2;


// Vertices temporaires

float verticesTmp[] = {-taille, -taille, -taille,   taille, -taille, -taille,   taille, taille, -taille,     // Face 1
                       -taille, -taille, -taille,   -taille, taille, -taille,   taille, taille, -taille,     // Face 1

                       taille, -taille, taille,   taille, -taille, -taille,   taille, taille, -taille,       // Face 2
                       taille, -taille, taille,   taille, taille, taille,   taille, taille, -taille,         // Face 2

                       -taille, -taille, taille,   taille, -taille, taille,   taille, -taille, -taille,      // Face 3
                       -taille, -taille, taille,   -taille, -taille, -taille,   taille, -taille, -taille,    // Face 3

                       -taille, -taille, taille,   taille, -taille, taille,   taille, taille, taille,        // Face 4
                       -taille, -taille, taille,   -taille, taille, taille,   taille, taille, taille,        // Face 4

                       -taille, -taille, -taille,   -taille, -taille, taille,   -taille, taille, taille,     // Face 5
                       -taille, -taille, -taille,   -taille, taille, -taille,   -taille, taille, taille,     // Face 5

                       -taille, taille, taille,   taille, taille, taille,   taille, taille, -taille,         // Face 6
                       -taille, taille, taille,   -taille, taille, -taille,   taille, taille, -taille};      // Face 6

Piouf le plus dur est derrière nous. :)

Le tableau de couleurs quant à lui est plus simple à faire puisqu'il suffit juste de reprendre celui que nous utilisions avant. Nous modifierons juste son nom en l'appelant couleursTmp vu qu'il s'agit de données temporaires :

// Couleurs temporaires

float couleursTmp[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1
                       1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1

                       0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2
                       0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2

                       0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 3
                       0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 3

                       1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 4
                       1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 4

                       0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 5
                       0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 5

                       0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 6
                       0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0};          // Face 6

Maintenant que nos données sont déclarées, il ne manque plus qu'à les transférer dans nos attributs.

Pour cela, nous allons utiliser une boucle qui va s'exécuter 108 fois, ce qui permettra donc de copier non seulement les coordonnées des sommets mais aussi les composantes des couleurs. En effet, les tableaux font tous les deux la même taille, on peut alors n'utiliser qu'une seule boucle. :)

// Copie des valeurs dans les tableaux finaux

for(int i(0); i < 108; i++)
{
    m_vertices[i] = verticesTmp[i];
    m_couleurs[i] = couleursTmp[i];
}

Si on réunit tous les bouts de code :

Cube::Cube(float taille, std::string const vertexShader, std::string const fragmentShader) : m_shader(vertexShader, fragmentShader)
{
    // Chargement du shader

    m_shader.charger();


    // Division de la taille

    taille /= 2;


    // Vertices temporaires

    float verticesTmp[] = {-taille, -taille, -taille,   taille, -taille, -taille,   taille, taille, -taille,     // Face 1
                           -taille, -taille, -taille,   -taille, taille, -taille,   taille, taille, -taille,     // Face 1

                           taille, -taille, taille,   taille, -taille, -taille,   taille, taille, -taille,       // Face 2
                           taille, -taille, taille,   taille, taille, taille,   taille, taille, -taille,         // Face 2

                           -taille, -taille, taille,   taille, -taille, taille,   taille, -taille, -taille,      // Face 3
                           -taille, -taille, taille,   -taille, -taille, -taille,   taille, -taille, -taille,    // Face 3

                           -taille, -taille, taille,   taille, -taille, taille,   taille, taille, taille,        // Face 4
                           -taille, -taille, taille,   -taille, taille, taille,   taille, taille, taille,        // Face 4

                           -taille, -taille, -taille,   -taille, -taille, taille,   -taille, taille, taille,     // Face 5
                           -taille, -taille, -taille,   -taille, taille, -taille,   -taille, taille, taille,     // Face 5

                           -taille, taille, taille,   taille, taille, taille,   taille, taille, -taille,         // Face 6
                           -taille, taille, taille,   -taille, taille, -taille,   taille, taille, -taille};      // Face 6


    // Couleurs temporaires

    float couleursTmp[] = {1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1
                           1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 1

                           0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2
                           0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 2

                           0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 3
                           0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 3

                           1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 4
                           1.0, 0.0, 0.0,   1.0, 0.0, 0.0,   1.0, 0.0, 0.0,           // Face 4

                           0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 5
                           0.0, 1.0, 0.0,   0.0, 1.0, 0.0,   0.0, 1.0, 0.0,           // Face 5

                           0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0,           // Face 6
                           0.0, 0.0, 1.0,   0.0, 0.0, 1.0,   0.0, 0.0, 1.0};          // Face 6


    // Copie des valeurs dans les tableaux finaux

    for(int i(0); i < 108; i++)
    {
        m_vertices[i] = verticesTmp[i];
        m_couleurs[i] = couleursTmp[i];
    }
}
Le destructeur

Comme vous le savez déjà, un destructeur est une méthode appelée au moment de la destruction de l'objet. Il permet de libérer la mémoire prise par l'objet au cours de sa vie, en particulier la mémoire allouée dynamiquement.

Heureusement pour nous, dans notre cas nous ne faisons aucune allocation dynamique. :p Le destructeur va donc être vide :

Cube::~Cube()
{

}
La méthode afficher

Comme son nom l'indique, la méthode afficher() va nous permettre ... d'afficher notre cube. ^^ Son prototype est assez simple :

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

Elle prend en paramètre une référence sur les deux matrices que l'on connait si bien maintenant. Elles sont indispensables pour afficher un modèle 3D. Dans cette méthode, nous en avons besoin pour les envoyer au shader.

Son implémentation va être ultra simple pour nous : il suffit juste de copier le code contenu entre l'activation et la désactivation du shader. Ce qui comprend :

  • Le shader évidemment

  • L'envoi des matrices

  • L'utilisation des tableaux Vertex Attrib

  • L'appel à la fonction glUseProgram()

Le code à copier est le suivant :

// Activation du shader

glUseProgram(shaderCouleur.getProgramID());


    // Envoi des vertices

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


    // Envoi de la couleur

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


    // Envoi des matrices

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


    // Rendu

    glDrawArrays(GL_TRIANGLES, 0, 36);


    // Désactivation des tableaux

    glDisableVertexAttribArray(1);
    glDisableVertexAttribArray(0);


// Désactivation du shader

glUseProgram(0);

Avant d'aller plus loin, il nous faut modifier le nom des variables anciennement utilisées. Ainsi :

  • L'objet shaderCouleur devient m_shader

  • Le tableau vertices devient m_vertices

  • Le tableau couleurs devient m_couleurs

Une fois le nom des variables modifié, on se retrouve avec la méthode afficher() suivante :

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

    glUseProgram(m_shader.getProgramID());


        // Envoi des vertices

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


        // Envoi de la couleur

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


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


        // Rendu

        glDrawArrays(GL_TRIANGLES, 0, 36);


        // Désactivation des tableaux

        glDisableVertexAttribArray(1);
        glDisableVertexAttribArray(0);


    // Désactivation du shader

    glUseProgram(0);
}

J'adore quand le copier-coller fonctionne aussi facilement. ^^ Nous avions déjà fait le plus gros avant, il ne nous restait plus qu'à adapter le nom des attributs.

La boucle principale

Il ne reste plus qu'une seule chose à faire : déclarer un objet de type Cube et utiliser sa méthode afficher() dans la boucle principale. On efface donc tout ce qu'on à fait avant (vertices, couleurs, affichage, ...). On ne doit garder que ceci :

void SceneOpenGL::bouclePrincipale()
{
    // Variable

    bool terminer(false);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Boucle principale

    while(!terminer)
    {
        // Gestion des évènements

        SDL_WaitEvent(&m_evenements);

        if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
            terminer = true;


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        // Placement de la caméra

        modelview = lookAt(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0));

    
        // Rendu (Rien pour le moment)

        ....



        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(m_fenetre);
    }
}

Ensuite, on déclare notre objet de type Cube qui sera initialisé automatiquement avec le constructeur. Nous lui donnerons la valeur 2.0 pour le paramètre taille (ou une autre qui vous plaira :p ) ainsi que les string"Shaders/couleur3D.vert" et "Shaders/couleur3D.frag" pour le shader.

void SceneOpenGL::bouclePrincipale()
{
    // Variable

    bool terminer(false);


    // Matrices

    mat4 projection;
    mat4 modelview;

    projection = perspective(70.0, (double) m_largeurFenetre / m_hauteurFenetre, 1.0, 100.0);
    modelview = mat4(1.0);


    // Déclaration d'un objet Cube

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


    // Boucle principale

    while(!terminer)    
    {
        ....
    }
}

Enfin, on utilise la méthode afficher() dans la boucle principale en donnant les matrices en paramètres :

while(!terminer)
{
    // Gestion des évènements

    SDL_WaitEvent(&m_evenements);

    if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        terminer = true;


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    // Placement de la caméra

    modelview = lookAt(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0));


    // Affichage du cube

    cube.afficher(projection, modelview);


    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);
}

Si vous compilez tout ça, vous obtiendrez le même résultat que tout à l'heure mais cette fois-ci, vous avez un véritable objet C++ permettant d'afficher un cube. ^^

Pseudo-animation

Attention, cette sous-partie s'appelle bien "pseudo-animation" et pas animation tout court. On va apprendre à faire pivoter notre cube pour voir toutes ses faces, et ce grâce à la méthode rotate() de la librairie GLM.

Le principe est simple : on incrémente un angle à chaque tour de boucle puis on fait pivoter le cube avec cet angle qui change sans arrêt donnant ainsi une impression de mouvement. Pour le moment, il faudra bouger la souris dans la fenêtre SDL pour constater la rotation puisque nous utilisons la fonction SDL_WaitEvent() qui bloque le programme quand il n'y a pas d'évènements.

On commence par déclarer un angle de type float que l'on incrémentera à chaque tour boucle :

void SceneOpenGL::bouclePrincipale()
{
    ....


    // Variable angle

    float angle(0.0);


    // Boucle principale

    while(!terminer)
    {
        .... 
    }
}

Ensuite, on incrémente l'angle de rotation à chaque tour de boucle. Petite précision, l'angle atteindra forcément les 360° vu qu'on l'incrémente sans arrêt. A chaque fois qu'il atteindra 360°, il faudra donc le remettre à zéro. Il est inutile d'avoir un angle incompréhensible de 1604°. :lol:

// Incrémentation de l'angle

angle += 4.0;

if(angle >= 360.0)
    angle -= 360.0;

Une fois l'angle défini, il suffit de faire pivoter le repère en appelant la méthode rotate() de la matrice modelview :

// Rotation du repère

modelview = rotate(modelview, angle, vec3(0, 1, 0));

Si on place ce code au bon endroit :

while(!terminer)
{
    // Gestion des évènements

    ....



    // Placement de la caméra

    modelview = lookAt(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0));


    // Incrémentation de l'angle

    angle += 4.0;

    if(angle >= 360.0)
        angle -= 360.0;


    // Rotation du repère

    modelview = rotate(modelview, angle, vec3(0, 1, 0));


    // Affichage du cube

    cube.afficher(projection, modelview);



    // Fin de la boucle

    ....
}

Avec ce code, votre cube devrait tourner sur lui-même. :p

Le Frame Rate

Calcul du FPS

Je profite de ce chapitre pour vous introduire une nouvelle notion : celle du Frame Rate.

Le Frame Rate est le nombre de fois par seconde où la boucle principale est exécutée. En France, on prend généralement une valeur 50 fps (Frames Per Second ou Frames par seconde). Pour le moment, notre jeu fonctionne avec 0 fps étant donné que l'on utilise une fonction qui bloque le programme : SDL_WaitEvent(). La boucle ne s'exécute que si on fait quelque chose, sinon le jeu est bloqué.

Or, dans un jeu-vidéo, si on ne touche pas à la souris il se passe quand même quelque chose. Nous allons régler ce problème en introduisant la notion de frame rate. La première chose à faire est de changer la fonction SDL_WaitEvent() par la fonction SDL_PollEvent() qui, elle, ne bloque pas le programme :

SDL_PollEvent(&m_evenements);

L'utilisation de cette fonction va cependant nous poser un problème : le CPU va être totalement surchargé. Heureusement, le frame rate est là pour nous aider. Grâce à lui, nous n'exécuterons pas la boucle principale des centaines de fois par seconde mais uniquement quelques dizaines de fois, ce qui est pour le CPU largement calculable. :) Pour imposer cette limitation, il suffit de bloquer le programme quelques millisecondes à un certain moment. Ce petit intervalle dépend du nombre de FPS que l'on souhaite afficher.

Pour calculer ce temps de blocage, il suffit de diviser 1000 millisecondes (soit une seconde) par le nombre de FPS que l'on veut. Par exemple, si on veut 50 FPS il faut faire : 1000 / 50 = 20 millisecondes.

Pour 50 FPS, la boucle principale devra mettre 20 millisecondes à s'exécuter, quitte à mettre le programme en pause jusqu'à atteindre cet intervalle de 20 ms.

Programmation

La théorie c'est bien mais il faut maintenant adapter la notion de FPS dans notre code. Le principe est simple : on calcule le temps écoulé entre le début et la fin de la boucle. Si ce temps est inférieur à 20 ms alors on met en pause le programme jusqu'à atteindre les 20 ms.

L'implémentation se fait en plusieurs étapes :

  • À chaque tour de boucle, on enregistre le temps où on commence la boucle.

  • Puis on enregistre le temps une fois qu'elle est terminée.

  • On soustrait le temps enregistré au début par le temps de fin de boucle.

  • Si ce temps est inférieur à 20 ms, alors on met en pause le programme jusqu'à atteindre 20 ms.

Pour capturer le temps, on utilise une fonction de la SDL : SDL_GetTicks(). Elle retourne le temps actuel de l'ordinateur dans une structure de type Uint32 :

Uint32 SDL_GetTicks(void);

Une autre fonction qui va nous être utile est la fonction SDL_Delay(). Cette fonction va nous permettre de mettre en pause le programme lorsque nous en aurons besoin. Elle prend un seul paramètre : le temps en millisecondes durant lequel elle va bloquer le programme :

void SDL_Delay(Uint32 ms);

Bref, commençons déjà par déclarer nos variables juste après le booléen terminer de la méthode bouclePrincipale() :

void SceneOpenGL::bouclePrincipale()
{
    // Variables relatives à la boucle

    bool terminer(false);
    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    /* **** Reste du code **** */
}

N'oubliez pas que le temps de blocage est égal à : 1000 / Frame Rate.

Occupons-nous maintenant de la limitation. La première chose à faire est de déterminer le temps où la boucle commence :

// Boucle principale

while(!terminer)
{
    // On définit le temps de début de boucle

    debutBoucle = SDL_GetTicks();


    /* ***** Boucle Principale ***** */
}

Ensuite, il faut déterminer le temps qu'a mis la boucle pour s'exécuter. Pour ça, on enregistre le temps de fin de boucle que l'on soustrait par le temps du début :

while(!terminer)
{
    /* ***** Boucle Principale ***** */


    // Calcul du temps écoulé

    finBoucle = SDL_GetTicks();
    tempsEcoule = finBoucle - debutBoucle;
}

Enfin, si le temps est inférieur à 20 ms, on met en pause le programme jusqu'à atteindre les 20 ms :

// Si nécessaire, on met en pause le programme

if(tempsEcoule < frameRate)
    SDL_Delay(frameRate - tempsEcoule);

Si on résume tout ça :

void SceneOpenGL::bouclePrincipale()
{
    // Variables relatives à la boucle

    bool terminer(false);
    unsigned int frameRate (1000 / 50);
    Uint32 debutBoucle(0), finBoucle(0), tempsEcoule(0);


    // Boucle principale

    while(!terminer)
    {
        // On définit le temps de début de boucle

        debutBoucle = SDL_GetTicks();


        // Gestion des évènements

        SDL_PollEvent(&m_evenements);

        if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
            terminer = true;
    



        /* ***** Rendu OpenGL ***** */

    


        // Calcul du temps écoulé

        finBoucle = SDL_GetTicks();
        tempsEcoule = finBoucle - debutBoucle;


        // Si nécessaire, on met en pause le programme

        if(tempsEcoule < frameRate)
            SDL_Delay(frameRate - tempsEcoule);
    }
}

Avec ce code, la boucle principale ne s'exécutera que 50 fois par seconde. Si vous observez l'activité de votre processeur, vous remarquerez que le programme ne le monopolise pas malgré l'utilisation de la fonction SDL_PollEvent(). Maintenant, nous ne sommes plus obligés de toucher la souris pour voir quelque chose bouger à l'écran. Essayez avec votre cube, il tourne même si vous ne faites rien. :p

Télécharger : Code Source C++ du Chapitre 7

Et voilà, nous avons fait nos premiers pas dans la 3D. Nous avons vu les principes de base et nous sommes maintenant capables d'afficher des polygones en 3 dimensions avec OpenGL. :p
Dans le chapitre suivant, nous allons nous reposer un peu et étudier quelques points divers dont je n'ai pas encore parlés. Ce chapitre sera un peu plus court que les autres mais tout aussi important. ;)

L'auteur

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