3. É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 à faire des tests des différentes fonctionnalités que je présente. Vous pouvez retrouver une feuille récapitulative de toutes les mots clés du GLSL ici (version 150, OpenGL 4.1), et ici (version 150 OpenGL 3.2).
3-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.
3-A-1. Les types de données▲
Dans le GLSL, de nombreux types ont été implémenté 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 à 6 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 :
vec2 texture(0.3
f,0.2
f);
vec3 couleur(1.0
f,0.0
f,0.0
f);
vec4 position(0.4
f,0.3
f,0.2
f,1.0
f);
L'accès aux composantes des vecteurs peut se faire de la manière suivante :
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 :
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:
vec2 t1(1.0
f, 0.0
f);
vec2 t2(0.0
f, 0.5
f);
vec4 position(t1.xy, t2.xy);
Il n'est pas possible de mixer les composants (ex : colour.stx ; colour.rgz ; ...)
3-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 ECRITURE), envoyée au fragment shader après avoir été interpolé (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é 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 légèrement différente entre deux points.
3-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éfinie 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 ligne du compilateur (pour changer le numéro de ligne du rapport d'erreurs)
- __LINE__ : macro qui sera remplacée par le numéroe 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 :
//
Pour
forcer
le
GLSL
en
version
1.50
#
version
15
0
- #extension {nom_de_l_extension | all} : {require | enable | warn |disable} : permet de connaître si une extension est présente, mais aussi de compiler que si celle ci est présente. Par défaut, toutes les extensions sont désactivées (#extension all : disable). Exemple d'utilisation :
#
extension
GL_EXT_gpu_shader4
:
enabl
e
3-A-4. Autres mot clés▲
Il reste encore quelques mots clés qui ne vont pas dans les catégories précédentes :
- struct : Défini une structure. Exemple :
struct
MyVector
{
vec4 position;
vec4 colour;
}
;
L'accès aux membres ce fait de la manière suivante :
MyVector.position;
MyVector.colour;
3-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 ECRITURE) :
- 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 d'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 : normal 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 ECRITURE) :
- 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.
3-A-6. Variables prédéfinies du fragment shader▲
Variables de sorties (LECTURE et ECRITURE) :
- 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 uniforms, le nombre de lumière 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 d'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 certaine 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.
3-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).
3-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é au 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 :
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 contenu 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 tout 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 rajouté ce code, dans la fin de la fonction loadShader() :
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êt
glUseProgram
(
programID);
glUniform1f
(
uniformId,couleurBleu);
errorState =
glGetError
(
);
if
(
errorState !
=
GL_NO_ERROR )
{
fprintf
(
stderr,"
Erreur
(
%
d
)
lors
de
l
'
envoie
de
valeur
à
la
variable
uniforme
\n
"
,errorState);
}
Pour ce premier essai, le fragment shader est le suivant :
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 fasse 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:
Il faut savoir que les échanges entre le programme OpenGL (CPU) et le shader (GPU), sont lent, c'est pourquoi il faut passer le plus de données en une fois (en remplissant une matrice 3x3 si vous avec 9 variables à la place de faire 9 appel à glUniform*())
3-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 est une telle variable se définie de la façon suivante :
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 :
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êt
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és sont devenues obsolètes dans cet exemple.
Bien sûr, dans la fonction quitScene() j'ai rajouté la ligne suivante :
glDisableVertexAttribArray
(
attributeId);
Le shader accompagnant notre programme :
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 :
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 :
3-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.