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 :
- premièrement, la création d'un shader ;
- nous verrons ensuite comment attribuer un contenu à ce shader, autrement dit, spécifier son code source ;
- une fois le code source spécifié, il nous faudra le compiler. Il faudra également gérer les erreurs, car comme toute compilation, la compilation d'un shader peut échouer ;
- notre shader est fin prêt, mais il n'est pas encore utilisable. Il nous faudra pour cela créer un « program », car celui-ci peut être exécuté directement par la carte graphique.
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 :
- GL_ARB_shading_language_100 : support du langage GLSL (dans sa version 1.0) ;
- GL_ARB_shader_objects : support des objets de shader (cf plus bas) ;
- GL_ARB_vertex_shader : support des vertex shaders ;
- GL_ARB_fragment_shader : support des pixel shaders.
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 :
- GL_VERTEX_SHADER
- GL_PIXEL_SHADER
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);
|
- shader : c'est l'identifiant du shader auquel on souhaite spécifier le code source
- nombre : c'est le nombre de chaînes contenues dans sources.
- sources : c'est notre code source, décomposé en plusieurs chaînes.
- longueur : tableau des longueurs de chaque chaîne dans sources.
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 :
- la taille de la chaîne correspondante dans sources si la valeur est positive ;
- que la chaîne correspondante se termine par un caractère nul si la valeur est négative.
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é ».
| Notez 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);
|
- shader : c'est l'identifiant du shader que vous souhaitez compiler.
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);
|
- shader : identifiant du shader dont on souhaite récupérer une information.
- type : type d'info demandé. Nous recherchons l'état de la compilation du shader, pour cela nous metterons GL_COMPILE_STATUS.
- result : pointeur sur un entier dans lequel OpenGL écrira la valeur de l'info demandé.
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 );
if (log = = NULL )
{
fprintf (stderr, " erreur d'allocation memoire !\n " );
return - 1 ;
}
|
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);
|
- shader : identifiant de notre shader.
- max_size : nombre maximum de bytes qu'OpenGL a le droit d'écrire dans notre mémoire.
- longueur : OpenGL écrira à l'adresse de ce pointeur le nombre de bytes écrits dans info_log.
- info_log : adresse mémoire dans laquelle OpenGL écrira le message d'erreur.
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 ;
shader = glCreateShader (type);
if (shader = = 0 )
{
fprintf (stderr, " impossible de creer le shader\n " );
return 0 ;
}
src = LoadSource (filename);
if (src = = NULL )
{
glDeleteShader (shader);
return 0 ;
}
glShaderSource (shader, 1 , (const GLchar* * )& src, NULL );
glCompileShader (shader);
free (src);
src = NULL ;
glGetShaderiv (shader, GL_COMPILE_STATUS, & compile_status);
if (compile_status ! = GL_TRUE)
{
glGetShaderiv (shader, GL_INFO_LOG_LENGTH, & logsize);
log = malloc (logsize + 1 );
if (log = = NULL )
{
fprintf (stderr, " impossible d'allouer de la memoire !\n " );
return 0 ;
}
memset (log, ' \0 ' , logsize + 1 );
glGetShaderInfoLog (shader, logsize, & logsize, log);
fprintf (stderr, " impossible de compiler le shader '%s' :\n%s " ,
filename, log);
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 :
- créer un vertex shader (facultatif si création d'un pixel shader) ;
- créer un pixel shader (facultatif si création d'un vertex shader) ;
- créer un program, appellons-le Program ;
- rattacher le vertex et le pixel shader à Program ;
- rendre Program exécutable en le liant ;
- le program étant lié, nous pouvons supprimer nos shaders si nous en avons plus besoin ;
- utiliser Program à notre guise ;
- 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);
|
- program : il s'agit là du program qui recevra le shader.
- shader : identifiant du shader que l'on veut associer à program.
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 :
- le liage en lui même ;
- la vérification du succès de ce liage.
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);
|
- program : program que l'on souhaite lier.
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);
|
- program : il s'agit là de l'identifiant du program que l'on souhaite activer, si cet identifiant vaut 0, OpenGL désactivera l'utilisation des programs de shaders.
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);
glUsePogram (prog2);
glUsePogram (prog3);
glUseProgram (0 );
|
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.
Cette création est mise à disposition sous un contrat Creative Commons.