• 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 !

TP : Une relique retrouvée

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

Piouf, nous en avons fait du chemin depuis le début du tutoriel. Nous avons vu toutes les notions de base de la programmation OpenGL depuis l'affichage de triangles simples jusqu'à la gestion des caméras mobiles.

Pour conclure cette première partie, nous allons faire un gros TP récapitulatif qui reprendra tout ce que l'on a vu à travers les différents chapitres. Il y aura donc des matrices, des textures, la caméra, etc. Je vous donnerai toutes les indications nécessaires ainsi que quelques conseils pour que votre TP se déroule dans de bonnes conditions.

Si vous vous sentez prêt, vous pouvez continuer. :)

Les consignes

Les consignes

Objectif

Ce premier TP va vous permettre de mettre en pratique toutes les notions que nous avons abordées au cours de cette première partie. L'objectif est de faire une petite scène 3D dans laquelle vous pourrez vous balader à l'aide de votre caméra mobile.

Bien entendu, c'est un projet simple, ne vous attendez pas à avoir de la physique ou des effets avancés. Nous ne sommes qu'à la première partie, nous faisons juste un petit programmer pour débuter. :)

Je vais vous donner les consignes et des petits conseils pour bien démarrer ce TP. Mais avant tout, voyons ensemble à quoi devra ressembler votre rendu final :

Image utilisateur
Image utilisateur

Vous voyez ici la présence d'une cabane ainsi que de trois caisses à l'intérieur. Sur l'une d'elles se trouve d'ailleurs une petite relique en forme de cristal (lancer musique de Tomb Raider ici) qui tourne sur elle-même un peu à la manière du cube dans le chapitre sur la troisième dimension.

Vous devez donc recréer cette scène en incluant tous les éléments suivants :

  • Une cabane placée au centre (0, 0, 0)

  • Plusieurs caisses

  • Un cristal

  • Un sol herbeux entourant toute la scène

  • Un sol terreux à l'intérieur de la cabane

Les dimensions

Afin d'avoir un rendu similaire à celui des images précédentes, je vais vous donner toutes les dimensions nécessaires. Vous n'aurez pas à vous prendre la tête pour les définir vous-même. Je vous donnerai également la répétition de texture à utiliser. Au cas où vous n'auriez pas le gros pack d'images donné dans le chapitre 10, je vous fournirai celles dont vous aurez besoin à la fin des explications.

  • On commence avec les données relatives à la cabane, et plus précisément à ses murs :

Image utilisateur
  • Puis celles des combles (partie triangulaire située au fond de la cabane) :

Image utilisateur
  • Et enfin, celles du toit (vu de haut) :

Image utilisateur

Les deux vertices jaunes sont de cette couleur uniquement pour mieux les voir sur la texture. Ils n'ont rien de spécial.

Aucune des parties de la cabane n'a besoin de la répétition de texture. Vos coordonnées ne doivent donc pas dépasser de l'intervalle [0; 1];

En ce qui concerne les dimensions du sol, on a :

  • Le sol herbeux :

Image utilisateur

Cette fois, votre sol aura besoin de répéter sa texture. Vous devrez le faire 225 fois (15 fois en longueur et en largeur). Ne vous inquiétez pas, l'image n'est chargée qu'une seule fois comme nous l'avons vu. Il n'y a donc rien de dangereux pour votre carte graphique. ^^

  • Le sol terreux :

Image utilisateur

Même chose pour la texture, vous devrez la répéter. Faites le 25 fois ici (5 fois en longueur et en largeur).

Au niveau des caisses, je pense que vous n'avez pas besoin de savoir grand chose. :p Il en faut juste 3 avec un coté de 2.0 unités et les textures Caisse.jpg et Caisse2.jpg.

Enfin, en ce qui concerne le cristal, vous aurez besoin des dimensions suivantes :

Image utilisateur

Il s'agit d'une double pyramide à 4 cotés, il y a donc 8 triangles à créer.

Quelques conseils

Maintenant que nous avons vu ensemble les consignes, je vais vous donner quelques conseils pour vous aider dans votre travail. :)

Les classes

Premièrement, au niveau même de la base de votre projet, je vous conseille de faire différentes classes pour gérer les modèles que vous devez afficher. C'est-à-dire qu'il vaut mieux faire une classe pour la cabane, une autre pour le cristal, etc. Prenez exemple sur le Cube que nous avons codé ensemble mais en intégrant en plus la gestion des textures. Ne vous compliquez pas la tâche en faisant de l’héritage cette fois. ^^

D'ailleurs, vous remarquerez que toutes vos classes contiendront les mêmes attributs : les vertices, les coordonnées de texture, un shader et une ou plusieurs textures. La méthode afficher() sera également identique, seuls les paramètres de la fonction glDrawArrays() vont varier.

La cabane

Au niveau de la cabane, je vous conseille de ne faire qu'une seule et unique classe. Celle-ci contiendrait toutes les données relatives aux murs, au toit et aux combles. Organisez votre tableau de vertices de cette façon pour avoir un code propre :

float verticesTmp[] = {1, 0, 1,   1, 0, 1,   1, 0, 1,      // Mur du Fond
		       1, 0, 1,   1, 0, 1,   1, 0, 1,      // Mur du Fond

		       1, 0, 1,   1, 0, 1,   1, 0, 1,      // Mur Gauche
		       1, 0, 1,   1, 0, 1,   1, 0, 1,      // Mur Gauche

		       ....

		       1, 0, 1,   1, 0, 1,   1, 0, 1,      // Toit Gauche
		       1, 0, 1,   1, 0, 1,   1, 0, 1};     // Toit Droit

Même chose pour le tableau de coordonnées de texture :

float coordTexture[] = {0, 0,   0, 0,   0, 0,        // Mur du Fond 
			0, 0,   0, 0,   0, 0,        // Mur du Fond

			0, 0,   0, 0,   0, 0,        // Mur Gauche
			0, 0,   0, 0,   0, 0,        // Mur Gauche

			.... 

			0, 0,   0, 0,   0, 0,        // Toit Gauche
			0, 0,   0, 0,   0, 0};       // Toit Droit

Pour éviter d'avoir à chercher l'endroit où se situe votre erreur, testez vos vertices triangle par triangle au lieu de tout faire d'un coup. Croyez-moi, ça va vous faire gagner du temps.

Autre point important pour la cabane, il y a deux textures à gérer pour un seul tableau de vertices. Vous devrez jouer avec les paramètres de la fonction glDrawArrays() pour verrouiller vos textures au bon moment.
Par exemple (et je dis bien par exemple), si vos 20 premiers vertices concernent la texture du mur, alors vous devrez faire comme ceci :

// Verrouillage de la texture du mur

glBindTexture(GL_TEXTURE_2D, m_textureMur.getID());


// Affichage des murs (20 premiers vertices)

glDrawArrays(GL_TRIANGLES, 0, 20);


// Déverrouillage de la texture

glBindTexture(GL_TEXTURE_2D, 0);

Et si les 10 vertices suivants concernent la texture du toit, alors vous devrez verrouiller la seconde texture et recommencer l'affichage en partant du 21ième vertex :

// Verrouillage de la texture du toit

glBindTexture(GL_TEXTURE_2D, m_textureToit.getID());


// Affichage du toit (10 vertices suivants)

glDrawArrays(GL_TRIANGLES, 20, 10);


// Déverrouillage de la texture

glBindTexture(GL_TEXTURE_2D, 0);

Les deux verrouillages doivent se passer dans la même méthode bien sûr.

Le cristal

Le cristal ne devrait pas trop vous poser de problème, il suffit de reprendre le même principe que le cube en y ajoutant la gestion de la texture. Faites juste attention à spécifier les coordonnées de texture de cette façon pour chaque triangle :

Image utilisateur

A part ça, n'oubliez pas de l'animer en le faisant tourner sur elle-même. Essayez de faire ça doucement en incrémentant votre angle de 1 degré à chaque tour de boucle au lieu de 4. :)

Les sols

Au niveau des deux types de sol, vous pouvez parfaitement faire deux classes pour les différencier, c'est plus facile et c'est efficace. Cependant, si vous pensez que vous pouvez le faire, je vous suggère de ne faire qu'une seule et unique classe. Vous pourrez l'instancier deux fois en modifiant juste la texture et sa répétition. Un constructeur que vous pouvez utiliser (sans être obligés) est le suivant :

Sol(float longueur, float largeur, int repitionLongueur, int repitionLargeur, std::string vertexShader, std::string fragmentShader, std::string texture);

Détail 1 : La terre doit évidemment se trouver au niveau de la cabane et pas autre part.

Détail 2 : Pour éviter de vous retrouvez avec un bug de texture, faites translater le sol herbeux de 0.01 sur l'axe Y :

// Sauvegarde de la matrice

mat4 sauvegardeModelview = modelview;


    // Affichage du sol herbeux

    modelview = translate(modelview, vec3(0, 0.01, 0));
    solHerbeux.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Vous pouvez tenter de faire sans cette translation si vous le souhaitez pour voir ce que ça donne.

A vos classes !

Bon, je pense vous en avoir assez dit, je ne vais pas vous donner tout le code non plus. ^^ Vous avez à disposition toutes les données dont vous avez besoin ainsi que quelques conseils pour commencer.

C'est à vous de jouer maintenant !

Télécharger : Pack de textures à utiliser pour le TP

Correction

Stop, arrêtez là ! Rendez-moi vos codes. :p

Bon, cette phrase ne sert pas à grand chose mais ça veut au moins dire que nous pouvons passer à la correction. Je vous propose une des solutions possibles, il y avait plusieurs façons d’aborder ce TP donc ne vous inquiétez pas si vous avez fait différemment.

Nous allons nous mettre sans plus tarder dans le bain en commençant par le code de base de la boucle principale.

La boucle principale

La méthode bouclePrincipale() est celle qui va nous permettre de mettre en scène tous nos objets. Elle devra donc contenir la cabane, les caisses, ... Mais pour le moment, il y a juste les matrices, la caméra, le nettoyage et l'actualisation de la fenêtre :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

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


    // Matrices

    mat4 projection;
    mat4 modelview;

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


    // Caméra mobile

    Camera camera(vec3(3, 4, 10), vec3(0, 2.1, 2), vec3(0, 1, 0), 0.5, 0.5);
    m_input.afficherPointeur(false);
    m_input.capturerPointeur(true);


    // Boucle principale

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

        debutBoucle = SDL_GetTicks();


        // Gestion des évènements

        m_input.updateEvenements();

        if(m_input.getTouche(SDL_SCANCODE_ESCAPE))
            break;

        camera.deplacer(m_input);


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        // Gestion de la caméra

        camera.lookAt(modelview);


        

        // Rendu

        ....




        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(m_fenetre);


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

Les objets à afficher seront ajoutés après.

La cabane

Le header

On passe maintenant aux différents modèles qui doivent être placés dans notre scène, on commence par la cabane.

La première chose avec elle est évidemment de créer une classe Cabane. Elle doit contenir tous les attributs nécessaires à l'affichage comme le tableau de vertices, celui des coordonnées de texture, le shader et les textures (une pour le toit et l'autre pour les murs) :

#ifndef DEF_CABANE
#define DEF_CABANE


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


// Autres includes

#include "Shader.h"
#include "Texture.h"


// Classe Cabane

class Cabane
{
    public:

    Cabane(std::string const vertexShader, std::string const fragmentShader);
    ~Cabane();

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


    private:

    Shader m_shader;
    Texture m_textureMur;
    Texture m_textureToit;

    float m_vertices[99];
    float m_coordTexture[66];
};

#endif

Remarquez la méthode afficher() qui est la même que celle du Cube.

Mis à part le fait qu'il faille deux textures ici, cette architecture de classe est la même pour tous les objets présents dans ce TP. ;)

Le constructeur

Le constructeur est la méthode qui vous a probablement posés le plus de problèmes. Celui-ci doit initialiser tous les attributs et les premiers sont le shader et les deux textures :

Cabane::Cabane(std::string const vertexShader, std::string const fragmentShader) : m_shader(vertexShader, fragmentShader),
                                                                                   m_textureMur("Textures/Mur.jpg"), m_textureToit("Textures/Toit.jpg")
{
    // Chargement du shader

    m_shader.charger();


    // Chargement des textures

    m_textureMur.charger();
    m_textureToit.charger();
}

L'attribut le plus compliqué à initialiser était le tableau de vertices, il fallait créer tous les triangles nécessaires à l'aide des schémas fournis dans l'énoncé. Je vous avais donné quelques conseils pour y arriver comme le fait de vérifier votre affichage triangle par triangle par exemple.

La chose à ne pas faire était de créer une classe pour chaque partie de la cabane (Murs, Toit et Combles). C'est plus compliqué de gérer toutes ces parties indépendamment plutôt que tout réunir dans un seul tableau.

En prenant votre temps, vous avez pu trouver le tableau de vertices suivant :

// Vertices temporaires

float verticesTmp[] = {-5, 0, -5,   5, 0, -5,   5, 5, -5,      // Mur du Fond
                       -5, 0, -5,   -5, 5, -5,   5, 5, -5,     // Mur du Fond

                       -5, 0, -5,   -5, 0, 5,   -5, 5, 5,      // Mur Gauche
                       -5, 0, -5,   -5, 5, -5,   -5, 5, 5,     // Mur Gauche

                       5, 0, -5,   5, 0, 5,   5, 5, 5,         // Mur Droit
                       5, 0, -5,   5, 5, -5,   5, 5, 5,        // Mur Droit

                       -5, 5, -5,   5, 5, -5,   0, 6, -5,      // Combles

                       -6, 4.8, -6,   -6, 4.8, 6,   0, 6, 6,   // Toit Gauche
                       -6, 4.8, -6,   0, 6, -6,   0, 6, 6,     // Toit Gauche

                       6, 4.8, -6,   6, 4.8, 6,   0, 6, 6,     // Toit Droit
                       6, 4.8, -6,   0, 6, -6,   0, 6, 6};     // Toit Droit

J'ai aéré le code pour que l'on puisse bien distinguer les parties de la cabane. Je vous recommande de faire ça pour vos modèles. :)

Au niveau du tableau de coordonnées de texture, il faut juste faire correspondre les triangles aux bonnes coordonnées de la même façon que nous le faisions dans les chapitres précédents :

// Coordonnées de texture temporaires

float coordTexture[] = {0, 0,   1, 0,   1, 1,        // Mur du Fond
                        0, 0,   0, 1,   1, 1,        // Mur du Fond

                        0, 0,   1, 0,   1, 1,        // Mur Gauche
                        0, 0,   0, 1,   1, 1,        // Mur Gauche

                        0, 0,   1, 0,   1, 1,        // Mur Droit
                        0, 0,   0, 1,   1, 1,        // Mur Droit

                        0, 0,   1, 0,   0.5, 0.5,    // Combles

                        0, 0,   1, 0,   1, 1,        // Toit Gauche
                        0, 0,   0, 1,   1, 1,        // Toit Gauche

                        0, 0,   1, 0,   1, 1,        // Toit Droit
                        0, 0,   0, 1,   1, 1};       // Toit Droit

Attention cependant aux combles qui ne sont représentés que par un seul triangle.

Une fois les tableaux définis, il ne manque plus qu'à copier leurs valeurs dans les attributs finaux :

// Copie des vertices

for(int i(0); i < 99; i++)
    m_vertices[i] = verticesTmp[i];


// Copie des coordonnées

for(int i(0); i < 66; i++)
    m_coordTexture[i] = coordTexture[i];

Si on résume :

Cabane::Cabane(std::string const vertexShader, std::string const fragmentShader) : m_shader(vertexShader, fragmentShader), 
                                                                                   m_textureMur("Textures/Mur.jpg"), m_textureToit("Textures/Toit.jpg")
{
    // Chargement du shader

    m_shader.charger();


    // Chargement des textures

    m_textureMur.charger();
    m_textureToit.charger();


    // Vertices temporaires

    float verticesTmp[] = {-5, 0, -5,   5, 0, -5,   5, 5, -5,      // Mur du Fond
                           -5, 0, -5,   -5, 5, -5,   5, 5, -5,     // Mur du Fond

                           -5, 0, -5,   -5, 0, 5,   -5, 5, 5,      // Mur Gauche
                           -5, 0, -5,   -5, 5, -5,   -5, 5, 5,     // Mur Gauche

                           5, 0, -5,   5, 0, 5,   5, 5, 5,         // Mur Droit
                           5, 0, -5,   5, 5, -5,   5, 5, 5,        // Mur Droit

                           -5, 5, -5,   5, 5, -5,   0, 6, -5,      // Combles

                           -6, 4.8, -6,   -6, 4.8, 6,   0, 6, 6,   // Toit Gauche
                           -6, 4.8, -6,   0, 6, -6,   0, 6, 6,     // Toit Gauche

                           6, 4.8, -6,   6, 4.8, 6,   0, 6, 6,     // Toit Droit
                           6, 4.8, -6,   0, 6, -6,   0, 6, 6};     // Toit Droit


    // Coordonnées de texture temporaires

    float coordTexture[] = {0, 0,   1, 0,   1, 1,        // Mur du Fond
                            0, 0,   0, 1,   1, 1,        // Mur du Fond

                            0, 0,   1, 0,   1, 1,        // Mur Gauche
                            0, 0,   0, 1,   1, 1,        // Mur Gauche

                            0, 0,   1, 0,   1, 1,        // Mur Droit
                            0, 0,   0, 1,   1, 1,        // Mur Droit

                            0, 0,   1, 0,   0.5, 0.5,    // Combles

                            0, 0,   1, 0,   1, 1,        // Toit Gauche
                            0, 0,   0, 1,   1, 1,        // Toit Gauche

                            0, 0,   1, 0,   1, 1,        // Toit Droit
                            0, 0,   0, 1,   1, 1};       // Toit Droit



    // Copie des vertices

    for(int i(0); i < 99; i++)
        m_vertices[i] = verticesTmp[i];


    // Copie des coordonnées

    for(int i(0); i < 66; i++)
        m_coordTexture[i] = coordTexture[i];
}
La méthode afficher()

En ce qui concerne la méthode afficher(), on peut reprendre celle de la classe Caisse mais en y faisant quelques modifications. En effet, il faut gérer l'affichage de deux textures ici et donc faire attention à savoir quels vertices correspondent à quelle texture.

D'après le tableau m_vertices, les 21 premiers vertices, représentés par les 63 premières cases, correspondent aux murs et aux combles qui utilisent la texture Mur.jpg. Il faut donc appeler la fonction glDrawArrays() pour afficher les 21 vertices en partant de celui d'indice 0 (le premier) :

// Verrouillage de la texture du Mur

glBindTexture(GL_TEXTURE_2D, m_textureMur.getID());


// Rendu

glDrawArrays(GL_TRIANGLES, 0, 21);


// Déverrouillage de la texture

glBindTexture(GL_TEXTURE_2D, 0);

Bien entendu, on n'oublie pas de verrouiller la texture du mur. ;)

Ensuite, il faut s'occuper du toit qui occupe les 12 derniers vertices, représentés par les 36 dernières cases du tableau. On ré-appelle donc la fonction glDrawArrays() pour afficher les 12 vertices en partant de celui d'indice 21 (le 22ième) :

// Verrouillage de la texture du Toit

glBindTexture(GL_TEXTURE_2D, m_textureToit.getID());


// Rendu

glDrawArrays(GL_TRIANGLES, 21, 12);


// Déverrouillage de la texture

glBindTexture(GL_TEXTURE_2D, 0);

N'oubliez pas de verrouiller l'autre texture maintenant.

Ce qui donne la méthode final :

void Cabane::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 des coordonnées de texture

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


        // 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 du Mur

        glBindTexture(GL_TEXTURE_2D, m_textureMur.getID());


        // Rendu

        glDrawArrays(GL_TRIANGLES, 0, 21);


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


        // Verrouillage de la texture du Toit

        glBindTexture(GL_TEXTURE_2D, m_textureToit.getID());


        // Rendu

        glDrawArrays(GL_TRIANGLES, 21, 12);


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


        // Désactivation des tableaux

        glDisableVertexAttribArray(2);
        glDisableVertexAttribArray(0);


    // Désactivation du shader

    glUseProgram(0);
}

Le sol

Le header

La correction du sol va être un peu spécial. En fait, tout dépend de la manière dont vous l'avez codé. Je vais considérer le fait que vous n'ayez fait qu'une seule classe pour gérer les deux types de sol (herbeux et terreux). Bien entendu, si vous en avez fait deux ça fonctionne quand même, il n'y a pas qu'une seule solution je le répète. ^^

Commençons par le header qui ressemble quasiment trait pour trait à celui de la classe Cabane

#ifndef DEF_SOL
#define DEF_SOL


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


// Autres includes

#include "Shader.h"
#include "Texture.h"


// Classe Sol

class Sol
{
    public:

    Sol(float longueur, float largeur, int repetitionLongueur, int repetitionLargeur, std::string const vertexShader, std::string const fragmentShader, std::string const texture);
    ~Sol();

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


    private:

    Shader m_shader;
    Texture m_texture;

    float m_vertices[18];
    float m_coordTexture[12];
};

#endif

La seule différence par rapport au précédent est le fait que nous n'avons besoin qu'une d'une seule texture ici.

Le constructeur

Si vous n'avez utilisé qu'une seule classe pour le sol, vous vous êtes peut-être confrontés au problème de la répétition de texture. Je vous avais donné une petite piste avec le constructeur suivant :

Sol(float longueur, float largeur, int repetitionLongueur, int repetitionLargeur, std::string const vertexShader, std::string const fragmentShader, std::string const texture);

Celui-ci prend 7 paramètres :

  • longueur et largeur : pour définir la taille du sol

  • repitionLongueur et repitionLargeur : pour définir la répétition de la texture

  • vertexShader et fragmentShader : qui représentent les fichiers sources du shader

  • texture : qui représente le chemin vers l'image à utiliser

Je passe très rapidement sur le début du constructeur qui ne fait qu'initialiser le shader et la texture :

Sol::Sol(float longeur, float largeur, int repetitionLongueur, int repetitionLargeur, std::string const vertexShader, std::string const fragmentShader, std::string const texture) : 
         m_shader(vertexShader, fragmentShader), m_texture(texture)
{
    // Chargement du shader

    m_shader.charger();


    // Chargement de la texture

    m_texture.charger();
}

Les deux premiers paramètres de la liste ci-dessus servent à initialiser le tableau de vertices en créant deux triangles. Ces derniers formeront eux-mêmes un rectangle qui représentera le sol :

// Vertices temporaires

float verticesTmp[] = {-longueur, 0, -largeur,   longueur, 0, -largeur,   longueur, 0, largeur,     // Triangle 1
                       -longueur, 0, -largeur,   -longueur, 0, largeur,   longueur, 0, largeur};    // Triangle 2

Le petit piège ici serait de ne pas toucher aux paramètres longueur et largeur avant d'initialiser les vertices. En effet, si on ne le fait pas, le sol sera doublé :

Image utilisateur

Pour éviter cela, il faut diviser par 2 les paramètres longueur et largeur avant de les utiliser dans le tableau :

Image utilisateur

Nous avons fait la même chose dans la classe Cube pour éviter de doubler la taille de nos modèles.

// Division de la taille

longueur /= 2.0;
largeur /= 2.0;


// Vertices temporaires

float verticesTmp[] = {-longueur, 0, -largeur,   longueur, 0, -largeur,   longueur, 0, largeur,     // Triangle 1
                       -longueur, 0, -largeur,   -longueur, 0, largeur,   longueur, 0, largeur};    // Triangle 2

A noter que si vos vertices partent d'un coin, comme le coin inférieur gauche par exemple, alors vous ne devez pas faire ces divisions :

Image utilisateur

En ce qui concerne le tableau de coordonnées de texture, il suffit de reprendre celui que nous avons utilisé pour les carrés dans les chapitres précédents et d'y remplacer les valeurs 1 par les paramètres repitionLongueur et repitionLargeur :

// Coordonnées de texture temporaires

float coordTexture[] = {0, 0,   repetitionLongueur, 0,   repetitionLongueur, repetitionLargeur,      // Triangle 1
                        0, 0,   0, repetitionLargeur,   repetitionLongueur, repetitionLargeur};      // Triangle 2

Une fois les tableaux définis, on copie leurs valeurs dans les attributs :

// Copie des vertices

for(int i(0); i < 18; i++)
    m_vertices[i] = verticesTmp[i];


// Copie des coordonnées

for(int i(0); i < 12; i++)
    m_coordTexture[i] = coordTexture[i];

Ce qui donne :

Sol::Sol(float longueur, float largeur, int repetitionLongueur, int repetitionLargeur, std::string const vertexShader, std::string const fragmentShader, std::string const texture) : 
         m_shader(vertexShader, fragmentShader), m_texture(texture)
{
    // Chargement du shader

    m_shader.charger();


    // Chargement de la texture

    m_texture.charger();


    // Division de la taille

    longueur /= 2.0;
    largeur /= 2.0;


    // Vertices temporaires

    float verticesTmp[] = {-longueur, 0, -largeur,   longueur, 0, -largeur,   longueur, 0, largeur,     // Triangle 1
                           -longueur, 0, -largeur,   -longueur, 0, largeur,   longueur, 0, largeur};    // Triangle 2


    // Coordonnées de texture temporaires

    float coordTexture[] = {0, 0,   repetitionLongueur, 0,   repetitionLongueur, repetitionLargeur,      // Triangle 1
                            0, 0,   0, repetitionLargeur,   repetitionLongueur, repetitionLargeur};      // Triangle 2


    // Copie des vertices

    for(int i(0); i < 18; i++)
        m_vertices[i] = verticesTmp[i];


    // Copie des coordonnées

    for(int i(0); i < 12; i++)
        m_coordTexture[i] = coordTexture[i];
}
La méthode afficher()

Pour l'affichage, il suffit juste de reprendre la méthode afficher() de la classe Caisse, et non Cube cette fois-ci, et d'y modifier le nombre de vertices à afficher. Il y a 2 triangles dans notre cas, il en faut donc 6 :

void Sol::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 des coordonnées de texture

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


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


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


        // Désactivation des tableaux

        glDisableVertexAttribArray(2);
        glDisableVertexAttribArray(0);


    // Désactivation du shader

    glUseProgram(0);
}

Le cristal

Le header

Le cristal est le modèle le plus simple du TP. Il suffit de reprendre le même principe que la classe Cube en ajoutant la gestion de texture. Voici le header :

#ifndef DEF_CRISTAL
#define DEF_CRISTAL


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


// Autres includes

#include "Shader.h"
#include "Texture.h"


// Classe Cristal

class Cristal
{
    public:

    Cristal(std::string const vertexShader, std::string const fragmentShader, std::string const texture);
    Cristal();

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


    private:

    Shader m_shader;
    Texture m_texture;

    float m_vertices[72];
    float m_coordTexture[48];
};

#endif
Le constructeur

Le tableau de vertices :

// Vertices temporaires

float verticesTmp[] = {-0.5, 0, -0.5,   0.5, 0, -0.5,   0, 1, 0,      // Triangle 1
                       0.5, 0, -0.5,   0.5, 0, 0.5,  0, 1, 0,         // Triangle 2
                       0.5, 0, 0.5,   -0.5, 0, 0.5,   0, 1, 0,        // Triangle 3
                       -0.5, 0, 0.5,   -0.5, 0, -0.5,   0, 1, 0,      // Triangle 4

                       -0.5, 0, -0.5,   0.5, 0, -0.5,   0, -1, 0,     // Triangle 5
                       0.5, 0, -0.5,   0.5, 0, 0.5,  0, -1, 0,        // Triangle 6
                       0.5, 0, 0.5,   -0.5, 0, 0.5,   0, -1, 0,       // Triangle 7
                       -0.5, 0, 0.5,   -0.5, 0, -0.5,   0, -1, 0};    // Triangle 8

Et le tableau de coordonnées de texture :

// Coordonnées de texture temporaires

float coordTexture[] = {0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 1
                        0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 2
                        0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 3
                        0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 4
                        0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 5
                        0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 6
                        0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 7
                        0, 0,   0.5, 0,   0.5, 0.5};     // Triangle 8

Ce qui donne le constructeur :

Cristal::Cristal(std::string const vertexShader, std::string const fragmentShader, std::string const texture) : m_shader(vertexShader, fragmentShader), m_texture(texture)
{
    // Chargement du shader

    m_shader.charger();


    // Chargement de la texture

    m_texture.charger();


    // Vertices temporaires

    float verticesTmp[] = {-0.5, 0, -0.5,   0.5, 0, -0.5,   0, 1, 0,      // Triangle 1
                           0.5, 0, -0.5,   0.5, 0, 0.5,  0, 1, 0,         // Triangle 2
                           0.5, 0, 0.5,   -0.5, 0, 0.5,   0, 1, 0,        // Triangle 3
                           -0.5, 0, 0.5,   -0.5, 0, -0.5,   0, 1, 0,      // Triangle 4

                           -0.5, 0, -0.5,   0.5, 0, -0.5,   0, -1, 0,     // Triangle 5
                           0.5, 0, -0.5,   0.5, 0, 0.5,  0, -1, 0,        // Triangle 6
                           0.5, 0, 0.5,   -0.5, 0, 0.5,   0, -1, 0,       // Triangle 7
                           -0.5, 0, 0.5,   -0.5, 0, -0.5,   0, -1, 0};    // Triangle 8


    // Coordonnées de texture temporaires

    float coordTexture[] = {0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 1
                            0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 2
                            0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 3
                            0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 4
                            0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 5
                            0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 6
                            0, 0,   0.5, 0,   0.5, 0.5,      // Triangle 7
                            0, 0,   0.5, 0,   0.5, 0.5};     // Triangle 8


    // Copie des vertices

    for(int i(0); i < 72; i++)
        m_vertices[i] = verticesTmp[i];


    // Copie des coordonnées

    for(int i(0); i < 48; i++)
        m_coordTexture[i] = coordTexture[i];
}
La méthode afficher()

Le reste de la classe est identique à celle du sol. La seule différence concerne la fonction glDrawArrays() qui doit afficher 24 vertices et non 6 :

void Cristal::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 des coordonnées de texture

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


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


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


        // Désactivation des tableaux

        glDisableVertexAttribArray(2);
        glDisableVertexAttribArray(0);


    // Désactivation du shader

    glUseProgram(0);
}

Afficher le tout

Maintenant que nous avons toutes nos classes, il ne nous reste plus qu'à les instancier pour remplir notre scène. Celle-ci doit contenir une cabane, deux types de sol, un cristal et quelques caisses :

// Cabanne

Cabane cabane("Shaders/texture.vert", "Shaders/texture.frag");


// Sols

Sol solHerbeux(30.0, 30.0, 15, 15, "Shaders/texture.vert", "Shaders/texture.frag", "Textures/Herbe.jpg");
Sol solTerreux(10.0, 10.0, 5, 5, "Shaders/texture.vert", "Shaders/texture.frag", "Textures/Sol.jpg");


// Caisses

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


// Cristal

Cristal cristal("Shaders/texture.vert", "Shaders/texture.frag", "Textures/Cristal.tga");
float angle(0.0);

Au niveau de l'affichage, il suffit d'appeler la méthode afficher() pour tous les objets. On commence avec la cabane et le sol :

// Affichage de la cabane

cabane.afficher(projection, modelview);


// Affichage du sol terreux

solTerreux.afficher(projection, modelview);


// Affichage du sol herbeux

mat4 sauvegardeModelview = modelview;

    modelview = translate(modelview, vec3(0, -0.01, 0));
    solHerbeux.afficher(projection, modelview);

modelview = sauvegardeModelview;

Il y a deux détails qui doivent retenir notre attention ici :

  • La translation de 0.01 sur l'axe Y qui permet d'éviter un gros bug de texture.

  • Les objets cabane et solTerreux qui ne sont pas encadrés une sauvegarde de la matrice. Vu que nous n'utilisons aucune transformation pour les afficher, nous n'en avons pas besoin.

Il ne manque plus qu'à placer les caisses et le cristal à l'aide de différentes translations. On commence avec les caisses sans oublier de sauvegarder et restaurer la matrice modelview. D'ailleurs, il n'y a pas besoin de redéclarer la sauvegarde vu qu'elle l'a déjà été avant. ;)

// Sauvegarde de la matrice

sauvegardeModelview = modelview;


    // Première caisse

    modelview = translate(modelview, vec3(-2.5, 1, -3));
    caisse.afficher(projection, modelview);


    // Deuxième caisse

    modelview = translate(modelview, vec3(5, 0, 1));
    caisseDanger.afficher(projection, modelview);


    // Troisième caisse

    modelview = translate(modelview, vec3(-2.5, 0, 4));
    caisse.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Vu que les caisses sont assez proches, nous pouvons nous permettre de ne sauvegarder et de ne restaurer la matrice modelview qu'une seule fois.

On termine l'affichage par le fameux cristal qui doit pivoter sur lui-même indéfiniment. On utilise pour cela la même technique que celle du cube dans le chapitre sur la 3D, à savoir l'utilisation d'un angle incrémenté de 1 à chaque tour de boucle. Si cet angle dépasse 360° alors on lui soustrait justement 360° :

// Sauvegarde de la matrice

sauvegardeModelview = modelview;


    // Affichage des caisses

    ....


    // Rotation du cristal

    angle++;
 
    if(angle > 360)
        angle -= 360;


    // Affichage du cristal

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

    cristal.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Faites attention à placer ce bout de code dans le bloc "sauvegarde/restauration de la matrice" des caisses, ne refaites pas de sauvegarde juste pour afficher le cristal. ;)

Téléchargement

Comme toute fin de chapitre, je vous propose de télécharger une archive contenant le code que nous avons fait. Ou plutôt que vous avez fait dans ce cas. :p

Télécharger (Windows - UNIX/Linux) : Correction du TP - Une relique retrouvée

Nous venons de terminer notre premier TP, celui-ci nous a permis d'utiliser toutes les notions que avons vues sur OpenGL. :D

Je vous conseille de mettre de coté le code que vous avez réalisé vous-même, le TP de la deuxième partie sera basé dessus. Bien évidemment, nous y rajouterons une multitude de nouveautés, nous les verrons toutes ensemble.

Je vous invite d'ailleurs à lire la deuxième partie de ce tutoriel qui sera consacrée aux notions avancées d'OpenGL. Je vous parlerai enfin des shaders en détails, vous saurez tout à leur sujet depuis la classe qui leur est dédiée jusqu'aux petits fichiers sources du dossier Shaders. Nous verrons également pas mal d'autres choses qui seront principalement axées autour de la carte graphique. ^^

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