Developpez.com - 2D - 3D - Jeux
X

Choisissez d'abord la catégorieensuite la rubrique :


Implémentation du GLSL côté API

Date de publication : 23/10/2007 , Date de mise à jour : 23/10/2007

Par Antony Martin (Autres articles)
 


Introduction
1. Création d'un shader
1.1. Les extensions des shaders
1.2. Création et suppression d'un objet de shader
1.3. Envoi d'un code source
2. Compilation d'un shader
2.1. Vérification du succès d'une compilation
2.2. Récupérer les messages d'erreur de la compilation
3. Création et utilisation d'un program
3.1. Association d'un ou plusieurs shaders à un program
3.2. Liage d'un program de shader
3.3. Utiliser un program de shader
Conclusion


Introduction

Après une courte introduction sur les shaders, je vous propose maintenant de rentrer un peu plus dans le vif du sujet, en étudiant la manière dont nous allons utiliser les shaders dans nos applications grâce à notre API. À ne pas confondre avec la programmation des shaders, comme je vous ai prévenu en partie 3. de l'introduction. L'implémentation des shaders au sein de notre application se décompose en plusieurs parties :



1. Création d'un shader

Nous allons voir ici la simple opération qu'est la création d'un shader fonctionnel prêt à l'emploi, mais avant cela, il est important de savoir si votre implémentation d'OpenGL supporte les shaders GLSL.


1.1. Les extensions des shaders

Si vous possédez une ancienne carte graphique, vous risquez de ne pas pouvoir utiliser les shaders ou alors leur utilisation conduirait à une erreur indéterminée, il est donc important de tester si l'extension correspondante aux shaders GLSL est disponible. Voici la liste des noms des extensions nécessaires à l'utilisation de shaders GLSL dans un programme OpenGL :

Globalement, on peut dire que les shaders GLSL sont supportés à partir des GeForce 5 FX chez nVidia, et à partir du Radeon 9500 chez ATI.


1.2. Création et suppression d'un objet de shader

Un objet de shader en OpenGL est représenté par son identifiant, stocké dans un entier non signé (GLuint) comme tous les types d'objets OpenGL (textures, listes, ...). Cependant, le maniement des objets de shaders ne s'effectue pas à travers nos habituelles fonctions glGen*(), glBind*(), glIs*() (etc...). Ainsi, pour créer un objet de shader, nous appellerons la fonction glCreateShader() :
GLuint glCreateShader(GLenum type);
Nous remarquerons que cette fonction dispose d'un paramètre : type. Celui-ci permet d'indiquer le type du shader que l'on souhaite créer : un vertex ou un pixel shader. Il existe respectivement deux constantes acceptables pour type :

La valeur renvoyée par glCreateShader() correspond à l'identifiant de notre objet de shader. Tout comme pour tous les types d'objets OpenGL, 0 n'est pas un identifiant valide pour un shader. Qui dit création dit aussi suppression, ainsi la fonction permettant de supprimer un objet de shader est glDeleteShader(), tout simplement.
void glDeleteShader(GLuint shader);
Je vais maintenant vous présenter une fonction dont la syntaxe devrait vous être familière : glIsShader(). À l'instar des autres fonctions glIs*() d'OpenGL, cette fonction permet de savoir si l'identifiant qu'elle reçoit en paramètre est un identifiant de shader valide. Voici le prototype de cette fonction :
GLboolean glIsShader(GLuint shader);


1.3. Envoi d'un code source

Les codes source de nos shaders seront généralement contenus dans des fichiers à part, afin de laisser toute sa souplesse au programme en n'étant pas obligé de le recompiler à chaque changement dans le code source de notre shader. Nous nous contenterons simplement de stocker les octets d'un fichier donné en mémoire Une fois les données récupérées, nous devons voir à présent comment envoyer notre code source à notre objet shader. OpenGL nous propose pour cela de spécifier le code source de notre shader via un char*. Voici la fonction qui permet d'envoyer le code source d'un shader :
void glShaderSource(GLuint shader, GLsizei nombre, const GLchar **sources, const GLint *longueur);

longueur est un paramètre un peu particulier. Positionné à NULL, il indique que chaque chaîne dans sources se termine par un caractère nul. Sinon, longueur est un tableau de taille nombre où chaque valeur désigne soit :

Outre ce paramètre longueur qui vous permet de faire à peu près tout et n'importe quoi, le fait que vous puissiez envoyer le code source d'un shader en plusieurs chaînes offre une encore plus grande souplesse dans la création du shader. Cela peut vous permettre par exemple de créer un shader complet à partir de plusieurs fragments de code, assemblés selon des instructions contenues dans votre application. Voici comment l'on pourrait simplement envoyer un code source lu à partir d'un fichier en langage C :
char *src = ReadFromFile("filename");
glShaderSource(shader, 1, &src, NULL);
La fonction glShaderSource() ne fait qu'assigner un code source à un shader sans le compiler. En effet, rien n'est laissé au hasard dans OpenGL, c'est vous qui contrôlez intégralement le déroulement des opérations, vous pourrez ainsi laisser de côté les contraintes du type « un code source envoyé = un shader compilé ».

warningNotez qu'il n'est pas possible de procéder à plusieurs appels de la fonction glShaderSource() afin "d'empiler" les codes source, seul les paramètres du dernier appel seront prit en compte.


2. Compilation d'un shader

La compilation d'un shader GLSL se déroule à peu près comme la compilation de n'importe quel code source, sauf qu'au lieu de cliquer sur un bouton de votre EDI vous appellerez une fonction. Il en est de même pour la gestion des erreurs ; en effet, la récupération des erreurs se fait également par appels de fontions. Imaginez que vous ayez fait une faute ou une erreur dans l'écriture du code de votre shader, OpenGL va alors refuser la compilation et générera une erreur accompagnée d'un message en vous indiquant où se trouve le problème. Récupérer une erreur de compilation c'est aussi et surtout récupérer un numéro de ligne dans le fichier du code, c'est grâce à elle que vous pourrez localiser plus précisément votre erreur. Nous allons dans un premier temps voir comment compiler un shader, puis nous verrons comment vérifier le succès de cette compilation en récupérant un code d'erreur, en l'analysant, et en agissant en fonction de celui-ci. Pour compiler un shader GLSL, rien de compliqué, il existe une fonction dédiée à cela. Invoquez la fonction glCompileShader(), dont le prototype est le suivant :
void glCompileShader(GLuint shader);



2.1. Vérification du succès d'une compilation

Comme pour toute compilation, des erreurs sont possibles et leur nature doit être connue. Pour cela, il nous faut savoir si il y a eu une erreur, et s'il y en a eu une, récupérer le message qu'elle contient et l'afficher à l'écran. Il existe une fonction pour récupérer l'état de la compilation. Plus globalement, il s'agit d'une fonction permettant de récupérer un entier relatif à une information spécifique d'un shader :
void glGetShaderiv(GLuint shader, GLenum type, GLint *result);

Avec GL_COMPILE_STATUS, la valeur écrite dans result est un booléen valant GL_TRUE si tout s'est bien déroulé, GL_FALSE dans le cas contraire.


2.2. Récupérer les messages d'erreur de la compilation

Nous allons à présent étudier la marche à suivre pour récupérer le message Le message d'erreur se trouve sous la forme d'une chaîne de caractères, mais attention, OpenGL n'allouera aucune mémoire pour nous, il va donc falloir lui demander quelle est la taille du message d'erreur, allouer une chaîne de cette taille puis demander à OpenGL d'écrire le message d'erreur dans notre mémoire fraîchement allouée. Nous allons reprendre notre fonction glGetShaderiv, mais cette fois-ci en lui envoyant comme second paramètre (type) la constante GL_INFO_LOG_LENGTH, afin d'obtenir la longueur du message d'erreur :
GLint logsize;

...

glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logsize);
A présent, il nous faut allouer un espace mémoire de la taille de logsize :
char *log = NULL;

...

log = malloc(logsize + 1); /* +1 pour le caractere de fin de chaine '\0' */
if(log == NULL)
{
    fprintf(stderr, "erreur d'allocation memoire !\n");
    return -1; /* ou autre code approprie */
}
Et pour finir, nous allons récupérer le message d'erreur en envoyant notre pointeur log à une fonction appelée glGetShaderInfoLog(), dont voici le prototype :
void glGetShaderInfoLog(GLuint shader, GLsizei max_size, Glsizei *longueur, char *info_log);

Il ne faudra évidemment pas oublier de libérer notre mémoire log après avoir affiché son contenu. Voyons à présent si vous le voulez bien, un exemple de code complet :

GLuint LoadShader(GLenum type, const char *filename)
{
    GLuint shader = 0;
    GLsizei logsize = 0;
    GLint compile_status = GL_TRUE;
    char *log = NULL;
    char *src = NULL;

    /* creation d'un shader de sommet */
    shader = glCreateShader(type);
    if(shader == 0)
    {
        fprintf(stderr, "impossible de creer le shader\n");
        return 0;
    }

    /* chargement du code source */
    src = LoadSource(filename);
    if(src == NULL)
    {
        /* theoriquement, la fonction LoadSource a deja affiche un message
           d'erreur, nous nous contenterons de supprimer notre shader
           et de retourner 0 */
        
        glDeleteShader(shader);
        return 0;
    }

    /* assignation du code source */
    glShaderSource(shader, 1, (const GLchar**)&src, NULL);

    /* compilation du shader */
    glCompileShader(shader);

    /* liberation de la memoire du code source */
    free(src);
    src = NULL;

    /* verification du succes de la compilation */
    glGetShaderiv(shader, GL_COMPILE_STATUS, &compile_status);
    if(compile_status != GL_TRUE)
    {
        /* erreur a la compilation, recuperation du log d'erreur */

        /* on recupere la taille du message d'erreur */
        glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &logsize);

        /* on alloue un espace memoire dans lequel OpenGL ecrira le message */
        log = malloc(logsize + 1);
        if(log == NULL)
        {
            fprintf(stderr, "impossible d'allouer de la memoire !\n");
            return 0;
        }
        /* initialisation du contenu */
        memset(log, '\0', logsize + 1);

        glGetShaderInfoLog(shader, logsize, &logsize, log);
        fprintf(stderr, "impossible de compiler le shader '%s' :\n%s",
                filename, log);

        /* ne pas oublier de liberer la memoire et notre shader */
        free(log);
        glDeleteShader(shader);

        return 0;
    }
    
    return shader;
}
La fonction renvoie 0 en cas d'erreur, ou bien l'identifiant du shader créé si elle a réussi. Notez ici la présence de la fonction LoadSource(), je considère ici qu'elle charge et renvoie le contenu d'un fichier donné.


3. Création et utilisation d'un program

Les shaders que nous venons de créer ne peuvent pas encore être utilisés directement pour un rendu, il va nous falloir les attribuer à un program, ce dernier pourra ensuite être spécifié comme program actif pour le rendu. Nous allons d'abord étudier les quelques fonctions de manipulations basiques d'un program, création, destruction, etc... Le type des objets de programs est simplement GLuint, un identifiant valide est retourné par la fonction glCreateProgram(), aucun paramètre n'est à envoyer à cette fonction. Pour vérifier la validité d'un identifiant de program de shader, utilisez glIsProgram(GLuint program). Enfin, pour detruire un program, vous aurez besoin de faire appel à glDeleteProgram(GLuint program). Après la création d'un program, il est nécessaire de lui attribuer un ou deux shaders à exécuter. On peut y mettre soit un vertex shader, soit un pixel shader, soit les deux. Afin de résumer l'ordre des opérations nécessaires à la création d'un program fonctionnel, voici la procédure à suivre pour créer un program exécutable :

  1. créer un vertex shader (facultatif si création d'un pixel shader) ;
  2. créer un pixel shader (facultatif si création d'un vertex shader) ;
  3. créer un program, appellons-le Program ;
  4. rattacher le vertex et le pixel shader à Program ;
  5. rendre Program exécutable en le liant ;
  6. le program étant lié, nous pouvons supprimer nos shaders si nous en avons plus besoin ;
  7. utiliser Program à notre guise ;
  8. détruire les shaders (si ce n'est pas déjà fait) et Program.


3.1. Association d'un ou plusieurs shaders à un program

Nous allons commencer par "rattacher" nos shaders à notre program, et pour cela rien de plus simple, nous utiliserons la fonction glAttachShader() :
void glAttachShader(GLuint program, GLuint shader);

A l'inverse de glAttachShader(), il existe une fonction qui a l'effet contraire. Supposez que vous vouliez mettre à jour un shader, il faudra d'abord que vous le détachiez du program auquel il a été attaché via glAttachShader(), utilisez pour cela glDetachShader() :
void glDetachShader(GLuint program, GLuint shader);


3.2. Liage d'un program de shader

Le liage d'un program peut se comparer à la compilation des shaders car il se décompose également en deux étapes :

Lorsqu'on lie un program de shader on demande à OpenGL de lier le vertex shader avec le pixel shader. Cette liaison peut échouer dans la mesure où les deux shaders peuvent être incompatibles entre eux. La liaison vérifie également certaines erreurs qui n'apparaîsse pas à la compilation car ce ne sont pas des erreurs de codage. Utilisez glLinkProgram() pour lier un program de shader :
void glLinkProgram(GLuint program);

Comme je l'ai dit plus haut, l'étape suivante consiste à vérifier que le liage a bien fonctionné. La méthode employée est très similaire à celle des shaders ; on vérifie si une erreur est présente, s'il y en a une, on récupère la taille de la chaîne contenant le message d'erreur, on alloue un espace mémoire de cette taille puis on indique l'adresse de cette mémoire à OpenGL pour qu'il puisse y écrire le message d'erreur. Voici la fonction permettant (entre autres) de savoir si une erreur a été levée :
void glGetProgramiv(GLuint program, GLenum type, GLint *result);
Son fonctionnement est identique à glGetShaderiv() pour les shaders, je vais donc passer les descriptions minutieuses. Tout ce que vous devez savoir ici, c'est la constante à passer à type pour obtenir ce que l'on cherche. Nous cherchons l'état du précédent liage du program, pour cela nous allons passer la constante GL_LINK_STATUS. La vérification de la valeur de result fonctionne de la même façon que pour les shaders, si elle est différente de GL_TRUE, alors une erreur est survenue. Pour ensuite récupérer la longueur du message d'erreur, nous allons à nouveau utiliser glGetProgramiv() mais avec cette fois-ci GL_INFO_LOG_LENGTH pour type. Enfin, pour récupérer le message d'erreur, nous avons à notre disposition la fonction glGetProgramInfoLog() :
void glGetProgramInfoLog(GLuint program, GLsizei max_size, GLsizei *longueur, char *log);
Son fonctionnement est lui aussi identique à la fonction glGetShaderInfoLog() pour les shaders.


3.3. Utiliser un program de shader

L'utilisation d'un shader se résume par l'appel d'une simple fonction :
void glUseProgram(GLuint program);

Cette fonction rend actif le program spécifié pour tous les prochains rendus jusqu'à ce qu'un appel à glUseProgram(0) soit intercepté. Tout comme les textures, vous pouvez charger plusieurs shaders en début de programme et ensuite les utiliser simultanément, comme ceci :
glUsePogram(prog1);

/* rendus utilisant prog1 */

glUsePogram(prog2);

/* rendus utilisant prog2 */

glUsePogram(prog3);

/* rendus utilisant prog3 */

glUseProgram(0);

/* fin des rendus utilisant les shaders */
Attention toutefois, comme avec les textures, il est impossible d'utiliser deux programs à la fois. Ici, le second appel à glUseProgram() défini prog2 comme étant actif à la place de prog1.


Conclusion

Nous avons vu dans cette partie comment intégrer des shaders à notre application OpenGL pour ensuite les utiliser. Attardons-nous sans plus attendre au langage GLSL, je vous apprendrai dans la prochaine partie les règles de base du langage, nous créerons également nos premiers vertex et pixel shaders.



Valid XHTML 1.1!Valid CSS!

Cette création est mise à disposition sous un contrat Creative Commons.
Responsable bénévole de la rubrique 2D - 3D - Jeux : LittleWhite -