Développez vos applications 3D avec OpenGL 3.3
Last updated on Friday, June 21, 2013
  • Moyen

Free online content available in this course.

Got it!

Premier affichage

Nouveau chapitre, nouvelles notions.

Nous allons apprendre ici à faire nos premiers affichages dans la fenêtre SDL. Les notions que nous allons aborder nous seront utiles tout au long de ce tutoriel et constituent réellement la base du fonctionnement d'OpenGL. Vous aurez l'occasion de vous exercer à travers quelques exercices. Je ré-utiliserai ce procédé régulièrement car il n'y a rien de mieux que la pratique pour apprendre quelque chose.

Ceci étant dit, commençons dès maintenant ce nouveau chapitre. :)

Le fonctionnement d'OpenGL

Nous allons commencer ce chapitre par un peu de théorie. :p

Comment fonctionne OpenGL ?

Basiquement, OpenGL fonctionne avec un système de "points" que nous plaçons dans un monde 3D. Nous appelons un point, un vertex (au pluriel : vertices), en français on peut dire un sommet. La première chose à faire avec ces vertices, c'est leur donner une position.

Une position dans un monde 3D est constituée de 3 coordonnées: x (la largeur), y (la hauteur) et z (la profondeur). Chaque vertex doit avoir ses propres coordonnées. Les vertices sont à la base de tout, ils forment tout ce que vous voyez dans un jeu-vidéo : le héros, une arme, une voiture, des chaussettes ...

Une fois les vertices définis, on les relie entre eux pour former des formes géométriques simples : des carrés, des lignes, des triangles, ...

Image utilisateur

Voici comment OpenGL "voit" une map : une succession de vertices reliés entre eux pour former un terrain en mode filaire.

Maintenant que l'on a nos formes géométriques, il faut les "colorier", sinon nous n'aurions que des fils difformes qui ne ressembleraient à rien. Imaginez si on laissait tout comme ça, le jeu serait un peu particulier.

On peut distinguer deux formes de "coloriage": les textures et les couleurs (que nous spécifions nous-même). Les textures sont utilisées dans 99% des cas. En effet, nous ne spécifions que très rarement la couleur de nos objets directement, en général on le fait pour des tests ou dans un shader (retenez bien ce mot, nous verrons cela plus tard). Dans les premiers chapitres nous n'utiliserons pas les textures, nous accorderons un chapitre entier pour cela.

Ce qu'il faut retenir c'est qu'au final nous définissons les coordonnées de nos vertices (sommets) dans l'espace, puis nous les relions entre eux pour former une forme géométrique, enfin nous colorions cette forme pour avoir quelque chose de "consistant".

Nomenclature des fonctions OpenGL

Comme vous l'aurez remarqué avec la SDL, chaque fonction de la librairie commence par "SDL_" . Bonne nouvelle, avec OpenGL c'est pareil, chaque fonction commence par la même chose : "gl".

Autre point, il se peut que vous soyez surpris en voyant le nom de certaines fonctions se répéter plusieurs fois, c'est normal.

Prenons un exemple : glUniform2f(...) et glUniform2i(...)

Nous verrons l'utilité de ces fonctions plus tard. Vous voyez la différence ? Dans la première, la dernière lettre est un "f" et dans la deuxième c'est un "i".

En général, la dernière lettre d'une fonction OpenGL sert à indiquer le type du paramètre à envoyer :

  • i : integer (entier)

  • f : float (réel)

  • d : double (réel plus puissant)

  • ub : unsigned byte (octet entre 0 et 255)

  • ...

Selon le type de paramètre que vous enverrez il faudra utiliser la fonction correspondant au type de vos variables. :)

Vous remarquez aussi que l'avant-dernière lettre est un chiffre, ce chiffre peut lui aussi changer. En général, si le chiffre est 1 on envoie un seul paramètre, si c'est 2 on en envoie deux, ...

Autre point que vous remarquerez plus tard : les types de variables OpenGL. Vous tomberez souvent sur des fonctions demandant des paramètres de types GLfloat, GLbool, GLchar, ... Ce sont simplement des variables de type float, char, int, ... Le type GLbool ne pourra prendre que deux valeurs : soit GL_FALSE (faux) soit GL_TRUE (vrai).

Boucle principale

Comme avec la SDL, OpenGL fonctionne avec une boucle principale. Tous les calculs se feront dans cette boucle. De plus, à chaque affichage il va falloir effacer l'écran car la scène aura légèrement changée, puis ré-afficher chaque élément un à un. Voici la fonction permettant d'effacer l'écran :

glClear(GLbitfield mask)

Dans un premier temps on effacera uniquement ce qui se trouve à l'écran grâce au paramètre : GL_COLOR_BUFFER_BIT.

Avec la SDL, pour actualiser l'affichage on utilisait la fonction SDL_Flip(), mais avec OpenGL on utilisera la fonction :

SDL_GL_SwapWindow(SDL_Window* window);

Le paramètre window étant notre structure SDL_WindowID. ;)

Malheureusement il existe encore une différence entre Linux et Windows. Comme vous l'avez vu dans le chapitre précédent, pour utiliser OpenGL, Windows est obligé de passer par une autre librairie du nom de GLEW. Cette librairie va nous permettre d'utiliser les fonctions d'OpenGL 3. Mais comme toute librairie, il va falloir l'initialiser. Sous Windows vous devrez inclure l'en-tête suivant :

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

Pour initialiser la librairie GLEW, il faudra ajouter la fonction : glewInit(). Comme toute librairie, l'initialisation peut échouer, il faut donc tester son initialisation :

// On initialise GLEW

GLenum initialisationGLEW( glewInit() );


// Si l'initialisation a échouée :

if(initialisationGLEW != GLEW_OK)
{
    // On affiche l'erreur grâce à la fonction : glewGetErrorString(GLenum code)

    std::cout << "Erreur d'initialisation de GLEW : " << glewGetErrorString(initialisationGLEW) << std::endl;


    // On quitte la SDL

    SDL_GL_DeleteContext(contexteOpenGL);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return -1;
}

Voila pour Windows. Pour Linux c'est beaucoup plus simple, il suffit de placer une "define" spéciale pour indiquer que nous utiliserons les fonctions d'OpenGL 3 puis d'inclure le fichier d'en-tête "gl3.h" :

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

Bien, récapitulons tout ceci :

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

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

#endif

#include <SDL2/SDL.h>
#include <iostream>


int main(int argc, char **argv)
{

    /* *** Création de la fenêtre SDL *** */


    #ifdef WIN32
    
        // On initialise GLEW

        GLenum initialisationGLEW( glewInit() );


        // Si l'initialisation a échouée :

        if(initialisationGLEW != GLEW_OK)
        {
            // On affiche l'erreur grâce à la fonction : glewGetErrorString(GLenum code)

            std::cout << "Erreur d'initialisation de GLEW : " << glewGetErrorString(initialisationGLEW) << std::endl;


            // On quitte la SDL
    
            SDL_GL_DeleteContext(contexteOpenGL);
            SDL_DestroyWindow(fenetre);
           SDL_Quit();

            return -1;
        }

    #endif


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

	SDL_WaitEvent(&evenements);
		
	if(evenements.window.event == SDL_WINDOWEVENT_CLOSE)
	    terminer = true;


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT);


        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(fenetre);
    }


    // On quitte la SDL
	
    SDL_GL_DeleteContext(contexteOpenGL);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return 0;
}

Repère et Origine

J'espère que vous connaissez la définition de ces deux termes. Un repère est un ensemble d'axes représentant au moins les axes X et Y, l'origine est le point de départ de ces axes.

En théorie voici ce que donne le repère d'OpenGL :

Image utilisateur

Mais dans un premier temps, nous utiliserons le repère par défaut d'OpenGL que voici :

Image utilisateur

L'origine du repère se trouve au centre de l'écran, et les coordonnées maximales affichables sont comprises entre (-1, -1) et (1, 1).

Nous utiliserons l'autre repère dès que nous y aurons inclus les matrices :) . De plus, nous serons dans un premier temps dans un monde 2D, nous ne ferons pas de cube ou autre forme complexe pour commencer. Nous verrons cela un peu plus tard. :p

Afficher un triangle

Afficher un triangle

Maintenant que le blabla théorique est terminé, nous pouvons reprendre la programmation. :magicien:

Dans le chapitre précédent, nous avons appris que tous les modèles 3D présents dans un jeu étaient constitués de sommets (vertices), puis qu'il fallait les relier pour former une surface. Et si on assemble ces formes, ça donne nos modèles 3D.

Notre premier exercice est simple : afficher un triangle. Pour pouvoir afficher n'importe quel modèle 3D il faut déjà spécifier ses vertices que nous allons ensuite donner à OpenGL. En général, on définie nos vertices dans un seul tableau, on place chaque coordonnée de vertex à la chaine comme ceci :

float vertices = {X_vertex_1, Y_vertex_1, Z_vertex_1,   X_vertex_2, Y_vertex_2, Z_vertex_2,   ...};

Dans un triangle il y a trois sommets, nous aurons donc 3 vertices.

float vertex1[] = {-0.5, -0.5};
float vertex2[] = {0.0, 0.5};
float vertex3[] = {0.5, -0.5};

N'oubliez pas que nous sommes en 2D pour l'instant, nous n'avons donc que les coordonnées x et y à définir. Ce début de code est bien mais si on utilise 3 tableaux, ça ne fonctionnera pas. Nous devons combiner les trois tableaux pour en former un seul :

float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};

Maintenant, il faut envoyer ces coordonnées à OpenGL. Pour envoyer des informations nous avons besoin d'un tableau appelé "Vertex Attribut". Pour ceux qui connaissent les "Vertex Arrays", c'est la même chose ;) . Sauf que maintenant ce n'est plus une optimisation mais belle et bien la méthode de base pour envoyer nos infos à OpenGL.

Ces "Vertex Attributs" sont déjà inclus dans l'API, il suffit de lui donner des valeurs (nos coordonnées) puis de l'activer. Voici le prototype de la fonction gérant les Vertex Attributs :

void glVertexAttribPointer(GLuint index, GLuint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid *pointer);
  • index : c'est le numéro du tableau, son identifiant.

  • size : c'est le nombre de coordonnées par vertex. En 2D ce sera 2 et en 3D ce sera 3.

  • type : c'est le type de données (float, int, ...).

  • normalized : booléen permettant à OpenGL de normaliser les données (comme les vecteurs).

  • stride : paramètre spécial, on le mettra à zéro tout le temps, nous ne l'utiliserons pas.

  • pointer : c'est le pointeur sur nos données, ici nos coordonnées.

Voyons ce que ça donne avec les données de notre triangle :

glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);

Malheureusement, en l'état, notre tableau est inutilisable, il faut d'abord l'activer avec la fonction :

void glEnableVertexAttribArray(GLuint index);

Le paramètre index étant notre identifiant de tableau, ici ce sera 0.

Bien, maintenant OpenGL sait quels vertices il doit afficher, il ne manque plus qu'à lui dire ... ce qu'il doit faire de ces points. :p

Pour ça, on utilise une fonction (il y en a en fait deux, mais pour le début de ce tutoriel nous n'utiliserons que la première). Cette fonction permet de dire à OpenGL quelle forme afficher avec les points donnés précédemment. Voici le prototype de la fonction :

glDrawArrays(GLenum mode, GLint first, GLsizei count);
  • mode : C'est la forme finale (nous verrons cela juste après).

  • first : C'est l'indice de notre premier vertex à afficher(nous lui donnerons un int).

  • count : C'est le nombre de vertices à afficher depuis first (nous lui donnerons également un int).

Alors Attention ! :

  • Le paramètre first est un indice (comme un indice de tableau), si vous voulez utiliser le premier vertex vous lui donnerez la valeur "0" et pas "1".

  • Pour count, c'est le contraire. Lui il veut le nombre de vertices à utiliser depuis first. Si vous utilisez 4 vertices vous lui donnerez la valeur "4" et pas "3".

Exemple : J'ai 4 vertices et je veux afficher un carré. Le premier vertex sera le vertex 0 (le premier vertex), et la valeur de "count" sera 4 car j'ai besoin du vertex 0 + les 3 vertices qui le suivent.

Passons au paramètre le plus intéressant : mode.

Ce paramètre peut prendre plusieurs formes donc voici les principales :

Valeur

Définition

GL_TRIANGLES

Avec ce mode, chaque groupe de 3 vertices formera un triangle. C'est le mode le plus utilisé.

GL_POLYGON

Tous les vertices s'assemblent pour former un polygone de type convexe (Hexagone, Heptagone, ...).

GL_LINES

Chaque duo de vertices formera une ligne.

GL_TRIANGLE_STRIP

Ici les triangles s'assembleront. Les deux derniers vertices d'un triangle s'assembleront avec un 4ième vertex pour former un nouveau triangle.

Toutes les valeurs possibles ne sont pas représentées mais je vous expose ici les plus utilisées. ;)

Revenons à notre code, nous avons désormais les différents modes d'affichage. Le but de ce chapitre est d'afficher un triangle, notre mode d'affichage sera donc : GL_TRIANGLES, ce qui donne :

glDrawArrays(GL_TRIANGLES, 0, 3);

La valeur "0" pour commencer l'affichage par le premier vertex, et le "3" pour utiliser les 3 vertices dont le premier sera le vertex "0".

Récapitulons tout ça :

// Vertices et coordonnées

float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};


// Boucle principale

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

    ....


    // On remplie puis on active le tableau Vertex Attrib 0

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


    // On affiche le triangle

    glDrawArrays(GL_TRIANGLES, 0, 3);


    // On désactive le tableau Vertex Attrib puisque l'on en a plus besoin

    glDisableVertexAttribArray(0);


    // Actualisation de la fenêtre

    ....
}

Vous avez dû remarquer la présence d'une nouvelle fonction : glDisableVertexAttribArray. Elle permet de désactiver le tableau Vertex Attrib utilisé, le paramètre est le même que celui de la fonction glEnableVertexAttribArray. On désactive le tableau juste après l'affichage des vertices.

Récapitulons tout avec le code SDL :

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

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

#endif

#include <SDL2/SDL.h>
#include <iostream>


int main(int argc, char **argv)
{	
    // Notre fenêtre
	
    SDL_Window* fenetre(0);
    SDL_GLContext contexteOpenGL(0);
	
    SDL_Event evenements;
    bool terminer(false);
	
	
    // Initialisation de la SDL
	
    if(SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        std::cout << "Erreur lors de l'initialisation de la SDL : " << SDL_GetError() << std::endl;
        SDL_Quit();
		
        return -1;
    }
	
	
    // Version d'OpenGL
	
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
	
	
    // Double Buffer
	
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
	
	
    // Création de la fenêtre

    fenetre = SDL_CreateWindow("Test SDL 2.0", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);

    if(fenetre == 0)
    {
        std::cout << "Erreur lors de la creation de la fenetre : " << SDL_GetError() << std::endl;
        SDL_Quit();

        return -1;
    }


    // Création du contexte OpenGL

    contexteOpenGL = SDL_GL_CreateContext(fenetre);

    if(contexteOpenGL == 0)
    {
        std::cout << SDL_GetError() << std::endl;
        SDL_DestroyWindow(fenetre);
        SDL_Quit();

        return -1;
    }


    #ifdef WIN32

        // On initialise GLEW

        GLenum initialisationGLEW( glewInit() );


        // Si l'initialisation a échouée :

        if(initialisationGLEW != GLEW_OK)
        {
            // On affiche l'erreur grâce à la fonction : glewGetErrorString(GLenum code)

            std::cout << "Erreur d'initialisation de GLEW : " << glewGetErrorString(initialisationGLEW) << std::endl;


            // On quitte la SDL

            SDL_GL_DeleteContext(contexteOpenGL);
            SDL_DestroyWindow(fenetre);
            SDL_Quit();

            return -1;
        }

    #endif


    // Vertices et coordonnées

    float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};


    // Boucle principale

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

        SDL_WaitEvent(&evenements);

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


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT);


        // On remplie puis on active le tableau Vertex Attrib 0

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


        // On affiche le triangle

        glDrawArrays(GL_TRIANGLES, 0, 3);


        // On désactive le tableau Vertex Attrib puisque l'on n'en a plus besoin

        glDisableVertexAttribArray(0);


        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(fenetre);
    }


    // On quitte la SDL

    SDL_GL_DeleteContext(contexteOpenGL);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();

    return 0;
}

Si tout se passe bien, vous devriez avoir une belle fenêtre comme celle-ci :

Image utilisateur

Afficher plusieurs triangles

Les vertices

Comme nous l'avons vu dans le tableau précédemment, le paramètre GL_TRIANGLES dans la fonction glDrawArrays() permet d'afficher un triangle pour chaque triplet de vertices.
Pour le moment, nous n'utilisons que 3 vertices, donc nous n'avons au final qu'un seul triangle. Or si nous en utilisons 6 nous aurons alors deux triangles.
On peut même aller plus loin et prendre 60, 390, ou 3000 vertices pour en afficher plein ! C'est d'ailleurs ce que font les décors et les personnages dans les jeux-vidéo, ils ne savent utiliser que ça. :p

Enfin, prenons un exemple plus simple avant d'aller aussi loin. Si nous voulons afficher le rendu suivant ... :

Image utilisateur

... Nous devrons utiliser deux triplets de vertices.

Ce qu'il faut savoir, c'est qu'il ne faut surtout pas utiliser un tableau pour chaque triangle. On perdrait beaucoup trop de temps à tous les envoyer. A la place, nous devons inclure tous les vertices dans un seul et unique tableau :

// Vertices

float vertices[] = {0.0, 0.0,   0.5, 0.0,   0.0, 0.5,          // Triangle 1
                    -0.8, -0.8,   -0.3, -0.8,   -0.8, -0.3};   // Triangle 2

Toutes nos données sont regroupées dans un seul tableau. Ça ne nous fait qu'un seul envoi à faire c'est plus facile à gérer, ça arrange même OpenGL. ;)

L'affichage

Au niveau de l'affichage, le code reste identique à celui du triangle unique. C'est-à-dire qu'il faut utiliser les fonctions :

  • glVertexAttribPointer() : pour donner les vertices à OpenGL

  • glEnableVertexAttribArray() : pour activer le tableau Vertex Attrib

  • glDrawArrays() : pour afficher le tout

La seule différence notable va être la valeur du paramètre count (nombre de vertices à prendre en compte) de la fonction glDrawArrays(). Celui-ci était égal à 3 pour un seul triangle, nous la passerons désormais à 6 pour en afficher deux. L'appel à la fonction ressemblerait donc à ceci :

// Affichage des triangles

glDrawArrays(GL_TRIANGLES, 0, 6);

Ce qui donne le code source de la boucle principale suivant :

// Vertices

float vertices[] = {0.0, 0.0,   0.5, 0.0,   0.0, 0.5,          // Triangle 1
                    -0.8, -0.8,   -0.3, -0.8,   -0.8, -0.3};   // Triangle 2


// Boucle principale

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

    SDL_WaitEvent(&evenements);

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


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT);


    // On remplie puis on active le tableau Vertex Attrib 0

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


    // On affiche des triangles

    glDrawArrays(GL_TRIANGLES, 0, 6);


    // On désactive le tableau Vertex Attrib puisque l'on n'en a plus besoin

    glDisableVertexAttribArray(0);


    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(fenetre);
}

Si vous compilez ce code, vous devriez obtenir :

Image utilisateur

Le tour est joué. :)

La classe SceneOpenGL

Je ne sais pas si vous l'avez remarqué, mais depuis le début du tutoriel nous n'avons pas codé une seule classe. À vrai dire c'est normal, nous n'en avions pas vraiment besoin jusqu'ici.
Cependant nos programmes vont commencer à se complexifier donc autant prendre les bonnes habitudes dès maintenant.

La classe SceneOpenGL

Le header

Pour le moment, nous n'aurons besoin que d'une seule classe dans nos programmes de test. Elle devra être capable de remplir plusieurs rôles :

  • Créer la fenêtre SDL et le contexte OpenGL.

  • Initialiser tous les objets d'une scène (personnages, caisses, sol, ... Nous verrons cela un peu plus tard ;) ).

  • Gérer l'interaction entre les objets tout au long du programme.

Pour remplir ces objetifs, nous allons créer plusieurs méthodes, mais avant cela nous devons déclarer la classe C++ qui s'occupera de tout ça. Cette classe s'appellera : SceneOpenGL. Commençons donc par créer deux fichiers SceneOpenGL.h et SceneOpenGL.cpp.

Une classe en C++ est composée de méthodes et d'attributs. Pour les méthodes, on voit déjà à peu près ce que l'on va faire. En revanche, on ne sait pas encore de quels attributs nous aurons besoin.

Si on regarde le début du code pour afficher le triangle blanc, nous voyons trois variables importantes :

// Variables 

SDL_Window* fenetre(0);
SDL_GLContext contexteOpenGL(0);
SDL_Event evenements;

On voit dans cette liste 3 variables correspondant :

  • À la fenêtre

  • Au contexte OpenGL

  • Aux évènements SDL

Ces 3 variables deviendront les attributs de notre classe. On en rajoutera même trois supplémentaires qui correspondront :

  • Au titre de la fenêtre.

  • À sa largeur

  • À sa hauteur

Si on résume tout ça, nous avons une classe SceneOpenGL avec 6 attributs. Le header ressemblera donc à ceci :

#ifndef DEF_SCENEOPENGL
#define DEF_SCENEOPENGL

#include <string>


class SceneOpenGL
{
    public:

    SceneOpenGL();
    ~SceneOpenGL();


    private:

    std::string m_titreFenetre;
    int m_largeurFenetre;
    int m_hauteurFenetre;

    SDL_Window* m_fenetre;
    SDL_GLContext m_contexteOpenGL;	
    SDL_Event m_evenements;
};

#endif

Et voici donc le squelette de tous nos futurs programmes. :D

Bon pour le moment c'est un peu vide, on va habiller un peu tout ça. On va notamment ajouter deux éléments. Premièrement, il faut ajouter tous les includes concernant la SDL et OpenGL :

// Includes 

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

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

#endif

#include <SDL2/SDL.h>
#include <iostream>

Deuxièmement, il faut modifier le constructeur de la classe pour prendre en compte les paramètres de création de la fenêtre (titre, largeur et hauteur) :

SceneOpenGL(std::string titreFenetre, int largeurFenetre, int hauteurFenetre);

Pour le moment, rien de bien compliqué. ^^

Les méthodes

Si on reprend la liste des objectifs de la classe, on retrouve trois points importants :

  • Créer la fenêtre SDL et le contexte OpenGL.

  • Initialiser tous les objets d'une scène (personnages, caisses, sol, ...).

  • Gérer l'interaction entre les objets tout au long du programme.

Nous allons créer une méthode pour chaque point de cette liste.

La première méthode consistera donc à initialiser la fenêtre et le contexte OpenGL dans lequel nous allons évoluer :

bool initialiserFenetre();

Elle renverra un booléen pour confirmer ou non la création de la fenêtre. Nous mettrons à l'intérieur tout le code permettant de générer la fenêtre.

Pour le deuxième point, nous devrons initialiser tout ce qui concerne OpenGL (mis à part le contexte vu qu'il est créé juste avant). Pour le moment, nous n'avons que la librairie GLEW à initialiser.

Généralement, la fonction qui s'occupe d'initialiser OpenGL s'appelle initGL(), nous appellerons donc notre méthode de la même façon :

bool initGL();

Comme la méthode précédente, elle renverra un booléen pour savoir si l'initialisation s'est bien passée.

Pour le troisième et dernier point, nous devons gérer la boucle principale du programme. Nous créerons donc une méthode bouclePrincipale() qui s'occupera de gérer tout ça :

void bouclePrincipale();

Elle ne renverra aucune valeur.

Résumé du Header

Si on met tout ce que l'on vient de voir dans le header, on a : une classe SceneOpenGL, des includes pour gérer la SDL et OpenGL, 6 attributs et 3 méthodes :

#ifndef DEF_SCENEOPENGL
#define DEF_SCENEOPENGL


// Includes 

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

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

#endif

#include <SDL2/SDL.h>
#include <iostream>
#include <string>


// Classe

class SceneOpenGL
{
    public:

    SceneOpenGL(std::string titreFenetre, int largeurFenetre, int hauteurFenetre);
    ~SceneOpenGL();

    bool initialiserFenetre();
    bool initGL();
    void bouclePrincipale();


    private:

    std::string m_titreFenetre;
    int m_largeurFenetre;
    int m_hauteurFenetre;

    SDL_Window* m_fenetre;
    SDL_GLContext m_contexteOpenGL;	
    SDL_Event m_evenements;
};

#endif

Implémentation de la classe

Dans cette dernière sous-partie, on va se reposer un peu. En effet, on a déjà tout codé avant, on n'a plus qu'à jouer au puzzle en coupant notre code et en mettant les bons morceaux au bon endroit. :p

Constructeur et Destructeur

Pour le constructeur rien de plus simple, on initialise nos attributs sans oublier de passer les 3 paramètres du constructeur concernant la fenêtre :

SceneOpenGL::SceneOpenGL(std::string titreFenetre, int largeurFenetre, int hauteurFenetre) : m_titreFenetre(titreFenetre), m_largeurFenetre(largeurFenetre), 
                                                                                             m_hauteurFenetre(hauteurFenetre), m_fenetre(0), m_contexteOpenGL(0)
{

}

Pour le destructeur, on détruit simplement le contexte et la fenêtre, puis on quitte la librairie SDL :

SceneOpenGL::~SceneOpenGL()
{
    SDL_GL_DeleteContext(m_contexteOpenGL);
    SDL_DestroyWindow(m_fenetre);
    SDL_Quit();
}
Les méthodes initialiserFenetre() et initGL()

Ici on sait déjà ce que l'on va mettre. On va implémenter le code permettant de créer la fenêtre et le contexte OpenGL, nous connaissons ce code depuis le chapitre précédent. Pour migrer celui-ci on va :

  • Prendre tout le code gérant l'initialisation de la SDL et du contexte.

  • Remplacer les deux occurrences de fenetre par "m_fenetre" et de contexteOpenGL par "m_contexteOpenGL".

  • Remplacer les valeurs retournées par "true" pour 0 et "false" pour -1.

  • Remplacer le paramètre title de la fonction SDL_CreateWindow() par la chaine de caractères de l'attribut m_titreFenetre soit m_titreFenetre.c_str().

Encore une fois rien de bien compliqué. ^^

En code, ça nous donne ceci :

bool SceneOpenGL::initialiserFenetre()
{
    // Initialisation de la SDL
	
    if(SDL_Init(SDL_INIT_VIDEO) < 0)
    {
        std::cout << "Erreur lors de l'initialisation de la SDL : " << SDL_GetError() << std::endl;
        SDL_Quit();
		
        return false;
    }
	
	
    // Version d'OpenGL
	
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);
	
	
    // Double Buffer
	
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
	
	
    // Création de la fenêtre
	
    m_fenetre = SDL_CreateWindow(m_titreFenetre.c_str(), SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, m_largeurFenetre, m_hauteurFenetre, SDL_WINDOW_SHOWN | SDL_WINDOW_OPENGL);

    if(m_fenetre == 0)
    {
        std::cout << "Erreur lors de la creation de la fenetre : " << SDL_GetError() << std::endl;
        SDL_Quit();

        return false;
    }


    // Création du contexte OpenGL

    m_contexteOpenGL = SDL_GL_CreateContext(m_fenetre);

    if(m_contexteOpenGL == 0)
    {
        std::cout << SDL_GetError() << std::endl;
        SDL_DestroyWindow(m_fenetre);
        SDL_Quit();

        return false;
    }

    return true;
}

On passe maintenant à la méthode initGL(). Pour le moment, nous n'avons pas grand chose à mettre à l’intérieur mise à part l'initialisation de la librairie GLEW pour Windows.

Et comme précédemment, il va falloir modifier le nom des attributs fenetre et contexteOpenGL en leur ajoutant le prefix "m_". Nous rajouterons également deux return dans la méthode :

  • Un si l'initialisation échoue (donc return false).

  • Et l'autre pour indiquer que l'initialisation s'est bien déroulée (return true).

bool SceneOpenGL::initGL()
{
    #ifdef WIN32

        // On initialise GLEW

        GLenum initialisationGLEW( glewInit() );


        // Si l'initialisation a échoué :

        if(initialisationGLEW != GLEW_OK)
        {
            // On affiche l'erreur grâce à la fonction : glewGetErrorString(GLenum code)

            std::cout << "Erreur d'initialisation de GLEW : " << glewGetErrorString(initialisationGLEW) << std::endl;


            // On quitte la SDL

            SDL_GL_DeleteContext(m_contexteOpenGL);
            SDL_DestroyWindow(m_fenetre);
            SDL_Quit();

            return false;
        }

    #endif


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

    return true;
}
La méthode bouclePrincipale()

Allez on passe à la dernière méthode, c'est dans celle-ci que va se passer la quasi totalité du programme.

Dans cette méthode, on commence par déclarer le booléen terminer que vous devez déjà connaitre. :p On ne l'a pas déclaré en temps qu'attribut car il ne sert que dans la boucle while.

Après ce booléen, on va en profiter pour déclarer notre fameux tableau de vertices. Dans le futur, nous ferons des objets spécialement dédiés pour afficher nos modèles. Mais pour le moment, nous n'avons que de simples vertices à afficher, on peut donc se passer de classe.

Voici donc le début de la méthode bouclePrincipale() :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

    bool terminer(false);
    float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};
}

Pour le reste de la méthode, il suffit simplement d'ajouter la boucle while que nous avons déjà codé. :)

Encore une fois, je me répète mais n'oubliez pas d'ajouter le préfixe "m_" aux attributs evenements et fenetre quand vous copiez le code :

void SceneOpenGL::bouclePrincipale()
{
    // Variables
	
    bool terminer(false);
    float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};
	
	
    // Boucle principale
	
    while(!terminer)
    {
        // Gestion des évènements
		
        SDL_WaitEvent(&m_evenements);
		
        if(m_evenements.window.event == SDL_WINDOWEVENT_CLOSE)
            terminer = 1;
		
		
        // Nettoyage de l'écran
		
        glClear(GL_COLOR_BUFFER_BIT);
		
		
        // On remplie puis on active le tableau Vertex Attrib 0
		
        glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
        glEnableVertexAttribArray(0);
	  	
		
        // On affiche le triangle
		
        glDrawArrays(GL_TRIANGLES, 0, 3);
		
		
        // On désactive le tableau Vertex Attrib puisque l'on n'en a plus besoin
		
        glDisableVertexAttribArray(0);
		
		
        // Actualisation de la fenêtre
		
        SDL_GL_SwapWindow(m_fenetre); 
    }
}
Le fichier main.cpp

Comme d'habitude en C++, la fonction main() sera la fonction la moins chargée du programme. Jusqu'à maintenant, nous codions tout à l'intérieur de cette fonction, ce qui la rendait un peu illisible.

Maintenant, nous allons juste déclarer un objet, l'initialiser et lancer sa boucle principale. ^^ Pour ça, il faut inclure le header de la classe SceneOpenGL dans le fichier main.cpp :

#include "SceneOpenGL.h"

Ensuite, on va déclarer un objet de type SceneOpenGL avec les bons paramètres :

int main(int argc, char **argv)
{
    // Création de la sène
	
    SceneOpenGL scene("Chapitre 3", 800, 600);
}

Maintenant, il faut appeler les deux méthodes qui permettent d'initialiser le programme correctement à savoir initialiserFenetre() et initGL(). De plus, il faut vérifier que ces méthodes retournent bien le booléen true et pas false. Si au moins une des deux initialisions échoue, alors on quitte le programme :

// Initialisation de la scène
	
if(scene.initialiserFenetre() == false)
    return -1;
	
if(scene.initGL() == false)
    return -1;

Enfin, on appelle la méthode bouclePrincipale() pour lancer la scène OpenGL :

// Boucle Principale
	
scene.bouclePrincipale();

N'oublions pas le return 0 lorsque l'on quittera le programme :

// Fin du programme

return 0;

Si on résume tout ça :

#include "SceneOpenGL.h"


int main(int argc, char **argv)
{
    // Création de la sène
	
    SceneOpenGL scene("Chapitre 3", 800, 600);
	
	
    // Initialisation de la scène
	
    if(scene.initialiserFenetre() == false)
	return -1;
	
    if(scene.initGL() == false)
	return -1;
	
	
    // Boucle Principale
	
    scene.bouclePrincipale();
	
	
    // Fin du programme
	
    return 0;
}

Et voilà ! Il ne vous reste plus qu'à compiler tout ça. Vous devriez avoir le même résultat qu'au dessus mais vous avez codée proprement une classe en C++ avec du code OpenGL à l'intérieur. ^^

Nous ferons plein de classes au fur et à mesure de ce tuto, vous allez vite en prendre l'habitude. ;)

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

Exercices

Énoncés

Tout au long de ce tutoriel, je vous ferai faire quelques petits exercices pour que vous appliquiez ce que nous aurons vu dans les chapitres. Ce seront des exercices assez simples, il n'y aura rien de farfelu je vous rassure. Évidemment, vous avez le droit de vous aider du cours, je ne vous demande pas de tout retenir d'un coup à chaque fois. D'ailleurs, les solutions sont fournies juste après les énoncés, ne les regardez pas avant sinon ça n'a aucun intérêt. ;)

Exercice 1 : Avec les notions vues dans ce chapitre, affichez un triangle ayant les coordonnées présentes ci-dessous. Vous n'avez besoin de modifier que les vertices par rapport aux exemples du cours, inutile de toucher au reste.

Image utilisateur

Exercice 2 : Reprenez le même triangle que l'exercice précédent mais modifiez ses vertices pour l'inverser :

Image utilisateur

Exercice 3 : On passe un cran au-dessus maintenant. Je vous demande d'afficher la forme suivante en utilisant deux triangles distincts (donc pas avec le paramètre GL_TRIANGLE_STRIP) :

Image utilisateur
Solutions

Exercice 1 :

La seule chose à modifier ici c'est le tableau de vertices. L’énoncé demandait un triangle avec des coordonnées spécifiques, le tableau de valeurs ressemble donc à ceci :

float vertices[] = {-0.5, 0.0,   0.5, -0.5,   0.5, 0.5};

Vous remarquerez que j'ai délimité les coordonnées de façon à bien différencier les vertices. ;)

Je devrais en théorie m’arrêter là pour la correction, mais pour ceux qui le souhaitent je donne en détail le code source de l'affichage des vertices. Celui-ci ne change absolument pas par rapport aux exemples du cours, mais si ça peut vous aider à comprendre un peu mieux je vais le ré-expliquer rapidement.

Premièrement, on reprend le code de la boucle principale avec sa gestion d'évènements et son actualisation de fenêtre :

// Boucle principale

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

    SDL_WaitEvent(&m_evenements);

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




    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);
}

Le code d'affichage consiste simplement à appeler les fonctions :

  • glClear() pour nettoyer ce qui était présent avant

  • glVertexAttribPointer() pour donner les vertices à OpenGL

  • glDrawArrays() pour les afficher

// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT);


// Tableau Vertex Attrib 0 pour envoyer les vertices

glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);


// Affichage du triangle

glDrawArrays(GL_TRIANGLES, 0, 3);

Il ne faut bien sûr pas oublier d'activer le tableau Vertex Attrib au moment d'envoyer les vertices, puis de le désactiver quand on n'en a plus besoin :

// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT);


// Tableau Vertex Attrib 0 pour envoyer les vertices

glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);


// Activation du tableau Vertex Attrib

glEnableVertexAttribArray(0);


// Affichage du triangle

glDrawArrays(GL_TRIANGLES, 0, 3);


// Désactivation du tableau Vertex Attrib

glDisableVertexAttribArray(0);

Ce qui donne le code source suivant pour la boucle principale :

// Vertices

float vertices[] = {-0.5, 0.0,   0.5, -0.5,   0.5, 0.5};


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


    // Tableau Vertex Attrib 0 pour envoyer les vertices

    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);


    // Activation du tableau Vertex Attrib

    glEnableVertexAttribArray(0);


    // Affichage du triangle

    glDrawArrays(GL_TRIANGLES, 0, 3);


    // Désactivation du tableau Vertex Attrib

    glDisableVertexAttribArray(0);


    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);
}

Exercice terminé. :)

Exercice 2 :

Pour cet exercice, il suffit juste de modifier notre tableau de vertices. On prend donc notre schéma pour en tirer les coordonnées suivantes :

// Vertices

float vertices[] = {0.5, 0.0,   -0.5, -0.5,   -0.5, 0.5};

Exercice 3 :

Cet exercice est un poil plus compliqué que les deux autres mais il n'y a rien de dur si on regarde dans le fond. :)

La forme demandée est évidemment constituée de deux triangles, il faut donc commencer par déclarer deux tableaux de vertices qui contiendront les coordonnées de ces deux triangles :

// Vertices

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

Il faut évidemment modifier l'appel à la fonction glDrawArrays() pour qu'elle prenne en compte les 6 vertices et non uniquement 3 :

// Affichage du triangle

glDrawArrays(GL_TRIANGLES, 0, 6);

Le reste du code ne change pas.

Ah, nous avons enfin pu afficher quelque chose (même si ce n'est un simple triangle), nous avons enfin pu faire bosser notre carte graphique qui commençait légèrement à s'endormir. :p

Rappelez-vous bien de ce que nous avons vu ici, nous réutiliserons tout ça dans les futurs chapitres.

Example of certificate of achievement
Example of certificate of achievement