IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Les shaders dans OpenGL

Introduction à la programmation de shaders GLSL


précédentsommairesuivant

II. Mise en place des shaders dans un programme

II-A. Utilisation des shaders (Documentation OpenGL 2.1)

Comme pour tout élément qui s'intègre dans OpenGL (textures, buffers…) il faut créer des objets OpenGL pour accueillir l'élément voulu (n'oubliez pas que, pour un programme shader, nous avons deux sources : une pour le vertex shader ; une pour le fragment shader, donc nous avons besoin de deux objets).

  • GLuint glCreateShader (GLenum shaderType)

    shaderType : doit contenir GL_VERTEX_SHADER pour créer un vertex shader.
    Retourne l'identifiant du shader.
  • GLuint glCreateShader (GLenum shaderType)

    shaderType : doit contenir GL_FRAGMENT_SHADER pour créer un fragment shader.
    Retourne l'identifiant du shader.


Lecture des fichiers, le contenu est passé à (encore une fois, il faut faire deux fois l'appel à la commande) :

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

    shader : contient l'identifiant du shader, retourné par glCreateShader().
    Count : indique le nombre d'éléments dans string et length.
    string : est un pointeur sur le code source de notre ou nos shaders.
    length : indique la taille de notre ou nos shaders.


Comme tout programme, nous compilons le code (deux appels nécessaires, encore) :

  • void glCompileShader (GLuint shader)

    shader : contient l'identifiant de notre shader.


La vérification des erreurs est très importante, sans quoi, vous ne savez pas si votre compilation fonctionne ou pas :

  • void glGetShader (GLuint shader, GLenum pname, GLint * params)

    shader : contient l'identifiant de notre shader.
    pname : contient un identifiant définissant la valeur qui va être retournée. Ici nous utilisons GL_COMPILE_STATUS.
    params : contiendra la valeur retournée par OpenGL.


Et pour connaître l'erreur (si elle est présente) :

  • void glGetShaderInfoLog (GLuint shader, GLsizei maxLength, GLsizei * length, GLchar * infoLog)

    shader : contient l'identifiant de notre shader.
    maxLength : contient la taille que vous avez allouée pour recevoir le message d'erreur.
    length : contiendra la taille du message retourné.
    infoLog : la chaîne de caractères qui contiendra le message d'erreur.


Maintenant que nos deux (vertex et fragment) shaders sont compilés, il faut créer le programme. Encore une fois, il nous faut un objet OpenGL et son identifiant :


Une fois l'ID créé, vous devez dire à OpenGL quel shader vous voulez mettre dans le programme (opération de liage ('linking' en anglais)) :

  • void glAttachShader (GLuint program, GLuint shader)

    program : contient l'identifiant du Program Shader auquel nous voulons attacher le shader.
    shader : contient l'identifiant du shader que nous voulons attacher.


Nos deux shaders liés au programme vont être mis ensemble pour former le programme. Cette dernière étape se fait avec la commande :

  • void glLinkProgram(GLuint program)

    program : contient l'identifiant du Program shader que nous voulons lier.


Enfin, le shader devrait être prêt à être utilisé. Mais je conseille vivement de vérifier les erreurs avec :

  • void glGetProgram (GLuint program, GLenum pname, GLint * params)

    program : contient l'identifiant du Program Shader.
    pname : contient un enum déterminant ce que nous voulons en retour. Ici nous utilisons GL_LINK_STATUS.
    params : contiendra la valeur retournée par OpenGL.


Si une erreur est rapportée, vous pouvez en connaître la cause avec :

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

    program : contient l'identifiant du Program Shader.
    maxLength : contient la taille que vous avez allouée pour recevoir le message d'erreur.
    length : contiendra la taille du message retourné.
    infoLog : la chaîne de caractères qui contiendra le message d'erreur.


Maintenant, vous pouvez utiliser le shader avec :

  • void glUseProgram (GLuint program)

    program : contient l'identifiant du Program Shader.

Si vous ne voulez plus utiliser de shader, vous pouvez appeler cette commande en passant 0.

Une fois que vous avez fini d'utiliser avec vos shaders, vous allez devoir les détruire.

Première chose à faire, nous arrêtons de les utiliser :

  • void glUseProgram (GLuint program)

    program : contient l'identifiant du Program Shader. Ici nous mettons 0 pour ne plus utiliser de shader.


Après, nous détachons les shaders du programme :

  • void glDetachShader (GLuint program, GLuint shader)

    program : contient l'identifiant du Program Shader.
    shader : contient l'identifiant du shader à détacher du programme.


Puis nous détruisons le programme :

  • void glDeleteProgram (GLuint program)

    program : contient l'identifiant du Program Shader.


Puis les deux shaders :

  • void glDeleteShader (GLuint shader)

    shader : contient l'identifiant du shader à détruire.



Sachez que :

  • si des shaders sont encore liés à un programme, l'appel à glDeleteShader() va mettre les shaders en attente pour suppression et ils seront supprimés lorsqu'ils seront détachés (glDetachShader()) ;
  • si le programme est encore utilisé, l'appel à glDeleteProgram() va mettre le programme en attente pour suppression, et il sera supprimé une fois inutilisé (glUseShader(0)) ;
  • le fait de supprimer un programme détache automatiquement les shaders qui lui sont attachés.


En prenant en considération tout ce qui vient d'être dit, il est conseillé de :

  • supprimer les shaders dès que vous les avez attachés (si vous ne voulez pas les attacher une autre fois, bien sûr) ;
  • supprimer le programme une fois qu'il n'y en a plus besoin. La suppression du programme détachera les shaders, ce qui fera qu'ils seront supprimés tout de suite avec le programme.

II-B. Lecture d'un fichier texte pour l'intégrer comme shader

Comme nous avons pu le voir, la commande glShaderSource() accepte un tableau de chaines de caractères et non le fichier directement. Ceci est fait pour que le code d'OpenGL soit le plus portable possible (par exemple nous pouvons mettre directement notre code source dans une variable dans le code de notre programme). Ici, nous allons voir comment nous pouvons lire un fichier pour OpenGL.

La première chose à faire sur notre fichier est de récupérer la taille du fichier. Une méthode consiste à mettre le pointeur du fichier directement sur la fin est à récupérer sa position :

Récupérer la taille d'un fichier
Sélectionnez
long getFileSize(FILE* pFile)
{
    long length = 0;
 
    fseek(pFile,0,SEEK_END);
 
    length = ftell(pFile);
    // Ne pas oublier de mettre le fichier à son début, sinon on ne peut pas le lire
    fseek(pFile,0,SEEK_SET);
    return length;
}

Ensuite, il faut récupérer le contenu du fichier en lui-même :

Récupérer les données du fichier
Sélectionnez
char* fileContent = (char*)malloc(fileLength+1); // +1 pour terminer la chaine proprement
if ( fileContent == NULL )
{
    return -3;
}
fread(fileContent,fileLength,1,pFile);
// Termine le tableau qui contient le shader
    fileContent[fileSize] = '\0';
// Utilisation avec glShaderSource()
free(fileContent);

Vous pouvez voir que l'on réutilise la taille du fichier trouvé plus tôt.

Ci-dessous, nous allons voir les sections les plus importantes de ce code. Afin de simplifier le code, je n'ai pas fait une vérification minutieuse des erreurs.

Fonction de vérification de la compilation.
Sélectionnez
// Pour plus de simplicité, j'ajoute une fonction qui vérifie la compilation des shaders
char checkShaderCompilation(GLuint shaderID)
{
    GLint compilationStatus = 0;
 
    // Vérification de la compilation pour le vertex shader
    glGetShaderiv(vertexID, GL_COMPILE_STATUS, &compilationStatus);
    if ( compilationStatus != GL_TRUE )
    {
        // Nous savons que la compilation a échoué, donc nous devons savoir pourquoi
        // Nous devons récupérer la taille du log
        GLint logLength = 0;
        GLchar* log = NULL;
 
        glGetShaderiv(shaderID, GL_INFO_LOG_LENGTH, &logLength);
 
        // Maintenant que nous avons la taille, nous pouvons allouer la mémoire suffisante pour ce log
        log = (GLchar*)malloc(logLength);
        if ( log == NULL )
        {
            fprintf(stderr,"Erreur d'allocation de mémoire pour le log de la compilation du shader\n");
            return 0;
        }
 
        glGetShaderInfoLog(shaderID, logLength, &logLength, log);
 
        // On peut afficher le message
        fprintf(stderr,"Erreur de compilation:\n%s",log);
 
        // Et on n'oublie pas de libérer la mémoire
        free(log);
        return 0;
    }
    return 1; // Pas d'erreur
}

Le code qui suit charge et compile nos shaders :

Chargement et compilation des shaders
Sélectionnez
void loadShader()
{
    // Lecture des fichiers
    // Certaines personnes aiment avoir le fichier du vertex shader avec l'extension .vert
    // et le fichier du fragment shader avec l'extension .frag
    GLchar* vertexSource = (GLchar*)readFile("data/simple.vert");
    GLchar* fragmentSource = (GLchar*)readFile("data/simple.frag");
    GLint programState = 0;
    GLint vertexSize = 0;
    GLint fragmentSize = 0;
 
    // Création des ID
    vertexID = glCreateShader(GL_VERTEX_SHADER);
    fragmentID = glCreateShader(GL_FRAGMENT_SHADER);
 
    // Vérification des fichiers
    if ( !vertexSource || !fragmentSource )
    {
        // Ici, il faudrait faire en sorte que le programme s'arrête
        deleteShader();    // Nettoyage
        return;
    }
 
    // Chargement des sources dans OpenGL
    vertexSize = strlen(vertexSource);
    fragmentSize = strlen(fragmentSource);
    glShaderSource(vertexID, 1, (const GLchar**)(&vertexSource), &vertexSize);
    glShaderSource(fragmentID, 1, (const GLchar**)(&fragmentSource), &fragmentSize);
 
    // Compilation du vertex shader
    glCompileShader(vertexID);
    glCompileShader(fragmentID);
 
    // Vérification des erreurs
    if ( !checkShaderCompilation(vertexID) || !checkShaderCompilation(fragmentID))
    {
        deleteShader();
        return;
    }
 
 
    // Création de l'ID pour le programme
    programID = glCreateProgram();
 
    // On attache les shaders ensemble
    glAttachShader(programID, vertexID);
    glAttachShader(programID, fragmentID);
 
    // On peut enfin passer à la liaison.
    glLinkProgram(programID);
 
    // Et encore une fois on vérifie si tout se passe bien
    glGetProgramiv(programID , GL_LINK_STATUS  , &programState);
    if ( programState != GL_TRUE)
    {
        // On récupère la taille du log
        GLint logSize = 0;
        GLchar* log = NULL;
 
        glGetProgramiv(programID, GL_INFO_LOG_LENGTH, &logSize);
 
        // On peut allouer la mémoire, une fois que l'on a la taille du log
        log = (GLchar*)malloc(logSize);
        if ( log == NULL )
        {
            fprintf(stderr,"Erreur d'allocation de mémoire pour le log de la compilation du programme\n");
            deleteShader();
            return;
        }
 
        // Et on récupère le log
        glGetProgramInfoLog(programID, logSize, &logSize, log);
 
        // On affiche
        fprintf(stderr,"Erreur lors du liage du shader:\n%s",log);
        free(log);
        deleteShader();
        return;
    }
    // Voilà, nous sommes prêts
    glUseProgram(programID);
}

Et pour notre premier essai, nous utilisons le vertex shader ci-dessous. Ce vertex shader reproduit le comportement du pipeline fixe afin de définir la position du sommet. La deuxième ligne copie la couleur du sommet au fragment shader.

Vertex shader
Sélectionnez
void main (void)
{
    gl_Position = ftransform();
    gl_FrontColor = gl_Color;
}

Le fragment shader suivant copie la couleur attribuée aux sommets dans les pixels.

Fragment shader
Sélectionnez
void main(void)
{
    gl_FragColor = gl_Color;
}

Si vous lancez le programme, vous remarquez que rien de spectaculaire ne se passe. Il n'y a que le cube qui tourne et il a les couleurs comme indiqué dans le programme OpenGL. En plus, si vous supprimez tout le code lié au shader, ou même, plus simple, si vous enlevez le code glUseShader() de la fonction loadShader(), vous aurez exactement le même résultat :

Notre première application utilisant des shaders!
Notre première application utilisant des shaders !

Vous pouvez télécharger le projet (Code::Blocks / Windows) en cliquant iciProgramme OpenGL basique pour travailler avec les shaders.

Voilà, nous avons le premier programme OpenGL utilisant des shaders en état de marche. Certes le résultat n'est pas spectaculaire, mais cela est un très bon début.

Maintenant, vous allez remplacer votre vertex shader, par le suivant :

Vertex shader 2
Sélectionnez
void main (void)
{
    gl_Position = ftransform();
    gl_Position = vec4(gl_Position.xyz * 0.3,gl_Position.w);
    gl_FrontColor = gl_Color;
}

Voilà, notre cube est devenu tout petit. Effectivement, le vertex shader que j'ai fait rétrécit les dimensions des vertex :

Un plus petit cube
Un plus petit cube

Vous pouvez reprendre le premier vertex shader que je vous ai présenté, mais remplacer le fragment shader par le suivant :

Fragment shader 2
Sélectionnez
void main(void)
{
    gl_FragColor = vec4(1.0,0.0,0.0,1.0);
}

Maintenant, notre cube est tout rouge alors que je n'ai touché aucune ligne de code du programme OpenGL :

Un cube rouge
Un cube rouge

Mais, sachez que les shaders ne permettent pas uniquement de changer la taille ou la couleur des objets, ils permettent beaucoup plus et nous allons le voir dans la suite de ce tutoriel.


précédentsommairesuivant

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2011 Alexandre Laurent. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.