• 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

Les shaders démystifiés (Partie 2/2)

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

Précédemment, nous avons commencé à démystifier un peu les shaders en étudiant leur comportement dans un environnement en deux dimensions. Nous avons même vu comment gérer la couleur grâce aux tableaux Vertex Attrib et envoyer des variables d'un shader à un autre.

Ces échanges de données seront quasiment toujours présents quelque soit les effets que l'on voudra programmer. Si vous n'êtes pas à l'aise avec ça, je vous conseille vivement de relire le chapitre précédent. ;)

D'ailleurs, ce que nous allons voir aujourd'hui concerne également les échanges de données sauf qu'ici, nous parlerons d'échanges entre l'application principale (en C++) et les shaders (en GLSL). Grâce à eux, nous pourrons enfin envoyer nos matrices à nos codes sources et nous pourrons enfin gérer la 3D. :D

Nous parlerons aussi des derniers petits points à connaitre sur la programmation GLSL.

Les variables uniform

Qu'est-ce qu'une variable uniform ?

On commence ce chapitre par une notion que l'on retrouvera dans tous les shaders un tant soit peu développés. ;)

Les variables uniform sont des variables envoyées depuis une application classique (codée en C++) jusqu'à un shader. Elles peuvent être utilisées pour envoyer tous les types de données possibles au niveau du GLSL soit les variables classiques (float, int, etc.), les vecteurs et les matrices. Nous connaissons particulièrement ce dernier cas car nous avons déjà eu l'occasion d'envoyer les fameuses matrices projection et modelview, ce qui permettait d'intégrer la troisième dimension à nos applications.

D'ailleurs lorsque nous les avons vues, je vous avais fourni deux fonctions totalement incompréhensibles qui permettaient d'envoyer des matrices aux shaders. Elles ressemblaient à ceci :

// Envoi des matrices au shader

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

Complément incompréhensible bien sûr. :p

Vous remarquerez cependant l'utilisation du terme uniform dans le nom de la fonction glUniformMatrix4fv(). Ce qui signifie que les matrices sont bien considérées comme des variables uniform. Elles arrivent donc directement dans le shader. C'est un exemple d'utilisation que l'on rencontrera tout le temps, mais les matrices ne sont pas les seules valeurs à pouvoir être envoyées comme nous allons le voir.

Envoyer une variable simple

Le mot-clef uniform

Avant d'étudier en détails les fonctions que l'on vient de voir, nous allons faire un petit détour au niveau des codes sources GLSL. Les variables uniform se comportent un peu comme les in et les out dans le sens où ils possèdent un mot-clef qui leur est propres. Ce mot-clef est simplement uniform.

Prenons un petit exemple en déclarant une variable float en tant qu'uniform :

// Version du GLSL

#version 150 core


// Variable uniform

uniform float maVariable;


// Fonction main

void main()
{
}

Ce nouveau mot-clef indique au shader que l'application enverra une variable au code source GLSL. Bien évidemment, celle-ci se comportera comme une variable classique sauf qu'on ne pourra pas la modifier. Nous pourrons toujours l'utiliser dans des opérations arithmétiques, dans des conditions, ... mais nous ne pourrons jamais modifier sa valeur directement.

Il faut savoir également qu'une variable uniform est accessible dans les deux types de shaders (Vertex et Fragment), nous n'avons donc pas besoin de les manipuler avec les entrées-sorties ni de spécifier dans quel code source les utiliser. Ça les rend plus simple à utiliser. :)

La localisation et son utilisation

On revient maintenant aux fonctions de gestion des uniform au niveau de l'application. Nous en avons rapidement survolé une que nous connaissions déjà depuis un moment et qui s’appelait glUniformMatrix4fv(). Celle-ci permet d'envoyer une matrice directement au shader.

Cependant, ce n'est pas la seule fonction qui permet d'envoyer des données. Elle possède plusieurs 'sœurs' qui prennent en compte d'autres types comme les variables simples (float, int, etc.) et les vecteurs. Tous ces cas se gèrent un peu de la même façon, il n'y a que les paramètres à utiliser qui vont changer.

D'ailleurs nous allons étudier en premier le cas des variables simples car c'est celui qui en requiert le moins. :p Nous aurons besoin pour cela d'utiliser une fonction dont le nom se rapproche fortement de celui que nous connaissons déjà. Elle s'appelle glUniform1f() :

void glUniform1f(GLint location, GLfloat value);
  • location : paramètre permettant de retrouver la variable utilisant le mot-clef uniform dans le code source GLSL

  • value : La valeur (de type float) que vous souhaitez envoyer

Le premier paramètre peut vous sembler un peu flou et c'est tout à fait normal. Pour le comprendre, nous allons faire une analogie avec la fonction glBindAttribLocation(). Celle-ci demandait, parmi ses paramètres, le nom correspondant à la variable de destination dans le code GLSL. Par exemple, nous l'utilisions pour faire le lien entre la variable in_Vertex et le tableau de vertices. Même chose avec la variable in_Color et le tableau de couleurs.

Cette fonction nous permettait donc de spécifier directement un nom pour retrouver son destinataire dans le code GLSL.

Le problème avec les uniform, c'est que ce système de nom n'existe pas directement. Il faut passer par une fonction intermédiaire pour faire le lien entre le shader et l'application. Cette fonction intermédiaire s'appelle glGetUniformLocation() :

GLint glGetUniformLocation(GLuint program, const GLchar *name)
  • program : l'ID du programme shader. Nous utiliserons la méthode glGetProgramID() pour lui affecter une valeur

  • name : nom de la variable dans le code source GLSL

Grâce à elle, nous pouvons trouver facilement le paramètre location de glUniform1f(). ^^

On reprend notre petit exemple précédent qui contenait la variable uniformmaVariable. Pour récupérer sa localisation au sein du shader, on appelle donc la fonction glGetUniformLocation() en donnant en paramètre l'ID du programme ainsi qu'une chaine de caractère contenant le nom "maVariable" :

// Localisation de maVariable

int localisation = glGetUniformLocation(shader.getProgramID(), "maVariable");

Maintenant que l'on connait la localisation de maVariable, nous pouvons lui envoyer une valeur. On utilise pour cela la fonction glUniform1f() en donnant en paramètre cette fameuse localisation ainsi qu'une valeur (par exemple 8.5) :

// Localisation de maVariable

int localisation = glGetUniformLocation(shader.getProgramID(), "maVariable");


// Envoi d'une valeur à maVariable

glUniform1f(localisation, 8.5);

La variable uniform dans le code GLSL possèdera maintenant la valeur 8.5. ;)

Petit détail, il est tout à fait possible de combiner les deux fonctions utilisées comme nous l'avons vu pour les matrices :

// Envoi d'une valeur à maVariable

glUniform1f(glGetUniformLocation(shader.getProgramID(), "maVariable"), 8.5);
Point récapitulatif

Nous allons faire un petit point récapitulatif car ce que nous venons de voir constitue la base de l'envoi de variable au shader. Il n'y a que trois points à retenir alors ouvrez grand vos oreilles (ou plutôt vos yeux :p ) :

  • Premièrement, il faut déclarer une variable avec le mot-clef uniform dans le code source GLSL.

  • Ensuite, on récupère sa localisation grâce à la fonction glGetUniformLocation() du coté de l'application C++

  • Puis, on envoie la valeur souhaitée grâce à la fonction glUniform1f() ou ses variantes

Je le répète, ces 3 points constituent la base du fonctionnement des uniform. Retenez-les bien. Nous ferons quelques exercices à la fin de cette partie pour vous familiariser avec eux.

Envoyer un vecteur

Coté GLSL

Comme nous l'avons vu précédemment, il existe une multitude de variantes de la fonction glUniform1f() qui permettent d'envoyer d'autres types de variable que les float ou les int. Nous allons justement voir une de ces variantes qui nous servira à envoyer des vecteurs (à 2, 3, ou 4 coordonnées).

Pour illustrer cela, nous allons déclarer une variable position de type vec3 avec le mot-clef uniform dans le Vertex Shader :

// Version du GLSL

#version 150 core


// Variable uniform

uniform vec3 position;


// Fonction main

void main()
{
}
Coté application

Le détail qui vous a peut-être frappé dans le nom de la fonction glUniform1f() est l'utilisation du chiffre1 qui, apparemment, n'avait rien à faire ici. Ce chiffre est en fait très important car il permet de déterminer le nombre de données à envoyer à la variable de destination.

Lorsqu'il s'agit d'un variable simple comme un float, nous n'avons besoin d'envoyer qu'une seule valeur. Cependant lorsque l'on parle de vecteur, le nombre de données à envoyer devient un peu plus important car il faut affecter une valeur à chacune de ses coordonnées.

Vu qu'il existe 3 types de vecteur (vec2, vec3 et vec4), il existe donc 3 formes possibles pour la fonction glUniform*(). La seule chose qui va être modifiée avec celle que l'on connait déjà est le fameux chiffre utilisé à la fin du nom. Pour envoyer un vecteur à 2 coordonnées par exemple, il faudra mettre la valeur 2, ce qui donnera la fonction suivante :

// Vecteur à 2 coordonnées

void glUniform2f(GLint location, GLfloat valu0, GLfloat value1);
  • location : localisation du vecteur au niveau GLSL

  • value0 : valeur pour la coordonnée x

  • valeur1 : valeur pour la coordonnée y

Pour les vecteurs à 3 ou 4 coordonnées, il faudra mettre le chiffre correspondant au nom de la fonction glUniform() :

// Vecteur à 3 coordonnées

void glUniform3f(GLint location, GLfloat valu0, GLfloat value1, GLfloat valu2);


// Vecteur à 4 coordonnées

void glUniform4f(GLint location, GLfloat valu0, GLfloat value1, GLfloat valu2, GLfloat value3);
  • location : localisation du vecteur au niveau GLSL

  • value0 : valeur pour la coordonnée x

  • valeur1 : valeur pour la coordonnée y

  • valeur2 : valeur pour la coordonnée z

  • valeur3 : valeur pour la coordonnée w

Si on reprend l'exemple du vecteur vec3position déclaré dans le shader, la fonction à utiliser sera glUniform3f(). Il faudra lui donner en paramètre la localisation de la variable ainsi qu'une valeur pour chacune de ses 3 coordonnées :

// Localisation du vecteur position

int localisation = glGetUniformLocation(shader.getProgramID(), "position");


// Envoi d'une valeur au vecteur position

glUniform3f(localisation, 1.0, 2.0, 3.0);

Le vecteur position est maintenant utilisable dans notre shader :

// Version du GLSL

#version 150 core


// Variable uniform

uniform vec3 position;


// Fonction main

void main()
{
    // Exemple d'utilisation

    if(position.x >= 0)
        .... ;

    else
        .... ;
}

Le fonctionnement reste évidemment le même pour les autres types de vecteur.

Envoyer une matrice

Coté GLSL

Avant de s'occuper de la partie application, nous allons déclarer une matrice dans le Vertex Shader. Nous l’appellerons modelview en référence avec une certaine matrice que l'on connait déjà. ;)

Nous déclarons donc une matrice carrée d'ordre 4 (mat4) avec le mot-clef uniform :

// Version du GLSL

#version 150 core


// Variable uniform

uniform mat4 modelview;


// Fonction main

void main()
{
}
"Transposer" une matrice

Les envois de matrice se passent quasiment de la même façon que ceux que nous venons d'étudier. A vrai dire, il n'y a que deux paramètres supplémentaires à gérer. Pour envoyer une matrice carrée d'ordre 4 par exemple, la fonction à utiliser s'appelle glUniformMatrix4fv() :

void glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat *value);
  • location : localisation de la matrice au niveau GLSL

  • count : nombre de sous-tableaux utilisés par la matrice. Nous lui affecterons la valeur 1.0 car nous n'en enverrons qu'un seul.

  • transpose : booléen pouvant prendre la valeur GL_TRUE ou GL_FALSE et qui permet de transposer une matrice. Nous verrons ce que cela signifie dans un instant

  • value : pointeur sur les valeurs de la matrice. Nous lui donnerons le résultat de la méthode getValeurs()

Le paramètre transpose peut vous sembler un peu flou pour le moment. C'est parce que nous n'avons pas encore vu ce que voulait dire le terme "transposer" une matrice. C'est pourtant une notion assez simple qui signifie "inverser" les valeurs d'une matrice pour que les lignes se retrouvent à la place des colonnes et les colonnes à la place des lignes :

Image utilisateur

Si vous utilisez GLM, ce qui est normalement le cas, vous n'avez pas à vous soucier de ce paramètre car la librairie est en parfaite adéquation avec le GLSL. C'est-à-dire que les objets mat4 en C++ se lisent aussi en colonne et c'est exactement ce que veut le shader :

Image utilisateur

Si vous utilisez une autre librairie mathématique pour gérer vos matrices, je vous conseille de vérifier leur ordre de lecture car elles doivent peut-être être transposées. C'est-à-dire qu'elles se lisent peut-être en ligne :

Image utilisateur

Si c'est le cas, vous devez les transposer à l'aide du paramètre transpose.

Coté application

Ceci étant dit, nous pouvons maintenant repasser à la fonction . Nous allons pouvoir l'utiliser pour envoyer notre matrice modelview à la variable du même nom dans le code source GLSL.

Nous savons qu'elle prendra 4 paramètres :

  • location : qu'il faudra déterminer comme nous savons déjà le faire

  • count : auquel nous affecterons la valeur 1.0

  • transpose : auquel il faudra affecter la valeur GL_FALSE car nous n'avons pas besoin de transposer nos matrices

  • value : qui demande un pointeur sur les valeurs à envoyer

Ainsi, pour envoyer la matrice modelview au sahder, nous devrons d'abord la localiser à l'aide de la fonction glGetUniformLocation() :

// Localisation de la matrice modelview

int localisation = glGetUniformLocation(shader.getProgramID(), "modelview");

Puis, nous envoyons ses valeurs grâce à la fonction glUniformMatrix4fv() avec les paramètres cités précédemment :

// Localisation de la matrice modelview

int localisation = glGetUniformLocation(shader.getProgramID(), "modelview");


// Envoi de la matrice

glUniformMatrix4fv(localisation, 1, GL_FALSE, value_ptr(modelview));

La variable uniform au niveau du shader contient maintenant les valeurs de la matrice modelview.

Vous êtes à présent capables de comprendre les fameux envois que nous utilisons depuis le chapitre sur la 3D. :p

// Envoi des matrices au shader

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

Envoyer un tableau

Coté GLSL

L'envoi de tableaux est parfois utile lorsque vous souhaitez envoyer plusieurs variables en une seule fois. Nous savons déjà comment faire en plus car ce type d'envoi s'effectue exactement de la même façon que les matrices. Les paramètres sont eux-aussi identiques, il n'y a que le nom de la fonction à utiliser qui va changer.

Au niveau du GLSL, il suffit simplement de déclarer un tableau comme nous le ferions en temps normal mais précédé par le mot-clef uniform. Pour déclarer un tableau de 10 cases par exemple, nous ferions ainsi :

// Version du GLSL

#version 150 core


// Variable uniform

uniform float monTableau[10];


// Fonction main

void main()
{
}
Coté application

La fonction à utiliser est très similaire à celle des matrices au niveau des paramètres. Elle s’appelle : glUniform1fv() :

void glUniform1fv(GLint location, GLsizei count, const GLfloat *value);
  • location : localisation du tableau au niveau GLSL

  • count : taille du tableau

  • value : pointeur sur les données

Pour envoyer un tableau de 10 float à l'uniform précédent, il suffit donc d'appeler la fonction glUniform1fv() avec les paramètres suivants :

// Tableau de 10 float

float tableau[10] = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 1.0};


// Localisation du tableau monTableau

int localisation = glGetUniformLocation(shader.getProgramID(), "monTableau");


// Envoi des valeurs

glUniform1fv(localisation, 10, tableau);

Ce qu'il faut retenir

Les variables uniform sont un peu complexes au premier abord mais vous avez remarqué qu'ils se gèrent tous de la même façon. La seule difficulté vient du nombre de variantes de la fonction glUniform*() à utiliser. Nous allons faire un petit résumé de ce qu'il faut retenir pour éviter de vous emmêler les pinceaux dans le futur.

Tout d'abord, il faut savoir que les uniform sont des variables envoyées de l'application jusqu'au shader. Elles doivent être déclarées avec le mot-clef uniform dans le code GLSL.

Pour envoyer des données, il faut commencer par localiser les variables contenues dans le shader grâce à la fonction glGetUniformLocation(). Puis, il faut appeler l'une des variantes suivantes :

  • glUniform1f() : pour envoyer des variables simples

  • glUniform*f() : pour envoyer des vecteurs. L'étoile dans le nom permet de définir le type utilisé dans le shader (vec2, vec3 ou vec4).

  • glUniformMatrix*fv() : pour envoyer des matrices. Même remarque pour l'étoile mais pour les types de matrice cette fois

  • glUniform1fv() : pour envoyer des tableaux

Enfin, la lettre f dans le nom des fonctions peut être remplacée par i ou ui pour envoyer respectivement des entiers ou des entiers non-signés. Si vous laissez la lettre f, alors vous enverrez des flottants.

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

Exercices

Énoncés

Comme d'habitude, nous allons faire quelques exercices pour assimiler ce que nous avons vu dans cette partie. Vous avez évidemment le droit de revenir sur le cours pour vous aider. ;)

Exercice 1 : Créez une variable de type float dans votre application que vous appellerez maCouleur. Envoyez-la ensuite au Fragment Shader pour qu'elle puisse remplacer la composante rouge de la variable d'entrée inColor au moment de l'affecter à la variable de sortie out_Color.

Exercice 2 : Créez un objet de type Vecteur dans votre application, ses attributs représenteront une couleur quelconque. Envoyez justement ces attributs au Fragment Shader pour définir la couleur du pixel.

Exercice 3 : Re-déclarez les matrices projection et modelview dans votre application (vous n'avez pas besoin des les initialiser). Envoyez-les ensuite dans le Vertex Shader pour les multiplier dans la fonction main(). Le résultat doit être contenu dans une variable de type mat4.

Solutions

Exercice 1 :

On commence par créer la variable maCouleur dans l'application, puis on l'envoie au shader depuis la boucle principale grâce à la fonction glUniform1f() :

// Variable à envoyer

float maCouleur(1.0);

....


// Boucle principale

while(!m_input.terminer())
{
    ....


    // Localisation de la variable maCouleur

    int localisationMaCouleur = glGetUniformLocation(shader.getProgramID(), "maCouleur");


    // Envoi de la variable

    glUniform1f(localisationMaCouleur, maCouleur);


    ....
}

Au niveau du Fragment Shader, on déclare la variable maCouleur avec le mot-clef uniform :

// Uniform

uniform float maCouleur;

Puis on modifie l'affectation de la variable de sortie out_Color en prenant en compte l'uniform et les composantes (y, z) de la variable d'entrée Color :

void main()
{
    // Couleur du pixel

    out_Color = vec4(maCouleur, color.y, color.z, 1.0);
}

Ce qui donne le code source suivant :

// Version du GLSL

#version 150 core


// Entrée

in vec3 color;


// Uniform

uniform float maCouleur;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur du pixel

    out_Color = vec4(maCouleur, color.y, color.z, 1.0);
}

Exercice 2 :

On commence par créer un objet Vecteur dans l'application, puis on envoie ses coordonnées grâce à la fonction glUniform3f() :

// Vecteur à envoyer

Vecteur monVecteur(1.0, 0.0, 1.0);

....


// Boucle principale

while(!m_input.terminer())
{
    ....


    // Localisation de la variable monVecteur

    int localisationMonVecteur = glGetUniformLocation(shader.getProgramID(), "monVecteur");


    // Envoi de la variable

    glUniform3f(localisationMonVecteur, monVecteur.getX(), monVecteur.getY(), monVecteur.getZ());


    ....
}

Au niveau du Fragment Shader, on déclare la variable monVecteur de type vec3 avec le mot-clef uniform :

// Uniform

uniform vec3 monVecteur;

Puis on utilise le constructeur vec4() en donnant en paramètre monVecteur ainsi qu'une valeur représentant la composante Alpha (1.0) :

void main()
{
    // Couleur du pixel

    out_Color = vec4(monVecteur, 1.0);
}

Ce qui donne le code source suivant :

// Version du GLSL

#version 150 core


// Entrée

in vec3 color;


// Uniform

uniform vec3 monVecteur;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur du pixel

    out_Color = vec4(monVecteur, 1.0);
}

Exercice 3 :

On commence par déclarer les matrices projection et modelview comme nous savons le faire depuis un moment déjà. Puis on les localise toutes les deux grâce à la fonction glGetUniformLocation() :

// Matrices

mat4 projection;
mat4 modelview;


....


// Boucle principale

while(!m_input.terminer())
{
    ....


    // Localisation des matrices

    int localisationProjection = glGetUniformLocation(shader.getProgramID(), "projection");
    int localisationModelview = glGetUniformLocation(shader.getProgramID(), "modelview");
}

Ensuite, on les envoie au shader grâce à la fonction glUniformMatrix4fv() en n'oubliant pas des les transposer :

// Localisation des matrices

int localisationProjection = glGetUniformLocation(shader.getProgramID(), "projection");
int localisationModelview = glGetUniformLocation(shader.getProgramID(), "modelview");


// Envoi des matrices

glUniformMatrix4fv(localisationProjection, 1, GL_FALSE, value_ptr(projection));
glUniformMatrix4fv(localisationModelview, 1, GL_FALSE, value_ptr(modelview));

Au niveau du Vertex Shader, on déclare deux variables uniform de type mat4 :

// Uniform

uniform mat4 projection;
uniform mat4 modelview;

Enfin, il ne reste plus qu'à les multiplier en prenant soin de créer une variable pour contenir le résultat de l'opération :

void main()
{
    // Multiplication des deux matrices

    mat4 resultat = projection * modelview;


    ....
}

Ce qui donne le code source suivant :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec3 in_Color;


// Uniform

uniform mat4 projection;
uniform mat4 modelview;


// Sortie

out vec3 color;


// Fonction main

void main()
{
    // Multiplication des deux matrices

    mat4 resultat = projection * modelview;
	
	
    // Position finale du vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);


    // Envoi de la couleur au Fragment Shader

    color = in_Color;
}

Gestion de la 3D

Préparation

Dans la partie précédente, nous avons vu ce qu'étaient les variables uniform et comment elles fonctionnaient. Nous allons maintenant pouvoir en tirer profit en envoyant nos fameuses matrices à notre shader. Pour rappel, la matrice modelview permet de "placer" un modèle dans un monde en 3D. La matrice de projection quant à elle permet de transformer, ou plutôt projeter, ce monde vers notre écran; ce dernier n'étant qu'en 2 dimensions.

Ce qu'il faut comprendre c'est qu'elles ont chacune une utilité bien particulière. Elles sont complémentaires pour afficher quelque chose mais elles sont totalement différentes dans le fond.

Pour afficher des modèles en 3D, nous aurons donc besoin de les envoyer toutes les deux au shader que l'on utilisera grâce aux uniform.

Le gros avantage avec la préparation du code c'est qu'on va pouvoir supprimer le gros code tout moche de la boucle principale. En effet, nous n'avons plus besoin d'afficher un carré en 2D pour faire nos tests, nous avons besoin de modèles 3D. Et nous avons justement codé deux classes qui permettent d'en afficher (Cube et Caisse), nous n'allons donc pas nous priver de leur utilisation. :p

On peut donc supprimer tout le code relatif au carré pour ne garder que ceci (sans la boucle while) :

void SceneOpenGL::bouclePrincipale()
{
    // Variables

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


    // Matrices

    mat4 projection;
    mat4 modelview;

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


    // Boucle principale

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

Profitons-en dès maintenant pour réutiliser notre bonne vielle caméra en déclarant un objet Camera et en la positionnant en 3D au point de coordonnées (3, 3, 3) :

// Caméra mobile

Camera camera(vec3(3, 3, 3), vec3(0, 0, 0), vec3(0, 1, 0), 0.5, 0.5);

Pour tester l'implémentation de la 3D, nous allons utiliser un objet Cube. Nous verrons par la suite la façon de gérer les textures avec un objet Caisse. Nous lui donnerons une taille de 2.0 ainsi que le chemin vers deux nouveaux fichiers que nous appellerons couleur3D.vert et couleur3D.frag (n'oubliez pas de les placer dans le dossier Shaders de votre projet) :

// Objet Cube

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

Au niveau de la boucle principale, on supprime tout le code relatif à l'ancien carré pour ne garder que ceci :

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


    // Gestion du déplacement de la caméra

    camera.deplacer();


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


    // Placement de la caméra

    camera.lookAt(modelview);


    
    // Rendu




    // Actualisation de la fenêtre

    SDL_GL_SwapWindow(m_fenetre);


    // Calcul du temps écoulé

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


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

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

On y ajoute ensuite l'appel à la méthode afficher() de l'objet cube. On lui donnera au passage les matrices projection et modelview :

// Placement de la caméra

....



// Affichage du cube

cube.afficher(projection, modelview);


// Actualisation de la fenêtre

....

La préparation du code est terminée. ;)

Intégrer la troisième dimension

Coté application

Pour programmer nos premiers shaders "3D", nous avons vu à l'instant qu'il fallait envoyer les matrices projection et modelview en tant que variable uniform. Nous aurons besoin pour cela de la fonction glUniformMatrix4fv() dont nous avons enfin appris son fonctionnement dans la partie précédente.

Vérifiez donc bien (même s'il ne devrait pas y avoir de problème) qu'elles sont bien présentes dans la méthode afficher() du cube :

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


    // Rendu

    glDrawArrays(GL_TRIANGLES, 0, 36);


// Déverrouillage du VAO

glBindVertexArray(0);
Le Vertex Shader

La première chose à faire au niveau du GLSL va être la déclaration des matrices avec le mot-clef uniform. D'ailleurs, vu qu'elles sont relatives aux vertices, et non aux pixels, il faut les déclarer dans le Vertex Shader.

On reprend donc le code que nous avons laissé au chapitre précédent auquel on va rajouter les nouveaux uniform :

// Uniform

uniform mat4 projection;
uniform mat4 modelview;

Ce qui donne :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec3 in_Color;


// Uniform

uniform mat4 projection;
uniform mat4 modelview;


// Sortie

out vec3 color;


// Fonction main

void main()
{
    // Position finale du vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);


    // Envoi de la couleur au Fragment Shader

    color = in_Color;
}

Avant d'aller plus loin, pensez à modifier le type de la variable d'entrée in_Vertex. Vu qu'elle possède maintenant 3 coordonnées, son type devient donc vec3 :

// Entrées

in vec3 in_Vertex;

La variable in_Color ne change pas, elles a toujours ses 3 composantes.

Contrairement à ce qu'on pourrait penser, la gestion de la 3D est une chose d'assez simple à réaliser. Ce qu'il y a de plus dur à comprendre, c'est l'envoi des variables uniform. :p
En fait, il suffit juste de multiplier la variable in_Vertex par la matrice modelview et projection. De cette façon, on peut positionner le vertex dans le monde 3D et le projeter sur l'écran.

Le seul point sensible auquel il faut faire attention concerne l'ordre des membres dans la multiplication qui doivent se présenter de la manière suivante : projection x modelview x vertex. Si vous inversez un seul de ces membres alors votre calcul sera totalement faux. Dans le meilleur des cas, vous afficherez un bout de votre modèle et dans le pire, vous aurez un bel écran noir.

Euh au fait, c'est normal qu'on puisse multiplier une matrice par un vecteur ?

Oui tout à fait, nous avons même vu comment faire dans le chapitre sur les matrices :

Image utilisateur

La seule condition à respecter était le fait que le nombre de colonnes de la matrice soit égal au nombre de coordonnées du vecteur :

Image utilisateur

Pour le moment, notre vertex ne possède que 3 coordonnées, la multiplication est donc impossible. Cependant grâce aux constructeurs, nous allons pouvoir régler ce problème, il suffira juste d'ajouter la valeur 1.0 à la coordonnée W comme nous le faisions avant.

Pour résumer, nous devons multiplier les matrices que nous avons envoyées en uniform par la variable d'entrée correspondant au vertex. Vu qu'elles sont toutes considérées comme des variables en GLSL, nous allons pouvoir faire cette opération très simplement :

void main()
{
    // Position finale du vertex en 3D

    gl_Position = projection * modelview * vec4(in_Vertex, 1.0);


    // Envoi de la couleur au Fragment Shader

    color = in_Color;
}

Ce qui donne le code source final :

// Version du GLSL

#version 150 core


// Entrées

in vec3 in_Vertex;
in vec3 in_Color;


// Uniform

uniform mat4 projection;
uniform mat4 modelview;


// Sortie

out vec3 color;


// Fonction main

void main()
{
    // Position finale du vertex en 3D

    gl_Position = projection * modelview * vec4(in_Vertex, 1.0);


    // Envoi de la couleur au Fragment Shader

    color = in_Color;
}
Le Fragment Shader

Le code du Fragment Shader va être super simple à faire car il n'y a aucun modificatione à faire dessus. :p

En effet, nous n'avons pas modifié le contenu de la variable de sortie Color dans le Vertex Shader précédent. Il n'y a donc aucune modification à apporter au Fragment Shader puisqu'il se contente simplement de recopier cette variable dans sa sortie. Nous pouvons reprendre sans problème le code source du chapitre précédent (même s'il était prévu pour une utilisation 2D à la base) :

// Version du GLSL

#version 150 core


// Entrée

in vec3 color;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(color, 1.0);
}

Vous pouvez compiler votre projet pour admirer le retour de la 3D dans vos programmes. :D

Image utilisateur

Gestion des textures

Afficher une texture

Coté application

Nous allons maintenant passer au dernier shader qui reste encore un peu mystérieux pour le moment : celui qui permet d'afficher des textures.

Depuis que nous les utilisons, nous n'avons plus besoin de couleur pour afficher quelque chose à l'écran. Ce qui rajoute un semblant de réalisme à nos scènes. Pour les intégrer dans l'application, nous devons utiliser un tableau (différent de celui des couleurs) qui contient des coordonnées de texture. Ces coordonnées permettent de savoir où "plaquer" l'image que l'on veut afficher :

Image utilisateur

Avant de commencer, veuillez créer deux nouveaux fichiers sources que vous appellerez texture.vert et texture.frag.

Pour la suite, nous allons supprimer le code relatif au cube pour le remplacer par celui de la classe Caisse. Nous déclarons donc un objet de ce type avec en paramètres le chemin vers le deux nouveaux fichiers créés ainsi qu'un chemin supplémentaire pour sa texture :

// Objet Caisse

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

Au niveau de la boucle principale, nous n'avons qu'à supprimer l'appel à la méthode afficher() du cube pour le remplacer par celui de notre nouvelle caisse :

// Placement de la caméra

....



// Affichage de la caisse

caisse.afficher(projection, modelview);


// Actualisation de la fenêtre

....

Préparation terminée. :)

Le Vertex Shader

Pour intégrer les textures, nous n'avons pas de grande modification à faire dans le Vertex Shader car ce ne sera pas à lui de gérer leur affichage. En effet, ce n'est pas lui qui va prendre les pixels de l'image pour les mettre dans sa sortie, ça c'est le rôle du Fragment Shader. ;) La seule chose que l'on va modifier, c'est l'envoi de données au Fragment Shader. On n'enverra plus de couleurs mais des coordonnées de texture.

Dans notre code, on commence par reprendre celui que l'on utilisait précédemment auquel on va supprimer la variable d'entrée in_Color, qui est devenue inutile ici, ainsi que la variable de sortie color :

// Version du GLSL

#version 150 core


// Entrée

in vec3 in_Vertex;


// Uniform

uniform mat4 projection;
uniform mat4 modelview;


// Fonction main

void main()
{
    // Position finale du vertex en 3D

    gl_Position = projection * modelview * vec4(in_Vertex, 1.0);
}

Ensuite, on rajoute une nouvelle variable d'entrée qui va nous permettre de récupérer les coordonnées de texture. Nous l'avions appelée in_TexCoord0 lorsque nous avons utilisé la fonction glBindAttribLocation() pour faire le lien avec le tableau Vertex Attrib 2. Nous n'avons donc qu'à déclarer cette variable accompagnée du type vec2 (car il n'y a que 2 coordonnées) ainsi que du mot-clef in :

// Entrées

in vec3 in_Vertex;
in vec2 in_TexCoord0;

On en profite également pour déclarer une variable de sortie qui copiera le contenu de in_TexCoord0 pour l'envoyer au Fragment Shader. Nous l'appellerons coordTexture et sera elle-aussi du type vec2. Elle utilisera cependant le mot-clef out vu qu'elle sort du shader :

// Sortie

out vec2 coordTexture;

Enfin, pour terminer notre code source, nous devons simplement copier le contenu de la variable d'entrée dans celle de sortie. Le Fragment Shader se chargera du reste.

void main()
{
    // Position finale du vertex en 3D

    gl_Position = projection * modelview * vec4(in_Vertex, 1.0);


    // Envoi des coordonnées de texture au Fragment Shader

    coordTexture = in_TexCoord0;
}

Ce qui donne le code source final :

// Version du GLSL

#version 150 core


// Entrées

in vec3 in_Vertex;
in vec2 in_TexCoord0;


// Uniform

uniform mat4 projection;
uniform mat4 modelview;


// Sortie

out vec2 coordTexture;


// Fonction main

void main()
{
    // Position finale du vertex en 3D

    gl_Position = projection * modelview * vec4(in_Vertex, 1.0);


    // Envoi des coordonnées de texture au Fragment Shader

    coordTexture = in_TexCoord0;
}
Le Fragment Shader

Le Fragment Shader va subir un peu plus de modifications que son prédécesseur. Le premier sera évidemment la disparition de la variable d'entrée color qui n'est maintenant plus utilisée je vous le rappelle. ;)

A la place, nous mettrons la variable coordTexture que le Vertex Shader nous a gentiment envoyée à l'instant :

// Version du GLSL

#version 150 core


// Entrée

in vec2 coordTexture;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(color, 1.0);
}

Pour la suite, vous vous demandez peut-être comment intégrer une texture au sein d'un code source GLSL ? Vu que nous ne savons pas encore le faire, c'est une question qui peut paraître légitime.

Au risque de vous étonner, les textures sont en fait des variables uniform !

Euh des uniform ? Mais on a jamais utilisé la fonction glUniform*() pour en envoyer ?

Alors oui certes, nous ne l'avons jamais fait. Mais nous aurions pu !

L'appel à la fonction glUniform*() est facultatif si on n'envoie qu'une seule texture par shader. En revanche, si on en envoie plusieurs, il faut le faire pour pouvoir les différencier dans le code source GLSL. Nous aurons l'occasion de voir cela dans la partie sur les effets avancés. ;)

En attendant, cet appel reste facultatif. Cependant, nous devons quand même déclarer les textures en tant que variables uniform dans les shaders. D'ailleurs, elles possèdent un type particulier qui s'appelle sampler2D :

// Uniform

uniform sampler2D texture;

Vous pouvez donner n'importe quel nom à votre texture du moment que vous n'en envoyez qu'une. Le shader saura automatiquement laquelle utiliser.

L'objectif de cette variable en tout cas va être de récupérer la couleur du pixel recherché pour l'affecter à la variable de sortie out_Color. Pour le récupérer, nous allons utiliser notre première fonction prédéfinie dans le GLSL. Celle-ci s'appelle texture() :

vec4 texture(sampler2D texture, vec2 textCoord);
  • texture : la texture contenant les pixels. Nous lui donnerons la variable uniform texture

  • textCoord : le couple de coordonnées permettant de retrouver un pixel. Nous lui donnerons notre variable d'entrée coordTexture

Cette fonction renvoie un vec4 contentant la couleur du pixel que l'on recherche (composante Alpha comprise).

Nous devons donc faire appel à cette fonction pour trouver notre couleur finale. Le résultat sera affecté à la variable out_Color :

void main()
{
    // Couleur du pixel

    out_Color = texture(texture, coordTexture);
}

Ce qui donne le code source final :

// Version du GLSL

#version 150 core


// Entrée

in vec2 coordTexture;


// Uniform

uniform sampler2D texture;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur du pixel

    out_Color = texture(texture, coordTexture);
}

Si vous compilez votre code, vous devriez avoir une belle caisse (sans jeu de mot :p ) affichée sur votre écran.

Image utilisateur

Ce qu'il faut retenir

Point récapitulatif

Nous allons faire un dernier point récapitulatif sur ce que nous avons vu dans ces deux dernières parties. Il n'y aura pas eu beaucoup de code nouveau mais il vaut mieux synthétiser toute ça pour éviter d’éventuelles zones d'ombre.

Ainsi, pour intégrer la troisième dimension, nous devons :

  • Envoyer les matrices projection et modelview au Vertex Shader grâce aux variables uniform

  • Les multiplier toutes les deux par le vertex en cours dans l'ordre : projection * modelview * vertex. Le résultat final doit être contenu dans la variable prédéfinie gl_Position

Pour gérer l'affichage de texture, nous devons :

  • Passer les coordonnées du tableau Vertex Attrib 2 depuis le Vertex Shader jusqu'au Fragment Shader (avec les mots-clés out et in)

  • Déclarer la texture en tant que variable uniformsampler2D dans le Fragment Shader

  • Appeler la fonction texture() pour trouver la couleur du pixel cherché. On affecte le résultat à la variable de sortie out_Color

Quand faut-il "changer" de shader ?

Avant de terminer, nous allons faire un dernier point sur le changement de shader. Vous vous êtes déjà peut-être demandé quand est-ce que vous devez changer de shader pour afficher un modèle ? Faut-il que vous en reprogrammiez un ou pouvez-vous utiliser celui que vous avez déjà fait ?

Pour faire simple, vous devez changer de shader à chaque fois que :

  • Vous touchez aux tableaux Vertex Attribs

  • Vous voulez envoyer d'autres variables uniform

Nous avons vu dans ces deux derniers chapitres qu'à chaque fois que nous modifions nos tableaux Vertex Attrib (pour les vertices, les couleurs, etc.), nous devions programmer un autre shader. Nous avions créé de nouveaux fichiers pour chaque changement.

Évidemment, si vous envoyez une variable uniform vous devrez créer un autre shader pour le prendre en compte.

Si vous ne souhaitez pas utiliser d'effets avancés dans vos applications alors les exemples que nous avons eus l'occasion de voir seront ceux que vous utiliserez 90% du temps. Ce sont les shaders de base de la programmation GLSL. :)

Bonnes pratiques

Optimisation du Vertex Shader

Actuellement, nos shaders soufrent d'un énorme manque d'optimisation. En effet si vous avez remarqué, nous multiplions les matrices projection et modelview dans le Vertex Shader. Le problème de cette multiplication c'est qu'elle s'effectue autant de fois qu'il y a de vertex à traiter. Donc si avons par exemple 5000 vertices à gérer alors elle s'effectuera également 5000 fois !

En temps normal, je vous dirais que ça ne pose pas de problème, que la carte graphique est faite pour ça, etc. Mais là, nous pouvons quand même alléger les calculs en effectuant simplement la multiplication avant d'envoyer les matrices au shader. C'est une opération toute simple à faire car nous avons déjà surchargé l'opérateur *.

D'ailleurs, c'est de cette façon que ça se passait dans les anciennes versions d'OpenGL. ;)

Au niveau du code C++, il nous suffit juste de déclarer une nouvelle matrice que nous appellerons modelviewProjection (en référence à gl_ModelviewProjectionMatrix d'OpenGL 2.1) au moment de l'envoyer au shader qui contiendra le résultat de la multiplication de projection et modelview. Ensuite, nous n'aurons plus qu'à l'envoyer à la place d'envoyer les deux matrices séparément

// Verrouillage du VAO

glBindVertexArrays(m_vao);


    // Multiplication des matrices

    mat4 modelviewProjection = projection * modelview;


    // Envoi du résultat

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


    // Rendu

    ....

 
// Déverrouillage du VAO

glBindVertexArray(0);

Au niveau du Vertex Shader, nous devons supprimer les deux anciens uniform pour les remplacer par un nouveau du nom de modelviewProjection :

// Uniform

uniform mat4 modelviewProjection;

Il ne manque plus qu'à prendre en compte la nouvelle matrice dans le calcul de la position du vertex :

void main()
{
    // Position finale du vertex en 3D

    gl_Position = modelviewProjection * vec4(in_Vertex, 1.0);


    // Envoi des coordonnées de texture

    coordTexture = in_TexCoord0;
}

Notre Vertex Shader ne perd maintenant plus de temps à effectuer une multiplication à chaque fois qu'il doit traiter un vertex. :)

Il n'y avait que deux lignes à changer au final. Voici à quoi ressemblerait le code permettant d'afficher une texture avec ces petits changements :

// Version du GLSL

#version 150 core


// Entrées

in vec3 in_Vertex;
in vec2 in_TexCoord0;


// Sortie

out vec2 coordTexture;


// Uniform

uniform mat4 modelviewProjection;


// Fonction main

void main()
{
    // Position finale du vertex en 3D

    gl_Position = modelviewProjection * vec4(in_Vertex, 1.0);


    // Envoi des coordonnées de texture

    coordTexture = in_TexCoord0;
}

La méthode envoyerMat4()

Nous terminons ce chapitre par une méthode simple qui va nous permettre d'économiser l'écriture d'une longue ligne de code répétitive. Je ne sais pas si ça vous fait le même effet mais personnellement, l'appel à la fonction glUniformMatrix4fv() m'est extrêmement pénible du fait de sa longueur :

// Envoi de la matrice modelviewProjection

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

Étant donné que nous sommes obligés de faire ceci à chaque fois que l'on veut afficher un modèle, il serait judicieux de coder une méthode qui nous permettrait d'enfermer cette fonction sans avoir à la réutiliser tout le temps. Je pense que vous serez d'accord sur le principe. :p

Nous allons donc créer une méthode, dans la classe Shader, qui nous permettra de faire cette économie. Nous l'appellerons envoyerMat4() car elle n'enverra que des matrices carrées d'ordre 4. Elle prendra en paramètre le nom de la matrice dans le code source GLSL ainsi qu'une référence sur un objet de type mat4 :

void envoyerMat4(std::string nom, glm::mat4 matrice);

Dans un premier temps, nous devons localiser la variable de destination grâce à la fonction glGetUniformLocation() :

void Shader::envoyerMat4(std::string nom, glm::mat4 matrice)
{
    // Localisation de la matrice 

    int localisation = glGetUniformLocation(m_programID, nom.c_str());
}

Ensuite, nous appelons la fameuse fonction glUniformMatrix4fv() pour envoyer les valeurs de la matrice :

void Shader::envoyerMat4(std::string nom, glm::mat4 matrice)
{
    // Localisation de la matrice 

    int localisation = glGetUniformLocation(m_programID, nom.c_str());


    // Envoi des valeurs

    glUniformMatrix4fv(localisation, 1, GL_FALSE, value_ptr(matrice));
}

Méthode terminée. :)

Elle va nous faire gagner un peu temps à chaque fois que nous devrons afficher un modèle. D'ailleurs, le code source deviendra un peu plus compréhensible également :

// Mutltiplication des matrices

mat4 modelviewProjection = projection * modelview;


// Envoi du résultat

m_shader.envoyerMat4("modelviewProjection", modelviewProjection);

Nous pouvons même inclure la multiplication directement dans l'appel à la méthode envoyerMat4(). Ce qui fait que l'envoi d'une matrice se résumera maintenant à une seule et unique ligne :

// Envoi d'une matrice au shader

m_shader.envoyerMat4("modelviewProjection", projection * modelview);

A partir de maintenant, on ne reverra plus les lignes d'envoi de matrice. :D

L'avantage de cette méthode en plus, c'est qu'elle fonctionne aussi pour les envois séparés de projection et de modelview. En effet, elle ne fait qu'appeler la fonction glUniformMatrix4fv(), ce qui la rend parfaitement compatible avec les autres matrices carrées d'ordre 4.

Télécharger (Windows, UNIX/Linux, Mac OS X) : Code Source C++ du chapitre sur les shaders (Partie 2/2)

Nous sommes enfin arrivés à la fin de cette série de chapitres consacrés aux shaders. Nous avons vu pas mal de notions relatives à leur programmation de la compilation à l'aide d'une classe dédiée jusqu'à l'envoi de variable depuis l'application principale.

J'ai préféré vous occulter toute cette partie au début du tuto car la plupart d'entre vous aurait arrêté sa lecture dès que je vous aurais parlé de ça. C'était beaucoup trop indigeste à mon gout pour être balancé ni vu ni connu en même temps que la découverte d'OpenGL. Un chapitre introductif sur les shaders me semblait plus approprié.

Néanmoins, toutes les zones d'ombre sont maintenant levées et vous êtes seuls maitres de vos applications. :D

En ce qui concerne le GLSL, nous avons vu les principales notions à connaitre pour faire une application, à savoir la gestion des couleurs, de la 3D et des textures. Si vous souhaitez réaliser une solution pour entreprise, vous n'avez besoin que de ça. En revanche, si vous souhaitez aller plus loin dans le développement, je serai heureux de vous retrouver dans la quatrième partie de ce tuto pour étudier les effets réalistes.

Mais avant cela, nous devons terminer la partie en cours avec encore un chapitre consacré aux Frame Buffer Objects. Nous en aurons besoin dans la quatrième partie alors ne faites pas l'impasse dessus. :p

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