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

III. Étude du GLSL

Maintenant que nous savons comment faire fonctionner nos shaders dans un programme OpenGL, nous allons pouvoir apprendre le langage utilisé avec les shaders. Je vous invite à tester les différentes fonctionnalités que je présente. Vous pouvez retrouver une feuille récapitulative de tous les mots clés du GLSL ici (version 150, OpenGL 4.1), et ici (version 150 OpenGL 3.2).

III-A. Le langage GLSL

Je ne vais pas m'étendre sur la théorie du langage (conditions, boucles…), car j'imagine que si vous lisez cet article c'est que vous connaissez déjà la programmation. Pour information, la syntaxe du GLSL est souvent rapprochée à celle du C.

III-A-1. Les types de données

Dans le GLSL, de nombreux types ont été implémentés afin de faciliter les calculs des opérations graphiques.

  • float : représente un nombre à virgule flottante (ex: 0.4543).
  • vec2 / vec3 / vec4 : représentent des vecteurs (à 2 / 3 / 4 dimensions) de float.
  • int : représente un nombre entier (ex. : 42).
  • ivec2 / ivec3 / ivec4 : représentent des vecteurs (à 2 / 3 / 4 dimensions) de int.
  • bool : représente un booléen (soit vrai, soit faux).
  • bvec2 / bvec3 / bvec4 : représentent des vecteurs (à 2 / 3 / 4 éléments) de booléens.
  • mat2 / mat3 / mat4 : représentent des matrices (2x2 / 3x3 / 4x4) de floats.
  • void : ne représente rien (Type non spécifié ou absence de valeur).
  • sampler1D / sampler2D / sampler3D : représentent une texture (à 1 / 2 / 3 dimensions).
  • samplerCube : représente une texture cube (équivalent à six textures).
  • sampler1DShadow / sampler2DShadow : représente une texture de profondeur pour le calcul d'ombre (à 1 / 2 dimensions).

La déclaration des vecteurs peut se faire comme suit :

Déclaration d'un vecteur en GLSL
Sélectionnez
vec2 texture(0.3f,0.2f);
vec3 couleur(1.0f,0.0f,0.0f);
vec4 position(0.4f,0.3f,0.2f,1.0f);

L'accès aux composantes des vecteurs peut se faire de la manière suivante :

Accès aux membres des vecteurs
Sélectionnez
vec4 position;
position.x ; positions.y ; position.z ; position.w;
vec4 colour;
colour.r ; colour.g ; colour.b ; colour.a;
vec4 texture;
texture.s ; texture.t ; texture.p ; texture.q;

Les coordonnées 4D sont dans un repère x, y, z, w.

Les couleurs sont r, g, b, a pour red, green, blue, alpha

Les coordonnées pour les textures sont s, t, p, q. (p et non r à cause du conflit avec les couleurs.)

Vous pouvez aussi récupérer plusieurs composantes à la fois :

Accès à de multiples composantes d'un vecteur
Sélectionnez
vec4 position; // xyzw
position.xy ; position.xz ; position.xyz;
Vec4 colour; // rgba
colour.rg ; colour.ba ; colour.gba;
Vec4 texture; // stpq
texture.st ; texture.tp ; texture.pts

Et même faire des déclarations avec cet accès multiple :

Déclaration d'un vecteur se basant sur d'autres
Sélectionnez
vec2 t1(1.0f, 0.0f);
vec2 t2(0.0f, 0.5f);
vec4 position(t1.xy, t2.xy);

Il n'est pas possible de mixer les composants (ex. : colour.stx ; colour.rgz ; …)

III-A-2. Les qualificatifs de données

Il existe quelques mots clés permettant de donner des indications au compilateur sur la variable déclarée.

À la déclaration de la variable (en dehors des fonctions) :

  • uniform : indique que la variable est une donnée venant du programme OpenGL pour le vertex shader et/ou le fragment shader. (LECTURE UNIQUEMENT) ;
  • attribute : indique une variable par vertex, venant du programme OpenGL. Ne peut être placée que dans le vertex shader. (LECTURE UNIQUEMENT). Obsolète à partir de la version 150 du GLSL, remplacé par 'in' ;
  • varying : donnée en sortie du vertex shader (LECTURE et ÉCRITURE), envoyée au fragment shader après avoir été interpolée (LECTURE UNIQUEMENT). Obsolète à partir de la version 150 du GLSL, remplacé par 'out' ;
  • const : indique une variable qui est constante (LECTURE UNIQUEMENT).

Dans la déclaration des fonctions :

  • in : la variable est initialisée en entrée, mais n'est pas copiée en retour (qualificatifs par défaut) ;
  • out : la variable est copiée en retour, mais non initialisée en entrée ;
  • inout : la variable est initialisée en entrée, copiée en retour ;
  • const : variable d'entrée constante.

Les variables qui sont échangées entre le vertex shader et le fragment shader (varying) sont interpolées par le pipeline (la précision de l'interpolation est configurable). Ainsi, en ne renseignant que des données pour chaque vecteur, nous allons recevoir cette donnée pour tous les pixels et elle peut être légèrement différente entre deux points.

III-A-3. Le préprocesseur

Bien que je n'en aie pas du tout parlé jusqu'à présent, les shaders ont aussi leur préprocesseur.

Si vous connaissez le C, vous allez retrouver beaucoup de mots clés identiques :

  • #define NOM définition : définit une macro 'NOM' avec pour valeur 'définition' pour le préprocesseur. Si 'NOM' est appelé dans le code, 'définition' va être copié à la place de 'NOM' lors de la lecture par le préprocesseur ;
  • #undef NOM : permet d'enlever la déclaration de 'NOM' effectué avec #define ;
  • #if : test logique ;
  • #ifdef NOM : teste si 'NOM' est défini ;
  • #ifndef NOM : teste si 'NOM' n'est pas défini ;
  • #else : sinon, pour le #if ;
  • #elif : sinon si, suivi d'un test logique ;
  • #endif : fin de test (pour fermer le bloc commencé par #if) ;
  • #error : provoque une erreur lors de la compilation ;
  • #pragma : actionne une commande qui dépend de l'implémentation ;
  • #line : modifie le compteur de lignes du compilateur (pour changer le numéro de ligne du rapport d'erreurs) ;
  • __LINE__ : macro qui sera remplacée par le numéro de ligne courante ;
  • __FILE__ : macro indiquant le fichier courant ;
  • __VERSION__ : macro indiquant la version actuelle du GLSL ;
  • #version 'numéro_de_version' : indique au compilateur la version de GLSL nécessaire (ou voulue) pour ce shader. Exemple :
 
Sélectionnez
// Pour forcer le GLSL en version 1.50
               #version 150
  • #extension {nom_de_l_extension | all} : {require | enable | warn |disable} : permet de connaître si une extension est présente, mais aussi de ne compiler que si celle-ci est présente. Par défaut, toutes les extensions sont désactivées (#extension all : disable). Exemple d'utilisation :
 
Sélectionnez
#extension GL_EXT_gpu_shader4 : enable

III-A-4. Autres mots clés

Il reste encore quelques mots clés qui ne vont pas dans les catégories précédentes :

  • struct : définit une structure. Exemple :
Définition d'une structure en GLSL
Sélectionnez
struct MyVector
{
     vec4 position;
     vec4 colour;
};

L'accès aux membres se fait de la manière suivante :

Accès aux membres d'une structure
Sélectionnez
MyVector.position;
MyVector.colour;

III-A-5. Variables prédéfinies du vertex shader

Le GLSL permet d'accéder à des variables prédéfinies (builtin). Par prédéfinies, je sous-entends qu'elles existent et sont accessibles, mais qu'elles peuvent ne pas contenir de données essentielles (dépendantes de votre programme OpenGL).

Variables de sorties (LECTURE et ÉCRITURE) :

  • vec4 gl_Position : position du vertex dans l'espace écran. Votre vertex shader est obligé d'écrire quelque chose dans cette variable ;
  • float gl_PointSize : détermine la taille des points à dessiner. Le programme doit avoir GL_VERTEX_PROGRAM_POINT_SIZE activé ;
  • vec4 gl_ClipVertex : doit être rempli par la position du vertex, afin d'effectuer les opérations de découpage (clipping). Explication détaillée dans la section 4.A.4.


Variables d'attributs (LECTURE UNIQUEMENT) :

  • attribute vec4 gl_Vertex : position du vertex ;
  • attribute vec4 gl_Normal : normale du vertex ;
  • attribute vec4 gl_Color : couleur du vertex ;
  • attribute vec4 gl_SecondaryColour : couleur secondaire du vertex ;
  • attribute vec4 gl_MultiTexCoord0 : coordonnées de la texture 0 (le 0 peut être remplacé par un chiffre de 0 à 7 compris) ;
  • attribute float gl_FogCoord : coordonnées du brouillard.


Variables 'varying' (LECTURE et ÉCRITURE) :

  • varying vec4 gl_FrontColor : couleur de la face avant ;
  • varying vec4 gl_BackColor : couleur de la face arrière (GL_VERTEX_PROGRAM_TWO_SIDE doit être activé) ;
  • varying vec4 gl_FrontSecondaryColor : couleur secondaire de la face avant ;
  • varying vec4 gl_BackSecondaryColor : couleur secondaire de la face arrière ;
  • varying vec4 gl_TexCoord[] : tableau des coordonnées de texture. La taille du tableau maximum est : gl_MaxTextureCoords ;
  • varying float gl_FogFragCoord : coordonnée du brouillard.

III-A-6. Variables prédéfinies du fragment shader

Variables de sorties (LECTURE et ÉCRITURE) :

  • vec4 gl_FragColor : couleur du pixel ;
  • vec4 gl_FragData[] : donnée des buffers (la taille du tableau maximum est : gl_MaxDrawBuffers) ;
  • float gl_FragDepth : la profondeur du pixel (par défaut : glFragCoord.z).


Variables 'varying' (LECTURE UNIQUEMENT) :

  • varying vec4 gl_Color : la couleur du pixel, venant du vertex shader ;
  • varying vec4 gl_SecondaryColor : la couleur secondaire du pixel, venant du vertex shader ;
  • varying vec4 gl_TexCoord[] : coordonnées des textures (la taille maximale du tableau est gl_MaxTextureCoords) ;
  • varying float gl_FogFragCoord : coordonnée du brouillard.


Variables d'entrées (LECTURE UNIQUEMENT) :

  • vec4 gl_FragCoord : coordonnées du pixel en coordonnées relatives à l'écran ;
  • bool gl_FrontFacing : true si nous voyons la face avant.


Il y a aussi des variables constantes utiles pour récupérer des informations telles que le nombre de variables uniformes, le nombre de lumières et autres…

Des variables uniformes dans lesquelles les informations sur les lumières, les matériaux, le brouillard transitent.

Et finalement, il y a aussi des fonctions implémentées comme les fonctions sinus, cosinus, des opérations sur les vecteurs, les matrices, mais aussi des fonctions pour récupérer les couleurs d'une texture, ou certaines pour créer du bruit.

Pour trouver la liste, vous pouvez utiliser la carte de référence, la spécification GLSL ou le livre orange.

III-B. Communication entre l'application et les shaders

Il existe plusieurs moyens complémentaires pour envoyer des informations de notre application OpenGL aux shaders.

Dans ces méthodes nous pouvons citer :

  • les variables uniformes (uniform) ;
  • les attributs de sommets (attribute) ;
  • les textures (astuce un peu plus avancée).

III-B-1. Les variables uniformes

Les variables uniformes (uniform) sont des variables qui sont constantes pour toutes les instances du shader. Lorsque nous parlons de globalité du shader, cela correspond au fait que cette variable ne changera pas de valeur selon les vertex et que pour toute la surface, elle aura la même valeur.

Dans le shader, cette variable doit être déclarée avec la ligne suivante :

Déclaration d'une variable uniforme en GLSL
Sélectionnez
uniform type nom;

Cette ligne ne doit pas être dans une fonction du shader. Comme il est écrit plus haut, c'est une variable qui peut être dans le vertex shader ou dans le fragment shader.

Maintenant que nous savons comment la représenter dans notre shader, il ne nous reste plus qu'à l'envoyer à partir de notre programme OpenGL.

Les fonctions pour ce faire sont :

  • GLint glGetUniformLocation (GLuint program, const GLchar * name)

    program : contient l'identifiant du Program Shader.
    name : le nom de la variable dans le shader.

    Retourne un identifiant pour la variable contenue dans le shader.
  • void glUniform* (GLint location…)

    location : l'identifiant que l'on a obtenu avec glGetUniformLocation().

J'ai mis une étoile dans le nom de glUniform(), car il y a toute une série de fonctions glUniform() et chacune définit l'emploi d'une variable d'un type spécifique (rappel : en C, il n'y a pas de surcharges de fonctions).

La fonction glGetUniformLocation() demande le programme shader sur lequel nous voulons envoyer la variable ainsi que le nom de la variable. Elle va nous retourner un ID sur cette variable que nous pourrons utiliser dans une des fonctions glUniform(), pour remplir la variable en question.

Il est conseillé de vérifier s'il y a eu une erreur, à l'aide de glGetError() après les appels à ces deux commandes.

Par rapport au premier code, j'ai ajouté ce code, dans la fin de la fonction loadShader() :

c
Sélectionnez
uniformId  = glGetUniformLocation(programID, "couleurBleu");
errorState = glGetError();
if ( uniformId == -1 || errorState != GL_NO_ERROR )
{
    fprintf(stderr,"Erreur (%d) lors de la récupération de l'id de la variable uniforme 'couleur'\n",errorState);
}
// Voilà, nous sommes prêts
glUseProgram(programID);
glUniform1f(uniformId,couleurBleu);
errorState = glGetError();
if ( errorState != GL_NO_ERROR )
{
    fprintf(stderr,"Erreur (%d) lors de l'envoi de valeur à la variable uniforme\n",errorState);
}

Pour ce premier essai, le fragment shader est le suivant :

Fragment shader acceptant la composante bleu du programme.
Sélectionnez
uniform float couleurBleu = 0.0;
void main(void)
{
    gl_FragColor = vec4(gl_Color.xy,couleurBleu,1.0);
}

Le fragment shader attend couleurBleu du programme OpenGL (nous lui donnons une valeur par défaut juste au cas où le programme OpenGL ne ferait pas tout ce que l'on attend de lui) et utilise cette variable pour écrire la couleur des pixels.

Le résultat est le suivant :

Encore un cube
Encore un cube

Il faut savoir que les échanges entre le programme OpenGL (CPU) et le shader (GPU), sont lents, c'est pourquoi il faut passer le plus de données en une fois (en remplissant une matrice 3x3 si vous avez 9 variables à la place de faire 9 appels à glUniform*())

III-B-2. Les attributs de sommet

Les attributs de sommet (attribute) sont des variables que l'on passe avec chaque sommet. Cela veut dire que nous pouvons avoir une valeur différente pour chaque vertex. Grâce à cela vous pouvez ajouter des informations à vos sommets en complément de la position (qui est obligatoire), la couleur, les coordonnées de texture. Par contre, elles ne sont disponibles que dans les vertex shader (il est toujours possible de les faire transiter dans le fragment shader à l'aide d'une variable supplémentaire déclarée en varying).

Dans le GLSL, une telle variable se définit de la façon suivante :

Déclaration d'une variable 'attribute'
Sélectionnez
attribute vec3 nom;

Du côté de notre application OpenGL, nous allons utiliser les variables suivantes :

  • GLint glGetAttribLocation (GLuint program, const GLchar * name)

    program : contient l'identifiant du Program Shader.

    name : contient le nom de la variable du shader.

    Retourne un identifiant sur la variable contenu dans le shader.
  • void glVertexAttrib*(GLuint index)

    index : l'identifiant de la variable à remplir.

Comme pour les variables uniformes, nous allons dans un premier temps, récupérer un identifiant sur la variable que nous voulons remplir. La fonction accepte l'identifiant sur le programme shader, ainsi que le nom de la variable.

Une fois que nous avons l'identifiant de notre attribut, nous pouvons le remplir avec glVertexAttrib*() lors du dessin de nos polygones. Il faut appeler la fonction autant de fois que nous avons de sommets. Dans l'exemple, étant donné que je dessinais mon cube avec les VBO, je dois passer tout un tableau (telles les autres données du VBO).

Une fois de plus, il est conseillé de vérifier les erreurs avec glGetError().

Code exemple :

Envoi d'attribut au vertex shader
Sélectionnez
attributeId = glGetAttribLocation(programID, "couleur");
errorState = glGetError();
if ( attributeId == -1 || errorState != GL_NO_ERROR )
{
    fprintf(stderr,"Erreur (%d) lors de la récupération de l'id de la variable attribute 'couleur'\n",errorState);
}
 
// Voilà, nous sommes prêts
glUseProgram(programID);
 
glEnableVertexAttribArray(attributeId);
errorState = glGetError();
glVertexAttribPointer(attributeId,
                      3,
                      GL_FLOAT,
                      GL_TRUE,
                      0,
                      cubeColours);
if ( errorState != GL_NO_ERROR )
{
    fprintf(stderr,"Erreur (%d) lors du passage du tableau de valeur à la variable attribute 'couleur'\n",errorState);
}

Notez que comme j'ai décidé d'utiliser les attributs pour passer la couleur de mes sommets au shader, le glColorPointer() ainsi que les autres fonctions liées sont devenues obsolètes dans cet exemple.

Bien sûr, dans la fonction quitScene() j'ai ajouté la ligne suivante :

Désactivation du tableau d'attributs
Sélectionnez
glDisableVertexAttribArray(attributeId);

Le shader accompagnant notre programme :

Vertex shader
Sélectionnez
attribute vec3 couleur = vec3(0.0f,1.0f,0.0f);
void main (void)
{
    gl_Position = ftransform();
    gl_FrontColor = vec4(couleur,1.0);
}

Ce qui donne le résultat suivant :

Un cube utilisant les attributs de sommet
Un cube utilisant les attributs de sommet

Effectivement, il ressemble à notre tout premier cube, mais contrairement à la première fois, nous utilisons maintenant des attributs de sommet.

Pour pouvoir vous rappeler des différences entre les attributs de sommets et les variables uniformes, vous pouvez vous référer à ce diagramme :

Organisations des attribute et varying
Organisations des attribute et varying

III-B-3. Les textures

Cette technique utilise simplement les textures pour transférer des informations. La technique est la même que lorsque nous voulons appliquer une texture.


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.