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

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

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

Nous voici arrivés (enfin !) au chapitre qui va nous permettre de créer nos propres shaders. Nous avons appris la syntaxe du langage de programmation GLSL et nous savons même comment compiler des codes sources l'utilisant.

Aujourd'hui, nous allons lever le voile sur les dernières zones d'ombres qui subsistent dans nos programmes, notamment en étudiant tous les petits fichiers sources que l'on utilisés depuis le début. Nous en profiterons pour jouer un peu avec afin de créer nos premiers effets personnalisés. Ils seront un peu basiques certes, mais il faut bien commencer par quelque chose. ^^

J'ai préféré couper ce chapitre en deux car il était trop lourd à suivre en une seule fois. A la place, vous aurez le droit à deux chapitres plus petits dans lesquels vous vous sentirez moins étouffés.

Préparation

Préparation

Dans ce chapitre, nous allons nous concentrer sur les premiers shaders que nous avons utilisés au début du tuto. Exit donc les matrices et la caméra, nous verrons ça dans le prochain chapitre. Il vaut mieux ne pas s'en occuper pour le moment car si je vous montre tout d'un coup, vous risquez de vous emmêler les pinceaux. :p Il vaut mieux y aller en douceur. Mais ne vous inquiétez pas, nous en verrons assez pour faire nos premiers essais dans la programmation GLSL.

Avant de commencer, il va nous falloir nettoyer un peu la boucle principale en enlevant la gestion des matrices et de la caméra. Cependant, nous allons devoir conserver l'utilisation du VBO et du VAO car ceux-ci sont obligatoirement demandés par la version 3.3 d'OpenGL.

Nous testerons nos premières sources sur un petit carré à 2 dimensions pour le moment vu que les matrices ne sont pas encore utilisables. Nous finirons sur des cubes et des pyramides dans le chapitre suivant.

Bref pour commencer, je vous demande de vider votre méthode bouclePrincipale() de tout son contenu précédent. Ajoutez-y ensuite les vertices de notre petit carré :

void SceneOpenGL::bouclePrincipale()
{
    // Vertices 

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

Ce carré ne sera pas minuscule ne vous inquiétez pas. Vu que nous n'utilisons pas la caméra il faut revenir à l'ancien repère où les coordonnées sont comprises dans l'intervalle [-1; 1]. Si vous faites un carré dépassant cette taille alors il prendra toute votre fenêtre.

Enfin, maintenant qu'on déclaré nos vertices, nous pouvons passer à l'implémentation du VBO. Il n'y a qu'un seul tableau à envoyer pour le moment donc le calcul de sa taille sera simple. L'espace mémoire à allouer fait donc 3 vertices x 2 coordonnées x 2 triangles = 12 cases :

// VBO et taille des données

GLuint vbo;
int tailleVerticesBytes = 12 * sizeof(float);

Une fois la taille définie, on peut allouer VBO :

// Génération du VBO

glGenBuffers(1, &vbo);


// Verrouillage

glBindBuffer(GL_ARRAY_BUFFER, vbo);


    // Allocation

    glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes, 0, GL_STATIC_DRAW);


// Déverrouillage

glBindBuffer(GL_ARRAY_BUFFER, 0);

Puis, on peut le remplir avec les vertices :

// Allocation

glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes, 0, GL_STATIC_DRAW);


// Remplissage

glBufferSubData(GL_ARRAY_BUFFER, 0, tailleVerticesBytes, vertices);

Nous avons vu ce code dans le chapitre sur les VBO, il ne devrait pas vous poser de problème. ;)

Ce qui nous donne pour le moment :

void SceneOpenGL::bouclePrincipale()
{
    // Vertices 

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



    /* ***** Gestion du VBO ***** */


    GLuint vbo;
    int tailleVerticesBytes = 12 * sizeof(float);


    // Génération du VBO

    glGenBuffers(1, &vbo);


    // Verrouillage

    glBindBuffer(GL_ARRAY_BUFFER, vbo);


        // Remplissage

        glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes, 0, GL_STATIC_DRAW);
        glBufferSubData(GL_ARRAY_BUFFER, 0, tailleVerticesBytes, vertices);


    // Déverrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

On passe maintenant au VAO. On commence évidemment par lui générer un ID :

// VAO

GLuint vao;


// Génération du VAO

glGenVertexArrays(1, &vao);

Nous devons mettre à l'intérieur tous nos appels aux tableaux Vertex Attrib (activation comprise). Pour le moment, nous n'avons que celui des vertices à appeler donc c'est assez simple.

D'ailleurs en parlant de ça, faites attention aux paramètres de ce tableau car nos vertices possèdent 2 coordonnées et non 3. Affectez donc la valeur 2 au paramètre size :

// Verrouillage du VAO

glBindVertexArray(vao);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, vbo);


        // Vertex Attrib 0 (Vertices)

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


    // Déverrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, 0);


// Déverrouillage du VAO

glBindVertexArray(0);

Si on résume tout ça :

void SceneOpenGL::bouclePrincipale()
{
    // Vertices 

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



    /* ***** Gestion du VBO ***** */


    GLuint vbo;
    int tailleVerticesBytes = 12 * sizeof(float);


    // Génération du VBO

    glGenBuffers(1, &vbo);


    // Verrouillage

    glBindBuffer(GL_ARRAY_BUFFER, vbo);


        // Remplissage

        glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes, 0, GL_STATIC_DRAW);
        glBufferSubData(GL_ARRAY_BUFFER, 0, tailleVerticesBytes, vertices);


    // Déverrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, 0);

 

    /* ***** Gestion du VAO ***** */


    GLuint vao;
    glGenVertexArrays(1, &vao);


    // Verrouillage du VAO

    glBindVertexArray(vao);


        // Verrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, vbo);


            // Vertex Attrib 0 (Vertices)

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


        // Déverrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, 0);


    // Déverrouillage du VAO

    glBindVertexArray(0);
}

Pfiouuu, tout ce code pour envoyer deux simples triangles ! :lol:

Et encore ce n'est pas fini, il nous manque toujours la boucle while. On commence à bien la connaitre celle-la :

// Variables relatives au framerate

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


// Boucle principale

while(!m_input.terminer())
{
    // Gestion des évènements, etc.

    .... 
}

On inclut évidemment le code de gestion des évèndments :

// Boucle principale

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

    debutBoucle = SDL_GetTicks();


    // Gestion des évènements

    m_input.updateEvenements();

    if(m_input.getTouche(SDL_SCANCODE_ESCAPE))
       break; 


    // Rendu 

    ....



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

Et enfin, on rajoute le code permettant de nettoyer et d'actualiser ce qui est affiché à l'écran :

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


    // Nettoyage de l'écran

    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);




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

Il ne reste plus qu'à afficher notre rendu. Celui-ci va être très simple car il suffit de verrouiller le VAO, puis d'appeler la fonction glDrawArrays() :

// Verrouillage du VAO

glBindVertexArray(vao);


// Rendu

glDrawArrays(GL_TRIANGLES, 0, 6);


// Déverrouillage du VAO

glBindVertexArray(0);

Ah dernier point, il faut penser à détruire le VBO et le VAO une fois la boucle terminée. ;)

// Destruction du VAO et du VBO

glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);

Récap final :

void SceneOpenGL::bouclePrincipale()
{
    // Vertices

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



    /* ***** Gestion du VBO ***** */


    GLuint vbo;
    int tailleVerticesBytes = 12 * sizeof(float);


    // Génération du VBO

    glGenBuffers(1, &vbo);


    // Verrouillage

    glBindBuffer(GL_ARRAY_BUFFER, vbo);


        // Remplissage

        glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes, 0, GL_STATIC_DRAW);
        glBufferSubData(GL_ARRAY_BUFFER, 0, tailleVerticesBytes, vertices);


    // Déverrouillage

    glBindBuffer(GL_ARRAY_BUFFER, 0);



    /* ***** Gestion du VAO ***** */


    GLuint vao;


    // Génération du VAO

    glGenVertexArrays(1, &vao);


    // Verrouillage du VAO

    glBindVertexArray(vao);


        // Verrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, vbo);


            // Vertex Attrib 0 (Vertices)

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


        // Déverrouillage du VBO

        glBindBuffer(GL_ARRAY_BUFFER, 0);


    // Déverrouillage du VAO

    glBindVertexArray(0);


    // Variables relatives au framerate

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


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


        // Nettoyage de l'écran

        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);


        // Verrouillage du VAO

        glBindVertexArray(vao);


        // Rendu

        glDrawArrays(GL_TRIANGLES, 0, 6);


        // Déverrouillage du VAO

        glBindVertexArray(0);


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


    // Destruction du VAO et du VBO

    glDeleteBuffers(1, &vbo);
    glDeleteVertexArrays(1, &vao);
}

Nous avons maintenant un code propre sur lequel nous pouvons nous exercer. Nous augmenterons son contenu au fur et à mesure jusqu'à réintégrer les matrices, la caméra et les textures.

J'ai préféré faire une partie dédiée pour cela, en vous expliquant chaque étape, plutôt que de vous jeter le code d'un coup. Vous connaissez absolument tout ce qui se trouve à l'intérieur, il n'y a rien de nouveau (pour le moment). ^^

Premier shader

Affichage simple

Chargement

Il est enfin temps de programmer notre premier shader, celui-ci sera très basique car il ne se contentera que d'afficher le carré à l'écran. Dans un premier temps nous lui donnerons la couleur blanche, nous gèrerons les autres couleurs petit à petit.

Pour commencer, je vais vous demander de vider complétement le dossier Shaders de votre projet afin d'enlever tous les précédents codes sources. Une fois fait, créez à la main deux nouveaux fichiers qui porteront le nom de basique2D.vert et basique2D.frag. Ensuite, déclarez un objet de type Shader dans la méthode bouclePrincipale() avec en paramètres le chemin vers ces fichiers :

void SceneOpenGL::bouclePrincipale()
{
    // Shader

    Shader shader("Shaders/basique2D.vert", "Shaders/basique2D.frag");
    shader.charger();


    // Vertices 

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


    ....
}

Activez-le ensuite au moment du rendu de façon à ce qu'il soit prit en compte par OpenGL :

// Activation du shader

glUseProgram(shader.getProgramID());


    // Rendu

    glBindVertexArray(vao);

        glDrawArrays(GL_TRIANGLES, 0, 6);

    glBindVertexArray(0);


// Désactivation du shader

glUseProgram(0);
Le Vertex Shader

Nos outils sont en place et les fichiers sont crées ... Nous pouvons maintenant passer à la programmation du code source.

Nous commencerons par celui du Vertex Shader car c'est celui-ci qu'OpenGL appellera en premier dans le pipeline 3D. Son rôle consiste à prendre un vertex (avec la couleur et les coordonnées de texture qui lui sont associées) pour travailler dessus.

Son code source commencera par l'utilisation de la balise #version, pour indiquer la version du GLSL utilisée, suivie par la fonction main() :

// Version du GLSL

#version 150 core


// Fonction main

void main()
{

}

Pour la suite, nous aurons besoin d'une notion que l'on a déjà vue précédemment : les entrées shader. Nous avons évoqué ce terme il y a à peine deux chapitres quand nous parlions du linkage du shader. Nous utilisions alors la fonction glBindAttribLocation() qui permettait de créer une passerelle entre un tableau Vertex Attrib et sa variable présente dans le GLSL.

Pour les vertices par exemple, le nom de cette variable était in_Vertex :

Image utilisateur

Nous avons utilisé la fonction glBindAttribLocation() pour verrouiller cette entrée, donc la variable in_Vertex est accessible dans notre code source. D'ailleurs, les autres variables comme in_Color sont elles aussi accessibles, nous verrons cela un peu plus loin. ;)

Pour utiliser in_Vertex, il faut déclarer une variable globale portant son nom. Je dis bien globale parce qu'elle doit être déclarée en dehors de la fonction main(). Elle doit être représentée par un type vec dont le nombre de coordonnées dépend de celui renseigné dans le tableau Vertex Attrib. Dans notre cas, nos vertices possèdent 2 coordonnées, donc in_Vertex sera de type vec2 :

// Version du GLSL

#version 150 core


// Entrée Shader

vec2 in_Vertex;


// Fonction main

void main()
{

}

Cette déclaration n'est pas encore tout à fait complète. En effet, si nous la laissons ainsi, OpenGL croira qu'il s'agit d'une variable normale sans aucun rapport avec les tableaux Vertex Attrib. Pour corriger ça, il faut lui ajouter le mot-clef in juste avant son type :

// Version du GLSL

#version 150 core


// Entrée Shader

in vec2 in_Vertex;


// Fonction main

void main()
{

}

Grâce à ce mot-clef, OpenGL saura que cette variable est reliée à un tableau VertexAttrib.

Enfin, maintenant que l'on a récupéré le vertex en cours, il faut l'utiliser pour qu'il puisse servir à quelque chose. :p Pour cela, nous allons affecter son contenu à une variable prédéfinie dans le GLSL (comme les fonctions mathématiques) qui se nomme gl_Position :

vec4 gl_Position;

Cette variable est prédéfinie dans le GLSL, nous n'avons donc pas besoin de la déclarer en in. Elle permet, entre autres, de définir la position finale du vertex à l'écran, elle demande pour ça le contenu de la variable in_Vertex.

La seule difficulté que l'on peut rencontrer lors de cette affectation concerne le conflit de type entre les deux variables. En effet, in_Vertex est de type vec2 alors que gl_Position est de type vec4. Cependant, vous devriez savoir comment surmonter ce problème non ? ;)

Pour combler ce manque de coordonnées, il suffit simplement d'utiliser un constructeur ! Et plus précisément, le constructeur vec4(). Nous lui donnerons la variable in_Vertex ainsi que deux autres coordonnées égales à 0.0 pour Z, car le carré n'est qu'en 2D pour le moment, et 1.0 pour W.

La coordonnée W d'un vertex est un peu spéciale. Si vous mettez d'autres valeurs comme 2.0 par exemple, vous remarquerez que la taille de votre rendu sera divisée par deux. Pour conserver une taille normale, il faut laisser la valeur 1.0 à W. C'est ce que nous ferons pour tous nos shaders.

// Position finale du vertex

gl_Position = vec4(in_Vertex, 0.0, 1.0);

Ce qui donne :

// Version du GLSL

#version 150 core


// Entrée Shader

in vec2 in_Vertex;


// Fonction main

void main()
{
    // Position finale du vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);
}

Je vous avais dit que les constructeurs étaient utiles. :p

Et voilà ! Notre premier Vertex Shader est terminé ! Certes, il ne fait pas grand chose mais au moins il fait exactement ce qu'on veut : il définit la position d'un vertex à l'écran. Sachez que ce bout de code sera présent dans TOUS vos futurs shaders, quelque soit leur complexité.

Les Entrées-Sorties

Avant d'aller plus loin dans le développement des codes sources, j'aimerais que l'on voit ensemble le fonctionnement des entrées-sorties au niveau des shaders. Vous connaissez déjà le mot-clé in qui permet de définir des données entrantes. Sachez qu'il existe aussi son opposé, le mot-clef out, qui permet de définir des données sortantes.

Ces termes sont assez subtiles car ils ne signifient pas la même chose en fonction du shader où ils sont utilisés.

Pour le Vertex Shader, ils signifient :

  • in : Variable représentant les données des tableaux Vertex Attrib (vertices, couleurs et tutti quanti)

  • out : Variable qui sera envoyée au shader suivant, soit le Fragment Shader dans notre cas

Pour le Fragment Shader, ces termes signifient :

  • in : Variable représentant les données reçues depuis le shader précédent, soit celles étant déclarées avec le mot-clef in dans le Vertex Shader

  • out : Variable représentant la couleur finale d'un pixel

Vous voyez qu'il y a une différence selon le shader qu'on utilise, faites bien la différence.

La variable gl_Position est un peu spéciale et ne fait pas partie de ce système d'entrées-sorties, elle est spécifique au Vertex Shader. Il y en a quelques unes comme ça mais c'est la la seule vraiment importante.

Le Fragment Shader

On passe maintenant au Fragment Shader. Celui-ci permet de définir la couleur de chaque pixel d'une surface affichée. Si vous avez utilisé une résolution de 800x600 pour votre fenêtre alors votre carte graphique devra gérer 120 000 pixels pour votre carré ! Non non vous ne rêvez pas, votre Fragment Shader devra gérer 120 000 pixels différent et cela 60 fois par seconde. :p

Mais ne vous inquiétez pas, elle est justement faite pour ça. Pour vous dire, 120 000 c'est un nombre extrêmement petit, elle en gère beaucoup plus dans des applications développées.

Enfin, le code source du Fragment Shader commence par la même chose que précédemment : la balise #version et la fonction main().

// Version du GLSL

#version 150 core


// Fonction main

void main()
{

}

Contrairement au Vertex Shader, nous n'aurons pas (pour le moment) de variable d'entrée ici, nous n'aurons donc pas besoin d'utiliser le mot-clef in. En revanche, nous devrons gérer une variable de sortie, donc nous devrons utiliser le mot-clef out.

Comme nous l'avons vu juste avant, la sortie d'un Fragment Shader correspond à la couleur finale du pixel. Il s'agit d'une variable vec4 tout comme gl_Position sauf que cette fois-ci elle n'est pas prédéfinie, nous devons la déclarer nous-même. Nous l'appellerons out_Color :

// Version du GLSL

#version 150 core


// Sortie Shader

out vec4 out_Color;


// Fonction main

void main()
{

}

Cette variable demande 4 valeurs représentant les composantes RGBA d'une couleur. Chaque valeur doit être comprise entre 0 et 1, comme lorsque nous utilisions le tableau Vertex Attrib au début du tuto.

Pour affecter ces composantes à la variable out_Color, nous aurons besoin une fois de plus du constructeur vec4(). Pour le moment, nous leur affecterons 4 fois la valeur 1.0 de façon à avoir la couleur blanche :

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(1.0, 1.0, 1.0, 1.0);
}

Ce qui donne :

// Version du GLSL

#version 150 core


// Sortie Shader

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(1.0, 1.0, 1.0, 1.0);
}

Et là, vous pouvez enfin compiler pour voir ce que ça donne :

Image utilisateur

Félicitation, vous venez de programmer votre premier shader ! Champagne ! :D

Ce qu'il faut absolument retenir

Avant de passer à la suite, j'aimerais faire un petit point sur ce que vous devez absolument retenir de cette partie.

En ce qui concerne le Vertex Shader, vous devez savoir que :

  • Les variables in représentent les tableaux Vertex Attrib

  • Les variables out sont envoyées au shader suivant, soit le Fragment Shader dans notre cas

  • La variable gl_Position doit être remplie avec la variable in_Vertex. Elle ne doit pas être déclarée en out

En ce qui concerne le Fragment Shader, vous devez savoir que :

  • Les variables in représentent les variables out du shader précédent, soit le Vertex Shader dans notre cas

  • La sortie du Fragment Shader représente la couleur finale du pixel

  • Elle est représentée par une variable out

Il faut absolument que vous connaissez ces points par cœur. Ce sont vraiment les bases de la programmation GLSL. ;)

Utilisation de couleur

Changer de couleur

Le blanc n'est évidemment pas la seule couleur que l'on peut utiliser. Pour en prendre une autre, il suffit simplement de changer les valeurs données à la variable out_Color.

Par exemple, pour afficher le carré avec la couleur bleue, nous pouvons affecter les valeurs RGB : 0.0, 0.0, 1.0. Pour la valeur Alpha, il vaut mieux la laisser à 1.0 pour le moment, quelque soit la couleur que l'on veut utiliser :

// Version du GLSL

#version 150 core


// Sortie Shader

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(0.0, 0.0, 1.0, 1.0);
}

Si vous relancez le projet (sans le compiler ;) ), vous devriez avoir ceci :

Image utilisateur
Exercices

Allez, je vais vous donner vos premiers exercices utilisant la programmation en GLSL. Ils sont très simples. ^^

Exercice 1 : Coloriez le carré en rouge.

Exercice 2 : Coloriez le carré en violet/rose (avec les composantes rouge et bleu).

Exercice 3 : Coloriez le carré en jaune.

Exercice 4 : Coloriez le carré en gris (n'importe quelle nuance).

Solutions

Exercice 1 :

Il n'y a que le Fragment Shader à modifier (pour les 4 exercices), l'autre ne change absolument pas. ;)

// Version du GLSL

#version 150 core


// Sortie Shader

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(1.0, 0.0, 0.0, 1.0);
}

Exercice 2 :

 

// Version du GLSL

#version 150 core


// Sortie Shader

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(1.0, 0.0, 1.0, 1.0);
}

Exercice 3 :

 

// Version du GLSL

#version 150 core


// Sortie Shader

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(1.0, 1.0, 0.0, 1.0);
}

Exercice 4 :

 

// Version du GLSL

#version 150 core


// Sortie Shader

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(0.5, 0.5, 0.5, 1.0);
}

Gestion de la couleur

Préparation

Changement de shader

Avant toute chose, vu que nous ajoutons une nouvelle fonctionnalité à notre rendu il nous faut donc créer un nouveau shader. Créons donc deux nouveaux fichiers appelés couleur2D.vert et couleur2D.frag dans le dossier Shaders.

Il nous faut ensuite ajouter leur chemin dans notre objet shader :

void SceneOpenGL::bouclePrincipale()
{
    // Shader

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

    ....
}
Déclaration du tableau de couleurs

Il y a bien longtemps, dans une galaxy loin..., nous utilisions des tableaux de couleurs pour afficher nos modèles. Nous ne connaissions pas encore les textures, c'était alors le seul moyen d'afficher quelque chose de non-blanc à l'écran.

Pour fonctionner correctement, il fallait affecter une couleur pour chaque vertex et chacune d'elles possédait 3 composantes (Rouge Vert Bleu):

// Exemple de couleur

float rouge = {1.0, 0.0, 0.0);

Dans notre code test actuel nous avons 6 vertices, nous aurons donc besoin d'un tableau de 6 couleurs x 3 composantes soit 18 cases. Nous utiliserons la couleur bleu pour le premier triangle et le rouge pour le second :

void SceneOpenGL::bouclePrincipale()
{
    // Shader

    ....



    // Vertices 

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


    // Couleurs

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


    ....
}

Avec ce tableau, chacun de nos vertices aura sa propre couleur.

Gestion du VBO et du VAO

Bien évidemment, déclarer un tableau comme ça ne sert pas à grand chose, il faut l'intégrer au VBO et au VAO.

Pour ce qui est du premier, il va falloir le redimensionner car il ne peut accueillir que les vertices pour le moment. On commence donc pas créer une variable tailleCouleursBytes qui contiendra la taille du tableau de couleurs en bytes :

// VBO et taille des données

GLuint vbo;

int tailleVerticesBytes = 12 * sizeof(float);
int tailleCouleursBytes = 18 * sizeof(float);

On redimensionne ensuite le VBO en additionnant les deux tailles de données :

// Nouvelle taille du VBO

glBufferData(GL_ARRAY_BUFFER, tailleVerticesBytes + tailleCouleursBytes, 0, GL_STATIC_DRAW);

Et enfin, on le remplie avec le tableau de couleurs :

// Envoi des données

glBufferSubData(GL_ARRAY_BUFFER, 0, tailleVerticesBytes, vertices);
glBufferSubData(GL_ARRAY_BUFFER, tailleVerticesBytes, tailleCouleursBytes, couleurs);

Au niveau du VAO, nous devons juste appeler puis activer le tableau Vertex Attrib 1 qui correspond à l'envoi des couleurs :

// Verrouillage du VAO

glBindVertexArray(vao);


    // Verrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, vbo);


        // Vertex Attrib 0 (Vertices)

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


        // Vertex Attrib 1 (Couleurs)

        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(tailleVerticesBytes));
        glEnableVertexAttribArray(1);


    // Déverrouillage du VBO

    glBindBuffer(GL_ARRAY_BUFFER, 0);


// Déverrouillage du VAO

glBindVertexArray(0);

Gestions des shaders

Le Vertex Shader

Contrairement à ce qu'on pourrait penser au premier abord, les couleurs ne sont absolument pas envoyées au Fragment Shader mais bel et bien au Vertex Shader. C'est lui qui récupère toutes les données issues des tableaux Vertex Attrib. Si nous voulons travailler sur les pixels avec une couleur envoyée, alors il faudra la transférer manuellement au Fragment Shader.

Cependant, avant de faire cela il va falloir la récupérer dans notre source, tout comme nous l'avons fait avec le vertex (in_Vertex). La variable qui permet d'accéder aux couleurs s'appelle in_Color, nous l'avions appelée ainsi lors du linkage du shader. Vu qu'elle possède 3 composantes RGB alors elle sera de type vec3. Si nous avions spécifié la valeur Alpha dans le tableau de couleurs, elle aurait été de type vec4.

// Entrée Shader

in vec3 in_Color;

Ce qui donne le code source suivant :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec3 in_Color;


// Fonction main

void main()
{
    // Position finale du vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);
}

Maintenant que l'on a récupéré la couleur du vertex, nous allons pouvoir l'envoyer au Fragment Shader. Pour cela, nous allons utiliser ..... une variable out !

Et oui, rappelez-vous que les variables out du Vertex Shader sont automatiquement envoyées au shader suivant. La seule condition pour pouvoir les utiliser est qu'elles doivent être strictement identiques dans les deux codes sources. C'est-à-dire qu'elles doivent avoir le même type et surtout le même nom. Si vous ne respectez pas l'une de ces deux conditions, alors OpenGL ne fera pas le lien entre les deux. ;)

Pour envoyer notre couleur donc, nous devons utiliser une variable out qui sera du type vec3 de façon à pouvoir envoyer toutes les composantes. Nous l'appellerons simplement color :

// Sortie

out vec3 color;

Ce qui donne le code suivant :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec3 in_Color;


// Sortie

out vec3 color;


// Fonction main

void main()
{
    // Position finale du vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);
}

Pourquoi on ne l'appelle pas out_Color ? Vu qu'elle sort du shader on peut la nommer ainsi ?

Vous pouvez certes, mais il y aura un petit problème de logique. En effet, je vous ai dit qu'elle doit avoir le même nom dans les deux codes sources, ce qui voudrait dire que la variable d'entrée in du Fragment Shader aurait un nom qui commencerait pas out_ ? Ce n'est pas logique évidemment. ^^

Enfin bref, maintenant que nous avons un moyen de communiquer avec le Fragment Shader, nous allons pouvoir lui envoyer le contenu de la variable in_Color. Nous n'avons pas besoin de constructeur vu qu'elles sont de même type :

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

Ce qui donne le code source final :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec3 in_Color;


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

Et voilà, Vertex Shader terminé. :D

Le Fragment Shader

La Fragment Shader va être très simple à programmer car il ne fera que récupérer une couleur pour la renvoyer ensuite dans une variable de sortie out.

La première chose à faire va être de re-déclarer la variable color qui sort du Vertex Shader précédent. Et comme vous le savez maintenant, elle doit être exactement identique au code source précédent. On conserve donc le même type et le même nom, la seule différence va être le mot-clef utilisé car elle ne sort pas mais elle rentre dans le Fragment Shader. On utilise donc non pas le mot-clef out mais in :

// Entrée

in vec3 color;

Ce qui donne :

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

Le nom et le type sont respectés, la variable color est donc utilisable. D'ailleurs, nous allons l'utiliser tout de suite en enlevant le code de la couleur blanc pour affecter son contenu à la variable out_Color.

Si vous faites attention, vous remarquerez qu'il y a un conflit de type entre les deux variables. En effet, l'une est de type vec3 alors que l'autre est de type vec4. Pour régler ce problème, il va falloir utiliser .... un constructeur, encore une fois ! ^^

Et pour mon plus grand plaisir, je vais vous demander de convertir la variable color vous-même. Ce n'est pas compliqué bien sûr, nous avons déjà fait cet opération plusieurs fois, vous devez juste convertir un vec3 et vec4 en rajoutant la composante Alpha (1.0).

.....

Vous avez trouvé ?

Voici la solution :

 

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(color, 1.0);
}

Il ne manque qu'une coordonnée au vec3 pour devenir un vec4. On appelle donc le bon constructeur en ajoutant la composante Alpha à la variable color. Le résultat sera une variable de type vec4.

Ce qui donne le code source final :

// 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 maintenant relancer votre projet, vous devriez avoir le résultat suivant :

Image utilisateur

Notre shader est maintenant capable de gérer les couleurs. :D Vous pouvez même changer le tableau des composantes pour vérifier que tout fonctionne correctement :

float couleurs[] = {0.5, 1.0, 0.0,   0.5, 1.0, 0.0,   0.5, 1.0, 0.0,    // Triangle 1
                    0.5, 0.0, 1.0,   0.5, 0.0, 1.0,   0.5, 0.0, 1.0};   // Triangle 2
Image utilisateur

Exercices

Énoncés

On continue notre petite vague d'exercices à l'instar de la partie précédente. Je vais monter un peu le niveau cette fois, il faudra réfléchir un peu plus. :p

Exercice 1 : Ajoutez la composante Alpha pour chaque couleur dans le tableau couleurs. Modifiez ensuite le shader pour gérer les couleurs entrantes à 4 composantes. (Pensez à modifier le VBO et le VAO pour prendre en compte la nouvelle composante.)

Exercice 2 : Inversez la couleur entrante dans le Fragment Shader avant de l'affecter à la couleur sortante. Pour vous donner un indice : inverser une couleur revient à inverser l'ordre des composantes en passant de l'ordre RGB à BGR. La composante Alpha reste cependant toujours à la fin. Vous pouvez reprendre le code de l’exercice précédent, ou utiliser celui du cours.

Exercice 3 : Oubliez complétement le tableau de couleurs et créez une variable maCouleur (à la place de Color) de type vec4 dans le Vertex Shader. Ses composantes doivent permettre d'afficher du bleu. L'objectif est d'envoyer cette variable au Fragment Shader à la place des données issues du tableau Vertex Attrib.

Exercice 4 : Reprenez le même principe que l'exercice précédent (voire correction si besoin) sauf que la couleur à envoyer doit dépendre de l’abscisse du vertex. Si la coordonnée x est supérieure à zéro alors vous devez envoyer la couleur bleu, si elle est inférieure ou égale à zéro alors vous devez envoyer la couleur rouge.

Solutions

Exercice 1 :

Pour ajouter la composante Alpha aux couleurs, il suffit de rajouter la valeur 1.0 au tableau couleurs[] :

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

Pensez à modifier la variable tailleCouleursBytes pour prendre en compte les nouvelles données soit 4 composantes x 3 coordonnées x 2 triangles = 24 valeurs :

// Gestion du VBO

GLuint vbo;
int tailleVerticesBytes = 12 * sizeof(float);
int tailleCouleursBytes = 24 * sizeof(float);

Pensez également à modifier le paramètre size du tableau Vertex Attrib 1 pour le passer à 4 (pour 4 composantes) :

// Vertex Attrib 1 (Couleurs)

glVertexAttribPointer(1, 4, GL_FLOAT, GL_FALSE, 0, BUFFER_OFFSET(tailleVerticesBytes));
glEnableVertexAttribArray(1);

Dans le Vertex Shader, la variable entrante in_Color n'est plus de type vec3 mais de type vec4 maintenant. La variable sortante color devient donc elle-aussi une vec4 :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec4 in_Color;


// Sortie

out vec4 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;
}

Dans le Fragment Shader, on commence évidemment par changer le type de la variable color en vec4. Puis on enlève le constructeur vu qu'elle possède maintenant le même type que la variable sortante out_Color. On peut donc l'affecter normalement :

// Version du GLSL

#version 150 core


// Entrée

in vec4 color;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = color;
}

Exercice 2 :

Pour inverser une couleur, il suffit juste d'inverser les 'sous-variables' (x, y, z) avant d'affecter la variable color à out_Color. On peut appeler le constructeur vec4() ou affecter les valeurs à la main, comme vous voulez. Mais il est préférable d'utiliser la première solution :

// Version du GLSL

#version 150 core


// Entrée

in vec4 color;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = vec4(color.z, color.y, color.x, color.w);
}

Il y a un moyen de raccourcir encore plus ce code et même d'utiliser d'autres noms pour les composantes, mais je ne vous en parle pas maintenant pour éviter de nous embrouiller l'esprit. ^^

Le Vertex Shader n'a pas besoin d'être modifié car il ne fait qu'envoyer la couleur, il ne fait pas de traitement dessus.

Exercice 3 :

On commence par supprimer la variable Color dans le Vertex Shader pour la remplacer par une nouvelle. On la nomme maCouleur et on lui donne le type vec4. On utilise également le mot-clef out puisqu'elle sort du shader :

// Sortie

out vec4 maCouleur;

Ensuite, on lui assigne les 4 composantes permettant d'afficher du bleu :

void main()
{
    // Position finale du Vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);


    // Envoi de la couleur au Fragment Shader

    maCouleur = vec4(0.0, 0.0, 1.0, 1.0);
}

Ce qui donne le code source :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec4 in_Color;


// Sortie

out vec4 maCouleur;


// Fonction main

void main()
{
    // Position du vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);


    // Envoi de la couleur au Fragment Shader

    maCouleur = vec4(0.0, 0.0, 1.0, 1.0);
}

Quant au Fragment Shader, on supprime simplement la variable inColor pour la remplacer par maCouleur afin de respecter les conditions des entrées-sorties :

// Entrée

in vec4 maCouleur;

Il ne reste plus qu'à assigner le contenu de la variable maCouleur à out_Color. On n'utilise pas de constructeur vu qu'elles sont de même type :

// Version du GLSL

#version 150 core


// Entrée

in vec4 maCouleur;


// Sortie 

out vec4 out_Color;


// Fonction main

void main()
{
    // Couleur finale du pixel

    out_Color = maCouleur;
}

Exercice 4 :

On reprend le même code que précédemment auquel on va rajouter une condition pour choisir une couleur dans le Vertex Shader. Si la coordonnée x de la variable in_Vertex est supérieure à zéro alors on assigne la couleur bleu à maCouleur. Dans le cas contraire, on assigne la couleur rouge :

// Version du GLSL

#version 150 core


// Entrées

in vec2 in_Vertex;
in vec4 in_Color;


// Sortie

out vec4 maCouleur;


// Fonction main

void main()
{
    // Position du vertex

    gl_Position = vec4(in_Vertex, 0.0, 1.0);


    // Si la coordonnée x est supérieure à 0

    if(in_Vertex.x > 0)
        maCouleur = vec4(0.0, 0.0, 1.0, 1.0);


    // Dans le cas inverse

    else
        maCouleur = vec4(1.0, 0.0, 0.0, 1.0);
}

Il n'y a rien à changer au niveau du Fragment Shader.

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

Nous avons vu pas mal de choses dans ce chapitre, nous avons même appris à programmer nos premiers effets ! :D

Vous savez maintenant comment gérer les vertices du coté des shaders pour afficher quelque chose à l'écran. Vous savez même gérer les tableaux de couleur pour colorier toutes les surfaces à votre guise. Ces effets sont assez simples et ne gèrent pas la 3D mais au moins, vous avez fait vos premiers pas dans le développement de shaders.

Si vous êtes prêts, je vous invite à lire le prochain chapitre qui va nous permettre d'apprendre à intégrer la troisième dimension et afficher des textures. ^^

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