Préparation pour GLSL

GLSL est le langage de shader d'OpenGL, l'équivalent du HLSL de DirectX. Il permet de manipuler le rendu au niveau des sommets ou des fragments (pixels), donnant donc un contrôle total sur la scène.

Commentez Donner une note à l'article (5)

Article lu   fois.

Les deux auteur et traducteur

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. L'article original

Cet article est une adaptation en langue française de 1. GLSL Setup, de Donald Urquhart.

II. Introduction

Pour démarrer cette série de tutoriels sur GLSL, on repartira du code d'un tutoriel précédent, une rotation lisse. Pour ceux qui n'ont aucune idée de ce qu'est GLSL, cette introduction donnera également une vue d'ensemble assez brève de ce que les shaders peuvent faire, notamment en comparaison de DirectX.

Pour ceux qui ne savent rien à propos de GLSL, un programme shader est composé de soit un vertex shader et un fragment shader, soit un vertex shader et un geometry shader et un fragment shader, cette dernière possibilité n'étant supportée que sur les cartes graphiques NVIDIA 8000 et plus récentes.

Un vertex shader permet de manipuler des sommets et donne accès à leur normale, le pipeline d'éclairage, les matériaux des objets et d'autres fonctions.

Un fragment shader permet de manipuler les pixels qui sont affichés à l'écran, ils sont utilisés pour créer des effets tels que l'éclairage, les textures, le brouillard, etc.

Un geometry shader (qui sera utilisé plus loin) permet de créer et de supprimer des sommets à l'envi. Cette liberté est très intéressante pour gérer un niveau de détail dynamique, pour créer des polygones plus flous, exploser des formes et d'autres effets.

Ce tutoriel est prévu pour OpenGL 2 et plus récents, GLSL ne faisant partie d'OpenGL qu'à partir de la version 2. Toutes les versions précédentes devront utiliser les extensions ARB.

III. Fichier shader.h

Ce premier tutoriel risque d'être assez long, au vu du nombre de choses à préparer. Rien n'est vraiment compliqué et le code final est assez court, mais, avec la variété de choses à faire, il ne peut être que long.

Il utilisera également une approche orientée objet pour les shaders GLSL, il se finira sur une classe réutilisable pour chaque programme shader.

Pour démarrer la déclaration de la classe, on crée un nouveau fichier d'en-tête C++, shader.h. Puisque l'on souhaite créer une nouvelle classe, on y insère quelque chose comme ceci :

 
Sélectionnez
#ifndef __SHADER_H
#define __SHADER_H

#if ( (defined(__MACH__)) && (defined(__APPLE__)) )   
#include <stdlib.h>
#include <OpenGL/gl.h>
#include <GLUT/glut.h>
#include <OpenGL/glext.h>
#else
#include <stdlib.h>
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glut.h>
#include <GL/glext.h>
#endif

#include <string>

class Shader {
public:
    Shader();
    ~Shader();

private:

};
#endif

Il s'agit là d'un squelette assez propre sur lequel tout viendra s'ajouter. Du code un peu plus long est mis dans les inclusions, avec une condition, de telle sorte que les utilisateurs d'OS X puissent l'utiliser.

Maintenant, on veut ajouter quelques méthodes publiques à la classe. On souhaite que tout utilisateur de cette classe puisse créer un shader en utilisant le constructeur ou une méthode d'initialisation. Les deux prendront en paramètres les noms de fichier du vertex shader et du fragment shader.

On ajoute donc les lignes suivantes :

 
Sélectionnez
Shader(const char *vsFile, const char *fsFile);
void init(const char *vsFile, const char *fsFile);

La prochaine étape est d'utiliser ces shaders. On aura besoin de trois méthodes : bind et undind pour l'activer ou le désactiver, ainsi que id pour récupérer le numéro associé au shader, afin de le manipuler par après :

 
Sélectionnez
void bind();
void unbind(); 
unsigned int id();

Tout ce qu'il reste à faire est de créer trois variables privées : une pour le programme shader, une pour le vertex shader et une pour le fragment shader.

 
Sélectionnez
unsigned int shader_id;
unsigned int shader_vp;
unsigned int shader_fp;

Tout mis ensemble, cela donne le contenu suivant, qui est assez simple mais fait le nécessaire.

 
Sélectionnez
#ifndef __SHADER_H
#define __SHADER_H

#if ( (defined(__MACH__)) && (defined(__APPLE__)) )   
#include <stdlib.h>
#include <OpenGL/gl.h>
#include <GLUT/glut.h>
#include <OpenGL/glext.h>
#else
#include <stdlib.h>
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glut.h>
#include <GL/glext.h>
#endif
 
#include <string>

class Shader {
public:
    Shader();
    Shader(const char *vsFile, const char *fsFile);
    ~Shader();
    
    void init(const char *vsFile, const char *fsFile);
    
    void bind();
    void unbind();
    
    unsigned int id();
    
private:
    unsigned int shader_id;
    unsigned int shader_vp;
    unsigned int shader_fp;
};

#endif

IV. Fichier shader.cpp

On passe alors au fichier shader.cpp. Il contiendra le code de toutes les méthodes esquissées plus haut, en plus d'une nouvelle.

Autant commencer depuis le début, avec les constructeurs :

 
Sélectionnez
Shader::Shader() {  
}

Shader::Shader(const char *vsFile, const char *fsFile) {
    init(vsFile, fsFile);
}

Après, la méthode d'initialisation. Les deux premières lignes demanderont à OpenGL de créer un vertex shader et un fragment shader et assigneront leurs identifiants aux variables privées correspondantes.

 
Sélectionnez
    shader_vp = glCreateShader(GL_VERTEX_SHADER);
    shader_fp = glCreateShader(GL_FRAGMENT_SHADER);

Les deux lignes d'après liront les fichiers des shaders. Il s'agit simplement de fichiers texte, peu importe leur extension. La méthode textFileRead() est une méthode qui retourne le contenu d'un fichier de shader comme un char*. Le code de ce tutoriel comprend une implémentation, mais elle ne sera pas commentée, étant donné qu'une connaissance du C++ est attendue.

 
Sélectionnez
    const char* vsText = textFileRead(vsFile);
    const char* fsText = textFileRead(fsFile);

Ensuite, on effectue une petite vérification, pour s'assurer que les deux fichiers ont bien été lus. Si une erreur est survenue, on passe juste la méthode, OpenGL continuera normalement, sans les shaders.

 
Sélectionnez
    if (vsText == NULL || fsText == NULL) {
        cerr << "Either vertex shader or fragment shader file not found." << endl;
        return;
    }

Les deux lignes d'après passent le contenu des fichiers de shader à OpenGL pour qu'il les attache à ses variables.

 
Sélectionnez
    glShaderSource(shader_vp, 1, &vsText, 0);
    glShaderSource(shader_fp, 1, &fsText, 0);

Les deux suivantes demandent à OpenGL de compiler les shaders.

 
Sélectionnez
    glCompileShader(shader_vp);
    glCompileShader(shader_fp);

Finalement, on crée le shader_id comme un programme shader. On attache alors le vertex shader et le fragment shader au programme shader et on lie le programme.

 
Sélectionnez
    shader_id = glCreateProgram();
    glAttachShader(shader_id, shader_fp);
    glAttachShader(shader_id, shader_vp);
    glLinkProgram(shader_id);

Maintenant que l'on peut lire un fichier de shader et le compiler, on regarde les autres méthodes pour l'utiliser.

La première, id(), retourne l'identifiant du programme, shader_id. Il est utilisé pour lier le shader ou pour lui passer des variables.

 
Sélectionnez
unsigned int Shader::id() {
    return shader_id;
}

La seconde, bind(), attache simplement le shader : tout ce qui est dessiné après utilisera ce shader, jusqu'à ce qu'il soit détaché ou qu'un autre shader soit activé.

 
Sélectionnez
void Shader::bind() {
    glUseProgram(shader_id);
}

La troisième est unbind(), qui lie le shader 0, un identifiant de shader réservé à OpenGL, ce qui désactive le shader courant.

 
Sélectionnez
void Shader::unbind() {
    glUseProgram(0);
}

Que reste-t-il, à présent ? On peut créer un shader, l'utiliser. Il faut encore nettoyer le shader une fois qu'il n'est plus nécessaire.

On le fait dans le destructeur du shader, qui ne devrait être appelé qu'une seule fois, à la fin de l'application ; sinon, il faudra le recréer, ce qui est assez coûteux.

Le destructeur n'utilise que cinq appels à OpenGL. Les deux premiers ne font que détacher les deux shaders, tandis que les trois suivants suppriment tous les shaders puis le programme shader.

 
Sélectionnez
    glDetachShader(shader_id, shader_fp);
    glDetachShader(shader_id, shader_vp);
    
    glDeleteShader(shader_fp);
    glDeleteShader(shader_vp);
    glDeleteProgram(shader_id);

C'est là tout ce qu'il faut pour créer un programme shader. Ce n'est pas très difficile ou délicat et tout est logique quand on y repense.

 
Sélectionnez
#include "shader.h"
#include <string.h>
#include <iostream>
#include <stdlib.h>


using namespace std;

static char* textFileRead(const char *fileName) {
    char* text;
    
    if (fileName != NULL) {
        FILE *file = fopen(fileName, "rt");
        
        if (file != NULL) {
            fseek(file, 0, SEEK_END);
            int count = ftell(file);
            rewind(file);
            
            if (count > 0) {
                text = (char*)malloc(sizeof(char) * (count + 1));
                count = fread(text, sizeof(char), count, file);
                text[count] = ‘\0';
            }
            fclose(file);
        }
    }
    return text;
}

Shader::Shader() {
     
}

Shader::Shader(const char *vsFile, const char *fsFile) {
    init(vsFile, fsFile);
}

void Shader::init(const char *vsFile, const char *fsFile) {
    shader_vp = glCreateShader(GL_VERTEX_SHADER);
    shader_fp = glCreateShader(GL_FRAGMENT_SHADER);
    
    const char* vsText = textFileRead(vsFile);
    const char* fsText = textFileRead(fsFile);    
    
    if (vsText == NULL || fsText == NULL) {
        cerr << "Either vertex shader or fragment shader file not found." << endl;
        return;
    }
    
    glShaderSource(shader_vp, 1, &vsText, 0);
    glShaderSource(shader_fp, 1, &fsText, 0);
    
    glCompileShader(shader_vp);
    glCompileShader(shader_fp);
    
    shader_id = glCreateProgram();
    glAttachShader(shader_id, shader_fp);
    glAttachShader(shader_id, shader_vp);
    glLinkProgram(shader_id);
}

Shader::~Shader() {
    glDetachShader(shader_id, shader_fp);
    glDetachShader(shader_id, shader_vp);
    
    glDeleteShader(shader_fp);
    glDeleteShader(shader_vp);
    glDeleteProgram(shader_id);
}

unsigned int Shader::id() {
    return shader_id;
}

void Shader::bind() {
    glUseProgram(shader_id);
}

void Shader::unbind() {
    glUseProgram(0);
}

V. Fichier main.cpp

On regarde à présent le fichier main.cpp du projet. Comme la plupart de son contenu provient d'un tutoriel précédent, on n'expliquera que les parties liées à l'utilisation de GLSL et à l'implémentation de la nouvelle classe.

La première chose à faire est d'inclure le fichier d'en-tête :

 
Sélectionnez
#include "shader.h"

Maintenant que l'on a accès à la classe de shader, on déclare un objet Shader. On l'appelle shader pour le moment, faute d'avoir un terme plus approprié dans ce tutoriel.

 
Sélectionnez
Shader shader;

Avant de faire quoi que ce soit avec les shaders, on doit appeler glewInit(). Ceci initialise GLEW, de telle sorte que l'on peut utiliser les extensions requises pour les shaders. Ce code ira sous l'appel glutCreateWindow() de la méthode principale.

 
Sélectionnez
glewInit();

Jusqu'ici, les choses sont simples. On veut maintenant initialiser le shader. Dans la méthode init(void), on ajoute la ligne suivante :

 
Sélectionnez
shader.init("shader.vert", "shader.frag");

Pour ce tutoriel, les programmes shader s'appelleront shader.vert et shader.frag et se situeront dans le même dossier que l'application compilée.

Tout ce qu'il reste encore à faire est d'utiliser le shader. Dans la méthode display(void), on veut appliquer le shader au cube, de telle sorte que, avant de dessiner le cube, on lie le shader et on le délie après coup.

 
Sélectionnez
shader.bind();
cube();
shader.unbind();
 
Sélectionnez
#if ( (defined(__MACH__)) && (defined(__APPLE__)) )   
#include <stdlib.h>
#include <OpenGL/gl.h>
#include <GLUT/glut.h>
#include <OpenGL/glext.h>
#else
#include <stdlib.h>
#include <GL/glew.h>
#include <GL/gl.h>
#include <GL/glut.h>
#include <GL/glext.h>
#endif


#include "shader.h"

Shader shader;

GLfloat angle = 0.0; // Angle de rotation

void init(void) {
    glEnable(GL_DEPTH_TEST);
    glDepthFunc(GL_LESS);
    
    shader.init("shader.vert", "shader.frag");
}

void cube (void) {
    glRotatef(angle, 1.0, 0.0, 0.0); // Rotation sur l'axe X
    glRotatef(angle, 0.0, 1.0, 0.0); // Rotation sur l'axe Y
    glRotatef(angle, 0.0, 0.0, 1.0); // Rotation sur l'axe Z
    glColor4f(1.0, 0.0, 0.0, 1.0);
    glutWireCube(2);
}

void display (void) {
    glClearColor (0.0, 0.0, 0.0, 1.0);
    glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    glLoadIdentity();  
    gluLookAt (0.0, 0.0, 5.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);
    
    shader.bind();
    cube();
    shader.unbind();
    
    glutSwapBuffers();
    angle += 0.1f;
}

void reshape (int w, int h) {
    glViewport (0, 0, (GLsizei)w, (GLsizei)h);
    glMatrixMode (GL_PROJECTION);
    glLoadIdentity ();
    gluPerspective (60, (GLfloat)w / (GLfloat)h, 1.0, 100.0);
    glMatrixMode (GL_MODELVIEW);
}

int main (int argc, char **argv) {
    glutInit(&argc, argv);
    
    glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH;

    glutInitWindowSize(500, 500);
    glutInitWindowPosition(100, 100);
    glutCreateWindow("A basic OpenGL Window");
    
    glewInit()

    glutDisplayFunc(display);
    glutIdleFunc(display);
    
    glutReshapeFunc(reshape);
    
    init();
    
    glutMainLoop();
    
    return 0;
}

VI. Source du vertex shader

Un vertex shader permet de manipuler les sommets d'un objet. Le principal avantage se montre lors de la manipulation d'un groupe de sommets d'une certaine manière. Par exemple, de l'eau pourrait être créée avec un plan assez tesselé et un vertex shader serait bien plus rapide pour calculer les positions de ces sommets que le mode immédiat.

Ce shader, comme tous les shaders, dispose d'une fonction principale. Il est possible de définir d'autres fonctions dans un shader, mais OpenGL ne cherchera et n'appellera que la fonction principale.

Dans cette fonction, on définit la position des sommets à la même valeur que si l'on n'avait pas utilisé de shader. On multiplie la matrice de projection par le sommet courant pour le placer où il devrait être à l'écran.

La variable gl_Position est utilisée pour définir la position du sommet courant. Une autre manière de le positionner sans les shaders est de définir gl_Position à ftransform, une variable OpenGL qui fait la même chose que la multiplication glModelViewProjectionMatrix * glVertex.

Tous les vertex shaders sont appelés pour chaque sommet individuellement.

Chaque vertex shader doit avoir la ligne

 
Sélectionnez
gl_Position = *something*;

parce que l'objectif d'un vertex shader est de définir la position de sommets.

 
Sélectionnez
void main() {            
    // Définit la position du sommet courant. 
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

VII. Source du fragment shader

Le fragment shader requiert une fonction principale, comme le vertex shader. Par contre, contrairement au vertex shader, le fragment shader est appelé pour chaque pixel à l'écran.

Tous les fragment shaders doivent se terminer avec une ligne

 
Sélectionnez
gl_FragColor = vec4(*something*);

parce que leur objectif est de définir la couleur actuellement écrite à l'écran.

Dans le fragment shader, gl_FragColor sera la couleur à afficher à l'écran. Ici, la valeur est la couleur blanche, de telle sorte que tous les pixels de la forme seront blancs.

GLSL dispose d'une série de types, il est recommandé de regarder le guide de référence rapide de GLSL pour se faire une idée.

gl_FragColor prend la valeur de vec4(), un vecteur de quatre nombres en virgule flottante. On peut l'écrire vec4(red, green, blue, alpha) ou même passer un autre vec4 au constructeur. GLSL fournit même le raccourci vec4(1.0) pour mettre toutes les composantes à l'unité. Cela donne la couleur blanche.

 
Sélectionnez
void main() {
    // Définit la couleur de sortie du pixel courant. 
    gl_FragColor = vec4(1.0);
}

VIII. Remerciements

Merci à Alexandre Laurent pour son aide à la traduction, ainsi qu'à Maxime Gault et Winjerome pour leur relecture orthographique !

  

Copyright © 2010 Donald Urquhart. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.