Version en ligne

Tutoriel : Développez vos applications 3D avec OpenGL 3.3

Table des matières

Développez vos applications 3D avec OpenGL 3.3
Introduction
Qu'est-ce qu'OpenGL ?
Installer les différents outils
Différences entre OpenGL 2.1 et 3.3
Créer une fenêtre avec la SDL 2.0
Préparation de l'IDE
Premier programme
Les paramètres OpenGL
Premier affichage
Le fonctionnement d'OpenGL
Afficher un triangle
La classe SceneOpenGL
Introduction aux shaders
Qu'est-ce qu'un Shader ?
Utilisation des shaders
Un peu de couleurs
Les matrices
Introduction
Partie théorique
Transformations au niveau matricielle
Transformations au niveau géométrique
La troisième dimension (Partie 1/2)
La matrice de projection
Utilisation de la projection
Exemples de transformations
La troisième dimension (Partie 2/2)
La caméra
Affichage d'un Cube
La classe Cube
Le Frame Rate
Push et Pop
La problématique
Sauvegarde et Restauration
Les évènements avec la SDL 2.0
Différences entre la SDL 1.2 et 2.0
La classe Input
Les méthodes indispensables
Les textures
Introduction
Chargement
Plaquage
Améliorations
Aller plus loin
La caméra
Fonctionnement
Implémentation de la caméra
Fixer un point
Améliorations
TP : Une relique retrouvée
Les consignes
Correction
Les Vertex Buffer Objects
C'est quoi un VBO ?
Création
Utilisation
Mise à jour des données
Un autre exemple
Les Vertex Array Objects
Encore un objet OpenGL ?
Création et Utilisation
Un autre exemple
OpenGL 3.3 et Mac OS X
La compilation de shaders
Piqûre de rappel
Compilation des sources
Création du programme
Utilisation et améliorations
OpenGL Shading Language
Un peu d'histoire
Les variables
Les structures de contrôle
Fonctions prédéfinies
Les shaders démystifiés (Partie 1/2)
Préparation
Premier shader
Gestion de la couleur
Les shaders démystifiés (Partie 2/2)
Les variables uniform
Gestion de la 3D
Gestion des textures
Bonnes pratiques
Les Frame Buffer Objects
Qu'est-ce qu'un FBO ?
Le Color Buffer
Les Render Buffers
Le Frame Buffer
Utilisation
Améliorations simples
Gérer plusieurs Color Buffers

Développez vos applications 3D avec OpenGL 3.3

Salut à tous les Zéros, :magicien:

Vous souhaitez réaliser un jeu-vidéo, mais vous ne savez pas par où commencer ? Eh bien bonne nouvelle, vous êtes au bon endroit. Nous allons apprendre à utiliser une librairie graphique puissante : OpenGL (version 3.3). Nous découvrirons ensemble les bases essentielles de la programmation 3D qui pourront vous servir plus tard dans un projet de jeu multi-plateforme.

Sachez qu'avant d'aller plus loin, il vous faut un PC soit sous Windows soit sous une distribution UNIX/Linux. Pour les utilisateurs de Mac, c'est un peu spécial, mais pour continuer il vous faudra au moins avoir le système d'exploitation OS X Lion (10.7) d'installé. Si vous possédez un de ces trois OS, il vous faudra aussi une carte graphique compatible avec l'API OpenGL 3.3. Les plus anciennes cartes compatibles sont les GeForce de la série 8000 chez NVidia et les Radeon HD chez ATI. Si vous possédez une carte inférieure à celles-ci, vous pouvez toujours suivre le tutoriel de Kayl sur OpenGL 2.1 ici :

Un grand merci à Coyote pour ses corrections qui ne doit pas s'ennuyer chaque fois que je lui envoie un énorme pavé à lire. :lol:

Introduction

Qu'est-ce qu'OpenGL ?

Dans ce chapitre introductif, je vais vous parler rapidement d'OpenGL, de ses atouts et de l'installation des différents outils que nous utiliserons tout au long de ce tutoriel.

Qu'est-ce qu'OpenGL ?

Introduction Installer les différents outils

OpenGL est une librairie exploitant la carte graphique d'un ordinateur permettant ainsi aux développeurs de programmer des applications 2D et 3D. Cela permet en autres de développer des jeux-vidéo. L'avantage principal d'OpenGL est qu'il est multi-plateforme, c'est-à-dire qu'un programme codé avec OpenGL sera compatible avec Windows, Linux et Mac, sous réserve que la gestion de la fenêtre et des inputs soient également multi-plateforme (comme la SDL ;) ). Nous utiliserons une version récente d'OpenGL qui est la version 3.3 (sortie le 11 mars 2010). Cependant, nous n'utiliserons que la 3.1 pour la première partie du tuto, vous verrez pourquoi au fur et à mesure.

Actuellement, deux API existent pour exploiter notre matériel graphique : DirectX (uniquement utilisable sous Windows) et OpenGL (multi-plateforme). Vous l'aurez compris, OpenGL est dans un sens plus intéressant du fait de sa portabilité.

Image utilisateur

(Screenshot issu du jeu "Minecraft" proposant un affichage OpenGL)

Un des autres avantages d'OpenGL est que son utilisation est totalement gratuite, vous pouvez très bien coder des programmes gratuits voire commerciaux sans rendre de compte à personne. Vous pouvez aussi fournir votre code source pour en faire profiter la communauté ;) .

En plus d'être gratuite, cette librairie met à disposition son propre code source. Chacun d'entre nous peut aller voir le code source d'OpenGL librement; et c'est d'ailleurs ce que nous allons faire pour comprendre le fonctionnement de certaines fonctions ^^ .

Dans l'introduction du tutoriel, je vous ai parlé du langage C++. Ce langage de programmation est le langage le plus utilisé dans le monde du jeu vidéo et c'est pour cette raison que nous allons l'utiliser.

Le but de ce tutoriel est de vous apprendre les bases de la programmation d'un jeu vidéo. Et qui dit jeu vidéo, dit aussi ... mathématiques ! Alors oui je comprends que certains maudissent les maths au plus haut point à cause des mauvaises notes à l'école ou de son incompréhensible logique :colere2: . Mais si vous souhaitez vraiment développer un jeu vous ne passerez pas à coté.

N'ayez cependant pas peur des maths, il n'y a rien de compliqué dans ce que nous allons voir. Il suffit d'apprendre par cœur. Et si vous avez un trou de mémoire, vous pourrez toujours revenir voir les formules sur le site du zéro. Pas de contrôle non plus, donc pas de pression. La plupart des notions sont déjà expliquées dans les tutos du SdZ. Nous aborderons pas mal de domaines comme les vecteurs, les matrices, les quaternions, la trigonométrie, ... Que du bon en somme !

Bien, j'espère que la pilule est passée. :p

Que faut-il télécharger ?

Ah bonne question. Tout d'abord, comme dit dans l'introduction, il vous faut une carte graphique compatible avec OpenGL 3.x, soit au minimum les GeForce 8000 chez NVidia et les Radeon HD chez ATI. Ensuite, il faut mettre à jour vos drivers pour être sûr de ne pas avoir de problèmes plus tard.

Deuxièmement, il vous faut la librairie SDL installée. M@teo explique comment installer cette librairie dans son tuto, cependant la version donnée est incompatible avec la version d'OpenGL que nous allons utiliser. Il faudra donc passer par une autre version.


Introduction Installer les différents outils

Installer les différents outils

Qu'est-ce qu'OpenGL ? Différences entre OpenGL 2.1 et 3.3

Image utilisateur

Bien, passons à l'installation :) et commençons par la SDL :

Pourquoi a-t-on besoin de la SDL me direz-vous ?

La raison est simple : OpenGL a besoin d'un contexte d'exécution dans lequel travailler, il faut d'abord créer ce contexte avant de pouvoir créer notre monde 3D. De plus, OpenGL ne sait pas gérer les inputs (clavier, souris, ...), il nous faut donc une librairie capable de savoir ce que fait l'utilisateur. Heureusement pour nous, la librairie SDL (que vous devez déjà connaitre ;) ) sait créer un contexte OpenGL et gérer les évènements. En combinant la SDL et OpenGL nous nous retrouvons avec un monde 3D interactif.

Il y a d'autres librairies capables de remplir ce rôle mais l'avantage de la SDL est qu'elle est, elle-aussi, multi-plateformes. Les programmes codés avec ces deux librairies fonctionneront aussi bien sous Linux que sous Windows (ainsi que toutes les plates-formes gérant les deux librairies).

Actuellement, la version la plus stable de la SDL est la version 1.2.x mais depuis quelques temps la compatibilité de la SDL tend à s'étendre sur toutes les plateformes : sur iPhone, Androïd, et même sur PS3 ! Cette nouvelle version de la SDL est pour le moment la version 2.0. Elle est encore en développement et n'est pas stable à 100% mais elle le sera dans l'avenir. D'ailleurs, elle n'est pas disponible officiellement, notre principal problème va être de devoir la compiler par nous-même afin de pouvoir l'utiliser. ;)

Hein ?! Compiler la SDL ! Ça va pas, je sais pas faire ça moi !

Ne vous inquiétez pas ça va être très facile, les développeurs de la SDL sont intelligents, il nous suffit d'exécuter quelques commandes dans le terminal et hop on a la librairie compilée. ;)

Mais avant cela pour les utilisateurs de Windows : vérifiez bien que les drivers de votre carte graphique sont à jour. Pour le reste, je vais vous fournir directement les fichiers compilés à inclure dans le répertoire de votre IDE, pourvu qu'il soit équipé du compilateur MinGW. Pour les utilisateurs de Visual C++, vous devrez compiler la librairie SDL et télécharger la librairie GLEW.

Pour les utilisateurs de Linux, vérifiez également que les drivers de votre carte graphique (Pilotes Propriétaires) sont à jour.

Pour Mac OS X, ça va être un peu spécial. Dans un premier temps, il vous faut être obligatoirement sous OS X 10.7 Lion ou plus. Cependant, pour utiliser OpenGL chez vous il faudra utiliser des notions assez complexes que l'on ne verra que dans la deuxième partie du tuto. Je vous conseille donc d'utiliser une version libre de Linux (comme Ubuntu) ou Windows si vous avez bootcamp pour suivre ce début de tutoriel. Un aparté est prévu pour vous quand nous aurons appris tout ce qui est nécessaire pour coder sous Mac.

Pour en revenir à Windows, si vous êtes utilisateur de Visual C++ vous devriez compiler la librairie vous-même, je vous donne le lien pour que le faisiez sans problème. Vous devrez aussi télécharger la librairie GLEW.

Télécharger : Librairie SDL 2.0 + GLEW + GLM - Pour Windows MinGW

Télécharger : Code Source SDL 2.0 + GLM - Pour UNIX/Linux et Visual C++

Télécharger : Code Source GLEW - Pour Visual C++

Télécharger : Code Source GLM - Pour Visual C++

Pour MinGW sous Windows : dézippez l'archive et placez le dossier "SDL-2.0" dans le répertoire de MinGW. Si vous utilisez CodeBlocks, ce répertoire se trouve probablement dans C:\Program Files (x86)\CodeBlocks\MinGW. Attention cependant, placez bien le dossier "SDL-2.0" et pas ceux qui se trouvent à l'intérieur. Les dossiers "dll", "bin", "include" et "lib" doivent rester à l'intérieur.

Pour les linuxiens, téléchargez l'archive contenant le code source de la SDL et dézippez son contenu dans votre home (Exemple : /home/Boouh). Ensuite, ouvrez votre terminal et exécutez les commandes suivantes. Elle vont vous permettre de compiler puis d'installer la SDL :

sudo apt-get install libgl1-mesa-dev build-essential


cd
cd SDL-2.0/SDL-2.0.0-6713/


chmod +x configure
sudo ./configure
make
sudo make install


sudo ln -s /usr/local/bin/sdl2-config /usr/bin/sdl2-config
cd ..
sudo cp -r GL3/ /usr/local/include/
sudo cp -r glm/ /usr/local/include/
sudo chmod -R 715 /usr/local/include/GL3/

Grâce à ces commandes, vous avez maintenant la librairie SDL 2.0 installée sur votre ordinateur. ;)

Différences entre Windows et Linux

Dans l'archive pour Windows, j'ai inclus les fichiers compilés de la librairie GLEW. Pour ceux qui ne connaissent pas, GLEW est une librairie permettant de charger des extensions pour OpenGL (un peu comme les extensions des jeux Sims). Cette librairie est à l'origine utilisée sur Windows ET sur Linux. Mais avec la version 3.0 d'OpenGL, une grande partie des extensions de la version précédente sont devenues officielles et sont donc déjà inclues avec la version de "base".

Cependant, cette officialisation ne s'est pas faite sous Windows, il faudra donc toujours utiliser GLEW sous Windows. Nous verrons cela en détails un peu plus tard.

Pour les linuxiens, vous trouverez dans votre archive l'include "gl3.h" qui vient remplacer "gl.h". Vous utiliserez donc ce nouvel include (et non glew.h) pour les futurs chapitres.


Qu'est-ce qu'OpenGL ? Différences entre OpenGL 2.1 et 3.3

Différences entre OpenGL 2.1 et 3.3

Installer les différents outils Créer une fenêtre avec la SDL 2.0

Alors là, je conseille à ceux qui connaissent OpenGL 2.1 de bien s'assoir au risque de tomber dans les pommes :-° . Cette partie ne concerne pas uniquement ceux qui ont déjà codé avec OpenGL, vous ne comprendrez pas tout mais ça vous concerne aussi.

Tout d'abord, ce qu'il faut savoir c'est qu'avec la version 3, une grande partie des fonctions ont été marquées comme dépréciées. C'est-à-dire que le programmeur était fortement invité à ne plus les utiliser. Un peu plus tard, avec la version 3.1, le groupe Khronos a finalement décidé de supprimer ces fonctions dépréciées afin que les développeurs ne soient plus tentés de les utiliser.

Pourquoi a-t-on supprimé des fonctions me direz-vous ? Et bien tout simplement parce qu'elles ne sont plus adaptées aux jeux de nos jours. Soit elles ne sont plus utilisées, soit elles sont trop lentes. Imaginez une course de voitures avec des voitures qui vont à 30km/h ... Passionnant !

Ok des fonctions ont été supprimées, mais je ne vois pas ce qui peut me faire tomber dans les pommes. :p

Détrompez-vous, car certaines de ces fonctions étaient très utilisées avant.

Je vais prendre un exemple : glVertex(...)

Cette fonction permet avec OpenGL 2.1 de définir la position d'un point dans un espace 3D. Nous de notre coté, nous spécifions la position du point dans l'espace et la carte graphique s'occupait du reste. C'est-à-dire qu'elle multipliait les coordonnées du point par la matrice "modelView" puis par la matrice de projection, puis elle définissait sa couleur, etc ...

Maintenant c'est NOUS qui allons devoir faire TOUTES ces opérations.

QUOI ??!! Mais c'est nul ! On se retrouve avec quelque chose de plus compliqué maintenant. :(

Non c'est très bien au contraire puisqu'on se débarrasse d'une fonction lente, puis surtout, ça nous permet de faire ce que l'on veut. Pour reprendre l'exemple de la course, sans ces fonctions nous pourrons "tuner" notre voiture comme nous le voulons. Elle sera plus rapide, plus maniable et on se débarrassera de tout ce qui nous ralentit. Toutes les fonctions telles que glVertex, glColor, glLightv ... sont désormais inutilisables (et c'est tant mieux).

Ah oui, j'allais oublier, vous connaissez les matrices de projection et tout le reste ? Et bien comme vous le pensez (même si vous espérez vous tromper), ces matrices sont supprimées elles-aussi. Nous devrons donc créer notre propre matrice de projection, modelview, ...

La suppression des matrices entraine également la suppression des fonctions gluPerspective et gluLookAt. Heureusement pour nous, il existe une librairie parallèle à OpenGL qui s'appelle GLM (pour OpenGLMathematics). Cette librairie permet de faire pas mal de calculs mathématiques et permet surtout d'utiliser les matrices sans avoir à tout coder nous-même. Elle est incluse dans le téléchargement que vous avais fait juste avant. ;)

Vous vous dites peut-être que toutes ces suppressions sont injustes, tout est fait pour vous décourager. Eh bien non, ces suppressions ne peuvent être que positives car elles nous obligent à personnaliser complétement notre programme, nous pourrons donc mieux exploiter notre matériel et créer des jeux plus puissants.

Vous savez désormais ce qu'est OpenGL et avec quels outils nous allons travailler. J'expliquerai plus en détails le fonctionnement d'OpenGL dans une autre partie. Dans le chapitre suivant, nous allons écrire nos premières lignes de code, mais attention la 3D ce sera pour un peu plus tard. ;)


Installer les différents outils Créer une fenêtre avec la SDL 2.0

Créer une fenêtre avec la SDL 2.0

Différences entre OpenGL 2.1 et 3.3 Préparation de l'IDE

Dans ce chapitre nous allons créer une fenêtre avec la librairie SDL. Pour ceux qui connaissent la SDL avec le tuto de M@teo, vous pourrez comparer les deux versions. ;)

Préparation de l'IDE

Créer une fenêtre avec la SDL 2.0 Premier programme

Pour coder à travers ce tutoriel, je vous recommande d'utiliser l'IDE Code::Blocks qui est un outil très utilisé sur le site du Zéro. :) Vous pourrez donc trouver plus facilement de l'aide en cas de problème. Pour ceux qui souhaitent l'utiliser, voici un lien pour le télécharger (Windows uniquement). Il existe deux versions de cet IDE, vous pouvez choisir celle que vous voulez, les projets donnés devraient fonctionner sur les deux sans problème.

Télécharger : Code::Blocks 12.11 (Windows uniquement)

Pour les linuxiens :

sudo apt-get install codeblocks

D'ailleurs, si vous utilisez cet IDE je vais pouvoir vous fournir directement le template nécessaire pour créer un projet SDL 2.0. Ce template vous permettra de linker les librairies automatiquement sans que vous ayez à faire des manips compliquées. ;)

Télécharger : Template SDL 2.0 Code::Blocks 10.05 et 12.11 - Windows et Linux

Pour Windows, dézippez l'archive et fusionnez le dossier "share" de l'archive avec le dossier "share" de Code::Blocks (chez moi : C:\Program Files (x86)\CodeBlocks\share).

Pour Linux, dézippez l'archive où vous voulez puis exécutez les commandes suivantes (pas dans le dossier "share" mais dans celui qui le contient) :

sudo cp -r share/CodeBlocks/* /usr/share/codeblocks/
sudo chmod -R 715 /usr/share/codeblocks/templates/wizard/sdl2/
sudo chmod 715 /usr/share/codeblocks/templates/wizard/config.script

Nous pouvons maintenant créer notre premier programme en SDL 2.0. :D

Pour cela, il faut d'abord créer un projet SDL 2.0 et non pas OpenGL, ne vous trompez pas ! Sous Code::Blocks la procédure est la suivante :

. File -> New -> Project -> SDL 2.0 project

Vous vous souvenez du dossier "SDL-2.0" du chapitre précédent ? Si vous ne l'avez pas encore installé au bon endroit faites-le maintenant :) (reportez-vous au chapitre précédent).

Occupons-nous maintenant du linkage d'OpenGL, il faut dire à notre IDE que nous souhaitons utiliser cette librairie sinon il va nous mettre plein d'erreurs au moment de la compilation. Voici la procédure à effectuer sous Code::Blocks :

. Project -> Build options -> Linker settings -> Add

Un petit encadré vous demande quelle librairie vous souhaitez linker, cela dépend de votre système d'exploitation. Vous ne pouvez renseigner qu'une seule librairie par encadré. Ré-appuyez sur le bouton Add pour en ajouter une nouvelle.

IDE

Option

Code::Blocks Windows

opengl32
glew32

Code::Blocks Linux

GL

DevC++

-lopengl32
-lglew32

Visual Studio

opengl32.lib
glew32.lib

Selon votre OS, il suffit d'ajouter le ou les mot-clef(s) dans le petit encadré.

Pour les utilisateurs de CodeBlocks sous Windows et uniquement sous Windows, Code::Blocks va certainement vous demander où se trouve la SDL avec cette popup :

Image utilisateur

Dans le champ base qui vous est proposé, renseignez le chemin vers le répertoire "SDL-2.0" que vous avez installé précédemment (chez moi : C:\Program Files (x86)\CodeBlocks\MinGW\SDL-2.0). Ne renseignez aucun autre champ et ça devrait être bon. Une popup vous indiquera surement un message du genre "Please select a valid location", mais ce sera une fausse alerte ne vous en faites pas, appuyez sur le bouton next. ;)

En parlant de ce dossier, si vous avez jeté un coup d’œil à l'intérieur vous vous apercevrez qu'il y a un sous-dossier nommé "dll". Ce dossier contient les fichiers SDL2.dll et glew32.dll, ils sont indispensables pour lancer vos futurs programmes.

Vous avez deux solutions pour les utiliser : soit vous les incluez dans le dossier de chaque projet, soit vous les copier dans le dossier "bin" du compilateur MinGW (chez moi : C:\Program Files (x86)\CodeBlocks\MinGW\bin). La première solution est la plus propre mais aussi la plus contraignante, faites comme bon vous semble mais il faut que vous utilisiez au moins une de ces techniques pour lancer vos programmes. ;)

Pour ceux qui utilisent d'autres IDE comme Visual C++ ou Eclipse vous allez devoir renseigner d'autres librairies car je n'ai pas de template tout fait à vous proposer. Il faudra que vous renseigneriez les dossiers où se trouvent les librairies et les includes. De plus, vous devrez linker manuellement deux librairies supplémentaires :

Link

SDL2main

SDL2


Créer une fenêtre avec la SDL 2.0 Premier programme

Premier programme

Préparation de l'IDE Les paramètres OpenGL

Nous avons tous les outils, ceux-ci sont tous configurés. Bien, commençons. :magicien:

Voici un code de base permettant de créer une fenêtre avec la SDL 1.2 :

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


int main(int argc, char **argv)
{
   // Notre fenêtre et le clavier

   SDL_Surface *fenetre(0);
   SDL_Event evenements = {0};
   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;
   }
        

   // Création de la fenêtre

   fenetre = SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE); 


   // Boucle principale

   while(!terminer)
   {
        SDL_WaitEvent(&evenements);

        if(evenements.type == SDL_QUIT)
            terminer = true;
   }

	
   // On quitte la SDL

   SDL_Quit();

   return 0;
}

Facile non ? Et bien maintenant, je vais vous demander d'oublier la moitié de ce code. :D

Quoi ? C'est si différent que ça ?

Oui en effet, car souvenez-vous que nous travaillons avec la version 2.0. Et vu que l'on passe d'une version 1.x à une version 2.0, le code change beaucoup.

Commençons à coder avec la SDL 2.0 pour se mettre l'eau à la bouche :

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


int main(int argc, char **argv)
{    
    // 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;
    }


    // On quitte la SDL

    SDL_Quit();

    return 0;
}

Bon jusqu'ici, pas de grand changement. Mis à part l'inclusion de la SDL qui passe de "SDL/SDL.h" à "SDL2/SDL.h". En même temps on ne peut pas changer grand chose. :p

Voici le nouveau code qui permet de créer une fenêtre :

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


int main(int argc, char **argv)
{	
    // Notre fenêtre
	
    SDL_Window* fenetre(0);
	
	
    // 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;
    }
	
	
    // Création de la fenêtre
	
    fenetre = SDL_CreateWindow("Test SDL 2.0", SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, 800, 600, SDL_WINDOW_SHOWN);
	
	
    // On quitte la SDL
	
    SDL_DestroyWindow(fenetre);
    SDL_Quit();
	
    return 0;
}

Nous voyons ici deux choses importantes, premièrement :

SDL_Window* fenetre;

Le pointeur SDL_Window remplacera désormais notre fenêtre, il n'y a donc plus de SDL_Surface. Maintenant notre fenêtre aura sa structure à part entière bien différente des surfaces classiques.

Deuxièmement :

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

Vous l'aurez compris, cette fonction va nous permettre de créer notre fenêtre, elle vient donc remplacer notre bonne vielle SDL_SetVideoMode. Voici le prototype de cette fonction :

SDL_Window* SDL_CreateWindow(const char *title, int x, int y, int w, int h, Uint32 flags);

Cette fonction retourne notre fenêtre dans un pointeur de type SDL_Window.

Quant à la dernière fonction, son utilisation est simple : elle permet de détruire proprement notre fenêtre :

SDL_DestroyWindow(SDL_Window *window);

Nous lui donnerons la fenêtre créée au début du programme.

SDL_DestroyWindow(fenetre);

Puisque l'on parle de l'initialisation de la fenêtre, on va en profiter pour vérifier la valeur du pointeur SDL_Window. En effet dans de rares cas, celui-ci peut être nul à cause d'un éventuel problème logiciel ou matériel. On va donc tester sa valeur dans un if, s'il est nul alors on quitte la SDL en fournissant un message d'erreur, sinon c'est que tout va bien donc on continue :

// Création de la fenêtre

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

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

    return -1;
}

Ajoutons maintenant le code gérant les évènements qui connait, lui-aussi, son lot de modifications.

Avant, pour savoir si une fenêtre devait se fermer, nous devions vérifier la variable type d'une structure SDL_Event comme ceci par exemple :

// Variables

SDL_Event evenements;
bool terminer(false);


// Gestion des évènements

SDL_WaitEvent(&evenements); 

if(evenements.type == SDL_QUIT)
    terminer = true;

Avec la SDL 2.0, la gestion des évènements change quelques peu (nous verrons les différences un peu plus tard). Maintenant, pour savoir si on doit fermer une fenêtre, nous ferrons ceci :

// Variables

SDL_Event evenements;
bool terminer(false);


// Gestion des évènements

SDL_WaitEvent(&evenements); 

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

Bien, résumons tout cela en un seul code. :)

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


int main(int argc, char **argv)
{	
    // Notre fenêtre
	
    SDL_Window* fenetre(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;
    }
	
	
    // Création de la fenêtre

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

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

        return -1;
    }
	
	
    // Boucle principale
	
    while(!terminer)
    {
	SDL_WaitEvent(&evenements);
		
	if(evenements.window.event == SDL_WINDOWEVENT_CLOSE)
	    terminer = true;
    }
	
	
    // On quitte la SDL
	
    SDL_DestroyWindow(fenetre);
    SDL_Quit();
	
    return 0;
}

Préparation de l'IDE Les paramètres OpenGL

Les paramètres OpenGL

Premier programme Premier affichage

Tout ce code c'est bien mais il n'y a rien qui permet d'exploiter OpenGL, mais qu'attendons-nous ? :diable:

Il y a pas mal d'attributs (des options de configuration) à paramétrer pour rendre notre programme compatible avec OpenGL, commençons par le plus important :

SDL_GLContext contexteOpenGL;

Cette structure va permettre à la SDL de créer un contexte OpenGL. Ce contexte est très important, pour utiliser l'API graphique et ses fonctions. Il faut tout d'abord le configurer.

Voici le prototype de la fonction qui permet de spécifier des attributs OpenGL à la SDL :

SDL_GL_SetAttribute(SDL_GLattr attr, int value);

Avec cette simple fonction, nous allons pourvoir spécifier à la SDL quelle version d'OpenGL on souhaite utiliser :

SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 1);

Vous l'aurez compris :

Petite question pour ceux qui connaissent la SDL 1.2, vous savez comment on utilise le double buffering ? :-°

Voici la réponse :p :

SDL_SetVideoMode(800, 600, 32, SDL_HWSURFACE | SDL_DOUBLEBUF);

Comme avec la SDL, on peut aussi activer le Double Buffuring avec OpenGL. Cependant ça ne se passe pas de la même façon. Voici comment faire :

SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);

Bon on avance c'est bien :) . Mais tout à l'heure, tu as parlé d'une structure : SDL_GLContext, qu'est-ce qu'on en fait ?

Très bonne remarque, on va l'utiliser maintenant justement grâce à la fonction :

SDL_GLContext SDL_GL_CreateContext(SDL_Window *window);

Cette fonction demande un pointeur SDL_Window pour y attacher le contexte OpenGL. De plus, elle renvoie une structure SDL_GLContext, et justement c'est ce dont nous avons besoin :)

Dans notre cas, nous lui donnerons notre fenêtre puis nous récupéreront la structure retournée :

contexteOpenGL = SDL_GL_CreateContext(fenetre);

Comme pour la fenêtre SDL, la création du contexte OpenGL peut lui aussi échouer. Le plus souvent ce sera parce que la version OpenGL demandée ne sera pas supportée par la carte graphique. Il faut donc tester la valeur de la variable contexteOpenGL. Si elle est égale à 0 c'est qu'il y a eu un problème. Dans ce cas on affiche un message d'erreur, on détruit la fenêtre puis on quitte la SDL :

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

Si tout se passe bien lors de la création du contexte alors on continue.

Tout comme pour la fenêtre SDL encore une fois, il existe aussi une fonction qui permet de détruire le contexte lorsque nous n'en avons plus besoin. Son appel ressemble à ceci :

SDL_GL_DeleteContext(contexteOpenGL);

Bien, résumons tout cela :

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

    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;
    }
	
	
    // Boucle principale
	
    while(!terminer)
    {
        SDL_WaitEvent(&evenements);
		
        if(evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        terminer = true;
    }
	
	
    // On quitte la SDL
	
    SDL_GL_DeleteContext(contexteOpenGL);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();
	
    return 0;
}

On est presque au bout courage. :p Il nous faut juste rajouter un paramètre dans la fonction GL_CreateWindow. Avec la SDL 1.2, ce paramètre ressemblait à ça :

SDL_SetVideoMode(.., .., .., SDL_OPENGL);

Avec la SDL 2.0, le nom change légèrement :

SDL_WINDOW_OPENGL

Il se place également dans la fonction qui créé la fenêtre :

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

Et voila notre code final :

#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;
    }
	
	
    // Boucle principale
	
    while(!terminer)
    {
        SDL_WaitEvent(&evenements);
		
        if(evenements.window.event == SDL_WINDOWEVENT_CLOSE)
        terminer = true;
    }
	
	
    // On quitte la SDL
	
    SDL_GL_DeleteContext(contexteOpenGL);
    SDL_DestroyWindow(fenetre);
    SDL_Quit();
	
    return 0;
}

Vous devriez obtenir quelque chose comme ceci :

Image utilisateur

Quoi ! Mais on a une fenêtre blanche c'est nul !

Du calme, vous n'imaginez pas ce que l'on vient de faire (enfin ce que la SDL vient de faire), nous venons de créer un contexte dans lequel OpenGL va évoluer. En gros nous venons de créer l'univers. :D

Bref nous avons enfin une fenêtre SDL compatible avec OpenGL 3.1. Comparez le code avant et après, vous verrez la différence. Il y a plus de code certes, mais toutes ces instructions sont importantes. Ce nouvel environnement va nous permettre de créer des programmes plus paramétrables et donc plus puissants. ;)

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

Piouf, ça fait beaucoup de changements pour arriver grosso modo à la même chose. Mais au moins on passe à la nouvelle génération de programme.
Bon, on a une fenêtre c'est bien, mais pour le moment on a linké OpenGL pour rien. Passons maintenant à la partie OpenGL :pirate: .


Premier programme Premier affichage

Premier affichage

Les paramètres OpenGL Le fonctionnement d'OpenGL

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

Premier affichage Afficher un triangle

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 :

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


Premier affichage Afficher un triangle

Afficher un triangle

Le fonctionnement d'OpenGL La classe SceneOpenGL

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

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

Alors Attention ! :

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 :

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é. :)


Le fonctionnement d'OpenGL La classe SceneOpenGL

La classe SceneOpenGL

Afficher un triangle Introduction aux shaders

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 :

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 :

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

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 :

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 :

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 :

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 :

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


Afficher un triangle Introduction aux shaders

Introduction aux shaders

La classe SceneOpenGL Qu'est-ce qu'un Shader ?

Ah c'est un vaste sujet que nous allons aborder aujourd'hui :) . Tellement vaste que nous allons le découper en 4 chapitres dont le premier sera celui-ci. Nous verrons les trois autres beaucoup plus tard, vous comprendrez pourquoi. Bref, il est temps de découvrir ce que sont ces mystérieux Shaders. :magicien:

Qu'est-ce qu'un Shader ?

Introduction aux shaders Utilisation des shaders

Introduction

Commençons par un peu de théorie. ;)

Comme vous le savez, OpenGL est une librairie qui tire sa puissance de la carte graphique. Or lorsque nous programmons, nous n'avons pas accès à cette carte mais uniquement au processeur et à la RAM. Comment fait notre programme pour l'exploiter me direz-vous ?

En réalité, OpenGL va bidouiller pas mal de choses dans la carte lorsque nous appelons certaines fonctions comme glDrawArrays(). Voyons d’ailleurs ce qui se passe entre le moment où l’on appelle cette fonction et le moment où la scène s’affiche à l’écran :

Image utilisateur

Cette suite d’opérations constitue ce que l’on appelle: le pipeline 3D. Les parties qui nous intéressent dans ce chapitre sont : le Vertex Shader et le Fragment Shader.

Euh qu'est-ce que c'est que ça ? o_O

Un shader est simplement un programme exécuté non pas par le processeur mais par la carte graphique. Il en existe deux types (enfin 3 mais seuls 2 sont réellement importants) :

A quoi servent les shaders ?

Étant donné la complexité de cette notion, nous ne devrions étudier les shaders que beaucoup plus tard dans le tutoriel. Dans les anciennes versions d'OpenGL, ils étaient optionnels, c'était l'API elle-même qui se chargeait d'effectuer ces opérations. Mais avec la version 3.0, le groupe Khronos a voulu introduire une nouvelle philosophie : le tout shader. En clair, ils nous imposent leur utilisation.

Euh ... Pourquoi nous imposer un truc aussi compliqué alors que c'était géré automatiquement avant ?

Ce qu'il faut comprendre, c'est que les jeux-vidéo ont beaucoup évolué depuis qu'OpenGL existe. Il y a quelques années, il était plus simple de laisser l'API faire tout le boulot, les jeux ne prenaient pas énormément de ressources et surtout il y avait moins de choses à calculer. Mais de nos jours, les graphismes, le réalisme et la vitesse sont devenus les principales préoccupations des développeurs (et des joueurs ;) ). L'API n'est plus capable de gérer tout ça rapidement car elle le fait avec ses vielles méthodes (trop lentes pour les jeux d'aujourd'hui).

L'avantage d'une gestion personnalisée est que l'on peut faire ce que l'on veut et surtout on peut y mettre uniquement ce dont on a besoin, ce qui peut nous faire gagner parfois pas mal de vitesse.

Exemples de Shaders

Ah c'est certainement la partie qui va le plus vous intéresser. :p

Les shaders ne sont pas uniquement des étapes embêtantes qui sont là pour vous compliquer la vie :colere2: . Leur première utilité vient du fait qu'ils permettent d'afficher nos objets dans un monde 2D ou 3D. En gros, voila ce qui se passe lorsque nous voulons afficher un objet :

Ça c'est la première utilité, passons maintenant à la deuxième qui est certainement la plus importante (et la plus intéressante :p ) : ils permettent de faire tous les effets 3D magnifiques que vous voyez dans un jeu-vidéo tels que : les lumières, l'eau, les ombres, les explosions ... Ce sont les shaders qui font d'un jeu un jeu plus réaliste.

Image utilisateur
Image utilisateur

Alors ne vous emballez pas, ce n'est pas maintenant que nous allons apprendre à faire tous ces effets mais nous y viendrons. ;)

Le principal problème avec les shaders est que lorsque l'on débute dans la programmation 3D, il est très difficile d'apprendre à les utiliser du fait de leur complexité. C'est pour cela que dans un premier temps, je vais vous fournir le code source pour la création des shaders. Nous consacrerons trois chapitres entiers pour apprendre à les gérer une fois que vous serez plus habitués avec OpenGL.

En bref ce qu’il faut retenir c’est que :


Introduction aux shaders Utilisation des shaders

Utilisation des shaders

Qu'est-ce qu'un Shader ? Un peu de couleurs

Passons maintenant à la partie programmation. :magicien:

Pour commencer, je vais vous demander de télécharger l’archive ci-dessous (pour Linux et Windows), elle contient un fichier « Shader.h », « Shader.cpp » et un dossier « Shaders » contenant plein de petits fichiers. Vous placerez tout ça dans le répertoire de chaque projet que vous ferez (donc dans chaque chapitre que nous ferons).

Télécharger : Code Source C++ Shaders - Windows & Linux

Une fois les fichiers ajoutés à votre dossier, il vous suffit simplement d’ajouter le header « Shader.h » et le fichier source « Shader.cpp » à votre projet.

Comme vous le savez maintenant, un shader est un programme, différent d’un programme normal certes mais un programme quand même. Il doit donc respecter plusieurs règles :

Nous ne verrons pas ces deux étapes maintenant mais sachez au moins que ce n’est pas si différent d'un programme normal. Les codes sources sont dans le dossier « Shaders » que vous devriez avoir placé dans le répertoire de votre projet.

Avant toute chose, pour pouvoir utiliser les shaders il va falloir utiliser la classe Shader dont voici le constructeur :

Shader(std::string vertexSource, std::string fragmentSource);

Alors attention, appeler le constructeur ne suffit pas. Si vous n'appelez que lui, votre shader ne sera pas exploitable. Pour le rendre exploitable, il faut utiliser la méthode charger() qui permet en gros de lire les fichiers sources, de les compiler, ...

bool charger();

Cette méthode retourne un booléen pour savoir si la création du shader s'est bien passée.

En bref, voici un petit exemple de création de shader :

// Création du shader

Shader shaderBasique("Shaders/basique_2D.vert", "Shaders/basique.frag");
shaderBasique.charger();


// Début de la bouble principale

while(!terminer)
{
    // Utilisation
}

Bien, passons à l'utilisation qui est ma foi assez simple puisque nous n'utilisons qu'une seule fonction :

glUseProgram(GLuint program) ;

Elle prend un paramètre : l'ID d'une certain "program", nous lui donnerons l'attribut : "m_programID" de la classe Shader grâce à la méthode :

GLuint getProgramID() const;

Ne vous posez pas de question sur ça pour le moment ;)

Cette fonction a deux utilités :

Cette fonction se place juste avant glDrawArrays() lorsque nous voulons activer notre shader, puis après avec le paramètre 0 pour le désactiver :

// Activation du shader

glUseProgram(shaderBasique.getProgramID());


    // Affichage du triangle

    glDrawArrays(GL_TRIANGLES, 0, 3);


// Désactivation du shader

glUseProgram(0);

Simple non ? :-° D'ailleurs on peut même intégrer, entre ces deux appels de la fonction glUseProgram(), le code relatif à l'envoi des vertices au tableau Vertex Attrib. En général, on fait cela pour bien différencier le code d'affichage du reste du programme, ce qui donnerait pour nous :

// Activation du shader

glUseProgram(shaderBasique.getProgramID());


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

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


    // Affichage du triangle

    glDrawArrays(GL_TRIANGLES, 0, 3);


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

    glDisableVertexAttribArray(0);


// Désactivation du shader

glUseProgram(0);

Dans le chapitre précédent, nous nous sommes permis de ne pas utiliser ces fameux shaders. Mais comme je vous l'ai dit tout à l'heure, on ne peut plus continuer ainsi. Reprenons notre ancien code pour y ajouter les fonctions que nous venons de voir :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

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

    Shader shaderBasique("Shaders/basique_2D.vert", "Shaders/basique.frag");
    shaderBasique.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);


        // Activation du shader

        glUseProgram(shaderBasique.getProgramID());


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

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


            // Affichage du triangle

            glDrawArrays(GL_TRIANGLES, 0, 3);


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

            glDisableVertexAttribArray(0);


        // Désactivation du shader

        glUseProgram(0);


        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(m_fenetre);
    }
}

Vous devriez vous retrouver avec une fenêtre comme celle-la :

Image utilisateur

Quoi mais c’est nul ! C’est la même chose que dans le chapitre précédent mais en plus compliqué.

Oui c'est la même chose mais c'est comme cela que les choses doivent être faites ;). Pour le moment vous ne voyez pas trop l'intérêt d'utiliser les shaders mais vous allez vite voir que c'est indispensable.

D'ailleurs pourquoi attendons-nous ? Voyons dès maintenant ce qu'ils ont à nous offrir. :magicien:


Qu'est-ce qu'un Shader ? Un peu de couleurs

Un peu de couleurs

Utilisation des shaders Les matrices

Un peu de couleurs

Il est maintenant temps de faire passer notre triangle à la couleur, je le trouve un peu trop pâlot. :p

Comment fonctionnent les couleurs avec OpenGL ?

La réponse est simple, vous savez qu'avec la SDL, pour créer un rectangle coloré il fallait utiliser cette fonction :

SDL_MapRGB(SDL_Surface *surface, Uint8 red, Uint8 green, Uint8 blue);

Les trois paramètres étaient les composantes RGB (Rouge, Vert, Bleu). Il suffisait de combiner ces trois couleurs pour en former une seule au final. Bonne nouvelle, avec OpenGL c'est pareil. Pour fabriquer une couleur vous devez donner :

Si vous voulez faire des tests pour obtenir différentes couleurs, essayez la palette de couleurs Windows (ou équivalent sur Linux) :

Image utilisateur

Il y a deux façons de représenter les couleurs :

Malheureusement pour nous, nous allons devoir utiliser la seconde méthode. Mais pas de panique, nous allons utiliser une petite combine pour utiliser la première.

Jauger une couleur entre 0 et 255 est plus facile à comprendre, pour pouvoir utiliser cette méthode nous allons diviser la valeur de la couleur (par exemple : 128) par 255. De cette façon on se retrouve avec une valeur comprise entre 0 et 1 :

float rouge = 128.0/255.0; // = 0.50
float vert =  204.0/255.0; // = 0.80
float bleu =  36.0/255.0;  // = 0.141

La quantité de rouge sera de 128, le vert de 204 et le bleu de 36. De plus faites attention, nous travaillons avec des float donc n'oubliez pas de préciser les décimales même s'il n'y a en pas.

Au niveau du shader, on va changer le shader basique par le shader couleur2D car ce premier ne faisait qu'afficher ce que nous lui donnions en blanc. Maintenant que nous voulons de la couleur, il faut charger un autre shader gérer la couleur :

// Shader

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

Vu que nous avons changé le nom du shader (shaderCouleur), il faut effectuer le même changement de nom lorsque l'on récupère le programID :

// Activation du shader

glUseProgram(shaderCouleur.getProgramID());


    // Envoi des données et affichage

    ....


// Désactivation du shader

glUseProgram(0);

Au niveau du code d'affichage, vous connaissez déjà presque tout. Les fonctions utilisées sont les mêmes que celles des coordonnées de vertex. Les couleurs se gèrent également de la même façon : on place la valeur de chaque couleur (RGB) les unes à la suite des autres dans un tableau.

Le seul changement sera le numéro du tableau, au lieu de placer nos couleurs dans le tableau 0 nous les placerons dans le tableau 1. Le premier tableau (indice 0) servira à stocker tous nos vertices et le deuxième tableau (indice 1) servira à stocker nos couleurs.

// On définie les couleurs 

float couleurs[] = {0.0, 204.0 / 255.0, 1.0,    0.0, 204.0 / 255.0, 1.0,    0.0, 204.0 / 255.0, 1.0};


while(...)
{
    ....


    // Activation du shader

    glUseProgram(shaderCouleur.getProgramID());


        // Envoi des vertices

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


        // On rentre les couleurs dans le tableau Vertex Attrib 1

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


        ....


    // Désactivation du shader

    glUseProgram(0);


    ....
}

Voyons ce que donne toutes ces petites modifications sur un exemple concret :

void SceneOpenGL::bouclePrincipale()
{
    // Variables
	
    bool terminer(false);
    float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};
    float couleurs[] = {0.0, 204.0 / 255.0, 1.0,    0.0, 204.0 / 255.0, 1.0,    0.0, 204.0 / 255.0, 1.0};
	
	
    // Shader
	
    Shader shaderCouleur("Shaders/couleur2D.vert", "Shaders/couleur2D.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);
		
		
        // Activation du shader

        glUseProgram(shaderCouleur.getProgramID());


            // Envoi des vertices
		
            glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 0, vertices);
            glEnableVertexAttribArray(0);


            // Envoi des couleurs

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


            // Affichage du triangle

            glDrawArrays(GL_TRIANGLES, 0, 3);


            // Désactivation des tableaux Vertex Attrib

            glDisableVertexAttribArray(1);
            glDisableVertexAttribArray(0);


        // Désactivation du shader

        glUseProgram(0);
		
		
        // Actualisation de la fenêtre
		
        SDL_GL_SwapWindow(m_fenetre);
    }
}

Vous devriez obtenir un triangle coloré :

Image utilisateur

Vous commencez à voir l'intérêt des shaders ? ^^ Bon si vous n'êtes pas convaincus je vais vous montrer une autre façon de colorier notre triangle.

Si chaque sommet possède sa propre couleur alors OpenGL (avec l'aide des shaders) nous fera un joli petit dégradé entre les différentes couleurs. Prenons un exemple, nous allons définir une couleur différente pour chaque sommet :

// Remplaçons les couleurs par les suivantes ...

float couleurs[] = {1.0, 0.0, 0.0,  0.0, 1.0, 0.0,  0.0, 0.0, 1.0};


//... et voyons ce que ça donne;

Vous devriez avoir ceci :

Image utilisateur

Comme vous le constatez, les shaders ont calculé automatiquement la couleur de chaque pixel se trouvant entre les différents sommets.

Cette opération s’appelle l’interpolation. C’est-à-dire qu’OpenGL est capable de trouver la couleur de chaque pixel entre deux sommets ayant une couleur différente. Le gros avantage de l'interpolation c'est que nous ne nous en occupons pas :p . En effet, même si nous définissons nos shaders, nous ne nous occuperons pas de trouver la couleur de chaque pixel. Il nous suffira juste de donner notre code source pour seulement un et OpenGL se chargera de faire la même opération pour tous les autres. Magique n'est-ce pas ? :magicien:

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

Exercices

Énoncés

Au même titre que le chapitre précédent, vous avez maintenant le droit à votre petite série d'exercice. Votre objectif va être de colorier différents triangles avec une couleur spécifique.

Exercice 1 : Coloriez le triangle du haut avec les valeurs données ci-dessous :

Exercice 2 : Même consigne que précédemment mais avec les valeurs :

Exercice 3 : Coloriez le triangle uniquement avec la couleur vert (valeur 255.0). Simplifiez le tableau si possible.

Exercice 4 : Reprenez les 3 couleurs précédentes, au lieu de les appliquer au triangle appliquez en une pour chaque vertex. Le résultat final doit ressembler à ceci (ce n'est pas grave si les couleurs ne sont pas dans le même ordre) :

Image utilisateur

Exercice 5 : Le dernier exercice va allier ce que nous avons vu dans le chapitre précédent et celui-ci. L'objectif est d'afficher un losange multicolore avec les couleurs de l'exercice 4, le dernier vertex doit utiliser le bleu (255.0) :

Image utilisateur

Solutions

Exercice 1 :

Le but des exercices est de colorier un triangle, il n'y a donc que le tableau couleurs à modifier. Le reste du code ne change pas, on n'envoie toujours ce tableau à OpenGL grâce au Vertex Attrib 1. Pour ce premier exercice, il fallait trouver :

// Couleurs

float couleurs[] = {240.0 / 255.0, 210.0 / 255.0, 23.0 / 255.0,     // Vertex 1
                    240.0 / 255.0, 210.0 / 255.0, 23.0 / 255.0,     // Vertex 2
                    240.0 / 255.0, 210.0 / 255.0, 23.0 / 255.0};    // Vertex 3

Il ne fallait pas oublier de diviser les valeurs que je vous avais données par 255.0, sinon le code n'aurait pas fonctionné. ;)

Exercice 2 :

Le principe reste le même que le premier exercice, il suffit juste de modifier le tableau couleurs :

// Couleurs

float couleurs[] = {230.0 / 255.0, 0.0, 230.0 / 255.0,     // Vertex 1
                    230.0 / 255.0, 0.0, 230.0 / 255.0,     // Vertex 2
                    230.0 / 255.0, 0.0, 230.0 / 255.0};    // Vertex 3

Exercice 3 :

Encore une fois, on conserve le même principe que les exercices 1 et 2. La couleur demandée n'était que le vert ici, ce qui allège un peu notre tableau :

// Couleurs

float couleurs[] = {0.0, 255.0 / 255.0, 0.0,     // Vertex 1
                    0.0, 255.0 / 255.0, 0.0,     // Vertex 2
                    0.0, 255.0 / 255.0, 0.0};    // Vertex 3

Cependant il y avait un moyen de simplifier cette déclaration. En effet, je vous ai précisé dans le cours que l'on pouvait simplifier les divisions des couleurs brutes (rouge, vert et bleu). Donc au lieu de faire l'opération 255.0 / 255.0, on pouvait directement mettre la valeur 1.0. Ce qui allégeait encore plus le tableau final :

// Couleurs

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

Exercice 4 :

Le but de ce dernier exercice était bien évidemment d'appliquer chacune des couleurs mises en place précédemment sur un vertex en particulier. Nous avions déjà vu ce principe avec l'exemple du triangle multicolore :

float couleurs[] = {240.0 / 255.0, 210.0 / 255.0, 23.0 / 255.0,    // Vertex 1
                    230.0 / 255.0, 0.0, 230.0 / 255.0,             // Vertex 2
                    0.0, 1.0, 0.0};                                // Vertex 3

L'ordre des couleurs n'est pas important ici, ce n'est pas quelque chose de demandé.

Exercice 5 :

La seule difficulté ici était qu'il fallait reprendre la couleur de deux vertices pour le second triangle. En effet, ceux-ci sont doubler pour pouvoir afficher le triangle du bas, il fallait donc doubler leur couleur :

// Couleurs

float couleurs[] = {240.0 / 255.0, 210.0 / 255.0, 23.0 / 255.0,     // Vertex 1
                    230.0 / 255.0, 0.0, 230.0 / 255.0,              // Vertex 2
                    0.0, 1.0, 0.0,                                  // Vertex 3

                    240.0 / 255.0, 210.0 / 255.0, 23.0 / 255.0,     // Vertex 4 (Copie du 1)
                    0.0, 0.0, 1.0,                                  // Vertex 5
                    0.0, 1.0, 0.0};                                 // Vertex 6 (Copie du 3)

Il faut bien faire correspondre votre couleur avec votre vertex pour avoir un bon affichage. ;)

D'ailleurs, le tableau de vertices ne change absolument pas par rapport à avant.

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

Pensez à bien affecter la valeur 6 au paramètre count de la fonction glDrawArrays() pour afficher vos deux triangles, sinon elle n'en prendra en compte qu'un seul :

// Affichage des triangles

glDrawArrays(GL_TRIANGLES, 0, 6);

Ce chapitre (un peu compliqué je vous l'accorde) est enfin terminé. Vous savez maintenant ce que sont les shaders et à quoi ils servent. Je pense faire une partie entièrement consacrée aux effets assez sympas que l'on peut réaliser, mais bon ce n'est pas pour maintenant. ;)


Utilisation des shaders Les matrices

Les matrices

Un peu de couleurs Introduction

Alors là, nous allons aborder un chapitre assez compliqué. Nous allons découvrir ce que sont les matrices avec OpenGL, leur utilité, etc... C'est un chapitre assez technique et surtout rempli de mathématiques :-° . Je vous conseille de bien le relire plusieurs fois jusqu'à que vous ayez tout compris, car les matrices sont indispensables avec OpenGL. ;)

Introduction

Les matrices Partie théorique

Lançons nous tout de suite dans le monde des mathématiques. Je vous rappelle que si vous voulez développer des jeux-vidéo vous ne pouvez pas éviter les maths, elles sont indispensables au bon fonctionnement du jeu. Les premières notions que nous allons apprendre sont les transformations.

Qu'est-ce qu'une transformation vous allez me dire ? Au lieu d'une définition barbante que nous n'allons point retenir ( :p ) je vais vous donner des exemples de transformations et vous comprendrez tout de suite ce que c'est :

Une transformation est donc grosso-modo une modification apportée à un ensemble de points ou repère dans un espace donné (soit 2D, soit 3D, etc ...). Les 3 transformations que vous voyez là sont les principales transformations utilisées dans OpenGL, en particulier la translation.

Ok, on sait ce qu'est une transformation maintenant, mais à quoi ça sert ?

Elles vont nous servir à placer tous nos objets (personnages, arbres, maisons, ...) dans notre monde 3D. A chaque fois que nous voudrons placer un objet nous utiliserons les transformations. Elles seront essentiellement utilisées sur le repère.

Les matrices sous OpenGL 2.1

Avant de parler du fonctionnement des matrices dans la version 3, je vais vous parler de celui des anciennes versions, ce sera plus facile à comprendre. De plus le principe ne change absolument pas d'une version à l'autre.
Tout d'abord, qu'est-ce qu'une matrice ? C'est un tableau de nombres ordonnés en lignes et en colonnes entourés par des parenthèses. Sa syntaxe est semblable à celle d'un vecteur mais avec plus de nombres :

Image utilisateur

Une matrice n'est pas forcément un tableau de 9 cases, elle peut en contenir jusqu'à l'infini. Cependant ce n'est pas qu'un simple tableau, c'est une sorte de super-vecteur qui permet de faire pas mal de choses intéressantes. Un vecteur est en général utilisé en 3D pour gérer les points, les directions, les normales... Les matrices permettent de faire bien plus que ça. Elles servent principalement à convertir des données géométriques en données numériques. Il est plus facile de travailler avec des nombres qu'avec des compas et des équerres placés sur notre écran. :p Voyons d'ailleurs pourquoi OpenGL a besoin des matrices :

Ce sont les besoins fondamentaux d'OpenGL pour utiliser la 3D. Dans les anciennes versions, les matrices étaient gérées automatiquement, on n'utilisait que quelques fonctions pour les créer et les utiliser. Depuis la version 3.1, ces fonctions sont supprimées, car l'approche des jeux d'aujourd'hui est différente. Heureusement pour nous, nous avons en notre possession une librairie mathématique du nom de GLM qui permet de gérer les matrices (et bien plus), mais avant de commencer à les manipuler nous allons faire un peu de théorie pour bien comprendre ce que nous faisons.

En définitif, nous aurons besoin de deux matrices pour coder avec OpenGL :

Chaque vertex sera multiplié par ces deux matrices pour pouvoir être affiché sur notre écran, le tout en nous donnant une impression de 3D (magique n'est-ce pas :magicien: ).


Les matrices Partie théorique

Partie théorique

Introduction Transformations au niveau matricielle

Dans cette partie nous allons apprendre ce que sont les matrices et comment les utiliser pour nos programmes. Pour simplifier cet apprentissage, je ne vais vous montrer que la multiplication de matrices car c'est une notion que nous retrouverons assez souvent dans ce tuto. De plus, cela vous donnera une bonne idée des calculs matriciels sans pour autant voir les choses les plus compliquées (inversion, déterminant, etc.). :p

Matrice carrée

On commence tout de suite cette partie par les matrices carrées.

Une matrice carrée est une matrice ayant le même nombres de colonnes et de lignes :

Image utilisateur

Il y a 3 rangées de colonnes et 3 rangées de lignes, c'est donc ce que l'on appelle une matrice carrée. Son nom complet est d'ailleurs : matrice carrée d'ordre 3. Le chiffre à la fin permet de spécifier la taille. Si on avait mis le chiffre 4, alors la matrice aurait eu 4 colonnes et 4 lignes. Nous n'utiliserons que les matrices carrées dans OpenGL. Et tant mieux, car ça simplifie grandement les choses :-° .

Matrice d'identité

Sous ce nom barbare se cache en réalité la matrice la plus simple qu'il soit. Elle a la particularité d'avoir toutes ses valeurs égales à 0 hormis les valeurs de sa diagonale qui, elles, sont égales à 1. Quelle que soit la taille de la matrice, on retrouvera toujours cette particularité :

Image utilisateur
Image utilisateur

Si nous multiplions un vecteur par une telle matrice, le vecteur ne sera absolument pas changé :p :

Image utilisateur

Nous allons étudier la multiplication matricielle dans un instant. Mais vous pouvez déjà retenir ce principe : si un vecteur est multiplié par une matrice d'identité, alors il ne sera pas modifié.

Multiplication d'un vecteur par une matrice

Partie 1 : la vérification

Dans ce premier exemple, je vais prendre une matrice carrée d'ordre 3 pour simplifier les choses. Sachez cependant que le principe est le même quelle que soit la taille de la matrice.

Image utilisateur

Voyons comment arriver à ce résultat.
Premièrement, il faut vérifier que le nombre de colonnes de la matrice soit égal au nombre de coordonnées du vecteur. Si ce n'est pas le cas alors la multiplication est impossible.

Image utilisateur

La matrice possède 3 colonnes et le vecteur possède 3 coordonnées. La multiplication est donc possible.

Prenons un autre exemple :

Image utilisateur

La matrice possède 2 colonnes et le vecteur 3 coordonnées. La multiplication est impossible.
Vous avez compris le principe ? Bien, passons à la suite.

Partie 2 : La multiplication

Attaquons la partie la plus intéressante :diable: . La multiplication d'une matrice et d'un vecteur s'effectue comme ceci : pour une seule coordonnée (x, y, ou z) du vecteur résultat, nous allons faire la somme des multiplications de chaque nombre d'une ligne de la matrice par chaque nombre correspondant du vecteur. Un peu barbant comme explication, rien ne vaut un bon exemple :

Commençons déjà par la première ligne, vous devriez comprendre le principe une fois cet exemple fini :

Image utilisateur

Il faut multiplier les nombres de même couleur sur le schéma, puis additionner les différents résultats. Une ligne de la matrice ne donnera qu'une seule coordonnée du vecteur résultat, ici la coordonnée x. Passons à la deuxième ligne :

Image utilisateur

Vous comprenez le principe ? La première ligne nous a donné la première coordonnée, la deuxième ligne nous donne la deuxième coordonnée. Nous multiplions chaque ligne de la matrice par toutes les valeurs du vecteur. Passons à la troisième ligne :

Image utilisateur

La troisième ligne de la matrice nous donne la troisième coordonnée. Si la matrice avait eu 4 lignes (et donc 4 colonnes) alors il y aurait eu une quatrième opération du même type pour la quatrième coordonnée. Si on récapitule tout ça, on a :

Image utilisateur

Vous avez compris ? Je vous donne un autre exemple pour comparer les différents résultats :

Image utilisateur

N'hésitez pas à bien relire et essayez de comprendre ces deux exemples. C'est la base de la multiplication matricielle.

Passons à un pseudo-exemple, vous n'avez pas oublié ce qu'est la matrice d'identité j'espère :p , on sait qu'un vecteur multiplié par cette matrice ne changera pas. Maintenant que l'on connait un peu la multiplication on va voir pourquoi le vecteur n'est pas modifié :

Image utilisateur

Oh magie, le résultat ne change pas. Vous comprenez pourquoi cette matrice est la plus simple à utiliser. ;)
Je vais maintenant vous donner un exemple de multiplication avec une matrice carrée d'ordre 4. Le principe ne change absolument pas, nous allons voir cet exemple étape par étape pour bien comprendre. Commençons :

Image utilisateur

La matrice possède 4 colonnes et le vecteur 4 lignes, la multiplication est possible. Maintenant on passe à la première étape :

Image utilisateur

Vous ne voyez qu'une seule différence : on ajoute à la somme une multiplication supplémentaire. En effet, n'oubliez pas que l'on multiplie les nombres de la matrice avec les nombres correspondants du vecteur. Pour la deuxième ligne c'est pareil :

Image utilisateur

Pour la troisième ligne ça ne change pas :

Image utilisateur

Ah, une quatrième ligne, eh bien oui c'est plus long :p . Mais ça ne change toujours pas :

Image utilisateur

Ce qui nous donne au final :

Image utilisateur

Vous voyez, le principe de la multiplication ne change absolument pas.

Piouf, cette partie était un peu difficile à comprendre. Je vais vous donner quelques exercices pour vous entrainer à la multiplication. Faites-les sérieusement, vous avez besoin de comprendre ce que l'on vient de faire pour comprendre la suite. Sinon vous serez complétement largués :( .

Exercices de multiplications

Exercice 1 :

Image utilisateur

Exercice 2 :

Image utilisateur

Exercice 3 :

Image utilisateur

Solutions

Exercice 1 :

Image utilisateur

Exercice 2 :

La multiplication est impossible, la matrice possède 3 colonnes alors que le vecteur possède 4 coordonnées.

Exercice 3 :

Image utilisateur

Multiplication de deux matrices

La multiplication de deux matrices peut sembler plus compliquée à première vue mais si vous avez compris ce que l'on a fait avant, alors vous savez déjà multiplier deux matrices.

Partie 1

Avant tout, pour pouvoir multiplier deux matrices carrées entre-elles il faut absolument que les deux matrices soient de la même taille (donc du même ordre), c'est-à-dire qu'elles doivent avoir le même nombre de lignes et le même nombre de colonnes.

Image utilisateur

Les deux matrices ont la même taille, on peut donc les multiplier.
Prenons un autre exemple :

Image utilisateur

Ici, les deux matrices n'ont pas la même taille nous ne pouvons pas les multiplier.
Il ne devrait y avoir rien de compliqué pour le moment. :)

Partie 2

Pour pouvoir multiplier deux matrices carrées, nous allons utiliser une petite combine. Nous allons couper la deuxième matrice en 3 vecteurs (ou 4 selon la taille), puis nous appliquerons la multiplication que nous avons vue à l'instant pour chacun de ces vecteurs. Prenons deux matrices cobayes :

Image utilisateur

Bien, coupons la seconde matrice en 3 vecteurs :

Image utilisateur

Maintenant il nous suffit d'appliquer la multiplication d'une matrice et d'un vecteur pour chaque vecteur que nous venons de créer. Voici ce que ça donne pour le premier :

Image utilisateur

Au tour du deuxième :

Image utilisateur

Quand il y a un "0" dans une matrice ça nous facilite grandement le calcul :lol: . Bref, on fait la même chose avec le dernier vecteur :

Image utilisateur

Nous obtenons au final 3 vecteurs fraichement calculés. Il nous suffit ensuite de les assembler dans l'ordre de la division de la matrice !

Image utilisateur

Au final, nous obtenons :

Image utilisateur

Vous voyez, une fois que vous avez compris la première multiplication, vous savez déjà multiplier deux matrices.
Évidemment, la taille de la matrice ne change toujours pas le principe, voyons ensemble un exemple de multiplication de deux matrices carrées d'ordre 4.

Image utilisateur

Coupons la seconde matrice en 4 vecteurs (et oui car c'est une matrice carrée d'ordre 4 et pas 3) :

Image utilisateur

Il nous suffit maintenant d'appliquer la multiplication "Matrice - Vecteur" sur les 4 vecteurs que nous venons de créer. Voici le premier résultat :

Image utilisateur

Puis le second :

Image utilisateur

Le troisième :

Image utilisateur

Et enfin le quatrième et dernier vecteur :

Image utilisateur

Maintenant on réunit les vecteurs résultats dans le bon ordre :

Image utilisateur

Voici donc le résultat final :

Image utilisateur

Vous remarquez que la multiplication se passe exactement de la même façon que l'on soit en présence de matrices à 3 colonnes ou à 4 colonnes ou même à 1000 colonnes :p . Passons maintenant à quelques exercices pour vous entrainer. C'est important je le répète, essayez de faire ces exercices sérieusement.

Exercices de multiplications

Exercice 1 :

Image utilisateur

Exercice 2 :

Image utilisateur

Exercice 3 :

Image utilisateur

Solutions

Exercice 1 :

La multiplication est impossible, la première matrice possède 4 lignes et 4 colonnes alors que la seconde matrice ne possède que 3 lignes et 3 colonnes.

Exercice 2 :

Image utilisateur

Exercice 3 :

Image utilisateur

Et voilà, la partie la plus compliquée de ce chapitre est enfin terminée ! La multiplication matricielle est une notion difficile à comprendre (même s'il en existe bien d'autres plus complexes) mais vous êtes maintenant capables de la maitriser. :D


Introduction Transformations au niveau matricielle

Transformations au niveau matricielle

Partie théorique Transformations au niveau géométrique

Le but de la partie précédente était d'apprendre à multiplier deux matrices. J'ai volontairement inclus des exercices avec des matrices carrées d'ordre 4 car OpenGL aura besoin la plupart du temps de ce genre de matrices (et heureusement !). Dans cette partie, nous allons faire passer les transformations de la géométrie à l'algèbre (les nombres).

Comme vous le savez les transformations sont des outils géométriques, pour calculer une rotation par exemple nous avons besoin d'un rapporteur. Or c'est un peu compliqué de poser notre rapporteur sur l'écran, surtout si l'angle se trouve derrière le dos d'un personnage :lol: . Pour pouvoir utiliser les transformations numériquement nous devons utiliser... les matrices. ;)

La translation

La translation permet de déplacer un ensemble de points ou une forme dans un espace donné. En gros, on prend un vecteur qui servira de "rail" puis on fera glisser nos points sur ce rail. La forme finale ne sera pas modifiée, elle sera juste déplacée. :)

Une translation en 3 dimensions se traduit par la matrice suivante :

Image utilisateur

Vous remarquerez que cette matrice ressemble beaucoup à la matrice d'identité, il n'y a que les coordonnées du vecteur en plus.

L'homothétie

Une homothétie permet d'agrandir ou de réduire une forme géométrique, voici la matrice correspondante :

Image utilisateur

La rotation

Attention ! La matrice que vous allez voir est certainement la matrice la plus compliquée de tout le tutoriel :p . Cependant, il est inutile de la retenir, elle est trop complexe nous la verrons jamais directement :

Image utilisateur

Je vous avais dit que cette matrice était... particulière (pour vous dire, je ne la connais pas par cœur moi-même). Grâce à elle, nous pouvons faire pivoter en ensemble de points d'un angle thêta autour d'un axe défini par les coordonnées (x, y, z). Nul besoin de retenir cette matrice, je le répète. ;)


Partie théorique Transformations au niveau géométrique

Transformations au niveau géométrique

Transformations au niveau matricielle La troisième dimension (Partie 1/2)

Durant l'introduction, je vous ai brièvement parlé de deux matrices : la projection (qui permet de convertir un monde 3D en un monde 2D affichable sur notre écran) et la modelview (qui permet de placer nos objets dans ce même monde 3D).

C'est sur cette dernière matrice que l'on effectuera toutes les transformations que l'on a vues, à savoir : la translation, la rotation et l'homothétie. Voyons d'ailleurs comment placer un objet dans un monde 3D :

Voyons ce que cela donne si je fais pivoter un triangle de 90°...

Voici un triangle avant la rotation :

Image utilisateur

Et le revoilà après :

Image utilisateur

Vous voyez ce qui c'est passé ? Le triangle a pivoté de 90° grâce à une transformation.

Les Transformations sous OpenGL

Aaahh... on commence à relier OpenGL et les mathématiques (enfin !). Il est temps de voir le comportement des transformations dans un programme.

La première chose à savoir est que chaque transformation (rotation, ...) sera effectuée non pas sur un objet mais sur le REPÈRE du monde, c'est-à-dire que si l'on veut faire pivoter un objet de 120° alors il faudra faire pivoter non pas l'objet en lui-même mais son REPÈRE.

Par exemple, si vous voulez afficher un objet à 300 mètres d'un autre, vous n'allez pas ajouter 300 unités de longueur à chaque vertex de l'objet, autant garder les vertices déjà définis.

Hein ??? J'ai rien compris. o_O

Bon, mettons que vous ayez un méchant soldat ennemi de 500 vertices. Vous n'allez pas modifier ses 500 vertices pour afficher un autre ennemi deux mètres plus loin. Ça serait trop couteux en ressources, surtout si vous affichez une armée de 200 soldats...

A la place, on modifiera la position du repère (qui est en fait la matrice modelview) pour chaque objet que l'on veut dessiner. Un personnage gardera ses 500 vertices intactes quelque soit sa position.

Vous voyez la différence entre modifier toute une scène de dizaines de milliers de vertices et modifier une matrice de 16 valeurs.

Illustrations

Je vais vous donner quelques exemples de transformations effectuées sur un repère, ce sera plus simple à comprendre si vous voyez ce que donne chaque transformation.

Le repère de base

Le repère de base est en fait un repère qui n'a pas encore été modifié, c'est sur celui-ci que l'on effectuera notre première transformation. Graphiquement, on représente un repère comme ceci :

Image utilisateur

Au niveau des matrices, ce repère correspond simplement à une matrice d'identité d'ordre 4 :

Image utilisateur

La translation

La translation est la transformation la plus simple :p . N'oubliez pas que c'est le REPÈRE qui est modifié. Ici, on translate le repère par rapport au vecteur V(2, 1), soit deux unités de longueur sur l'axe X et 1 unité sur l'axe Y :

Image utilisateur

Avec les matrices on multiplierait la matrice modelview par la matrice de translation suivante :

Image utilisateur

La rotation

Le principe ne change pas pour la rotation, on effectue la transformation sur le repère. Voici un exemple d'une rotation de 45° sur l'axe Z :

Image utilisateur

Pour la rotation, on multipliera la matrice modelview par la matrice de rotation (toujours aussi effrayante :diable: ) :

Image utilisateur

L'homothétie

En temps normal, avec une homothétie on modifie la taille d'une forme géométrique. Sauf qu'ici on modifie la taille du repère, donc on modifie la taille de chaque unité de longueur. Voici ce que donne une homothétie de coordonnées (2, 2) sur le repère :

Image utilisateur

Vous voyez que la taille du repère à changé, désormais si on fait une translation par un vecteur(2, 1) alors la translation sera plus grande et le repère se retrouvera plus loin.

Pour ce qui est de la matrice modelview, il suffira de la multiplier par la matrice suivante :

Image utilisateur

Les matrices et la boucle principale

Vous n'êtes pas sans savoir qu'un jeu se passe en grande partie dans ce que l'on appelle la boucle principale, c'est une boucle qui va se répéter indéfiniment jusqu'à ce que le joueur arrête de jouer (enfin pas vraiment, mais partons de ce principe).

Il faudra à chaque tour de boucle réinitialiser la matrice modelview pour ne pas qu'elle garde les traces de transformations du tour précédent. Si c'était le cas, le jeu ne se redessinerait jamais au même endroit et serait totalement difforme.

Voici ce qui se passera à chaque tour de boucle :

Toutes ces étapes se répèteront indéfiniment dans notre programme. En général, on affiche tous les objets d'un niveau 50 à 60 fois par seconde, imaginez le fourbi que l'ordinateur doit calculer. :p

Accumulation de transformations

Attention, de même que l'ordre des matrices dans la multiplication, l'ordre des transformations a une importance capitale ! Si vous effectuez une translation puis une rotation, vous n'obtiendrez pas la même chose que si vous faisiez une rotation et une translation.

Comme d'habitude prenons un petit exemple. Dans un premier temps, je vais faire une translation du repère par rapport au vecteur V(2, 1), puis une rotation de 90° sur l'axe Z (l'axe Z est en fait pointé vers nous, c'est pour ça que nous ne le voyons pas) :

Image utilisateur

Maintenant je vais l'inverse, une rotation de 90° puis une translation de vecteur (2, 1) :

Image utilisateur

Oh tiens ! Les repères ne sont pas les mêmes. Et oui, les repères sont différents car l'ordre des transformations est important. Faites donc bien attention à ce que vous voulez faire et à l'ordre dans lequel effectuer ces transformations. ;)

Enfin ce chapitre est terminé, il nous aura fallu du temps mais n’oubliez pas que tout ce que je vous enseigne est nécessaire pour comprendre et utiliser OpenGL. Récapitulons ce que nous savons faire :

Bonne nouvelle ! Nous connaissons tout ce qui est nécessaire pour ajouter une troisième dimension à nos programmes. Il est d'ailleurs temps mes amis, attaquons-nous à cette nouvelle dimension qui s'offre à nous ! :pirate:


Transformations au niveau matricielle La troisième dimension (Partie 1/2)

La troisième dimension (Partie 1/2)

Transformations au niveau géométrique La matrice de projection

Le chapitre précédent était un peu compliqué. Mais grâce à lui, nous sommes maintenant capables d'utiliser les transformations pour façonner un monde 3D. Dans ce nouveau chapitre, nous allons mettre en commun ce que l'on sait sur OpenGL avec ce que l'on a appris concernant les matrices.

J'ai préféré diviser ce chapitre en deux parties, étant donné que nous allons aborder pas mal de nouvelles notions. Dans cette première partie, nous allons nous concentrer sur l'implémentation des matrices dans notre programme, puis dans la seconde partie, nous implémenterons la troisième dimension et surtout nous apprendrons à l'utiliser ! :p

La matrice de projection

La troisième dimension (Partie 1/2) Utilisation de la projection

Présentation

Dans le chapitre précédent, nous avons vu ce qu'étaient les matrices ainsi que leur fonctionnement. Nous nous sommes surtout concentrés sur la matrice modelview qui permet de placer nos objets dans un monde 3D. Les transformations que nous avons vues (translation, rotation et homothétie) sont appliquées sur elle.

Dans ce chapitre-la maintenant, nous allons nous concentrer sur une autre matrice : la matrice de projection.

Sa seule utilité est de permettre l'affichage d'un monde 3D sur notre écran qui lui ne possède que 2 dimensions. Cependant, vous vous imaginez bien que ce processus n'est pas aussi simple que cela, on a quand même besoin de quelques paramètres pour permettre une telle opération.

Pour expliquer son fonctionnement, je vais prendre une bonne vieille fonction connue des habitués d'OpenGL : la fonction gluPerspective().

Euh ouai, mais moi je viens ici pour apprendre OpenGL, je ne connais pas cette fonction. :colere2:

Ne vous inquiétez pas, je vais vous expliquer le fonctionnement de cette fonction en détails, et j'ai plutôt intérêt à le faire étant donné que nous allons la recoder entièrement. :p

Comme je vous l'ai dit dans les chapitres précédents, une bonne partie des anciennes fonctions d'OpenGL a disparu avec la nouvelle version. La fonction gluPerspective a elle aussi disparu (indirectement). Il nous faudra donc la coder de nos propres mains. Et pour vous faciliter la tâche, je vais vous expliquer en détails son fonctionnement.

Premièrement, voici le prototype de la fonction :

void gluPerspective(double angle, double ratio, double near, double far);

Hum tout plein de paramètres intéressants ^^ . Je vais commencer par les deux derniers paramètres : near et far.

Near

Le paramètre Near correspond à la distance entre votre écran et ce qui sera affiché.

Image utilisateur

La petite zone entre l'écran et la plaque est une zone où rien ne sera affiché. Même s'il y a un objet dans cet intervalle il ne sera pas affiché.

Pourquoi me direz-vous ? Simplement parce que les mathématiques nous l'imposent, il nous faut obligatoirement une zone non-affichable pour que le calcul de la projection puisse s'effectuer. Il se passe la même chose avec la division par zéro, c'est impossible. Il faut obligatoirement que le dénominateur soit au moins un peu plus grand (ou plus petit mais on s'en moque) que zéro.

Le paramètre near représente donc ce petit intervalle nécessaire au calcul de la projection.

Far

Le paramètre far est un peu plus simple, on peut dire que c'est l'inverse de near.

C'est également une distance entre votre écran et ce qui sera affiché. La différence est que tout objet se trouvant au delà de cette distance ne sera pas affiché.

Image utilisateur

Au final, pour qu'un objet puisse s'afficher sur l'écran, il faut qu'il se situe entre les zones near et far, sinon il ne sera pas affiché.

Ratio

Je pense que vous avez tous entendu parler des télévisions 4/3 (quatre tiers) et 16/9 (seize neuvièmes). Le ratio est le rapport entre la longueur de la télé et sa hauteur. Dans la plupart des cas nous avons soit un ratio de 4/3 soit un ratio de 16/9, voire 16/10 pour les jeux-vidéo.

Pour nous, le ratio s'appliquera à la fenêtre SDL que l'on a codée. Dans le chapitre 3, nous avions créé une fenêtre de 800 pixels par 600 pixels. Le ratio sera donc de 800/600, et si on simplifie la fraction on a un ratio de 4/3.

Dans les jeux-vidéo, on propose généralement plusieurs modes d'affichage afin de s'adapter à l'écran du joueur. En effet, tous les joueurs n'ont pas forcément le même écran. Nous donnerons au final comme paramètre la division entre la longueur de la fenêtre SDL et sa hauteur.

Angle

Ce paramètre est le plus spécial de la fonction. C'est en fait l'angle de vue avec lequel nous allons voir la scène. Plus cet angle sera petit, plus on aura l'impression de faire un effet de zoom sur la scène. Et à l'inverse, plus il sera grand, plus on aura l'impression que la scène s'éloigne et se déforme.

Voici trois exemples de la même scène avec trois angles de vue différents :

Image utilisateur
Image utilisateur
Image utilisateur

Merci à Kayl pour sa scène de test. ^^
Dans la première image, on utilise un angle de 70°, dans la deuxième un angle de 30° et dans la troisième un angle de 100°.

L'avantage d'un angle plus petit est que l'on a l'impression de faire un zoom de jumelle ou de sniper ;) . A l'inverse, un angle plus grand créera une ambiance plus particulière, mais ce genre d'angle s'utilise rarement.

En général, l'angle de vision "normal" est de 70° (70 degrés) et c'est d'ailleurs ce que l'on mettra dans notre code.

Utilisation de la librairie GLM

Dans les précédentes versions d'OpenGL, il existait une multitude de fonctionnalités mathématiques qui étaient gérées par OpenGL même, les matrices en faisait évidemment partie. Cependant, ces fonctionnalités sont maintenant supprimées et il faut trouver un autre moyen de gérer nos matrices nous-même.

Heureusement pour nous, il existe une librairie qui s'appelle GLM (pour OpenGLMathematics). Vous l'avez déjà téléchargée normalement donc vous n'avez rien à faire. Cette librairie nous permet d'utiliser les matrices, et bien plus encore, sans avoir à nous soucier de tout programmer à la main. Elle inclut même certaines fonctions dépréciées comme gluPerspective(). :)

Nous allons donc utiliser GLM pour simuler toutes les fonctions dont nous aurons besoin.


La troisième dimension (Partie 1/2) Utilisation de la projection

Utilisation de la projection

La matrice de projection Exemples de transformations

Inclusion des matrices

Toutes ces belles matrices (que vous adorez j'en suis sûr :p ) ne nous servent pas à grand chose pour l'instant. Nous allons maintenant les intégrer dans notre code OpenGL pour enfin voir ce qu'elles ont véritablement dans le ventre.

Reprenons le code de la boucle principale que nous avons laissé au chapitre 4 :

void SceneOpenGL::bouclePrincipale()
{
    // Variables
	
    bool terminer(false);
    float vertices[] = {-0.5, -0.5,   0.0, 0.5,   0.5, -0.5};
    float couleurs[] = {1.0, 0.0, 0.0,  0.0, 1.0, 0.0,  0.0, 0.0, 1.0};
	
	
    // Shader
	
    Shader shaderCouleur("Shaders/couleur2D.vert", "Shaders/couleur2D.frag");
    shaderCouleur.charger();
	
	
    // Boucle principale
	
    while(!terminer)
    {
        /* ***** Code d'affichage ***** */
    }
}

Je n'ai pas mis tout le code, seule cette partie nous intéresse pour le moment.

Les en-têtes de GLM

Vous savez maintenant que l'on a besoin de deux matrices pour faire fonctionner un jeu 3D : la matrice de projection et la modelview. Nous allons donc les déclarer dans notre code grâce à librairie GLM.

Pour commencer, veuillez inclure les en-têtes suivants dans le fichier SceneOpenGL.h qui permettent à notre programme d'utiliser les matrices :

// Includes OpenGL

....


// Includes GLM

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


// Autres includes

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

La première inclusion permet d'utiliser les fonctionnalités principales de GLM, la deuxième les transformations et la dernière permet de récupérer les valeurs des matrices (nous verrons cela dans un instant).

Elle est bizarre ton extension c'est marqué .hpp c'est normal ?

Oui tout à fait. :)

La particularité de GLM c'est qu'elle est codée directement dans les headers, il n'y a pas de librairie pré-compilée comme la SDL par exemple. Grâce à ce système, GLM est compatible avec toutes les plateformes et il n'y a pas besoin de fournir de fichier spécifique pour Linux, Windows, etc. Ni même pour compilateur (.a, .dll, etc.)

Inclusion des matrices

Après toute cette théorie nous pouvons enfin coder nos premières matrices. L'objectif est de créer deux matrices du nom de projection et modelview.

Pour cela, nous allons déclarer deux objets de type mat4, pour matrice carrée d'ordre 4. Il faut utiliser le namespace glm en même temps que la déclaration :

// Matrices projection et modelview

glm::mat4 projection;
glm::mat4 modelview;

Tous les objets et méthodes de GLM doivent utiliser le namespace glm. Pour éviter d'avoir à le taper à chaque fois, ajoutez la ligne de code suivante dans chaque classe où vous utiliser les matrices :

// Permet d'éviter la ré-écriture du namespace glm::

using namespace glm;

Avec cette ligne de code, la déclaration des matrices devient :

// Matrices projection et modelview

mat4 projection;
mat4 modelview;

Maintenant que les matrices sont déclarées, nous allons pouvoir les initialiser.

La matrice projection

Occupons-nous d’abord de la projection. Dans la partie précédente, nous avons parlé d'une ancienne fonction du nom de gluPerspective(). Celle-ci a disparu avec OpenGL 3 mais GLM l'a gentiment recoder pour nous sous la forme d'une méthode, voici son prototype :

mat4 perspective(double angle, double ratio, double near, double far);

On remarque qu'elle contient exactement les mêmes paramètres, pratique n'est-ce pas ? La seule différence est qu'elle renvoie un objet de type mat4, cet objet doit être affecté à la matrice projection.

Nous appellerons cette méthode avec les paramètres suivants :

Voici son appel :

// Matrices

mat4 projection;
mat4 modelview;


// Initialisation

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

La matrice modelview

Pour la matrice modelview c'est un peu plus simple. Pour l'initialiser, nous allons leur donner les valeurs d'une matrice d'identité (avec les 1 en diagonale). Elle ne doit pas avoir que des valeurs nulles car si nous multiplions des vecteurs par des 0, il risquerait d'y avoir un gros problème avec notre scène finale. :-°

Pour cela, nous allons utiliser un constructeur qui ne demande qu'une seule valeur : la valeur 1.0.

// Matrices

mat4 projection;
mat4 modelview;


// Initialisation

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

Le constructeur sait tout seul qu'il doit utiliser une matrice d’identité, cela éviter d'avoir à écrire toutes les valeurs à la main pour mettre des 1.0 en diagonal. :p

La boucle principale

Vous vous souvenez qu'au début du chapitre je vous ai parlé du comportement des matrices avec la boucle principale du programme ? Je vous avais dit qu'à chaque tour de boucle, il fallait réinitialiser la matrice modelview pour ne pas avoir les anciennes valeurs du tour précédent.

Cette réinitialisation se fait exactement de la même façon que l'initialisation. C'est-à-dire que l'on affecte le résultat du constructeur mat4(1.0) à la matrice modelview. Nous devons faire ceci juste après le nettoyage de l'écran :

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


    // Réinitialisation de la matrice modelview

    modelview = mat4(1.0);
}

Interaction entre le programme et les matrices

Notre code OpenGL devient de plus en plus beau :D . Mais les matrices ne nous servent toujours pas à grand chose elles n'interagissent toujours pas avec notre programme.

Je ne vais pas m'étaler sur le sujet pour le moment, mais sachez que l'interaction entre les matrices et le programme se fait dans le shader. C'est lui qui va faire tous les calculs pour projeter le monde 3D sur notre écran.

Pour le moment, le shader que nous avons chargé (couleur2D) est incapable de gérer la projection, il faut en charger un autre. Si vous n'avez rien modifié dans le dossier "Shaders" du chapitre 4, vous devriez trouver des codes sources qui s'appellent couleur3D.vert et couleur3D.frag(à ne pas confondre avec couleur2D). Si vous ne trouvez pas ces fichiers, re-téléchargez le dossier complet depuis le chapitre 4.

À la différence du premier shader, couleur3D est, lui, capable de gérer la projection. Nous allons donc le charger à la place de son prédécesseur :

// Shader gérant la couleur et les matrices

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

Ne vous inquiétez pas si le fragment shader ne change pas, c'est normal. ;)

Le shader n'est pas le seul à devoir changer, les vertices doivent elles aussi subir une modification. Il faut leur ajouter une troisième dimension pour les rendre compatibles avec la projection.

Ça veut dire que nos triangles seront en 3D ?

Oui et non ... En fait, ils sont en 3D mais pour le moment nous sommes mal placés pour les voir de cette façon. Nous verrons dans la deuxième partie de ce chapitre comment "bien se placer". Ne vous inquiétez pas ça arrivera très vite. ;)

Bref au final, il suffit d'ajouter la coordonnée Z à tous les vertices pour qu'ils soient en 3D. Nous mettrons la valeur -1 pour le moment :

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

Vu que nous ajoutons une coordonnée à nos vertices, il va falloir modifier un paramètre dans la fonction glVertexAttribPointer. Le paramètre size permet de spécifier la taille d'un vertex, donc son nombre de coordonnées. On le passe désormais à 3 car nous avons 3 coordonnées :

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

Revenons à notre shader, vu que c'est lui qui gère les calculs de projection, il va falloir lui envoyer les matrices que nous avons déclarées pour qu'il puisse travailler correctement. L'envoi de matrice au shader se fait avec cette fonction :

glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value);

Je vous expliquerai en détails le fonctionnement de cette fonction dans le chapitre sur les shaders. Sachez juste qu'elle nous permet d'envoyer nos matrices au shader.

Nous lui enverrons ces valeurs :

Grâce à cette fonction, notre shader va pouvoir travailler avec les matrices que nous avons déclarées dans le main.

Nous appellerons cette fonction deux fois vu que nous avons deux matrices à envoyer. Ces appels se feront juste avant une fonction que vous connaissez bien : glDrawArrays().

Dans notre cas, nous ferons ces appels toujours de la même façon :

// On envoie les matrices au shader

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

Si on les intègre au code d'affichage ça donne ceci :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Envoi des vertices et des couleurs

    ....


    // On envoie les matrices au shader

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


    // On affiche le polygone

    glDrawArrays(GL_TRIANGLES, 0, 3);


    // Désactivation des Vertex Attrib

    ....


// On n'utilise plus le shader

glUseProgram(0);

Je pense vous avoir parlé de tout, compilons tout ça pour voir que ça donne :

void SceneOpenGL::bouclePrincipale()
{
    // Variables
	
    bool terminer(false);
    float vertices[] = {-0.5, -0.5, -1.0,   0.0, 0.5, -1.0,   0.5, -0.5, -1.0};
    float couleurs[] = {1.0, 0.0, 0.0,  0.0, 1.0, 0.0,  0.0, 0.0, 1.0};
	
	
    // Shader
	
    Shader shaderCouleur("Shaders/couleur3D.vert", "Shaders/couleur3D.frag");
    shaderCouleur.charger();


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


        // Réinitialisation de la matrice modelview

        modelview = mat4(1.0);


        // On spécifie quel shader utiliser

        glUseProgram(shaderCouleur.getProgramID());


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

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


            // Même chose avec le tableau Vertex Attrib 1

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


            // On envoie les matrices au shader

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


            // On affiche le polygone

            glDrawArrays(GL_TRIANGLES, 0, 3);


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

            glDisableVertexAttribArray(1);
            glDisableVertexAttribArray(0);


        // On n'utilise plus le shader

        glUseProgram(0);


        // Actualisation de la fenêtre

        SDL_GL_SwapWindow(m_fenetre);
    }
}

Vous devriez avoir cette fenêtre :

Image utilisateur

C'est tout ? C'est pratiquement la même chose qu'avant. :(

À première vue oui, il ne se passe rien de plus sauf peut-être une légère déformation de notre triangle. Mais sachez que dans votre carte graphique il se passe pas mal de choses. De plus, nous pouvons maintenant utiliser les transformations dans nos programmes et ce sont justement elles qui forment la base d'un jeu-vidéo. Sans transformations, nous serions encore en train de jouer au Pong. :lol:


La matrice de projection Exemples de transformations

Exemples de transformations

Utilisation de la projection La troisième dimension (Partie 2/2)

Introduction

On va se reposer un peu dans cette partie, il n'y a rien de nouveau, on utilisera juste ce que l'on a déjà codé.

Tout d'abord, on va changer un peu nos vertices pour avoir un triangle plus petit. Sinon nous ne pourrions pas vraiment voir ce que donnent les transformations :

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

D'origine, voici la position du triangle :

Image utilisateur

Les transformations

La translation

Prenons un vecteur V de coordonnées (0.4, 0.0, 0.0) pour effectuer une translation du repère (matrice modelview) sur 0.4 unité sur l'axe X. Normalement, le triangle devrait se retrouver sur la gauche.

Pour faire une translation avec GLM, nous allons utiliser la méthode translate() dont voici le prototype :

mat4 translate(mat4 matrice, vec3 translation);

La méthode renvoie la matrice donnée en paramètre avec la translation ajoutée.

Pour contenter l'objet vec3, nous allons juste appeler le constructeur vec3 avec les coordonnées de la translation que l'on veut faire, ici (0.4, 0.0, 0.0).

L'appel à la méthode ressemble au final à ceci :

// Translation

modelview = translate(modelview, vec3(0.4, 0.0, 0.0));

On identifie bien ce que fait cette ligne de code. On appelle la méthode translate() qui va modifier la matrice modelview à l'aide du vecteur de coordonnées (0.4, 0.0, 0.0).

Nous devons inclure cette ligne juste avant l'envoi des matrices au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Remplissage des tableaux Vertex Attrib 0 et 1

    ....


    // Translation

    modelview = translate(modelview, vec3(0.4, 0.0, 0.0));


    // On envoie les matrices au shader

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


    // Affichage

    ....


// On n'utilise plus le shader

glUseProgram(0);

Ce qui nous donne :

Image utilisateur

Le triangle ne connait aucun changement, nous n'avons pas modifié ses coordonnées. En revanche, le repère lui a changé. On l'a translaté selon le vecteur V, le triangle se trouve donc un peu plus loin.

Bon, sur une si petite forme on ne voit pas trop l'intérêt, mais sur un personnage de plus de 1000 vectices on voit la différence croyez-moi. :p

La rotation

Nous allons maintenant faire pivoter le triangle d'un angle de 60° selon l'axe Z. L'axe Z est pointé vers nous mais nous ne le voyons pas, nous verrons cela dans la deuxième partie du chapitre.

Nous utiliserons pour cela la méthode GLMrotate() :

mat4 rotate(mat4 matrice, double angle, vec3 axis);

Les paramètres sont sensiblement les mêmes :

Pour effectuer une rotation de 60° sur l'axe Z, il suffit donc d'appeler la méthode rotate() de cette façon :

// Rotation

modelview = rotate(modelview, 60.0f, vec3(0.0, 0.0, 1.0));

On inclut cette ligne juste avant d'envoyer les matrices au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Remplissage des tableaux Vertex Attrib 0 et 1

    ....


    // Rotation

    modelview = rotate(modelview, 60.0f, vec3(0.0, 0.0, 1.0));


    // On envoie les matrices au shader

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


    // Affichage

    ....


// On n'utilise plus le shader

glUseProgram(0);
Image utilisateur

Hop, le triangle a pivoté de 60° (dans le sens trigonométrique). Encore une fois, on modifie le repère, on ne touche pas aux coordonnées du triangle.

L'homothétie

Allez encore une petit exemple. Nous allons faire une homothétie en multipliant par 2 les unités de mesure du repère. 1 unité sera plus longue, donc le triangle paraitra plus gros.

On appellera pour cela la méthode scale() :

mat4 scale(mat4 matrice, vec3 factors);

On appellera cette méthode ainsi pour agrandir le repère de 2 unités sur les axes X et Y. L'axe Z n'est pas encore visible pour nous, on le laisse donc comme ça.

// Homothétie

modelview = scale(modelview, vec3(2, 2, 1));

Et comme d'habitude, on l'appelle juste avant d'envoyer les matrices au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // Remplissage des tableaux Vertex Attrib 0 et 1

    ....


    // Homothétie

    modelview = scale(modelview, vec3(2, 2, 1));


    // On envoie les matrices au shader

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


    // Affichage

    ....


// On n'utilise plus le shader

glUseProgram(0);
Image utilisateur

Le triangle est tellement gros qu'il sort de la fenêtre :lol: . Bref vous avez compris le principe.

Je vais même vous montrer un truc sympa avec les homothéties, on va inverser le triangle. C'est une technique utilisée dans les jeux-vidéo pour les effets de reflets (comme un effet de miroir, d'eau, ...) :

// Inversion du repère

modelview = scale(modelview, vec3(1, -1, 1));

Les axes X et Z ne sont pas modifiés, ils sont multipliés par 1. Mais l'axe Y lui est inversé, voici ce que ça donne :

Image utilisateur

Ordre des transformations

Je vous ai dit au tout début que l'ordre des transformations était important. Vous pouvez désormais le constater par vous-même. Tout d'abord, on va translater notre triangle selon le vecteur V de coordonnées (0.4, 0.0, 0.0) puis on va le faire pivoter d'un angle de 60° sur l'axe Z :

// Translation puis rotation

modelview = translate(modelview, vec3(0.4, 0, 0));
modelview = rotate(modelview, 60.0f, vec3(0, 0, 1));

Voici le rendu :

Image utilisateur

Maintenant, on fait l'inverse :

// Rotation puis translation

modelview = rotate(modelview, 60.0f, vec3(0, 0, 1));
modelview = translate(modelview, vec3(0.4, 0, 0));

Rendu :

Image utilisateur

On n'obtient pas la même chose dans les deux exemples. Faites attention à l'ordre des transformations c'est important. ;)

Multi-affichage

Courage on aborde le dernier point :p . Dans le deuxième chapitre nous avons vu comment afficher plusieurs triangles sans les transformations. Maintenant que nous les avons, on va voir comment se faciliter la vie.

Si vous affichez plusieurs fois la même chose, pas besoin de remplacer les valeurs du tableau car les valeurs sont toutes les mêmes :p . On ne fait qu'appliquer les transformations sur le repère puis on ré-affiche le triangle. Bien sûr, si on change les valeurs de la matrice modelview alors il faut la ré-envoyer au shader :

// On spécifie quel shader utiliser

glUseProgram(shaderCouleur.getProgramID());


    // On translate le premier triangle

    modelview = translate(modelview, vec3(0.4, 0, 0));


    // On envoie les matrices au shader

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


    /* ***** Affichage du premier triangle ***** */

    glDrawArrays(GL_TRIANGLES, 0, 3);




    // On fait pivoter le deuxième triangle puis on le translate

    modelview = rotate(modelview, 60.0f, vec3(0, 0, 1));
    modelview = translate(modelview, vec3(0.4, 0, 0));


    // On envoie une deuxième fois les matrices au shader

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


    /* ***** Affichage du deuxième triangle ***** */

    glDrawArrays(GL_TRIANGLES, 0, 3);


// On n'utilise plus le shader

glUseProgram(0);

Je vous conseille de faire des petits tests avec les transformations, entrainez-vous avec avant de passer à la deuxième partie de ce chapitre. ;)

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

Nous arrivons à la fin de cette première partie. Nous avons vu pas mal de notions et il est important que vous les compreniez. Encore une fois, amusez-vous à faire des transformations pour vous familiariser avec OpenGL. Si vous vous sentez prêts, passez à la deuxième partie (qui est plus facile à comprendre que cette partie ;) ).


Utilisation de la projection La troisième dimension (Partie 2/2)

La troisième dimension (Partie 2/2)

Exemples de transformations La caméra

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

La troisième dimension (Partie 2/2) Affichage d'un Cube

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 :

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

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:


La troisième dimension (Partie 2/2) Affichage d'un Cube

Affichage d'un Cube

La caméra La classe 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 :

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 caméra La classe Cube

La classe Cube

Affichage d'un Cube Le Frame Rate

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 :

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

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

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


Affichage d'un Cube Le Frame Rate

Le Frame Rate

La classe Cube Push et Pop

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 :

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


La classe Cube Push et Pop

Push et Pop

Le Frame Rate La problématique

Les chapitres les plus compliqués de la partie 1 sont enfin derrière nous, les suivants le seront moins. Aujourd'hui, on va s'intéresser aux piles de matrices.

Retenez bien ce que nous allons voir, et profitez de ce chapitre assez soft pour vous reposer. :magicien:

La problématique

Push et Pop Sauvegarde et Restauration

Introduction

Je profite de cette partie vous faire un aparté sur la matrice modelview. En effet, pour le moment il y a un petit problème avec notre façon de l'utiliser, nous allons étudier un cas pour voir ce qui ne va pas. Jusqu'à maintenant, nous ne nous en sommes pas vraiment rendus compte car nos modèles 3D sont assez simples. Mais maintenant que nous avons fait nos premiers pas dans la 3D, il vaut mieux prendre les bonnes habitudes dès le début. :)

Un problème bien ennuyeux ...

Pour comprendre le problème, nous allons revenir un peu sur les transformations. Vous savez maintenant que chaque transformation que vous faites sera appliquée sur le repère de votre espace 3D et pas seulement sur l'objet que vous voulez afficher. Si vous faites une translation puis une rotation c'est le repère entier qui va être modifié.

Imaginez que vous vouliez afficher un toit pour une maison. Il faudra tout d'abord se placer en haut de la maison, puis faire une rotation pour que le toit soit légèrement penché et afficher le tout.

Maintenant si vous voulez afficher le jardin, comment faites-vous pour revenir en bas de la maison ? Comment faites-vous pour annuler la translation et la rotation qui permettaient d'arriver jusqu'au toit ?

Pour ça, nous avons trois solutions :

La première solution peut vous paraitre la plus simple à mettre en œuvre et sur un exemple comme celui-ci on peut le penser. Mais imaginez une maison de 500 vertices, l'afficher nécessitera plusieurs transformations. Vous pensez vraiment refaire toutes ces transformations dans le sens inverse pour retrouver votre position ?

La réponse est bien sûr non, d'une part parce que se rappeler de toutes les transformations est trop fastidieux et d'autre part parce que ça vous couterait trop de ressources pour refaire tout dans le sens inverse. Surtout que votre maison sera affichée 50 fois par seconde, imaginez le fourbi. :o

La deuxième solution nous pose les mêmes problèmes, réinitialiser la matrice et se replacer nous fait perdre du temps et des ressources.

Nous utiliserons donc la troisième solution qui nous permet de sauvegarder la matrice quand nous en avons besoin. De cette façon, on peut modifier le repère à volonté sans être obligé de retenir sur toutes les transformations. Une fois que nous voudrons revenir à la position sauvegardée, il suffira simplement de restaurer la sauvegarde du repère. :D

Les piles

Avec OpenGL 2

Dans les précédentes versions d'OpenGL, il existait deux fonctions qui permettaient de sauvegarder et de restaurer les matrices facilement. Celles-ci s’appelaient :

Ces deux fonctions fonctionnaient sur un système de pile qui permettait de stocker les sauvegardes les unes sur les autres.

Le principe d'une pile en programmation est d'entasser des variables ou des objets de même type les uns sur les autres comme une pile d'assiettes. Le but de la fonction glPush() était justement d'empiler des matrices entre elles pour former une pile de matrices :

Image utilisateur

On utilisait généralement les piles sur la matrice modelview, étant donné que c'est elle la plus utilisée. Elles étaient basées sur le principe LIFOLIFO (Last In First Out), littéralement sur le principe du dernier arrivé premier sorti. C'est-à-dire que la dernière sauvegarde était la première restaurée.

Prenons l'exemple des assiettes. Si vous empilez 18 assiettes et que vous voulez laver celle la plus en dessous. Il faudra d'abord laver les 17 premières assiettes qui sont au dessus. En revanche, si vous voulez laver la dernière arrivée, elle se trouvera en haut de la pile (l'endroit le plus simple à accéder).

Ok on sait ce qu'est une pile maintenant. :) Mais à quoi ça pouvait bien servir ?

Bonne question, les piles permettaient de sauvegarder la matrice modelview à un état donné pour pouvoir la restaurer plus tard. L'intérêt principal était de pouvoir empiler des matrices pour avoir un système de restauration assez simple : la dernière sauvegarde était la première restaurée.

De cette façon, nous n'avions plus besoin de réinitialiser la matrice modelview ou de retenir toutes les transformations pour savoir où se trouvait le repère.

Avec OpenGL 3

Comme beaucoup d'autres fonctions, OpenGL a déprécié l'utilisation de glPush() et glPop() mais bon il n'y a rien de surprenant là-dedans. En revanche, ce qui est surprenant c'est que la librairie GLM n'inclut pas de méthodes de substitution à ces fonctions. Nous ne pouvons donc pas utiliser les piles de matrices.

Ceci est dû au fait que les matrices sont maintenant des objets au sens propre du terme. C'est-à-dire que nous pouvons les manipuler, utiliser l'allocation dynamique dessus, intégrer la POO, etc. L'usage des piles n'est donc plus utile. C'est assez perturbant pour ceux qui les ont toujours utilisées mais lorsque l'on connait la puissance du C++, on se rend compte que ce n'est pas une si mauvaise idée que ça. :)

Au final, nous allons utiliser une autre manière de faire pour sauvegarder nos matrices. Les piles ne sont plus indispensables mais les sauvegardes, elles, le sont toujours. On ne peut pas résoudre notre problème de toit sinon. :p


Push et Pop Sauvegarde et Restauration

Sauvegarde et Restauration

La problématique Les évènements avec la SDL 2.0

Substitution aux piles

Comme nous l'avons vu à l'instant, les piles ne sont maintenant inutilisables, cependant les sauvegardes doivent quand même être faites.

Pour les faire, nous allons utiliser une des propriétés magiques du C++ : l'opérateur =. En effet, lorsque cet opérateur est surchargé, il permet de pouvoir copier un objet dans un autre objet. C'est ainsi que l'on peut, par exemple, copier deux voitures sans problème :

// Copie d'une voiture

Voiture maCopie = voitureOriginale;

Pour notre problème de matrices, nous allons faire exactement la même chose :

En code, cela donnerait :

// Sauvegarde de la matrice

mat4 sauvegardeModelview = modelview;


// Restauration de la matrice

modelview = sauvegardeModelview;

Facile non ? Je dirais même que c'est plus facile à comprendre que les piles. :p

Quand sauvegarder ?

Le problème des transformations

Maintenant que nous avons une méthode de substitution aux piles, nous allons pouvoir sauvegarder nos matrices dans nos programmes. En théorie, nous devrions faire cela à chaque fois que l'on fait une transformation. Par exemple, le cube du chapitre du chapitre précédent utilise une transformation, et plus précisément une rotation, qui nous permet de faire une pseudo-animation. Nous devons donc utiliser la sauvegarde de matrice ici.

Pourquoi me direz-vous ? Simplement parce que le prochain modèle que nous voudrons afficher (comme un autre cube) sera automatiquement affecté par la rotation. Ce qui fait qu'au lieu de faire pivoter le cube initial, nous les ferons pivoter tous les deux. Essayez ce code pour voir :

// Rotation du repère

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


// Affichage du premier cube

cube.afficher(projection, modelview);


// Affichage du second cube un peu plus loin

modelview = translate(modelview, vec3(10, 0, 0));
cube.afficher(projection, modelview);

Reculez votre caméra avant en la plaçant au point de coordonnées suivantes :

// Placement de la caméra

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

Compilez pour voir.

Vous verrez que les deux cubes sont affectés par la rotation.

Dans un jeu-vidéo, ça serait assez problématique si une simple rotation de caméra faisait pivoter tout un bâtiment. :p

Pour éviter ça, il faut sauvegarder l'état de la matrice modelview avant la rotation, puis la restaurer une fois le modèle affiché :

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

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


    // Affichage du premier cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;


// Affichage du second cube plus loin

modelview = translate(modelview, vec3(10, 0, 0));
cube.afficher(projection, modelview);

Si vous essayez ce code, vous remarquerez que le second cube ne pivote plus. La rotation n'est valable que pour le premier car la matrice avait été sauvegardée avant.

Un autre exemple

Bien entendu, le système de sauvegarde/restauration ne doit pas être utilisé seulement pour les rotations mais bien pour toutes les transformations (translation et homothétie comprises).

Ce qui fait que le code précédent est encore incomplet car la translation du second cube affectera les objets affichés après. Il faut donc réutiliser la sauvegarde de matrice :

// Affichage du premier cube

....


// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Affichage du second cube plus loin

    modelview = translate(modelview, vec3(2, 0, 0));
    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Le cas des objets proches

Ce système de sauvegarde nous permet de revenir à chaque fois au centre du repère. Nos modèles sont donc placés sans erreur vu que nous partons toujours du point de coordonnées (0, 0, 0). Aucune transformation ne les affecte.

Le seul cas où vous pourrez vous permettre de ne pas restaurer la matrice immédiatement est le cas où des objets se situeraient près les uns des autres. Par exemple, imaginez que votre cube devienne une caisse et que vous souhaitez en afficher plusieurs côte à côte. Vous n'allez pas sauvegarder votre matrice à chaque fois pour revenir quasiment au même point après.

Si elles sont assez proches, vous pourrez les afficher les unes à la suite des autres sans problème et ce même si vous utilisez d'autres transformations :

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Affichage du premier cube (au centre du repère)

    cube.afficher(projection, modelview);


    // Affichage du deuxième cube

    modelview = translate(modelview, vec3(3, 0, 0));
    cube.afficher(projection, modelview);


    // Affichage du troisième cube

    modelview = translate(modelview, vec3(3, 0, 0));
    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;
Image utilisateur

Vous voyez ici que les cubes sont assez proches, il est donc inutile de revenir au centre du repère pour repartir afficher la caisse suivante.

Ce qu'il faut retenir

Pour résumer tout ce blabla un peu confus :

C'est tout ce qu'il faut retenir dans ce chapitre. :)

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

Nous voici à la fin de ce chapitre. Nous avons appris à coder et à utiliser les sauvegardes de matrice. C'était un chapitre un peu technique mais il concernait une notion indispensable de la programmation avec OpenGL. N'oubliez pas qu'à partir de maintenant, nous utiliserons toujours les piles de matrices pour afficher nos modèles. ;)

Pour la suite du tuto je vous propose de vous reposer encore un peu, le prochain chapitre que nous allons aborder sera assez facile à comprendre. Il concernera les évènements avec la SDL 2.0. :)


La problématique Les évènements avec la SDL 2.0

Les évènements avec la SDL 2.0

Sauvegarde et Restauration Différences entre la SDL 1.2 et 2.0

Depuis le début du tutoriel, les évènements sont gérés automatiquement et sans contrôle sur le clavier et la souris. Nous pourrions continuer à coder ainsi, sans se préoccuper de rien mais on va profiter de ce petit chapitre pour créer une classe à part entière qui s'occupera de gérer tout ça pour nous. Avec elle, nous pourrons retrouver facilement les évènements qui nous intéressent comme les touches ou les boutons de souris qui sont utilisés, ...

Bien entendu, notre code devra respecter la règle de l'encapsulation. Il faudra donc faire attention à protéger les attributs pour ne pas accéder directement aux évènements.

Différences entre la SDL 1.2 et 2.0

Les évènements avec la SDL 2.0 La classe Input

Nous avons déjà eu l’occasion de constater les différences qu'ils existaient entre l'ancienne et la nouvelle version de la SDL, notamment lors de la création de fenêtre. Nous allons voir maintenant celles qui concernent les évènements car ils sont indispensables au développement d'un jeu vidéo. D'ailleurs, on les a déjà utilisés lorsque nous voulions savoir si une fenêtre devait se fermer ou pas.

En effet, à chaque tour de la boucle principale, on vérifie si l'évènement SDL_WINDOWEVENT_CLOSE est déclenché, ce qui nous permet de continuer ou d’arrêter la boucle :

// Boucle principale

bool terminer(false);

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

    SDL_PollEvent(&m_evenements);

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

Ce bout de code est pratique mais il ne nous permet pas de gérer la pression des touches. On pourrait par exemple appuyer sur la touche ECHAP pour terminer la boucle, ou utiliser un de souris.

Si vous vous rappelez du chapitre sur les évènements de la SDL 1.2 dans le cours de M@téo, vous vous souvenez surement de la façon de gérer les touches du clavier. Par exemple, pour gérer la pression des touches T ou ECHAP il fallait faire ceci :

// Structure

SDL_Event evenements;


// Récupération d'un évènement

SDL_PollEvent(&evenements);


// Switch sur le type d'évènement

switch(evenements.type)
{
    case SDL_KEYDOWN:
        
        // Gestion des touches

        switch(evenements.key.keysym.sym)
        {
            case SDLK_T:
                ....
            break;

            case SDLK_ESCAPE:
                ....
            break;
        }

    break;
}

Voilà comment on gérait les évènements avec la SDL 1.2, et la bonne nouvelle c'est qu'avec la version 2.0 ça se passe de la même façon. :p

Seulement deux choses vont être modifiées :

Si on modifie le code précédent on trouve :

// Structure

SDL_Event evenements;


// Récupération d'un évènement

SDL_PollEvent(&evenements);


// Switch sur le type d'évènement

switch(evenements.type)
{
    case SDL_KEYDOWN:
        
        // Gestion des touches

        switch(evenements.key.keysym.scancode)
        {
            case SDL_SCANCODE_T:
                ....
            break;

            case SDL_SCANCODE_ESCAPE:
                ....
            break;
        }

    break;
}

Aucun gros changement. ;)

Je ne peux vous parler plus explicitement des scancodes mais sachez simplement que mis à part le nom des constantes, le code restera le même pour nous. Si on veut aller plus loin au niveau du fonctionnement de la SDL on remarque que les scancodes sont une modification majeur de la librairie car on change de norme pour identifier les touches en interne. Mais bon dans le fond on s'en moque un peu. :p

Concernant la souris, il n'y a rien à signaler. Il n'y a pas de grands changements par rapport à la SDL 1.2. La gestion se fera de la même façon pour nous.


Les évènements avec la SDL 2.0 La classe Input

La classe Input

Différences entre la SDL 1.2 et 2.0 Les méthodes indispensables

La classe Input

Maintenant que nous avons vu les différences qu'il existait entre les deux versions de la SDL, nous allons passer à l'implémentation d'une classe qui gèrera tous les évènements toute seule. :)

Ça veut dire qu'on va remplacer la structure SDL_Event par une classe ?

Alors oui et non, car la structure SDL_Event existera toujours quelque part. Mais au lieu de l'utiliser dans la classe SceneOpenGL, on l’utilisera dans une classe à part. Grâce à ça, les évènements seront protégés par la règle de l'encapsulation et seront aussi plus simples à utiliser. Seul l'appel à une méthode nous permettra de savoir si telle ou telle touche est enfoncée. :)

On appellera cette classe : la classe Input.

En informatique, les inputs sont des systèmes qui permettent d'apporter à l'ordinateur des actions venant de l'utilisateur. Elles peuvent prendre la forme d'un mouvement de souris, d'une pression sur une touche du clavier, d'un geste de doigt sur un écran tactile, ... D'où le nom Input pour la classe qui gèrera tous ces évènements. :)

Voici sa déclaration :

#ifndef DEF_INPUT
#define DEF_INPUT

// Include

#include <SDL2/SDL.h>


// Classe

class Input
{
    public:

    Input();
    ~Input();


    private:
};

#endif

Les attributs

La classe Input possèdera plusieurs attributs :

On va directement s’intéresser aux tableaux de booléens. Chaque case de ces tableaux pourra prendre deux valeurs : si une touche, ou un bouton, est enfoncé(e) elle prendra la valeur true, dans le cas contraire ce sera la valeur false. Au début du programme, toutes les cases sont initialisées à false, vu que l'on appuie sur aucune des touches.

Pour définir la taille de ces tableaux on pourrait croire qu'il faille utiliser l'allocation dynamique - vu qu'il existe plusieurs types de claviers - mais pas du tout. En effet, la SDL gère toute seule les différents claviers. Elle est capable de nous fournir une seule et unique constante (SDL_NUM_SCANCODES) représentant le nombre maximal de touches d'un clavier, et par conséquent la taille du tableau m_touches[]. Grâce à elle, on pourra gérer toutes les touches automatiquement. ;)

Pour la souris, ce sera un peu différent. Il n'existe pas de constante que l'on peut donner au tableau. En revanche, on peut savoir que la SDL ne peut gérer que 7 boutons pour la souris. On donnera donc une taille de 7 cases au tableau m_boutonsSouris[].

Si on regroupe les attributs on a :

#ifndef DEF_INPUT
#define DEF_INPUT

// Include

#include <SDL2/SDL.h>


// Classe

class Input
{
    public:

    Input();
    ~Input();


    private:

    SDL_Event m_evenements;
    bool m_touches[SDL_NUM_SCANCODES];
    bool m_boutonsSouris[8];

    int m_x;
    int m_y;
    int m_xRel;
    int m_yRel;

    bool m_terminer;
};

#endif

Avec ces attributs, on pourra récupérer tous les évènements dont on aura besoin. On verra en dernière partie de ce chapitre quelques méthodes qui nous permettront d'accéder à ces attributs.

Le constructeur et le destructeur

Maintenant qu'on a défini la classe Input, on doit s'occuper d'initialiser ses attributs avec le constructeur. Dans cette classe, on aura besoin que d'un constructeur : la constructeur par défaut.

Input();

Il s'occupera de mettre tous les attributs soit à 0 soit à false.

Son implémentation commence par l'initialisation de tous les attributs (sauf les tableaux) :

Input::Input() : m_x(0), m_y(0), m_xRel(0), m_yRel(0), m_terminer(false)
{

}

Pour initialiser les tableaux de bool, il faudra juste faire une boucle pour affecter la valeur false aux différentes cases :

Input::Input() : m_x(0), m_y(0), m_xRel(0), m_yRel(0), m_terminer(false)
{
    // Initialisation du tableau m_touches[]

    for(int i(0); i < SDL_NUM_SCANCODES; i++)
        m_touches[i] = false;


    // Initialisation du tableau m_boutonsSouris[]

    for(int i(0); i < 8; i++)
        m_boutonsSouris[i] = false;
}

Voilà pour le constructeur. :)

On profite également de cette partie pour créer le destructeur. Même si on ne met rien dedans, on l'implémente quand même. C'est une bonne habitude à prendre. ;)

Input::~Input()
{

}

La boucle d'évènements

Pour la partie qui va suivre, je vais vous demander toute votre attention. Nous allons voir la notion la plus importante du chapitre : la boucle d'évènements.

Euh c'est quoi cette boucle ? Ça a un rapport avec les évènements SDL ?

Oui tout à fait. Il s'agit d'une boucle qui récupère tous les évènements à un moment donné, quelque soit leur nature (touche de clavier, mouvement de souris, ...).

Si je vous parle de cette boucle, c'est parce qu'à l'heure actuelle nous avons un gros problème : nous ne sommes pas capables de gérer plusieurs évènements à la fois. Je vais vous donner un exemple pour que vous compreniez bien le problème.

Dans votre vie, vous avez probablement déjà joué à un jeu-vidéo, comme Mario par exemple. Les jeux Mario sont en général des jeux de plateformes où notre petit plombier doit sauter et avancer sur des plateformes, tuyaux et autres éléments de l'environnement.

La plupart du temps, sans même vous en rendre compte, vous appuyez sur plusieurs touches (ou boutons) en même temps. Par exemple, un personnage saute et avance en même temps, il se passe deux actions : sauter et avancer. On peut même rajouter des actions à ça : tirer, sprinter, ... Dans ce cas, le programme doit gérer 4 évènements en même temps.

A l'heure actuelle, notre problème c'est qu'avec la SDL nous ne pouvons gérer qu'un seul et unique évènement à la fois. Si nous programmions un Mario avec la SDL, le personnage ne pourrait même pas avancer et sauter en même, ce qui est un peu contraignant ... :p

Le problème vient de la SDL alors ? Elle est un peu nulle cette librairie :(

Non pas du tout, car ça ne vient pas vraiment d'elle mais de notre code. Vous vous souvenez du switch au début du chapitre ? Celui-ci :

// Structure

SDL_Event evenements;


// Attente d'un évènement

SDL_PollEvent(&evenements);


// Switch sur le type d'évènement

switch(evenements.type)
{
    case SDL_KEYDOWN:
        
        // Gestion des touches

        switch(evenements.key.keysym.scancode)
        {
            case SDL_SCANCODE_T:
                ....
            break;

            case SDL_SCANCODE_ESCAPE:
                ....
            break;
        }

    break;
}

Avec ce code, on ne peut gérer qu'un seul évènement par tour de boucle OpenGL.

Pour régler ce problème, on va se servir d'une petite astuce de la fonction SDL_PollEvent(). En effet, si on s’intéresse à son prototype on peut remarquer une chose :

int SDL_PollEvent(SDL_Event *event);

La fonction SDL_PollEvent() renvoie une valeur (un int).

On ne s'est jamais servi de cette valeur (on ne savait même pas qu'elle existait d'ailleurs) et pourtant c'est elle qui va régler notre problème. Cet integer retourné peut prendre deux valeurs :

Pour capturer tous les évènements, il suffit de piéger la fonction SDL_PollEvent() dans une boucle. Tant qu'il reste quelque chose à récupérer dans la file d'attente, la fonction retourne 1, donc on continue de récupérer les évènements jusqu'à qu'il n'y en ait plus.
Grâce à cette astuce, on sera capable de gérer plusieurs touches/boutons en même temps. Le petit Mario de tout à l'heure pourra donc avancer, sauter, sprinter, tirer des boules de feu ... et tout ça en même temps. ;)
D'où la boucle d'évènements dont je vous ai parlée tout à l'heure. :p

Au niveau du code, il suffit d'enfermer la fonction SDL_PollEvent() dans une boucle while. Tant que la fonction retourne 1 on continue la boucle :

// Structure

SDL_Event evenements;


// Boucle d'évènements

while(SDL_PollEvent(&evenements) == 1)
{
    // Switch sur le type d'évènement

    switch(evenements.type)
    {
        case SDL_KEYDOWN:
        
            // Gestion des touches

            switch(evenements.key.keysym.scancode)
            {
                case SDL_SCANCODE_T:
                    ....
                break;

                case SDL_SCANCODE_ESCAPE:
                    ....
                break;
            }

        break;
    }
}

On peut même simplifier le while en enlevant la condition ' == 1 ' :

// Boucle d'évènements

while(SDL_PollEvent(&evenements))
{
    ...
}

Vous savez maintenant ce qu'est la boucle d'évènements. C'est une boucle qui permet de capturer toutes les actions qui concernent le clavier, la souris, ... en même temps.

Nous allons maintenant revenir à la classe Input pour intégrer cette boucle dans une méthode. :)

La méthode updateEvenements()

La méthode qui va implémenter cette boucle s'appellera updateEvenements(). Voici son prototype :

void updateEvenements();

Commençons cette méthode en codant la fameuse boucle while qui prendra en "condition" la valeur retournée par la fonction SDL_PollEvent().

D'ailleurs, on donnera à cette dernière l'adresse de l'attribut m_evenements - car oui la structure SDL_Event existe toujours quelque part, elle n'a pas disparue. :p

void Input::updateEvenements()
{
    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {

    }
}

La suite du code ne changera pas beaucoup par rapport à la SDL 1.2. On met en place un gros switch qui va tester la valeur du champ m_evenements.type . On commencera par gérer les conditions relatives au clavier.

Deux cas doivent être gérés par le clavier :

Dans les deux cas, on actualisera l'état de la touche correspondante dans le tableau m_touches[]. D'ailleurs, on utilisera le champ m_evenements.key.keysym.scancode qui nous servira d'indice pour retrouver la bonne case :

// Actualisation de l'état de la touche

m_touches[m_evenements.key.keysym.scancode] = true;

Le gros avantage du tableau de booléens c'est que, quelque soit la touche enfoncée, nous n'avons besoin que d'une seule ligne de code pour actualiser son état. Que ce soit la touche A, B, C, ESPACE, ... une seule ligne suffit. :D

En définitif, pour gérer les touches du clavier nous devons :

void Input::updateEvenements()
{
    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {
        // Switch sur le type d'évènement

        switch(m_evenements.type)
        {
            // Cas d'une touche enfoncée

            case SDL_KEYDOWN:
                m_touches[m_evenements.key.keysym.scancode] = true;
            break;


            // Cas d'une touche relâchée

            case SDL_KEYUP:
                m_touches[m_evenements.key.keysym.scancode] = false;
            break;


            default:
            break;
        }
    }
}

Grâce à ce code, toutes les touches du clavier peuvent être mises à jour en même temps (enfin, en une boucle :p ).

Gestion de la souris

Nous avons déjà fait la plus grosse part du travail, on sait désormais gérer toutes les touches du clavier. Nous allons maintenant passer à la gestion de la souris.

La bonne nouvelle, c'est que les boutons de la souris se gèrent de la même façon que les touches du clavier. Il suffit de rajouter deux cases au switch : un pour les boutons qui seront pressés (ce case utilisera la constante SDL_MOUSEBUTTONDOWN) et un autre pour les boutons qui seront relâchés (il utilisera la constante SDL_MOUSEBUTTONUP).

Pour récupérer l'indice du bouton dans le tableau m_boutonsSouris[], nous n'utiliserons pas le champ m_evenements.key.keysym.scancode, car ce champ ne concerne uniquement que le clavier. A la place, nous utiliserons le champ evenements.button.button qui lui est réservé à la souris.

On ajoute donc les deux cases suivants au switch :

// Cas de pression sur un bouton de la souris

case SDL_MOUSEBUTTONDOWN:

    m_boutonsSouris[m_evenements.button.button] = true;

break;


// Cas du relâchement d'un bouton de la souris

case SDL_MOUSEBUTTONUP:

    m_boutonsSouris[m_evenements.button.button] = false;

break;

Ça c'était la partie des boutons. Maintenant il faut s'occuper des mouvements de la souris.

Lorsque la souris est en mouvement, un évènement est déclenché dans la SDL. Cet évènement va permettre de mettre à jour les coordonnées (x, y) du pointeur ainsi que ses coordonnées relatives. Nous allons pouvoir détecter ces mouvements grâce à la constante SDL_MOUSEMOTION. Lorsque cet évènement sera déclenché, on mettra à jour les attributs qui concernent les coordonnées. On prendra les nouvelles valeurs dans le champ m_evenements.motion :

// Cas d'un mouvement de souris

case SDL_MOUSEMOTION:

    m_x = m_evenements.motion.x;
    m_y = m_evenements.motion.y;

    m_xRel = m_evenements.motion.xrel;
    m_yRel = m_evenements.motion.yrel;

break;

Fermeture de la fenêtre

Il ne reste plus qu'un seul évènement à gérer dans cette boucle : le cas de la fermeture de la fenêtre (la croix rouge en haut à droite sous Windows). Depuis le début du tutoriel, on utilise cet évènement dans la boucle principale d'OpenGL pour savoir si on doit quitter le programme :

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

Il faut maintenant enlever ce code pour le migrer dans la méthode updateEvenements(). N'oubliez pas que c'est elle et uniquement elle qui doit mettre à jour tous les évènements.

Nous allons donc ajouter un nouveau case dans le switch pour gérer cette fermeture. On utilisera la constante SDL_WINDOWSEVENT pour savoir si cet évènement a été déclenché. N'oubliez pas de modifier la variable terminer en m_terminer, car on met à jour non plus une variable mais un attribut :

// Cas de la fermeture de la fenêtre

case SDL_WINDOWEVENT:

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

break;

Si réunie tous ces cases dans la méthode, on trouve :

void Input::updateEvenements()
{
    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {
        // Switch sur le type d'évènement

        switch(m_evenements.type)
        {
            // Cas d'une touche enfoncée

            case SDL_KEYDOWN:
                m_touches[m_evenements.key.keysym.scancode] = true;
            break;


            // Cas d'une touche relâchée

            case SDL_KEYUP:
                m_touches[m_evenements.key.keysym.scancode] = false;
            break;


            // Cas de pression sur un bouton de la souris

            case SDL_MOUSEBUTTONDOWN:

                m_boutonsSouris[m_evenements.button.button] = true;

            break;


            // Cas du relâchement d'un bouton de la souris

            case SDL_MOUSEBUTTONUP:

                m_boutonsSouris[m_evenements.button.button] = false;

            break;


            // Cas d'un mouvement de souris

            case SDL_MOUSEMOTION:

                m_x = m_evenements.motion.x;
                m_y = m_evenements.motion.y;

                m_xRel = m_evenements.motion.xrel;
                m_yRel = m_evenements.motion.yrel;

            break;


            // Cas de la fermeture de la fenêtre

            case SDL_WINDOWEVENT:

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

            break;


            default:
            break;
        }
    }
}

Problème des coordonnées relatives

Cette méthode est presque complète, il reste juste un petit point à régler qui concerne les coordonnées relatives. Ces coordonnées représentent la différence entre la position actuelle et l'ancienne position, elles seront très utiles dans le chapitre sur la caméra.

Le problème avec ces coordonnées c'est que : s'il n'y a aucun évènement alors elles ne sont pas mises à jour, elles conservent donc leurs anciennes valeurs. Ce qui veut dire que le programme considère que la souris continue de bouger, même si elle est inactive.

Pour régler ce problème, on va ré-initialiser les coordonnées avec la valeur 0 au début de la méthode. Ne vous inquiétez pas, si les coordonnées doivent être mises à jour avec de vraies valeurs elles le seront dans le switch.

Grâce à cette astuce, nous n'aurons aucun problème de mouvement fictif. On rajoute donc ces deux lignes de code au début de la méthode :

// Pour éviter des mouvements fictifs de la souris, on réinitialise les coordonnées relatives

m_xRel = 0;
m_yRel = 0;

Notre méthode donne donc au final :

void Input::updateEvenements()
{
    // Pour éviter des mouvements fictifs de la souris, on réinitialise les coordonnées relatives

    m_xRel = 0;
    m_yRel = 0;


    // Boucle d'évènements

    while(SDL_PollEvent(&m_evenements))
    {
        // Switch sur le type d'évènement

        switch(m_evenements.type)
        {
            // Cas d'une touche enfoncée

            case SDL_KEYDOWN:
                m_touches[m_evenements.key.keysym.scancode] = true;
            break;


            // Cas d'une touche relâchée

            case SDL_KEYUP:
                m_touches[m_evenements.key.keysym.scancode] = false;
            break;


            // Cas de pression sur un bouton de la souris

            case SDL_MOUSEBUTTONDOWN:

                m_boutonsSouris[m_evenements.button.button] = true;

            break;


            // Cas du relâchement d'un bouton de la souris

            case SDL_MOUSEBUTTONUP:

                m_boutonsSouris[m_evenements.button.button] = false;

            break;


            // Cas d'un mouvement de souris

            case SDL_MOUSEMOTION:

                m_x = m_evenements.motion.x;
                m_y = m_evenements.motion.y;

                m_xRel = m_evenements.motion.xrel;
                m_yRel = m_evenements.motion.yrel;

            break;


            // Cas de la fermeture de la fenêtre

            case SDL_WINDOWEVENT:

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

            break;


            default:
            break;
        }
    }
}

Voilà ! Maintenant nous sommes capables de mettre à jour tous nos évènements, peu importe leur nombre ils seront tous gérés simultanément par la classe Input. :D

La méthode terminer()

Il ne reste plus qu'une chose à ajouter dans la classe : une méthode qui permet de dire si oui ou non l'utilisateur veut quitter le programme. Jusqu'à maintenant, on utilisait un booléen terminer pour fermer la fenêtre, mais maintenant ce booléen se trouve dans la classe Input.

Or, avec la règle de l'encapsulation on ne peut pas directement vérifier cet attribut. Il nous faut donc coder un accesseur pour récupérer la valeur du booléen.

Le prototype est terriblement simple. :p

bool terminer() const;

Son implémentation l'est tout autant :

bool Input::terminer() const
{
    return m_terminer;
}

Modification de la classe SceneOpenGL

On passe maintenant à l'utilisation de la classe Input dans notre scène 3D. Pour cela, on va remplacer l'ancien code de gestion des évènements par nos nouvelles méthodes.

On commence donc par déclarer un objet de type Input dans la classe SceneOpenGL qui viendra remplacer l'ancien attribut m_evenements :

#include "Input.h"

class SceneOpenGL
{
    public:

    /* *** Méthodes *** */


    private:

    /* *** Attributs *** */

    // Objet Input pour la gestion des évènements

    Input m_input;
}

On n'oublie pas de l'initialiser dans le constructeur :

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

Dans la boucle principale, on ne vérifie donc plus le booléen terminer mais la valeur retournée par la méthode terminer() :

// Boucle principale

while(!m_input.terminer())
{
    /* ***** RENDU ***** */
}

Enfin, on supprime notre ancien code de gestion d'évènements que l'on remplace par la méthode updateEvenements() de la classe Input :

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


    /* *** Rendu *** */
}

Voilà pour la classe Input ! Grâce à elle, tous nos évènements seront mis à jour automatiquement. :)

Maintenant que l'on a fait cela, on va pouvoir passer à l'implémentation de plusieurs méthodes "indispensables" qui vont nous permettre de connaitre l'état du clavier, de la souris, ... Nous devons passer par ces méthodes pour respecter la règle de l'encapsulation, sans quoi nous serions obligés d’accéder directement aux attributs pour avoir nos valeurs.


Différences entre la SDL 1.2 et 2.0 Les méthodes indispensables

Les méthodes indispensables

La classe Input Les textures

Méthodes

Cette dernière partie sera consacrée à une série de méthodes qui seront utilisées tout au long du tuto. Je vais vous donner une petite liste des fonctionnalités que l'on va implémenter. Il nous faudra une méthode pour :

Houla on va devoir coder tout ça ? o_O

Et bien oui. Mais sachez pour vous rassurer que la plus grosse méthode de cette liste ne fera que 4 lignes. :p

Ces méthodes ne se contenteront que de faire une simple action (renvoyer un booléen par exemple) avec parfois des bloc if else. Rien de compliqué ne vous inquiétez pas.

La méthode getTouche()

On commence par la méthode la plus importante : celle qui permet de savoir si une touche a été enfoncée (ou non). En gros, on va coder un getter sur le tableau m_touches[]. :p Elle prendra en paramètre une variable de type SDL_Scancode correspondant à la touche demandée :

bool getTouche(const SDL_Scancode touche) const;

Cette méthode renverra true si la touche est pressée ou false si elle ne l'est pas. N'oubliez pas de la déclarer en tant que méthode constante, vu que l'on ne modifie aucun attribut :

bool Input::getTouche(const SDL_Scancode touche) const
{
    return m_touches[touche];
}

Fini. :)

Bonus : On va utiliser cette nouvelle méthode dès maintenant. Désormais, je veux que chacune de vos fenêtres SDL de chaque projet que vous ferez puisse se fermer en appuyant sur la touche ECHAP.

Comment feriez-vous ça avec le getter que l'on vient de coder ? Je vous laisse réfléchir un peu. :p

Vous avez trouvé ?

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


    /* *** Rendu *** */
}

Il suffit simplement d'appeler le getter getTouche() avec le scancode SDL_SCANCODE_ESCAPE. Si le getter retourne true, on casse la boucle avec le mot-clef break.

Bonus 2 : Pour savoir si deux touches sont enfoncées simultanément, il suffira d'utiliser un bloc if avec les deux touches demandées :

// Est-ce que les touches Z et D sont pressées ?

if(m_input.getTouche(SDL_SCANCODE_Z) && m_input.getTouche(SDL_SCANCODE_D))
{
    ...
}

La méthode getBoutonSouris()

La méthode, ou plutôt le getter, getBoutonSouris() fera la même chose que getTouche(). C'est-à-dire qu'elle permettra de savoir si un bouton spécifié est enfoncé ou pas. La seule différence avec la méthode précédente c'est que cette fois-ci, on lui donnera en paramètre non pas un SDL_Scancode mais une variable de type Uint8 correspondant au bouton demandé (En réalité, ce sera une constante comme avec les scancodes).

bool getBoutonSouris(const Uint8 bouton) const;

Elle renverra l'état du bouton demandé dans le tableau m_boutonsSouris[] :

bool Input::getBoutonSouris(const Uint8 bouton) const
{
    return m_boutonsSouris[bouton];
}

La méthode mouvementSouris()

La méthode suivante nous permettra de savoir si le pointeur de la souris a bougé. Grâce à elle, nous pourrons déclencher une action dès que la souris bougera. On appellera cette méthode : mouvementSouris(), elle renverra un booléen.

bool mouvementSouris() const;

Pour détecter un mouvement de souris il suffit de comparer la position relative du pointeur grâce aux attributs m_xRel et m_yRel. Si ces deux attributs sont égals à 0, alors le pointeur n'a pas bougé. Si en revanche ils ont une valeur non nulle, alors c'est que le pointeur a bougé.

bool Input::mouvementSouris() const
{
    if(m_xRel == 0 && m_yRel == 0)
        return false;

    else
        return true;
}

Les Getters du pointeur

Les méthodes suivantes sont des getters, elles renvoient chacune un attribut qui concerne la position du pointeur. Vous savez déjà comment fonctionne un getter, je vous épargne donc les explications. :)

Voici leur constructeur :

// Getters

int getX() const;
int getY() const;

int getXRel() const;
int getYRel() const;

Implémentation :

// Getters concernant la position du curseur

int Input::getX() const
{
    return m_x;
}

int Input::getY() const
{
    return m_y;
}

int Input::getXRel() const
{
    return m_xRel;
}

int Input::getYRel() const
{
    return m_yRel;
}

La méthode afficherPointeur()

Les méthodes suivantes peuvent vous paraitre inutiles pour le moment, mais dès que nous utiliserons une caméra mobile pour comprendrez vite leur utilité.

La méthode afficherPointeur() va permettre d'afficher ou de cacher le pointeur à l'écran. Elle prendra en paramètre un booléen qui :

void afficherPointeur(bool reponse) const;

Cette méthode va faire appel à une fonction SDL pour afficher ou cacher le pointeur :

int SDL_ShowCursor(int toggle);

Cette fonction prend en paramètre une constante qui peut être égale soit à SDL_ENABLE soit à SDL_DISABLE, ce paramètre se comporte un peu comme un booléen. On ne s'occupera pas de la valeur retournée par la fonction.

Voici l'implémentation de la méthode :

void Input::afficherPointeur(bool reponse) const
{
    if(reponse)
        SDL_ShowCursor(SDL_ENABLE);

    else
        SDL_ShowCursor(SDL_DISABLE);
}

La méthode capturerPointeur()

La dernière méthode va permettre d'utiliser le Mode Relatif de la Souris. Ce mode permet de piéger le pointeur dans la fenêtre, il ne pourra pas en sortir. C'est utile voir obligatoire dans un jeu-vidéo par exemple. On appellera cette méthode : capturerPointeur().

Faites attention à cette méthode, si vous l'appelez dans votre code vous devrez prévoir quelque chose pour fermer votre programme. Utilisez un getTouche() par exemple. Si vous ne faites pas ça, vous ne pourrez plus fermer votre fenêtre. Vous devrez alors utiliser les combinaisons CTRL + MAJ + SUPPR ou WINDOWS + TAB (si vous êtes sous Windows) pour pouvoir ré-utiliser la souris.

Le prototype de la méthode est le suivant :

void capturerPointeur(bool reponse) const;

Elle se comportera exactement de la même manière que la méthode précédente. Elle utilisera la fonction SDL_SetRelativeMousseMode() de la SDL pour activer le mode relatif de la souris :

int SDL_SetRelativeMouseMode(SDL_bool enabled);

Cette fonction prendra en paramètre un SDL_bool. Sa valeur peut être soit SDL_TRUE soit SDL_FALSE. Voici son implémentation :

void Input::capturerPointeur(bool reponse) const
{
    if(reponse)
        SDL_SetRelativeMouseMode(SDL_TRUE);

    else
        SDL_SetRelativeMouseMode(SDL_FALSE);
}

Voilà pour l'implémentation des méthodes "indispensables". :)

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

Exercices

Énoncés

Je vais vous donner des exercices assez simples pour vous habituez à utiliser votre nouvelle classe. Ceux-ci seront axés sur la pseudo-animation du cube (celle qui permettait de le faire tourner). Vous n'aurez besoin d'utiliser que les méthodes qui sont déjà codées, vous n'aurez donc pas besoin d'en rajouter.

Il est préférable que vous incluiez le code relatif aux évènements avant la fonction glClear(). En effet, vous verrez dans le futur que nous aurons parfois de faire plusieurs affichages de la même scène. Pour économiser du temps de calcul les inputs doivent se gérer avant ces affichages. La seule chose qui peut se trouver après la fonction c'est la méthode rotate() car elle ne fait pas partie des évènements mais des matrices (et donc de l'affichage).

Autant prendre les bonnes habitudes dès maintenant, surtout que ça ne mange pas de pain. :)

Exercice 1 : L'animation initiale du cube permettait de le faire pivoter selon un angle et par rapport à l'axe Y. Votre objectif est de dé-automatiser cette rotation pour qu'elle réponde aux touches du clavier Flèche Gauche et Droite (SDL_SCANCODE_LEFT et SDL_SCANCODE_RIGHT). Vous pouvez vous aider du code que l'on avait utilisé dans le chapitre 7 :

// Incrémentation de l'angle

angle += 4.0;

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


// Sauvegarde de la matrice

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

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


    // Affichage du cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Exercice 2 : Même exercice que précédemment sauf que l'axe concerné n'est plus Y mais X et les touches sont Flèche Haut et Bas (SDL_SCANCODE_UP et SDL_SCANCODE_DOWN).

Exercice 3 : Rassemblez les deux animations précédentes pour le même cube. C'est-à-dire que vous devez pouvoir appuyer sur les 4 touches directionnelles pour le faire pivoter selon l'axe XetY (Petit indice : vous aurez besoin de deux angles).

Solution

Exercice 1 :

Le but de l'exercice était de "manualiser" la rotation en fonction des touches Haut et Bas. Pour cela, il fallait simplement appeler la méthode getTouche() de l'objet m_input deux fois avec les constantes SDL_SCANCODE_LEFT et SDL_SCANCODE_RIGHT :

// Gestion des évènements

....


// Rotation du cube vers la gauche

if(m_input.getTouche(SDL_SCANCODE_LEFT))
{

}


// Rotation du cube vers la droite

if(m_input.getTouche(SDL_SCANCODE_RIGHT))
{

}


// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

....

Une fois la vérification des touches pressées faite, il ne manquait plus qu'à copier le code de rotation. Bien entendu, il fallait additionner l'angle dans un cas et le soustraire dans l'autre :

// Gestion des évènements

....


// Rotation du cube vers la gauche

if(m_input.getTouche(SDL_SCANCODE_LEFT))
{
    // Modification de l'angle

    angle -= 4.0;


    // Limitation

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


// Rotation du cube vers la droite

if(m_input.getTouche(SDL_SCANCODE_RIGHT))
{
    // Modification de l'angle

    angle += 4.0;


    // Limitation

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


// Nettoyage de l'écran

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

....

Enfin, il ne manquait plus qu'à appeler la méthode rotate() avec l'angle de rotation en paramètre et afficher le cube. Le tout encadré par la sauvegarde et la restauration de la matrice modelview. ;)

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

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


    // Affichage du premier cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Exercice 2 :

Cet exercice reprend le même principe que le précédent sauf qu'il fallait, d'une part, modifier les constantes utilisées :

// Rotation du cube vers le bas

if(m_input.getTouche(SDL_SCANCODE_DOWN))
{
    // Modification de l'angle

    angle -= 4.0;


    // Limitation

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


// Rotation du cube vers le haut

if(m_input.getTouche(SDL_SCANCODE_UP))
{
    // Modification de l'angle

    angle += 4.0;


    // Limitation

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

Et d'autre part, il fallait également modifier l'axe de rotation de la méthode rotate() :

// Rotation du repère

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

Exercice 3 :

Le dernier exercice était un poil plus dur que les deux autres, il fallait utiliser deux angles et gérer 4 touches du clavier.

Vous pouviez donner n'importe quel nom aux angles :

// Angles de la rotation

float angleX(0.0);
float angleY(0.0);

La gestion des 4 touches n'était pas compliquée du moment que vous utilisiez le bon angle avec le bon axe de rotation :

// Rotation du cube vers la gauche

if(m_input.getTouche(SDL_SCANCODE_LEFT))
{
    angleY -= 5;

    if(angleY > 360)
        angleY -= 360;
}


// Rotation du cube vers la droite

if(m_input.getTouche(SDL_SCANCODE_RIGHT))
{
    angleY += 5;

    if(angleY < -360)
        angleY += 360;
}
// Rotation du cube vers le haut

if(m_input.getTouche(SDL_SCANCODE_UP))
{
    angleX -= 5;

    if(angleX > 360)
        angleX -= 360;
}


// Rotation du cube vers le bas

if(m_input.getTouche(SDL_SCANCODE_DOWN))
{
    angleX += 5;

    if(angleX < -360)
        angleX += 360;
}

J'ai divisé le code en deux pour le rendre plus lisible. Il fallait évidemment tout coder au même endroit. ;)

Enfin, pour gérer les deux rotations, il fallait simplement appeler la méthode rotate() deux fois. Un appel prenait en compte l'angle angleX et l'autre l'angle angleY :

// Sauvegarde de la matrice modelview

mat4 sauvegardeModelview = modelview;


    // Rotation du repère

    modelview = rotate(modelview, angleY, vec3(0, 1, 0));
    modelview = rotate(modelview, angleX, vec3(1, 0, 0));


    // Affichage du premier cube

    cube.afficher(projection, modelview);


// Restauration de la matrice

modelview = sauvegardeModelview;

Encore une fois, n'oubliez pas d'utiliser la sauvegarde/restauration lorsque vous faites une transformation. :)

Notre classe Input est maintenant complète et prête à l'emploi. Avec elle, nous pourrons gérer tous nos évènements simplement. Nous serons capable de gérer la pression de plusieurs touches, des mouvements de la souris, etc. Et tout ça de manière simultanée.
Pour utiliser les évènements, il suffira juste de passer un objet de type Input aux modèles que l'on veut afficher.
De plus, l'avantage d'avoir une classe à part entière c'est que l'on pourra ajouter de nouvelles méthodes au fur et à mesure du tutoriel. Nous n'aurons pas besoin de revenir en arrière pour modifier le code, il suffira d'ajouter ce qu'il faut dans la classe Input.

Après ce chapitre repos, je vous propose de passer à un chapitre hyper méga important qui concerne les Textures avec OpenGL ! :D


La classe Input Les textures

Les textures

Les méthodes indispensables Introduction

Après le chapitre quelque peu compliqué sur les piles de matrices, nous allons passer aujourd'hui à un chapitre beaucoup plus concret (et plus intéressant :p ) qui concerne le Texturing avec OpenGL. Avec ceci, nous pourrons rendre notre scène 3D plus réaliste. Nous verrons comment charger des textures stockées sur votre disque dur (ou SSD) et comment les appliquer à une surface 3D.

Introduction

Les textures Chargement

Introduction

Avant de commencer ce chapitre très intéressant, on va définir ensemble un mot que vous avez déjà probablement entendu quelque part : le mot Texture.

Une texture est simplement une sorte de papier peint que l'on va "coller" sur les modèles (2D ou 3D). Dans un jeu vidéo, toutes les surfaces que vous voyez (sols, herbe, murs, personnages, ...) sont constituées de simples images collées sur des formes géométriques. Pour un mur par exemple, il suffit de créer une surface carrée puis de coller une image représentant des briques.

Image utilisateur

L'objectif de ce chapitre sera d'apprendre à créer une texture OpenGL de A à Z. Pour ceux qui auraient suivi le cours de M@téo sur la SDL, vous devriez déjà avoir une petite idée sur la façon de charger les images. Vous vous apercevrez cependant que le chargement de texture est assez différent avec OpenGL. Mais il permet de faire des choses beaucoup plus avancées.

L'une des premières difficultés va concerner la taille des textures à afficher. En effet, OpenGL ne sait gérer que les textures dont les dimensions sont des puissances de 2 (64 pixels, 128, 256, 512, ...).

Alors bon, ce n'est pas une obligation car OpenGL redimensionne de toute façon les images par lui-même mais si vous ne voulez pas vous retrouver avec une texture déformée, il vaut mieux prendre l'habitude des dimensions en puissance de 2. Le mieux est encore de modifier la taille de vos images avec des logiciels spécialisés. ;)

Avant d'aller plus loin, je vais vous demander de télécharger la librairie SDL_image (ou plutôt SDL2_image) qui nous permettra de réaliser la première étape dans la création de nos textures. Elle va nous faire gagner du temps en chargeant tous les bits d'une image en mémoire. Remarquez une fois de plus que toutes les librairies que nous utilisons sont portables. :p

Télécharger : SDL_image - MinGW pour Windows
Télécharger : Code Source SDL_image + Librairies - Visual C++ et Linux

Pour MinGW sous Windows : fusionnez les dossiers bin, dll, include et lib avec ceux du dossier SDL-2.0 que vous avez placé au tout début du tutoriel (chez moi : C:\Program Files (x86)\CodeBlocks\MinGW\SDL-2.0).
Pensez à rajouter les nouvelles dll soit dans le dossier de chaque projet, soit dans le dossier bin de MinGW selon la méthode que vous avez choisie au début du tuto.

Pour Linux ça va être folklo. :lol: Vu que tout le monde n'a pas forcément la même distribution, il faudra que vous compiliez vous même SDL_image. Cependant, cette librairie fait appel à 5 autres librairies et il faudra également toutes les compiler !
Mais bon vous savez que je ne suis pas sadique (ah bon ?), je vais donc vous donner directement toutes les commandes à taper dans votre terminal.

Enfin, commencez par télécharger le code source de SDL_image et dézippez son contenu dans votre home. Ensuite, exécutez les commandes suivantes mais attention ! Si j'ai divisé les commandes en bloc ce n'est pas pour rien, je vous conseille d'exécuter les blocs un à un pour voir si tout compile normalement. Si vous copiez toute les commandes d'un coup vous risquez de zapper une erreur que vous regretterez plus tard. ;)

cd
cd SDL_image/tiff-4.0.3/
chmod +x configure
./configure
make
sudo make install


cd ../zlib-1.2.7/
chmod +x configure
./configure
make
sudo make install


cd ../libpng-1.5.13/
chmod +x configure
./configure
make
sudo make install


cd ../jpeg-8d/
chmod +x configure
./configure
make
sudo make install


cd ../libwebp-0.2.0/
chmod +x configure
./configure
make
sudo make install


cd ../SDL_image
chmod +x configure
./configure
make
sudo make install

On reprend au niveau des IDE pour tout le monde, il va falloir linker la librairie SDL_image avec vos projets. Voici un petit tableau avec le link à spécifier en fonction de votre IDE :

OS

Option

Code::Blocks Windows

SDL2_image

Code::Blocks Linux

SDL2_image

DevC++

-lSDL2_image

Visual Studio

SDL2_image.lib

Enfin pour terminer cette introduction, nous allons télécharger un pack de textures que l'on utilisera tout au long de ce tutoriel. Vous y verrez à l'intérieur plusieurs catégories d'images (sols, pierres, bois, ...). Je remercie au passage notre petit Kayl qui a fait découvrir ce pack dans son tuto. Je reprends le même vu qu'il est assez complet. :p

Télécharger : Pack de Textures Haute Résolution


Les textures Chargement

Chargement

Introduction Plaquage

Les Objets OpenGL

La librairie SDL_image va nous faciliter grandement la tâche dans le chargement de texture. Elle va nous faire gagner beaucoup de temps car elle sait charger une multitude de formats d'image, nous n'aurons donc pas à charger nos images manuellement. Cependant, elle ne peut pas tout faire pour nous. Les images chargées avec SDL_image seront, si on les laisse comme ça, inutilisables avec OpenGL.

En effet, la librairie permet de charger les textures uniquement pour la SDL et non pour les autres API. Il nous faut donc configurer OpenGL pour qu'il puisse reconnaitre ces nouvelles textures. Dans cette partie nous allons voir pas mal de fonctions spécifiques à la librairie OpenGL, et vous verrez que vous retrouvez certaines d'entre elles tout au long du tutoriel. Je vous dirai celles qui sont importantes à retenir. ;)

Les objets OpenGL

Avant d'aller plus loin, j'aimerais que l'on développe un point important de ce chapitre : les objets OpenGL. Les objets OpenGL sont semblables aux objets en C++ (même s'ils sont différents dans le fond), on peut les représenter par le laboratoire que l'on voit dans le chapitre sur les objets de M@téo. Ce sont donc des sortes de laboratoires dont on ne connait pas le fonctionnement, et d'ailleurs on s'en moque à partir du moment où ils fonctionnent. :p

Pourquoi je vous parle de ça ? Et bien simplement parce qu'une texture est un objet OpenGL. Vous verrez que l'on va apprendre à initialiser la texture mais vous n'aurez aucune idée de ce qui se passe à l'intérieur de la carte graphique, tout comme le laboratoire. Nous donnerons à la texture des pixels à afficher et OpenGL se chargera du reste. Bon je schématise un peu mais vous avez compris l'idée.

Comment on crée un objet OpenGL ? C'est dur à faire ? :(

Non pas du tout c'est en réalité très simple ! En effet, pour créer ces objets on utilise la plupart du temps la même fonction. Et cette fonction nous renverra toujours la même chose : un ID représentant l'objet créé. Cet ID est une variable de type unsigned int et va permettra à OpenGL de savoir sur quel objet il doit travailler.

En ce qui concerne la configuration de ces objets, nous procéderons ainsi :

Toutes ces parties se gèrent avec les mêmes fonctions pour la plupart des objets OpenGL (que ce soit une texture ou autre). Il n'y a que l'étape de la configuration qui va varier.

La classe Texture

On commence la partie programmation par le plus simple : la création d'une classe Texture. Mis à part le constructeur et le destructeur, cette classe ne contiendra que la méthode charger() qui s'occupera de charger la texture demandée. Elle retournera un booléen pour confirmer ou non le chargement :

bool charger();

La classe contiendra également 2 attributs :

Au niveau du code, ça donne ça :

// Attributs

GLuint m_id;
std::string m_fichierImage;

Le constructeur prendra en paramètre une string qui représentera le chemin vers le fichier image. L'ID quant à lui sera généré par OpenGL :

// Constructeur 

Texture(std::string fichierImage);

On rajoutera au passage un accesseur pour l'attribut m_id et un mutateur pour m_fichierImage au cas où devrions spécifier une image après déclaration de l'objet. L'accesseur sera important pour la suite :

GLuint getID() const;
void setFichierImage(const std::string &fichierImage);

Sans oublier les includes de la SDL, de SDL_image et quelques autres voici ce que ça nous donne :

#ifndef DEF_TEXTURE
#define DEF_TEXTURE


// Include

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

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

#endif

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


// Classe Texture

class Texture
{
    public:

    Texture(std::string fichierImage);
    ~Texture();
    bool charger();
    GLuint getID() const;
    void setFichierImage(const std::string &fichierImage);


    private:

    GLuint m_id;
    std::string m_fichierImage;
};

#endif

Pour l'implémentation de ce début de code vous savez comment faire : :p

#include "Texture.h"


// Constructeur

Texture::Texture(std::string fichierImage) : m_id(0), m_fichierImage(fichierImage)
{

}


// Destructeur

Texture::~Texture()
{

}


// Méthodes

bool Texture::charger()
{

}


GLuint Texture::getID() const
{
    return m_id;
}


void Texture::setFichierImage(const std::string &fichierImage)
{
    m_fichierImage = fichierImage;
}

La méthode charger

Chargement de l'image dans une SDL_Surface

Maintenant que l'on a un squelette de classe propre, nous pouvons nous lancer dans la création de texture. La première étape consiste à charger un fichier image en mémoire grâce à la librairie SDL_image. Pour cela rien de plus simple, il existe une et unique fonction pour charger plus d'une dizaine de formats d'image différents ! Que demande le peuple. ^^

La fonction est la suivante :

SDL_Surface *IMG_Load(const char *file)

La fonction renvoie une SDL_surface qui contiendra tous les pixels nécessaires.

Pour le chemin du fichier, nous donnerons l'attribut m_fichierImage, ou plutôt la chaine C de cet attribut car la fonction demande un tableau de caractère. ;)

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());
}

Attention cependant, la fonction peut renvoyer un pointeur sur 0. Il faut donc gérer cette erreur au cas où l'image n'existerait pas ou si le chemin donné contient une erreur. En cas de problème, on affiche alors un message d'erreur grâce à la fonction SDL_GetError() :

char* SDL_GetError(void);

Cette fonction permet de renvoyer la dernière erreur qu'a rencontrée la SDL (dans une chaine de char). Donc en cas d'erreur de chargement, on inclut le résultat de cette fonction dans un flux cout :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }
}

Génération de l'ID

On a vu tout à l'heure ce qu'étaient les objets OpenGL et on sait également que nous pouvons les gérer grâce à leur ID. Nous allons maintenant voir comment générer cet ID. Pour ce faire, il existe une fonction déjà toute prête dans OpenGL :

GLuint glGenTextures(GLsizei number,  GLuint *textures);

Pour générer un ID de texture, il suffit d'utiliser cette fonction en donnant en paramètre l'attribut m_id de notre classe Texture :

// Génération de l'ID

glGenTextures(1, &m_id);

On appelle cette fonction juste après avoir charger l'image en mémoire :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }

    // Génération de l'ID

    glGenTextures(1, &m_id);
}

Le verrouillage

Je vous ai parlé rapidement du verrouillage d'objet tout à l'heure, ceci permettait à OpenGL de verrouiller un objet pour travailler dessus. Tous les objets OpenGL doivent être verrouillés pour être configurés (et même pour être utilisés !) sinon vous ne pourrez rien faire avec.

On utilisera une fonction simple pour verrouiller nos objets. Ce sera d'ailleurs la même pour les déverrouiller. :p Voici son prototype :

void glBindTexture(GLenum target,  GLuint  texture);

Voici donc comment utiliser la fonction dans notre cas :

// Verrouillage

glBindTexture(GL_TEXTURE_2D, m_id);

Tiens au passage, vu que l'on a vu le verrouillage d'objets, nous allons voir maintenant le déverrouillage qui permet à OpenGL d’arrêter de se concentrer sur l'objet en cours, ce qui permet par extension d’empêcher les modifications.

Pour réaliser cette opération, on utilisera la même fonction mais avec le paramètre target non plus égal à la valeur de l'ID de la texture mais avec la valeur 0 (la valeur nulle quoi). En gros, on dit à OpenGL : "Verrouille l'objet possédant l'ID 0, soit rien du tout". :)

// Déverrouillage

glBindTexture(GL_TEXTURE_2D, 0);

Petit récap :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


    // Déverrouillage

    glBindTexture(GL_TEXTURE_2D, 0);
}

Configuration de la texture

Notre texture a un ID généré, elle est également verrouillée, on peut maintenant passer à sa configuration. :D

Grossièrement parlant, pour avoir une texture dans OpenGL il suffit de copier les pixels d'une image dans la texture. C'est aussi simple que ça. Seulement voilà, il existe plusieurs formats d'image et certaines contiennent plus de données que d'autres, ...

Hein je croyais que la librairie SDL_image permettait justement de gérer tous ces formats ? :(

Et bien oui vous avez raison, c'est bien SDL_image qui gère les différents formats de l'image. Il existe cependant une chose qu'elle ne peut pas nous dire automatiquement.

Vous n'êtes pas sans savoir qu'un pixel est composé de 3 couleurs (rouge, vert et bleu) ... Les pixels d'une image n’échappent pas à cette règle, chacun d'entre eux est composé de ces 3 couleurs. Seulement voilà, il existe, pour certains formats, une quatrième composante qui s'appelle la composante Alpha. Cette composante permet de stocker le "niveau de transparence" d'une image.

Pour charger correctement une texture, il faut savoir si cette valeur alpha est présente ou non, et heureusement pour nous, la librairie SDL_image est capable de nous le dire. En effet, dans la structure SDL_Surface utilisée au début de la méthode charger(), il existe un champ BytesPerPixel qui permet de dire s'il y a 3 ou 4 couleurs. Nous devrons donc d'abord récupérer cette valeur avant de copier les pixels dans la texture.

Bien on arrête là pour la théorie, on passe au code.

On veut savoir si une image possède 3 ou 4 couleurs, on récupère donc le champ imageSDL->format->BytesPerPixel pour le vérifier puis on met le tout dans un bloc if. Si on a une valeur inconnue, on arrête le chargement de la texture pour éviter de se retrouver avec une grosse erreur puis on n'oublie pas de libérer la surface SDL avant de quitter la méthode :

// Détermination du nombre de composantes

if(imageSDL->format->BytesPerPixel == 3)
{

}

else if(imageSDL->format->BytesPerPixel == 4)
{

}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

On sait maintenant qu'il faut faire attention au bidule alpha, mais qu'est qu'on met à l'intérieur des if ? Ils sont tout vide. :(

C'est normal, il manque encore quelque chose. Comme on l'a vu plus tôt, OpenGL a besoin de savoir si la composante alpha existe ou pas. Seulement si on lui donne la valeur 3 ou 4 ça ne va pas lui suffire, il faudra envoyer une autre valeur qui sera un peu comme le paramètre GL_TEXTURE_2D que l'on a vu plus haut. Avec ce paramètre, il comprendra mieux ce qu'on lui enverra.

Il y aura deux cas à gérer :

Au niveau du code, on utilisera une variable de type GLenum pour retenir cette valeur. On l'appellera formatInterne, vous verrez pourquoi juste après :

// Détermination du nombre de composantes

GLenum formatInterne(0);

if(imageSDL->format->BytesPerPixel == 3)
{
    formatInterne = GL_RGB;
}

else if(imageSDL->format->BytesPerPixel == 4)
{    
    formatInterne = GL_RGBA;
}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

Il ne manque plus qu'une chose à faire. Selon le système d'exploitation ou même les images que vous utiliserez, les pixels ne seront pas stockés dans le même ordre. Par exemple sous Windows, la plupart des formats stockent leurs pixels selon l'ordre Rouge Vert Bleu (RGB) sauf les images au format BMP. Ceux-ci voient leurs pixels stockés selon l'ordre Bleu Vert Rouge (BGR). C'est un problème que nous devons gérer car certains auront la belle surprise de voir leurs images avec des couleurs complétement inversées (Imaginez un Dark Vador en blanc :p ).

Il faut donc dire à OpenGL dans quel ordre les pixels sont stockés, et pour ça on va utiliser une autre variable de type GLenum que l'on appelera format :

// Détermination du format et du format interne

GLenum formatInterne(0);
GLenum format(0);

Pour connaitre l'ordre des pixels, nous devons utiliser un autre champ de la structure imageSDL. Ce champ sera imageSDL->format->Rmask.

Il existe 4 champs similaires Rmask, Gmask, Bmask et Amask qui représente chacun la position de sa couleur à l'aide d'une valeur hexadécimal. Nous utiliserons le premier champ (Rmask), même si nous pouvions utiliser n'importe lequel. Sauf le dernier car il se trouve toujours à la fin quelque soit le format d'image.

Nous devons donc tester cette valeur pour connaitre la position de la couleur rouge. Si sa valeur est égale à 0xff alors elle est placée au début, sinon c'est qu'elle se trouve à la fin :

// Format de l'image

GLenum formatInterne(0);
GLenum format(0);


// Détermination du format et du format interne

if(imageSDL->format->BytesPerPixel == 3)
{
    // Format interne

    formatInterne = GL_RGB;


    // Format

    if(imageSDL->format->Rmask == 0xff)
    {}

    else
    {}
}

else if(imageSDL->format->BytesPerPixel == 4)
{    
    // Format interne

    formatInterne = GL_RGBA;


    // Format

    if(imageSDL->format->Rmask == 0xff)
    {}

    else
    {}
}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

Il faut maintenant affecter une valeur à la variable format. Il y a 4 cas à gérer :

Ces quatre cas seront représentés par les constantes suivantes :

Il n'y a plus qu'à affecter la bonne constante à la variable format :

// Format pour 3 couleurs

if(imageSDL->format->Rmask == 0xff)
    format = GL_RGB;

else
    format = GL_BGR;


....


// Format pour 4 couleurs

if(imageSDL->format->Rmask == 0xff)
    format = GL_RGBA;

else
    format = GL_BGRA;

Ce qui donne au final :

// Format de l'image

GLenum formatInterne(0);
GLenum format(0);


// Détermination du format et du format interne pour les images à 3 composantes

if(imageSDL->format->BytesPerPixel == 3)
{
    // Format interne

    formatInterne = GL_RGB;


    // Format

    if(imageSDL->format->Rmask == 0xff)
        format = GL_RGB;

    else
        format = GL_BGR;
}


// Détermination du format et du format interne pour les images à 4 composantes

else if(imageSDL->format->BytesPerPixel == 4)
{    
    // Format interne

    formatInterne = GL_RGBA;


    // Format

    if(imageSDL->format->Rmask == 0xff)
        format = GL_RGBA;

    else
        format = GL_BGRA;
}


// Dans les autres cas, on arrête le chargement

else
{
    std::cout << "Erreur, format interne de l'image inconnu" << std::endl;
    SDL_FreeSurface(imageSDL);

    return false;
}

Pfiou tout ce gourbi pour déterminer deux valeurs ! :lol:

On a fait le plus dur, il ne nous reste plus qu'à copier les fameux pixels dans la texture. Pour ça, on va utiliser la fonction suivante (ne soyez pas surpris du nombre de paramètres :p ) :

void glTexImage2D(GLenum target,  GLint level,  GLint internalFormat,  GLsizei width,  GLsizei height,  GLint border,  GLenum format,  GLenum type,  const GLvoid * data);

Ça en fait des paramètres tout ça ! Mais au moins on n'utilise qu'une seule fonction pour remplir notre texture.

Voyons son implémentation dans le code :

// Copie des pixels

glTexImage2D(GL_TEXTURE_2D, 0, formatInterne, imageSDL->w, imageSDL->h, 0, format, GL_UNSIGNED_BYTE, imageSDL->pixels);

On refait un petit récap :

bool Texture::charger()
{
    // Chargement de l'image dans une surface SDL

    SDL_Surface *imageSDL = IMG_Load(m_fichierImage.c_str());

    if(imageSDL == 0)
    {
        std::cout << "Erreur : " << SDL_GetError() << std::endl;
        return false;
    }


    // Génération de l'ID

    glGenTextures(1, &m_id);


    // Verrouillage

    glBindTexture(GL_TEXTURE_2D, m_id);


    // Format de l'image

    GLenum