• Facile

Ce cours est visible gratuitement en ligne.

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

J'ai tout compris !

Mis à jour le 13/03/2017

La compilation de shaders

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

Re-bonjour à tous !

Aujourd'hui, nous allons attaquer un gros morceau de la programmation OpenGL, je vous conseille de préparer le café, le chocolat, les biscuits, etc. :p

Les shaders sont quelque chose d'assez monstrueux, nous allons les étudier à travers 4 chapitres (et encore sans parler des effets que l'on pourra faire avec) dont le premier est consacré à une classe que l'on utilise presque depuis le début du tutoriel sans jamais avoir vu son fonctionnement. ;)

Piqûre de rappel

Introduction

Depuis quasiment le début du tutoriel, nous utilisons une classe dans tous nos programmes sans même que nous sachions ce qu'il y a à l'intérieur. Je parle évidemment de la classe Shader.

En effet, que ce soit des couleurs ou des textures nous utilisons toujours cette classe pour afficher quelque chose à l'écran :

// Shader pour la colorisation

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



.....



// Shader pour les textures

Shader shaderTexture("Shaders/texture.vert", "Shaders/texture.frag");
shaderTexture.charger();

La question que vous vous êtes déjà probablement posée est : que fait cette mystérieuse classe ?
Je vous rassure elle ne fait rien de compliqué. En fait, elle ne fait qu'une seule chose : compiler les fichiers sources qu'on lui envoie.

L'objectif de ce chapitre va être la reprogrammation complète de cette classe de façon à ce que vous appreniez comment créer et utiliser un shader. A la fin, vous serez en mesure de comprendre totalement votre code source, il n'y aura plus de code mystère. ;) Mais avant cela, nous allons faire un petit rappel sur ce que nous savons déjà sur le shaders.

Rappel

Vertex et Fragment Shader

Comme nous l'avons vu dans le chapitre 4, nous savons que les shaders sont des programmes qui sont exécutés non pas par le CPU mais par la carte graphique. Il en existe deux types :

  • Le Vertex Shader : qui prend chaque vertex à part pour calculer sa position à l'écran. Si un vertex possède 3 coordonnées, alors le Vertex Shader aura besoin des matrices projection et modelview pour faire le calcul.

  • Le Fragment Shader (ou Pixel Shader) : qui prend à part chaque pixel des triangles formés par les vertices pour définir sa couleur. Si on utilise une texture, alors le Fragment Shader va chercher à l'intérieur de cette texture la couleur dont il a besoin.

Ces deux types de shader sont utilisés à deux moments différents du pipeline 3D :

Image utilisateur

Pour rappel :

  • Définition des coordonnées : Ce sont les vertices et toutes les données qui y sont associées. Depuis peu, tout ça se trouve dans la carte graphique grâce aux VBO.

  • Vertex Shader : Shader qui travaille sur les vertices

  • Pixelisation des triangles : Moment où une forme géométrique (formée par les vertices) est convertie en pixels

  • Fragment Shader : Shader qui travaille sur les pixels

  • Test de profondeur : Test qui permet d'afficher ou de cacher un pixel

  • Affichage : Sortie de la carte graphique (en général, il s'agit de l'écran)

Au final, nous voyons que les shaders sont exécutés à deux moments : une fois pour transformer les vertices et une autre pour définir leur couleur.

Geometry Shader

En réalité, les shaders devraient être exécutés 3 fois car il existe un troisième type qui s'appelle le Geometry Shader qui vient se placer juste entre les deux premiers. Celui-ci permet de modifier les primitives (triangles, ...) formées par les vertices. Cependant, nous ne l'utiliserons pas d'une part parce qu'il est optionnel et d'autre part car il est quasiment inutile pour nous. :p

Soit dit en passant, ce nouveau type a été introduit avec la version 3.2 d'OpenGL.

Utilité

Les shaders ne se limitent pas au positionnement de vertices ou à la colorisation de pixels, ils permettent aussi de créer une multitude d'effets réalistes comme l'eau, la lumière, le feu, ... (ce que vous n'avez surement pas oublié je pense :p ). D'ailleurs, c'est certainement leur première utilité dans le monde du jeu-vidéo. Nous consacrerons une partie entière de ce tutoriel à l'élaboration de ces effets.

Depuis la version 3.1 d'OpenGL, l'utilisation des shaders est devenue obligatoire car les développeurs de l'API ont introduit une nouvelle philosophie : celle du 'tout shader'. Dans les précédentes versions, OpenGL gérait lui-même tout le pipeline 3D sans rien demander à personne mises à part les données de base comme les vertices. Seulement, cette gestion est devenue trop lourde aujourd'hui, les développeurs de l'API préfèrent nous laisser gérer le maximum de tâches de façon à ce que nous les optimisions mieux en fonction de ce que nous voulons faire.

Cette philosophie complique grandement l'apprentissage d'OpenGL, c'est pour ça que nous n'avons pas vu les shaders tout de suite, sinon le tuto aurait été beaucoup plus complexe à suivre. Vous m'auriez surement fait une overdose avant même d'avoir atteint le chapitre sur les matrices. :lol:

La classe Shader

Enfin, maintenant que vous avez acquis assez d'expérience avec la programmation OpenGL, nous allons pouvoir voir en détails le fonctionnement des shaders. Nous commencerons en premier par étudier la fameuse classe Shader. Et comme je vous l'ai dit précédemment, elle ne fait rien de magique, elle ne fait que compiler du code source.

Car oui, comme tout programme qui se respecte, les shaders possèdent bel et bien un code source. La seule différence avec les programmes classiques c'est qu'il est compilé juste avant d'être utilisé et pas avant, il n'y a donc pas d'application de type .exe à donner à votre carte graphique. D'ailleurs en parlant de ça, ces codes sources sont écrits avec un langage proche du C++ (ou plutôt du C) qui s'appelle l'OpenGL Shading Language (ou GLSL). Nous l'étudierons dans le prochain chapitre, nous devons savoir avant comment le compiler. ;)

Compilation des sources

La classe Shader

Bien, après cette brève introduction j'espère vous avoir mis un peu l'eau à la bouche. :p

Il est temps de passer à la reprogrammation complète de la classe Shader, je vais vous demander de supprimer les fichiers Shader.h et Shader.cpp que vous utilisez actuellement et d'en recréer deux nouveaux (avec le même nom bien sûr). Si vous essayez de compiler votre code après ça, vous devriez avoir une belle petite erreur de compilation. ^^

Bref, on commence cette reprogrammation par le header en incluant tous les en-têtes nécessaires : ceux d'OpenGL, d'iostream et de string. Nous n'aurons pas besoin de ceux de la SDL car ils n'ont aucun rapport avec les shaders :

#ifndef DEF_SHADER
#define DEF_SHADER


// Include Windows

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


// Include Mac

#elif __APPLE__
#define GL3_PROTOTYPES 1
#include <OpenGL/gl3.h>


// Include UNIX/Linux

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

#endif


// Includes communs

#include <iostream>
#include <string>

#endif

Après les inclusions, on passe à la classe en elle-même. Elle possèdera 3 attributs majeurs : des variables de type GLuint représentant le Vertex Shader, le Fragment Shader et un programme. Nous ajouterons également le constructeur par défaut ainsi que le destructeur :

// Classe Shader

class Shader
{
    public:

    Shader();
    ~Shader();


    private:

    GLuint m_vertexID;
    GLuint m_fragmentID;
    GLuint m_programID;
};

Ah programID ? C'est le truc qu'on utilise dans la fonction glUseProgram() non ?

Oui tout à fait. ^^

Comme vous le savez un shader est un programme, c'est pour cette raison que nous avons cet attribut. Remarquez aussi que les 3 variables présentées sont en fait des objets OpenGL car elles ont le type GLuint au même titre que les textures, les VBO ou les VAO.

Nous allons également rajouter un autre constructeur à cette classe. C'est un constructeur que vous connaissez par cœur car c'est celui qui demande en paramètre le chemin vers les deux codes sources shaders. Son prototype est le suivant :

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

Il prend en paramètre deux string représentant le chemin vers les codes sources spécifiés. Nous aurons besoin de deux attributs supplémentaires pour les gérer dans la classe. ;)

Nous rajoutons donc deux variables m_vertexSource et m_fragmentSource de type string dans le header :

// Attributs

GLuint m_vertexID;
GLuint m_fragmentID;
GLuint m_programID;

std::string m_vertexSource;
std::string m_fragmentSource;

Si on récapitule tout ça :

#ifndef DEF_SHADER
#define DEF_SHADER


// Include Windows

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


// Include Mac

#elif __APPLE__
#define GL3_PROTOTYPES 1
#include <OpenGL/gl3.h>


// Include UNIX/Linux

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

#endif


// Includes communs

#include <iostream>
#include <string>


// Classe Shader

class Shader
{
    public:

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


    private:

    GLuint m_vertexID;
    GLuint m_fragmentID;
    GLuint m_programID;

    std::string m_vertexSource;
    std::string m_fragmentSource;
};

#endif

Header terminé. ^^

Les constructeurs et le destructeur

Maintenant que le header de la classe est terminé, on va pouvoir passer à son implémentation. Et comme d'habitude, on commence par les constructeurs et le destructeur.

Le premier est le constructeur par défaut, qui ne fait rien de particulier d'ailleurs :

// Constructeur par défaut

Shader::Shader() : m_vertexID(0), m_fragmentID(0), m_programID(0), m_vertexSource(), m_fragmentSource()
{
}

Le second, lui, fera la même chose sauf qu'il affectera les sources avec les string donnés en paramètres :

// Constructeur

Shader::Shader(std::string vertexSource, std::string fragmentSource) : m_vertexID(0), m_fragmentID(0), m_programID(0),
                                                                       m_vertexSource(vertexSource), m_fragmentSource(fragmentSource)
{
}

Quant au destructeur, vous savez maintenant ce qui se trouve à l'intérieur. Mais attention ! Celui-ci ne restera pas vide longtemps. :p

// Destructeur

Shader::~Shader()
{
}

La méthode getProgramID

L'attribut m_programID est quelque chose que l'on utilise assez souvent, notamment pour activer les shaders. Nous allons recoder le getter qui permet de le récupérer et qui s'appelle getProgramID() :

GLuint getProgramID() const;

Son implémentation se passe de commentaire :

GLuint Shader::getProgramID() const
{
    return m_programID;
}

La méthode charger (Partie 1/2)

La méthode charger() va être la principale méthode de cette classe. En effet, vous avez surement remarqué qu'on l'utilise à chaque fois que l'on veut initialiser un shader :

// Shader Texture

shaderTexture("Shaders/texture.vert", "Shaders/texture.frag");
shaderTexture.charger();

Son prototype est assez simple vu qu'elle ne demande rien en paramètre :

bool charger();

Elle renverra un booléen pour confirmer ou non le chargement du shader.

Cependant, contrairement à son prototype, la méthode charger() ne sera pas aussi simple à programmer. Elle devra être capable de faire plusieurs choses :

  • Compiler le code source du Vertex Shader

  • Compiler le code source du Fragment Shader

  • Réunir les fichiers compilés dans un seul et même programme

Pour effectuer les deux premières étapes, nous allons créer une autre méthode qui permettra de compiler le code source d'un shader. Il vaut mieux séparer la compilation du reste du code car vous allez voir que c'est un peu ... long. :p Cette nouvelle méthode s'appellera simplement compilerShader() :

bool compilerShader(GLuint &shader, GLenum type, std::string const &fichierSource);

Elle aura besoin de 3 paramètres :

  • shader : Le shader à compiler

  • type : Un paramètre semblable au paramètre target des objets OpenGL (comme GL_TEXTURE_2D) pour définir le type de shader. Il peut prendre la valeur GL_VERTEX_SHADER ou GL_FRAGMENT_SHADER

  • fichierSource : Le code source associé au shader

Un booléen sera également retourné pour confirmer ou non la réussite de la compilation.

Nous devons appeler cette nouvelle méthode deux fois afin de compiler nos deux codes sources. Le premier appel devra compiler un shader contenu dans l'attribut m_vertexID et dont le code source sera évidemment identifié par m_vertexSource. Le paramètre type sera quant à lui égal à la constante GL_VERTEX_SHADER. On n'oublie pas de vérifier le retour dans un bloc if :

bool Shader::charger()
{
    // Compilation du Vertex Shader

    if(!compilerShader(m_vertexID, GL_VERTEX_SHADER, m_vertexSource))
        return false;
}

Le second appel à cette méthode s'occupera cette fois du Fragment Shader. On lui donnera en paramètres les attributs m_fragmentID et m_fragmentSource. Le paramètre type sera égal à la constante GL_FRAGMENT_SOURCE :

bool Shader::charger()
{
    // Compilation des shaders

    if(!compilerShader(m_vertexID, GL_VERTEX_SHADER, m_vertexSource))
        return false;

    if(!compilerShader(m_fragmentID, GL_FRAGMENT_SHADER, m_fragmentSource))
        return false;
}

On ne retourne pas de message d'erreur ?

Si justement, les messages d'erreurs seront en fait affichés par la méthode compilerShader() car c'est à elle de dire si quelque chose ne va pas. Il est très important de vérifier les erreurs de compilation avec les shaders car nous n'avons pas d'IDE pour nous dire où elles seraient. Ce peut devenir vite embêtant si vous perdez trop de temps à chercher les erreurs par vous-même.

D'ailleurs la vérification de messages d'erreurs est quelque chose ... d'assez folklo à programmer vous verrez. :lol:

La méthode compilerShader

Créer un shader

Nous connaissons déjà le prototype ce cette méthode et nous savons également qu'elle a besoin de 3 paramètres :

bool compilerShader(GLuint &shader, GLenum type, std::string const &fichierSource);

Cette méthode devra être capable de faire plusieurs choses :

  • Générer un ID de shader

  • Lire son code source

  • Le compiler

La première étape va être très simple à réaliser car elle est très similaire à la création des autres objets OpenGL. Si je dis similaire, c'est parce que nous n'allons pas utiliser les fonctions classiques du type glGenXXX(), glBindXXX(), ... comme nous avons l'habitude de faire. Les shaders sont des objets un peu particuliers qui vont utiliser leurs propres fonctions. De plus, ils ne doivent pas être verrouillés pendant leur configuration mais seulement au moment de leur utilisation.

En définitif, nous ne devons pas utiliser la fonction glGenShaders() pour générer un ID mais la fonction glCreateShader() :

GLuint glCreateShader(GLenum shaderType);

Elle prend en paramètre une constante qui peut prendre la valeur GL_VERTEX_SHADER ou GL_FRAGMENT_SHADER. Nous lui donnerons la valeur du paramètre type de la méthode compilerShader().

Nous appellerons cette fonction en affectant la valeur retournée au paramètre shader :

bool Shader::compilerShader(GLuint &shader, GLenum type, std::string const &fichierSource)
{
    // Création du shader

    shader = glCreateShader(type);
}

Vu que nous utilisons un paramètre externe à la méthode pour définir le type de shader, il est donc possible possible que ce paramètre contienne une valeur erronée. Nous devons donc vérifier l'ID retourné par la fonction glCreateShader(). S'il est égal à 0 c'est qu'il y a eu un problème, il faut donc arrêter le chargement :

// Vérification du shader

if(shader == 0)
{
    std::cout << "Erreur, le type de shader (" << type << ") n'existe pas" << std::endl;
    return false;
}
Lecture du code source

La lecture du coude source est quelque chose de capital dans la création d'un shader. Sans lui, notre programme ne serait pas quoi faire et la carte graphique nous enverrait gentiment paitre. :-°

Nous devons lire les codes sources dont les chemins sont contenus dans les attributs m_vertexSource et m_fragmentSource. Pour cela, nous allons utiliser la bibliothèque fstream qui permet de la lecture et l'écriture de fichier en C++, je suppose que vous la connaissez déjà. :p Pensez à inclure l'en-tête fstream dans le fichier Shader.h.

Pour utiliser cette bibliothèque, nous allons commencer par déclarer un objet de type ifstream que nous appellerons simplement fichier. Son constructeur demande une chaine C représentant le chemin vers le fichier à lire, nous lui donnerons donc la chaine C du paramètre fichierSource de la méthode compilerShader() :

// Flux de lecture

std::ifstream fichier(fichierSource.c_str());

Avant de continuer, nous devons tester l'ouverture du fichier au cas où celui-ci n'existerait pas. Si c'est le cas alors on arrête le chargement, on affiche un message d'erreur et on détruit le shader que l'on a créé juste avant.

La fonction permettant de détruire un shader ressemble fortement aux fonctions du type glDeleteXXX() sauf qu'elle ne prend pas deux paramètres mais un seul, elle n'a également pas de s à la fin de son nom :

glDeleteShader(GLuint shader);

Bien évidemment, elle prend en paramètre le shader à détruire.

Avec cette fonction et le code précédent, notre test d'ouverture ressemble donc à ceci :

// Flux de lecture

std::ifstream fichier(fichierSource.c_str());


// Test d'ouverture

if(!fichier)
{
    std::cout << "Erreur le fichier " << fichierSource << " est introuvable" << std::endl;
    glDeleteShader(shader);

    return false;
}

Si le fichier s'est ouvert correctement alors nous pouvons copier son contenu sans problème. Pour cela, nous allons utiliser la fonction getline() qui permet de copier une ligne entière d'un fichier. Nous l’utiliserons à l'intérieur d'une boucle while, ce qui permettra de copier le code source ligne par ligne. ;)

Nous aurons besoin de deux string pour cette boucle : une qui contiendra chaque ligne lue, et une autre qui contiendra le code source final :

// Strings permettant de lire le code source

std::string ligne;
std::string codeSource;


// Lecture

while(getline(fichier, ligne))
    codeSource += ligne + '\n';

N'oubliez pas l'inclure le caractère de saut de ligne '\n' à la fin de chaque copie, sinon votre code source ne sera constitué que d'une gigantesque ligne incompréhensible. :p

Une fois la lecture terminée, on n'oublie pas de fermer le fichier pour éviter les fuites de mémoire :

// Strings permettant de lire le code source

std::string ligne;
std::string codeSource;


// Lecture

while(getline(fichier, ligne))
    codeSource += ligne + '\n';


// Fermeture du fichier

fichier.close();

On fait un petit récap de ce que nous avons fait jusqu'à maintenant :

bool Shader::compilerShader(GLuint &shader, GLenum type, std::string const &fichierSource)
{
    // Création du shader

    shader = glCreateShader(type);


    // Vérification du shader

    if(shader == 0)
    {
        std::cout << "Erreur, le type de shader (" << type << ") n'existe pas" << std::endl;
        return false;
    }


    // Flux de lecture

    std::ifstream fichier(fichierSource.c_str());


    // Test d'ouverture

    if(!fichier)
    {
        std::cout << "Erreur le fichier " << fichierSource << " est introuvable" << std::endl;
        glDeleteShader(shader);

        return false;
    }


    // Strings permettant de lire le code source

    std::string ligne;
    std::string codeSource;


    // Lecture

    while(getline(fichier, ligne))
        codeSource += ligne + '\n';


    // Fermeture du fichier

    fichier.close();
}
Compilation du shader

Faisons le point sur ce que nous avons fait jusqu'à présent : nous avons créé un shader et lu un code source qui se trouve maintenant en mémoire. Le problème c'est que les deux sont totalement dissociés pour le moment. Notre prochain objectif est donc d'envoyer ce code source à notre shader pour qu'il puisse enfin recevoir ses instructions.

Pour cela, nous allons utiliser une fonction OpenGL qui s'appelle glShaderSource() :

void glShaderSource(GLuint shader, GLsizei count, const GLchar **string, const GLint *length)
  • shader : Le shader concerné

  • count : Paramètre indiquant le nombre de chaines de caractère à envoyer. Nous lui donnerons la valeur 1 car notre code source n'est composé que d'une seule string

  • string : Sorte de "double pointeur" représentant un tableau de sous-chaines de caractère. :-° Nous utiliserons une petite astuce pour ne lui donner qu'une seule chaine

  • length : Tableau de taille des sous-chaines. Heureusement pour nous, nous n'aurons pas à utiliser d'astuce car OpenGL nous autorise à envoyer la valeur 0. Nous n'allons donc pas nous en priver :p

Cette fonction est un peu spéciale vous l'aurez remarqué.

Cependant, nous pouvons contourner son trop-plein de tableaux en ne fournissant qu'une seule et unique chaine de caractère. Il suffira d'envoyer l'adresse d'une chaine C. En effet, si on envoie l'adresse d'une chaine alors on se retrouve avec un pointeur du type **chaine. La fonction glShaderSource() demande justement cette sorte de double pointeur. ;)

On commence donc pas récupérer la chaine C du code source dans un pointeur grâce à la méthode c_str() :

// Récupération de la chaine C du code source

const GLchar* chaineCodeSource = codeSource.c_str();

Ensuite, nous appelons la fonction glShaderSource() avec les paramètres que nous venons de voir. Bien entendu, on pense à donner l'adresse du pointeur chaineCodeSource pour contenter le paramètre string :

// Récupération de la chaine C du code source

const GLchar* chaineCodeSource = codeSource.c_str();


// Envoi du code source au shader

glShaderSource(shader, 1, &chaineCodeSource, 0);

Notre shader vient enfin de récupérer son code source. :D Il ne manque plus qu'à le compiler.

Et pour ça, on va utiliser une fonction toute simple (ce qui fait du bien par rapport à la précédente) qui s'appelle glCompileShader() :

void glCompileShader(GLuint shader);

Elle ne prend qu'un seul paramètre : le shader à compiler. Nous l’appelons juste après avoir envoyé le code source :

// Récupération de la chaine C du code source

const GLchar* chaineCodeSource = codeSource.c_str();


// Envoi du code source au shader

glShaderSource(shader, 1, &chaineCodeSource, 0);


// Compilation du shader

glCompileShader(shader);
Vérification de la compilation

La vérification de la compilation est certainement l'étape la plus importante et la plus chiante de cette partie. Imaginez que vous compiliez un code source et qu'il y ait une erreur à l'intérieur, comment pouvez-vous savoir où elle se trouve ? C'est un problème qui peut devenir vite énervant surtout quand votre erreur n'est qu'un bête oubli de point-virgule. :p Pour éviter d'avoir à relire vos codes sources à chaque fois, nous allons vérifier l'état de leur compilation grâce à quelques fonctions OpenGL.

La première de ces fonctions va nous permettre de renvoyer pas mal d'informations sur un shader donné, la vérification d'erreur au moment de la compilation en fait justement partie. Elle s'appelle glGetShaderiv() :

void glGetShaderiv(GLuint shader, GLenum pname, GLint *params);
  • shader : Comme toujours, le shader sur lequel on travaille

  • pname : Le nom de l'information demandée

  • params : L'adresse d'une variable qui accueillera cette information

Pour connaitre l'état de la compilation, nous allons créer une variable de type GLint que l'on appellera erreurCompilation. Nous donnerons son adresse à la fonction glGetShaderiv() pour qu'elle puisse stocker l'information que l'on recherche. D'ailleurs, cette information se nomme GL_COMPILE_STATUS et sera la valeur du paramètre pname :

// Vérification de la compilation

GLint erreurCompilation(0);

glGetShaderiv(shader, GL_COMPILE_STATUS, &erreurCompilation);

L'appel à cette fonction seule ne sert pas à grand chose, il faut maintenant vérifier la valeur de la variable erreurCompilation. ;) Si elle différente de la constante GL_TRUE c'est qu'il y a eu une erreur, sinon c'est que tout va bien on peut retourner le booléen true pour clore le chargement :

// Vérification de la compilation

GLint erreurCompilation(0);
glGetShaderiv(shader, GL_COMPILE_STATUS, &erreurCompilation);


// S'il y a eu une erreur

if(erreurCompilation != GL_TRUE)
{
}


// Sinon c'est que tout s'est bien passé

else
    return true;

Si la fonction glGetShaderiv() a détecté une erreur alors il faut la récupérer.

Malheureusement, nous ne pouvons pas utiliser les objets string pour cette récupération car les fonctions OpenGL ne gère pas les objets C++. Il faut donc faire à l'ancienne et allouer de la mémoire à la main pour une chaine de caractère. :(

Mais avant ça, nous devons connaitre la taille du message d'erreur pour pouvoir allouer assez de mémoire. Pour ce faire, nous allons à nouveau utiliser la fonction glGetShaderiv() sauf que l'on demandera cette fois le paramètre GL_INFO_LOG_LENGTH qui correspond à la taille recherchée. Nous utiliserons la variable tailleErreur pour stocker la valeur qui sera retournée :

// Vérification de la compilation

GLint erreurCompilation(0);
glGetShaderiv(shader, GL_COMPILE_STATUS, &erreurCompilation);


// S'il y a eu une erreur

if(erreurCompilation != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &tailleErreur);
}


// Sinon c'est que tout s'est bien passé

else
    return true;

Une fois que l'on connait la taille de l'erreur, nous pouvons allouer de la mémoire pour une chaine de caractère grâce au mot-clef new[]. On ajoute au passage 1 case pour gérer le caractère de fin de chaine '\0' qui n'est pas fourni avec le message d'erreur :

// Vérification de la compilation

GLint erreurCompilation(0);
glGetShaderiv(shader, GL_COMPILE_STATUS, &erreurCompilation);


// S'il y a eu une erreur

if(erreurCompilation != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &tailleErreur);


    // Allocation de mémoire

    char *erreur = new char[tailleErreur + 1];
}


// Sinon c'est que tout s'est bien passé

else
    return true;

Maintenant que l'on a une chaine allouée, nous pouvons récupérer la fameuse erreur. Nous utiliserons pour cela la fonction glGetShaderInfoLog() :

void glGetShaderInfoLog(GLuint shader, GLsizei maxLength, GLsizei *length, GLchar *infoLog);
  • shader : Le shader sur lequel on travaille

  • maxLength : Taille de la chaine de caractère qui va accueillir l'erreur. Pour nous, il s'agit de la variable tailleErreur

  • length : Adresse de la variable qui contiendra la taille précédente. Je n'ai jamais compris l'utilité de ce paramètre mais bon, nous lui donnerons l'adresse de la variable tailleErreur

  • infolog : La chaine de caractère qui contiendra le message final. Nous lui donnerons la chaine erreur

L'appel à cette fonction ressemblera à ceci :

// Récupération de l'erreur

glGetShaderInfoLog(shader, tailleErreur, &tailleErreur, erreur);

Il faut également penser à rajouter le caractère de fin de chaine '\0' pour compléter le message :

// Récupération de l'erreur

glGetShaderInfoLog(shader, tailleErreur, &tailleErreur, erreur);
erreur[tailleErreur] = '\0';

Ce qui donne au final :

// Vérification de la compilation

GLint erreurCompilation(0);
glGetShaderiv(shader, GL_COMPILE_STATUS, &erreurCompilation);


// S'il y a eu une erreur

if(erreurCompilation != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &tailleErreur);


    // Allocation de mémoire

    char *erreur = new char[tailleErreur + 1];


    // Récupération de l'erreur

    glGetShaderInfoLog(shader, tailleErreur, &tailleErreur, erreur);
    erreur[tailleErreur] = '\0';
}


// Sinon c'est que tout s'est bien passé

else
    return true;

On arrive à la fin courage. :p

Il ne nous reste plus qu'à afficher le message d'erreur et à libérer la mémoire prise par la chaine de caractère. On n'oublie pas de détruire le shader car celui-ci est inutilisable puis on retourne le booléen false pour terminer la méthode :

// Vérification de la compilation

GLint erreurCompilation(0);
glGetShaderiv(shader, GL_COMPILE_STATUS, &erreurCompilation);


// S'il y a eu une erreur

if(erreurCompilation != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &tailleErreur);


    // Allocation de mémoire

    char *erreur = new char[tailleErreur + 1];


    // Récupération de l'erreur

    glGetShaderInfoLog(shader, tailleErreur, &tailleErreur, erreur);
    erreur[tailleErreur] = '\0';


    // Affichage de l'erreur

    std::cout << erreur << std::endl;


    // Libération de la mémoire et retour du booléen false

    delete[] erreur;
    glDeleteShader(shader);

    return false;
}


// Sinon c'est que tout s'est bien passé

else
    return true;

Pfiouu on est enfin arrivé au bout ... Tout ce code pour vérifier une simple erreur ! Et oui, je vous avais prévenus que le chargement des shaders était quelque chose d'assez folklo. :p

Faisons un petit récapitulatif final de la méthode compilerShader() :

bool Shader::compilerShader(GLuint &shader, GLenum type, std::string const &fichierSource)
{
    // Création du shader

    shader = glCreateShader(type);


    // Vérification du shader

    if(shader == 0)
    {
        std::cout << "Erreur, le type de shader (" << type << ") n'existe pas" << std::endl;
        return false;
    }


    // Flux de lecture

    std::ifstream fichier(fichierSource.c_str());


    // Test d'ouverture

    if(!fichier)
    {
        std::cout << "Erreur le fichier " << fichierSource << " est introuvable" << std::endl;
        glDeleteShader(shader);

        return false;
    }


    // Strings permettant de lire le code source

    std::string ligne;
    std::string codeSource;


    // Lecture

    while(getline(fichier, ligne))
        codeSource += ligne + '\n';


    // Fermeture du fichier

    fichier.close();


    // Récupération de la chaine C du code source

    const GLchar* chaineCodeSource = codeSource.c_str();


    // Envoi du code source au shader

    glShaderSource(shader, 1, &chaineCodeSource, 0);


    // Compilation du shader

    glCompileShader(shader);


    // Vérification de la compilation

    GLint erreurCompilation(0);
    glGetShaderiv(shader, GL_COMPILE_STATUS, &erreurCompilation);


    // S'il y a eu une erreur

    if(erreurCompilation != GL_TRUE)
    {
        // Récupération de la taille de l'erreur

        GLint tailleErreur(0);
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &tailleErreur);


        // Allocation de mémoire

        char *erreur = new char[tailleErreur + 1];


        // Récupération de l'erreur

        glGetShaderInfoLog(shader, tailleErreur, &tailleErreur, erreur);
        erreur[tailleErreur] = '\0';


        // Affichage de l'erreur

        std::cout << erreur << std::endl;


        // Libération de la mémoire et retour du booléen false

        delete[] erreur;
        glDeleteShader(shader);

        return false;
    }


    // Sinon c'est que tout s'est bien passé

    else
        return true;
}

Grâce à tout ce code, nous sommes maintenant capables de compiler tous nos shaders. Il valait mieux séparer ce code de la méthode charger() sinon il aurait fallu coder ça deux, imaginez un peu le truc ! :lol:

Cependant nous n'avons pas encore fini, il nous faut encore terminer la méthode charger(). Continuons. :)

Création du programme

La méthode charger (Partie 2/2)

Création du programme

Maintenant que nous avons codé une méthode permettant de compiler un shader, nous pouvons tranquillement retourner à la méthode charger() que nous avons laissée précédemment. D'ailleurs, elle est un peu vide pour le moment :

bool Shader::charger()
{
    // Compilation des shaders

    if(!compilerShader(m_vertexID, GL_VERTEX_SHADER, m_vertexSource))
        return false;

    if(!compilerShader(m_fragmentID, GL_FRAGMENT_SHADER, m_fragmentSource))
        return false;
}

Grâce à ce mini bout de code, nous avons enfin notre duo de shader compilé et validé. Cependant, ces shaders ne sont pas encore inutilisables pour le moment car ce ne sont que des objets intermédiaires qui ne peuvent pas être exécutés par la carte graphique, elle ne sait pas quoi faire avec. Pour régler le problème, il nous faut les réunir à l’intérieur d'un programme qui, lui, sera exécutable par la carte.

Bien entendu, il ne s'agit pas d'un programme classique que vous avez l'habitude d'exécuter sur votre ordinateur. :p C'est en fait un objet OpenGL qui va faire la passerelle entre les shaders et leur exécution au sein de la carte graphique. C'est pour cette raison que nous avons besoin du fameux attribut programID - attribut qu'on utilise depuis un bout de temps maintenant dans la fonction glUseProgram().

Pour la suite de la méthode charger(), il nous faut donc créer ce programme. Nous ferons cela grâce à une fonction qui ressemble fortement à glCreateShader() et qui s'appelle glCreateProgram() :

GLuint glCreateProgram();

C'est l'une des rares fonctions OpenGL qui ne demande aucun paramètre, profitez-en. :p Elle retourne juste l'ID d'un nouveau programme.

Nous l'appellerons donc pour créer un programme qui sera accessible via l'attribut m_programID :

bool Shader::charger()
{
    // Compilation des shaders

    if(!compilerShader(m_vertexID, GL_VERTEX_SHADER, m_vertexSource))
        return false;

    if(!compilerShader(m_fragmentID, GL_FRAGMENT_SHADER, m_fragmentSource))
        return false;


    // Création du programme

    m_programID = glCreateProgram();
}
Association des shaders au programme

Comme je vous l'ai dit précédemment, le Vertex et Fragment Shader sont inutilisables en l'état car ils ne peuvent pas être exécutés par la carte graphique. En revanche, le programme lui peut l'être. Il nous faut donc associer les shaders avec celui-ci de façon à ce que nos fichiers compilés puissent servir à quelque chose.

Image utilisateur

Cette étape d'association se fait très simplement puisqu'il suffit d'utiliser une seule fonction OpenGL : glAttachShader().

void glAttachShader(GLuint program, GLuint shader)
  • program : Le programme sur lequel on travaille

  • shader : Le shader à associer

Nous appellerons cette fonction deux fois pour associer nos deux shaders :

bool Shader::charger()
{
    // Compilation des shaders

    if(!compilerShader(m_vertexID, GL_VERTEX_SHADER, m_vertexSource))
        return false;

    if(!compilerShader(m_fragmentID, GL_FRAGMENT_SHADER, m_fragmentSource))
        return false;


    // Création du programme

    m_programID = glCreateProgram();


    // Association des shaders

    glAttachShader(m_programID, m_vertexID);
    glAttachShader(m_programID, m_fragmentID);
}

Étape terminée. :p

Verrouillage des entrées shader

Si je vous parle d'entrées shader, ça vous dit quelque chose ? Normalement vous devriez me répondre non. :p Et pourtant si je vous disais que vous savez déjà ce que c'est, vous me croiriez ?

En fait, les entrées shader sont tout simplement les tableaux Vertex Attrib que l'on utilise depuis le début du tuto ! ;) Et oui, les shaders ont besoin d'eux pour travailler sinon ils ne pourraient pas afficher grand chose. Les tableaux Vertex Attrib constituent donc leurs sources de données (vertices, couleurs, ...) que l'on appelle plus communément leurs entrées. On parle bien d'entrées parce qu'il existe aussi des sorties, mais ça ça sera pour le chapitre suivant.

Le verrouillage des entrées consiste simplement à dire au shader :

  • Le tableau Vertex Attrib 0 correspondra aux vertices

  • Le tableau Vertex Attrib 1 correspondra aux couleurs

  • ....

Pour faire ça, nous devons utiliser la fonction glBindAttribLocation() :

void glBindAttribLocation(GLuint program, GLuint index, const GLchar *name);
  • program : Le programme sur lequel on travaille

  • index : Le numéro du tableau Vertex Attrib à verrouiller

  • name : Le nom de la donnée dans le code source du shader

Le dernier paramètre peut vous sembler un peu confus pour le moment mais sachez juste qu'il s'agit d'une variable, dans le code source du shader, qui permet d'accéder aux données des tableaux Vertex Attrib. Par exemple, pour accéder aux vertices dans les shaders, nous utiliserons une variable qui s’appellera in_Vertex :

Image utilisateur

Pour les couleurs ce sera in_Color et pour les coordonnées de texture ce sera in_TexCoord0 :

Image utilisateur

Vu que nous utilisons 3 tableaux Vertex Attrib (vertices, couleurs et coordonnées de texture), nous devrons utiliser 3 fois la fonction glBindAttribLocation() pour verrouiller toutes les entrées. Pour le tableau d'indice 0, l'appel ressemblera à ceci :

// Verrouillage des entrées shader (Vertices)

glBindAttribLocation(m_programID, 0, "in_Vertex");

Pour les deux autres, ce sera :

// Verrouillage des entrées shader (Couleurs et Coordonnées de texture)

glBindAttribLocation(m_programID, 1, "in_Color");
glBindAttribLocation(m_programID, 2, "in_TexCoord0");

Euh j'ai une petite question, pourquoi il y a un 0 à la fin de la variable in_TexCoord0 ?

Ça peut paraitre un peu troublant mais le 0 est là pour indiquer au shader que les coordonnées de texture qu'il reçoit correspondent simplement à la première texture verrouillée (indice 0). Quand nous utiliserons le multitexturing, nous enverrons plusieurs textures au shader et celui-ci devra faire la différence entre les coordonnées reçues pour la première, celles reçues pour la deuxième, etc. Le chiffre de la variable in_TexCoord permet de faire cette différence. Enfin, ne vous triturez pas la tête avec ça pour le moment, ce n'est pas important. :)

Au final, nous plaçons les 3 appels à la fonction glBindAttribLocation() juste après l'association des shaders :

bool Shader::charger()
{
    // Compilation des shaders

    if(!compilerShader(m_vertexID, GL_VERTEX_SHADER, m_vertexSource))
        return false;

    if(!compilerShader(m_fragmentID, GL_FRAGMENT_SHADER, m_fragmentSource))
        return false;


    // Création du programme

    m_programID = glCreateProgram();


    // Association des shaders

    glAttachShader(m_programID, m_vertexID);
    glAttachShader(m_programID, m_fragmentID);


    // Verrouillage des entrées shader

    glBindAttribLocation(m_programID, 0, "in_Vertex");
    glBindAttribLocation(m_programID, 1, "in_Color");
    glBindAttribLocation(m_programID, 2, "in_TexCoord0");
}
Linkage

Le linkage constitue l'étape finale de la création d'un programme. Jusqu'à présent, nous avons compilé nos shaders séparément puis nous les avons associés à notre programme. Il ne reste plus maintenant qu'à le finaliser pour le rendre exécutable par la carte graphique.

Cette étape se fait grâce à la fonction glLinkProgram() :

void glLinkProgram(GLuint program);

Bien entendu, le paramètre program correspond au programme à linker. On appelle cette fonction juste après le verrouillage des entrées shader :

// Verrouillage des entrées shader

glBindAttribLocation(m_programID, 0, "in_Vertex");
glBindAttribLocation(m_programID, 1, "in_Color");
glBindAttribLocation(m_programID, 2, "in_TexCoord0");


// Linkage du programme

glLinkProgram(m_programID);
Vérification du linkage

Le linkage d'un programme est un processus qui peut malheureusement échouer, nous devons donc vérifier si tout s'est bien passé. La mauvaise nouvelle, c'est qu'il va falloir écrire un gros bloc de vérification comme pour la compilation de shader. :p

L'avantage cependant c'est que cet énorme bloc de vérification ressemble exactement au premier, seules quelques fonctions OpenGL vont devoir changer. Ainsi, pour vérifier si le linkage s'est bien passé nous devrons :

  • Vérifier s'il y a eu un problème

  • Si oui, récupérer la taille du message d'erreur

  • Allouer une chaine de caractère grâce à cette taille

  • Récupérer l'erreur et l'afficher

Pour la compilation de shader, nous avions utilisé la fonction glGetShaderiv() pour savoir s'il y avait eu une erreur. Pour le linkage d'un programme, la fonction est exactement identique, il n'y a que le nom qui change :

void glGetProgramiv(GLuint program, GLenum pname, GLint *params);

Les paramètres sont également identiques :

  • program : Le programme sur lequel on travaille

  • pname : Le nom de l'information demandée

  • params : L'adresse d'une variable qui accueillera cette information

On doit utiliser cette fonction, dans un premier temps, pour vérifier s'il y a eu une erreur au moment du linkage. Le nom du paramètre pname qui nous permet cela sera la constante GL_LINK_STATUS. On doit aussi créer une variable de type GLint pour contenir la valeur retournée, on l'appellera erreurLink. On en profite au passage pour implémenter que le début du bloc if :

// Linkage du programme

glLinkProgram(m_programID);


// Vérification du linkage

GLint erreurLink(0);
glGetProgramiv(m_programID, GL_LINK_STATUS, &erreurLink);


// S'il y a eu une erreur

if(erreurLink != GL_TRUE)
{
}


// Sinon c'est que tout s'est bien passé

else
    return true;

Si on se retrouve dans le bloc if, c'est que quelque chose s'est mal passé. Nous devons donc récupérer la taille du message d'erreur grâce à la fonction glGetProgramiv(). Elle prendra exactement les mêmes paramètres que la fonction glGetShaderiv() soient : une constante GL_INFO_LOG_LENGTH et l'adresse d'une variable tailleErreur de type GLint :

// Vérification du linkage

if(erreurLink != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetProgramiv(m_programID, GL_INFO_LOG_LENGTH, &tailleErreur);
}

Une fois la taille de l'erreur récupérée, on peut allouer une chaine de caractère pour la contenir sans oublier le caractère '\0' :

// Vérification du linkage

if(erreurLink != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetProgramiv(m_programID, GL_INFO_LOG_LENGTH, &tailleErreur);


    // Allocation de mémoire

    char *erreur = new char[tailleErreur + 1];
}

Maintenant que l'on a une chaine de caractère assez grande, on peut récupérer le message d'erreur grâce à une fonction très similaire à glGetShaderInfoLog() qui s'appelle glGetProgramInfoLog() :

void glGetProgramInfoLog(GLuint program, GLsizei maxLength, GLsizei *length, GLchar *infoLog);

Je vous rappelle que cette fonction permet de récupérer le message d'erreur. ;) D'ailleurs, elle prend elle-aussi les mêmes paramètres que sa sœur-jumelle :

  • program : Le programme sur lequel on travaille

  • maxLength : Taille de la chaine de caractère qui va accueillir l'erreur. Pour nous, il s'agit de la variable tailleErreur

  • length : Adresse de la variable qui contiendra la taille précédente. Ce paramètre est encore une fois bizarre, nous lui donnerons l'adresse de la variable tailleErreur

  • infoLog : La chaine de caractère qui contiendra le message final. Nous lui donnerons la chaine erreur

Nous appelons donc cette fonction avec les paramètres que nous venons juste de citer. N'oubliez pas de rajouter le caractère '\0' à la fin de la chaine :

// Vérification du linkage

if(erreurLink != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetProgramiv(m_programID, GL_INFO_LOG_LENGTH, &tailleErreur);


    // Allocation de mémoire

    char *erreur = new char[tailleErreur + 1];


    // Récupération de l'erreur

    glGetShaderInfoLog(m_programID, tailleErreur, &tailleErreur, erreur);
    erreur[tailleErreur] = '\0';
}

Pour terminer cette gestion d'erreur, il ne reste plus qu'à afficher le message tant convoité. Une fois que c'est fait, on libère la mémoire prise par la chaine de caractère puis on détruit le programme vu que celui-ci est inutilisable.

La fonction permettant de détruire un programme s'appelle simplement glDeleteProgram() :

void glDeleteProgram(GLuint program);

Elle prend en paramètre le programme à détruire.

On appelle donc cette fonction en plus du mot-clef delete[] pour détruire la chaine. On renverra également le booléen false pour indiquer que le chargement s'est mal passé :

// Vérification du linkage

if(erreurLink != GL_TRUE)
{
    // Récupération de la taille de l'erreur

    GLint tailleErreur(0);
    glGetProgramiv(m_programID, GL_INFO_LOG_LENGTH, &tailleErreur);


    // Allocation de mémoire

    char *erreur = new char[tailleErreur + 1];


    // Récupération de l'erreur

    glGetShaderInfoLog(m_programID, tailleErreur, &tailleErreur, erreur);
    erreur[tailleErreur] = '\0';


    // Affichage de l'erreur

    std::cout << erreur << std::endl;


    // Libération de la mémoire et retour du booléen false

    delete[] erreur;
    glDeleteProgram(m_programID);

    return false;
}

Récapitulatif de la méthode charger() :

bool Shader::charger()
{
    // Compilation des shaders

    if(!compilerShader(m_vertexID, GL_VERTEX_SHADER, m_vertexSource))
        return false;

    if(!compilerShader(m_fragmentID, GL_FRAGMENT_SHADER, m_fragmentSource))
        return false;


    // Création du programme

    m_programID = glCreateProgram();


    // Association des shaders

    glAttachShader(m_programID, m_vertexID);
    glAttachShader(m_programID, m_fragmentID);


    // Verrouillage des entrées shader

    glBindAttribLocation(m_programID, 0, "in_Vertex");
    glBindAttribLocation(m_programID, 1, "in_Color");
    glBindAttribLocation(m_programID, 2, "in_TexCoord0");


    // Linkage du programme

    glLinkProgram(m_programID);


	// Linkage du programme

    glLinkProgram(m_programID);


    // Vérification du linkage

    GLint erreurLink(0);
    glGetProgramiv(m_programID, GL_LINK_STATUS, &erreurLink);


    // S'il y a eu une erreur

    if(erreurLink != GL_TRUE)
    {
        // Récupération de la taille de l'erreur

        GLint tailleErreur(0);
        glGetProgramiv(m_programID, GL_INFO_LOG_LENGTH, &tailleErreur);


        // Allocation de mémoire

        char *erreur = new char[tailleErreur + 1];


        // Récupération de l'erreur

        glGetShaderInfoLog(m_programID, tailleErreur, &tailleErreur, erreur);
        erreur[tailleErreur] = '\0';


        // Affichage de l'erreur

        std::cout << erreur << std::endl;


        // Libération de la mémoire et retour du booléen false

        delete[] erreur;
        glDeleteProgram(m_programID);

        return false;
    }



    // Sinon c'est que tout s'est bien passé

    else
        return true;
}

Cette fois la méthode est bel et bien terminée, et je dirai même que l'implémentation de la classe Shader est également terminée. Vous êtes maintenant en mesure de comprendre tout le code C++ de vos applications, plus de classe mystère !

Après toutes ces péripéties, vous avez bien mérité une petite pause moi j'dis. :-°

D'ailleurs, vous pouvez continuer à lire la dernière partie de ce chapitre tout en faisant une pause car nous n'allons voir que quelques points mineurs. ^^

Utilisation et améliorations

Utilisation

La partie utilisation est vraiment quelque chose d’anecdotique car vous savez déjà comment utiliser un shader, mais nous allons tout de même faire un petit point dessus. Après tout, les rappels ça n'a jamais fait de mal. :p

Enfin bref. Pour créer un objet de type Shader, vous devez simplement déclarer un nouvel objet en lui donnant en paramètre le chemin vers deux codes sources : celui du Vertex Shader et celui du Fragment Shader, puis d'appeler la méthode charger(). Par exemple, pour charger le shader de texture :

// Shader Texture

Shader shaderTexture("Shaders/texture.vert", "Shaders/texture.frag");
shaderTexture.charger();

Une fois chargé, vous n'avez plus qu'à l'activer au moment d'afficher votre modèle grâce à la fonction glUseProgram() :

void glUseProgram(GLuint program);

La paramètre program correspond évidemment au shader à activer.

Cette fonction est vraiment similaire aux fonctions du type glBindXXX() puisqu'elle permet de dire à OpenGL : "Je souhaite utiliser ce shader-ci pendant toute la durée où il est activé". Ainsi, tous les modèles qui se trouvent à l'intérieur des deux appels à glUseProgram() seront obligés de l'utiliser pour être affichés.

// Activation du shader

glUseProgram(m_shader.getProgramID());


    // Verrouillage du VAO

    glBindVertexArray(m_vaoID);


        // Envoi des matrices

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


        // Verrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, m_texture.getID());


        // Rendu

        glDrawArrays(GL_TRIANGLES, 0, 36);


        // Déverrouillage de la texture

        glBindTexture(GL_TEXTURE_2D, 0);


    // Déverrouillage du VAO

    glBindVertexArray(0);


// Désactivation du shader

glUseProgram(0);

D'ailleurs en parlant de ça, je vous conseille fortement d'activer votre shader avant de verrouiller tous vos autres objets OpenGL. Si vous ne le faites pas, vous risquerez d'envoyer des données dans le vide (comme les matrices ou les textures) car aucun shader ne sera là pour les recevoir. ;)

Améliorations

Le destructeur

Dans cette petite partie améliorations, nous allons surtout parler de méthodes qui seront utiles si nous voulons copier des objets de type Shader entre eux. Mais avant ça, nous allons parler un peu de libération de mémoire car jusqu'à maintenant, lorsqu'un objet Shader est détruit, il ne libère à aucun moment les ressources qu'il a prises.

Pour l'aider à le faire, nous allons simplement détruire tous les attributs dans le destructeur. Sachant que les string savent le faire toutes seules, il ne reste plus qu'à gérer les attributs m_vertexID, m_fragmentID et m_programID. Les fonctions permettant de détruire ces objets OpenGL sont glDeleteShader() et glDeleteProgram().

Nous les appelons donc le destructeur :

Shader::~Shader()
{
    // Destruction du shader

    glDeleteShader(m_vertexID);
    glDeleteShader(m_fragmentID);
    glDeleteProgram(m_programID);
}

Comme quoi, le destructeur ne reste pas vide à chaque fois. :p

Le constructeur de copie

On continue cette dernière partie avec le constructeur de copie. J'espère que vous savez ce que c'est quand même, surtout qu'on l'utilise depuis un moment maintenant. :p

En temps normal, il est inutile de le coder tant qu'on n'utilise pas de pointeurs car seuls eux peuvent poser des problèmes. Cependant, les objets OpenGL peuvent être rapprochés aux pointeurs car leur ID se comporte un peu de la même façon. Si vous essayez de copier un ID dans un autre objet, alors les deux copies auront accès exactement aux mêmes données alors qu'ils devraient avoir chacun les leurs.

Il nous faut ré-écrire ce constructeur pour pouvoir être capables de copier deux shaders correctement sans avoir à nous soucier de ce problème. Voici d'ailleurs le prototype de cette pseudo-méthode :

Shader(Shader const &shaderACopier);

Pour la classe Shader, nous avons 5 attributs à gérer. Les deux strings peuvent être copiées directement sans problème :

Shader::Shader(Shader const &shaderACopier)
{
    // Copie des fichiers sources

    m_vertexSource = shaderACopier.m_vertexSource;
    m_fragmentSource = shaderACopier.m_fragmentSource;
}

Les 3 derniers attributs concernent les shaders et le programme. Ils ne peuvent pas être copiés de la même façon, il va falloir re-générer de nouvelles valeurs (de nouveaux ID) à partir des fichiers sources. Pour ce faire, il suffit juste d'appeler la méthode charger() car c'est elle qui s'occupe de ça :

Shader::Shader(Shader const &shaderACopier)
{
    // Copie des fichiers sources

    m_vertexSource = shaderACopier.m_vertexSource;
    m_fragmentSource = shaderACopier.m_fragmentSource;


    // Chargement du nouveau shader

    charger();
}

Le constructeur de copie est terminé. ;)

L'opérateur =

L'opérateur d'affectation = est un opérateur très pratique en C++ car il permet de copier un objet directement en utilisant le symbole = comme si on faisait une banale opération arithmétique. Nous allons l'implémenter, au même titre que le constructeur de copie, dans le cas où nous aurions à copier un shader.

Voici son prototype :

Shader& operator=(Shader const &shaderACopier);

Cette méthode prend et renvoie une référence constante.

Son contenu est strictement identique au constructeur de copie car elle fait exactement la même chose. :p On rajoutera juste le retour du pointeur *this pour retourner proprement notre objet :

Shader& Shader::operator=(Shader const &shaderACopier)
{
    // Copie des fichiers sources

    m_vertexSource = shaderACopier.m_vertexSource;
    m_fragmentSource = shaderACopier.m_fragmentSource;


    // Chargement du nouveau shader

    charger();


    // Retour du pointeur this

    return *this;
}

Encore une fois, je vous repose la question pour ce chapitre ( :p ) : vous vous souvenez de ce qui se passait si on chargeait deux fois un même objet OpenGL (texture, VBO, etc.) d'un coup ? Que cela entrainait une fuite de mémoire ? Et bien nous avons encore le même problème ici. Si vous chargez deux fois un shader, lors d'une copie par exemple, alors vous risquez de gâcher de la mémoire qui ne pourra pas être libérée avant la fermeture de votre application.

Pour éviter cela, nous devons appeler les fonctions de vérification d'objet OpenGL du type glIsXXX(), puis de les détruire si besoin est. Elles s'appellent ici glIsShader() et glIsProgram() :

// Vérification de shader

GLboolean glIsShader(GLuint shader);


// Vérification de programme

GLboolean glIsProgram(GLuint program);

Elles prennent en paramètre respectivement un identifiant de shader et un identifiant de programme pour savoir s'ils ont déjà été chargés. Elles renvoient toutes les deux la valeur GL_TRUE si c'est le cas, sinon c'est GL_FALSE.

Au final, pour les attributs m_vertexID, m_fragmentID et m_programID, nous devons faire un bloc if en vérifiant la valeur retournée par la fonction glIsXXX(). Si c'est GL_TRUE, alors on appelle leur fonction de destruction :

bool Shader::charger()
{
    // Destruction d'un éventuel ancien Shader

    if(glIsShader(m_vertexID) == GL_TRUE)
        glDeleteShader(m_vertexID);

    if(glIsShader(m_fragmentID) == GL_TRUE)
        glDeleteShader(m_fragmentID);

    if(glIsProgram(m_programID) == GL_TRUE)
        glDeleteProgram(m_programID);


    // Compilation des shaders

    if(!compilerShader(vertexShader, GL_VERTEX_SHADER, m_vertexSource)
        return false;

    if(!compilerShader(fragmentShader, GL_FRAGMENT_SHADER, m_fragmentSource)
        return false;


    ....
}

Grâce à ce bout de code, on évite toute fuite de mémoire car les éventuels anciens ID seront détruits avant d'être ré-initialisés.

Télécharger (Windows, UNIX/Linux, Mac OS X) : Code Source C++ du chapitre sur la compilation de Shader

A travers ce premier chapitre consacré aux shaders, nous avons pu voir comment compiler n'importe quel code source. Tous ceux que nous avons utilisés depuis la partie 1 sont passés par cette classe - classe dont nous sommes enfin capables de comprendre le fonctionnement.

Les notions que nous avons abordées sont assez costauds au niveau technique, je vous conseille de relire à tête reposée ceux qui vous paraissent encore un peu sombres.

Néanmoins, après le code de ce pseudo-compilateur, nous pouvons enfin passer aux codes sources mêmes des shaders. Les 3 prochains chapitres leur seront entièrement consacrés. Refaite le stock de café ou de chocolat moi j'dis ! :p

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