Aller au menu - Aller au contenu

Icône Notions supplémentaires

Par Avatar Yno
Mise à jour : 08/11/2008
Difficulté : Facile Facile Creative Commons BY-SA
1 757 visites depuis 7 jours , dont 119 sur ce chapitre , classé 73/777
Au terme de ce chapitre, vous serez fin prêts pour apprendre toutes les fonctionnalités avancées que propose le GLSL, et cela vous permettra de trouver une réelle utilité aux shaders.

Nous allons tout d'abord passer en revue toutes les notions du langage GLSL qui sont communes à celles du C, puis dans un second temps je vais vous montrer les quelques différences entre les deux langages, afin de mettre les choses au clair. Ensuite, nous apprendrons à créer des fonctions, mais surtout à les surcharger. Vous ne connaissez peut-être pas la notion de surcharge des fonctions, il est donc important que vous l'appreniez, vous verrez que cela ressemble à la surcharge des opérateurs.
Puis je finirai par vous présenter quelques fonctions natives du langage GLSL bien pratiques, qui sont très souvent utilisées.
Sommaire du chapitre :
Icône du chapitre
Chapitre précédent Sommaire Chapitre suivant

Notions communes et incompatibilités avec le C

Bien, commençons. Je voudrais tout d'abord vous présenter tout ce qui existe dans le langage GLSL qui se rapporte au C, afin de gagner du temps.

Le GLSL n'est pas un langage aussi rigoureux que le C, je le considérerais plutôt comme un langage "jouet", il suffit d'enchaîner quelques instructions dans un main() et le tour est joué. À partir du moment où votre shader fonctionne correctement, il y a peu de chances que vous ayez à le réviser pour une raison autre que sa performance.


Notions communes



Je vous propose de commencer par les instructions de contrôle.

Les instructions if...else



En C, il est possible de créer une condition de la façon suivante :

Code : C
1
2
3
4
5
6
7
8
if(condition)
{
    instructions;
}
else
{
    autres instructions;
}



Eh bien sachez que ce code fonctionne aussi en GLSL. Par exemple, ceci est tout à fait correct :

Code : Autre
1
2
3
4
5
6
7
8
9
10
11
12
int a, b;
 
...
 
if(a == b)
{
    instructions;
}
else
{
    autres instructions;
}



L'omission des accolades est également autorisée si les instructions se résument à une seule instruction :

Code : Autre
1
2
3
4
5
6
7
8
int a, b;
 
...
 
if(a == b)
    instruction;
else
    autre instruction;



Les opérateurs de comparaison sont les mêmes qu'en C.

Instructions break et continue



Idem qu'en C là encore, les instructions break et continue existent et ont le même effet qu'en langage C ; à savoir :

  • break : sortir de la boucle d'instructions courante ;
  • continue : poursuivre le déroulement de la boucle d'instructions à partir du "haut" du bloc.


La boucle do...while



Comme en C, le mot clé do est suivi d'un bloc d'instructions, puis d'une condition entre parenthèses après un while, comme dans l'exemple ci-dessous :


Code : Autre
1
2
3
4
5
do
{
    instructions;
}
while(condition);


Ce code veut dire :

Code : Autre
1
2
3
4
5
exécuter
{
    tout ceci
}
tant que (condition) est vraie


Ne pas oublier le point-virgule à la fin du while. Les accolades peuvent là aussi êtres omisent si il n'y a qu'une seule instruction dans le bloc.

La boucle while



S'utilise de la même façon qu'en C, et a le même effet, à savoir ; exécuter un bloc d'instructions en boucle tant que la condition contenue entre les parenthèses suivants le mot clé while est vraie, avec une vérification de celle-ci avant le premier lancement de la boucle (contrairement à do...while).

Code : Autre
1
2
3
4
while(condition)
{
    instructions;
}


Comme d'habitude, les accolades peuvent êtres enlevées si il n'y a qu'une instruction à exécuter.

La boucle for



Celle-ci permet, comme en C, d'intégrer facilement un compteur à une boucle. Voici un pseudo-code pour présenter l'instruction for :

Code : Autre
1
2
3
4
for( instructions1 ; conditions ; instructions2 )
{
    autres instructions;
}


Son effet est le même qu'en C :

  1. exécuter instructions1 ;
  2. tant que conditions est vrai, exécuter :
    1. autre instructions ;
    2. instructions2.


Les structures



Les structures sont également possibles en GLSL. Elles se définissent bien sûr de la même façon, en utilisant le mot clé struct :

Code : Autre
1
2
3
4
struct MaStructure
{
    int a, b;
};


Vous pouvez bien sûr créer toutes sortes de variables dans votre structure (des vecteurs, des matrices, etc...). Une structure se crée et s'utilise ainsi :

Code : Autre
1
2
3
4
MaStructure str; // declaration
 
str.a = 0; // acces aux variables
str.b = str.a;


Vous noterez qu'il n'est pas nécessaire de préfixer la déclaration des variables de type structure avec le mot clé struct. Eh oui, le mot clé typedef n'existe pas en GLSL, plus besoin de vous embêter avec.

Le préprocesseur



Il fonctionne comme en langage C : toutes les commandes de préprocesseur doivent être préfixées par « # ». Parmi ces commandes, on retrouvera entre autre le fameux #define, qui permet de définir des macros, #undef qui les "dé-défini", mais aussi les instructions #if, #ifdef, #ifndef, #else, #elif et #endif, qui ont toutes le même effet qu'en C.

Par exemple, vous pourriez changer l'intégrité de votre shader juste avec une macro, comme ceci :

Code : Autre
1
2
3
4
5
6
7
8
9
#ifdef SUPERSHADER
 
// code source du super shader
 
#else
 
// code source d'un shader un peu moins bien
 
#endif


Ainsi, si SUPERSHADER est définie, seules les instructions contenues entre #ifdef et #else seront compilées, sinon ça sera celles qui sont entre #else et #endif.


Incompatibilités et différences



Les déclarations de variables



Contrairement au C89 qui exige que les variables soient définies au début de votre code, le GLSL autorise quant à lui la création de variables n'importe où dans votre shader (autorisé également en C99). Avec cette liberté de création de variables, il est possible de créer une variable dans une instruction for, comme ceci par exemple :

Code : Autre
1
2
3
4
for(int i = 0; i < 5; i++)
{
    ...
}


Les pointeurs et l'instruction switch



Les pointeurs de même que l'instruction switch n'existent pas en GLSL.

Créer et surcharger des fonctions

Déclarer une simple fonction



Tout est quasiment identique au C, mais je préfère tout de même mettre les choses au clair. Comme en C, une fonction possède :

  • une valeur de retour d'un certain type ;
  • un nom ;
  • des paramètres ;
  • un contenu, entre accolades {}.


La déclaration d'une fonction se fait comme en C, on commence par mettre son type de retour, son nom, puis ses paramètres entre parenthèses. Pour finir, on ouvre une accolade puis on place le contenu de notre fonction à l'intérieur :

Code : Autre
1
2
3
4
void my_func(void)
{
    // contenu
}


Vous voyez ici l'emploi du type void, qui signifie comme en C : vide. Donc notre fonction ne retourne rien et ne prend aucun paramètre. De même qu'en C, la notion de prototype existe. Déclarez vos prototypes tout en haut du code source, ainsi vous n'aurez aucun problème :

Code : Autre
1
2
3
4
5
6
7
8
9
10
11
float my_func(void);

void main(void)
{
    ...
}

float my_func(void)
{
    ...
}


Notez que d'une façon générale, si votre fonction n'a pas été déclarée, vous ne pourrez pas l'utiliser. Ainsi, soit vous déclarez son prototype tout en haut de votre code et vous vous épargnez tout problème, soit vous triez vos fonctions de façon sélective afin que les dépendances soient satisfaites.

Paramètres et valeur de retour



Nous pouvons également écrire une fonction qui prend un ou plusieurs paramètres, et renvoie une variable. Pour renvoyer une variable, vous devez mettre son type avant le nom de la fonction. Vous pouvez renvoyer n'importe quel type de variable en GLSL :

Code : Autre
1
2
3
4
5
6
7
8
vec3 my_func(void)
{
    vec3 result;
    
    ... // calculs horriblement complexes

    return result; // on renvoie le resultat
}


Notez ici le mot clé return, il existe également en GLSL et a le même effet qu'en C : renvoyer une valeur de retour en terminant l'exécution de la fonction.

Pour donner un paramètre à une fonction, il suffit de rajouter son type suivi du nom de la variable qui contiendra la valeur du paramètre, entre les parenthèses qui suivent le nom de la fonction, comme ceci :

Code : Autre
1
2
3
4
5
6
7
8
vec3 my_func(vec3 v)
{
    vec3 result;

    result = v * 2; // calculs horriblement complexes

    return result; // on renvoie le resultat
}


Comme en C, on accède à un paramètre en écrivant son nom. Ici, la super fonction que j'ai écrit relève réellement du génie : elle renvoie un vecteur qui est celui envoyé en paramètre multiplié par 2.


La surcharge des fonctions



Notion de surcharge



Nous avons déjà vu la surcharge des opérateurs dans le précédent chapitre. La notion de surcharge existe également pour les fonctions. Surcharger une fonction signifie, en gros, qu'on va attribuer un seul nom de fonction à plusieurs fonctions.

Par exemple, supposez que vous vouliez écrire une fonction qui renvoie le produit scalaire de deux vecteurs. En C, vous auriez écrit une fonction pour chaque type de vecteur : une pour les vecteurs à 2 dimensions et une autre pour les vecteurs en 3 dimensions, et elles auraient chacune un nom différent. Pas très pratique à utiliser.

Pour remédier à cela, la surcharge permet de ne créer qu'un seul nom de fonction pour deux fonctions différentes. Ainsi, vous pourrez appeler votre fonction de produit scalaire indifféremment avec des vecteurs 2D ou 3D :

Code : Autre
1
2
3
4
5
vec2 a, b;
vec3 x, y;

float resultat1 = produitScalaire(a, b);
float resultat2 = produitScalaire(x, y);


Cela évite de créer 36 noms de fonctions juste parce que le nombre et/ou le type de leur(s) paramètre(s) change. Nous pouvons aussi imaginer une fonction qui pourrait travailler aussi bien sur des entiers (int) que sur des flottants (float), dans ce cas la surcharge serait également utile.


Surcharger une fonction



Nous allons prendre l'exemple du produit scalaire, qui est un très bon exemple. Voici une fonction qui calcule le produit scalaire de deux vecteurs 3D :

Code : Autre
1
2
3
4
5
6
7
8
9
10
float produitScalaire(vec3 a, vec3 b)
{
    float resultat;

    // on calcul le produit scalaire (3D)
    resultat = (a.x * b.x) + (a.y * b.y) + (a.z * b.z);

    // on retourne le resultat
    return resultat;
}


Notez que les parenthèses dans le calcul du produit scalaire sont facultatives dans la mesure où * a la priorité sur +, elles ne sont présentes que pour une meilleure lisibilité.

Maintenant, nous voudrions que cette fonction marche aussi pour les vecteurs à 2 dimensions. En fait, il n'y a pas de secret : il faut re-écrire la fonction en entier. Ce qu'accepte le GLSL, contrairement au C, c'est d'avoir plusieurs fonctions du même nom, qui ne se différencient que par leurs paramètres et/ou leur type respectif.

Ainsi, pour surcharger notre fonction produitScalaire(), il nous suffit de rajouter une version de notre fonction qui calculera le produit scalaire de deux vecteurs 2D, comme ceci :

Code : Autre
1
2
3
4
5
6
7
8
9
10
float produitScalaire(vec2 a, vec2 b)
{
    float resultat;

    // on calcul le produit scalaire (2D)
    resultat = (a.x * b.x) + (a.y * b.y);

    // on retourne le resultat
    return resultat;
}


On n'oubliera pas de rajouter un prototype en haut de notre code pour chaque version de notre fonction.

Code : Autre
1
2
float produitScalaire(vec3, vec3);
float produitScalaire(vec2, vec2);



Exemple complet



Je vous propose un petit vertex shader tout simple pour illustrer tout ce que nous venons de voir, afin que vous sachiez comment emballer tout ça dans un joli code tout propre tout fini :

Code : Autre
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// prototypes de nos fonctions
float produitScalaire(vec4, vec4);
float produitScalaire(vec3, vec3);
float produitScalaire(vec2, vec2);

void main(void)
{
    // il est aussi possible d'acceder aux composantes d'un vecteur en utilisant
    // les noms r, g ou b, pour red, green et blue respectivement
    
    // calculs au hasard, pour donner un effet rigolo
    gl_FrontColor.b = produitScalaire(gl_Color, gl_Vertex);
    gl_FrontColor.g = produitScalaire(gl_Color, gl_Vertex * 2.0);

    gl_Position = gl_Vertex;
}

// version 4D
float produitScalaire(vec4 a, vec4 b)
{
    float resultat;

    // on calcul le produit scalaire (3D)
    resultat = (a.x * b.x) + (a.y * b.y) + (a.z * b.z);

    // on retourne le resultat
    return resultat;
}

// version 3D
float produitScalaire(vec3 a, vec3 b)
{
    float resultat;

    // on calcul le produit scalaire (3D)
    resultat = (a.x * b.x) + (a.y * b.y) + (a.z * b.z);

    // on retourne le resultat
    return resultat;
}

// version 2D
float produitScalaire(vec2 a, vec2 b)

{
    float resultat;

    // on calcul le produit scalaire (2D)
    resultat = (a.x * b.x) + (a.y * b.y);

    // on retourne le resultat
    return resultat;
}


Je vous avoue cependant que ce vertex shader ne fait rien de génial, il ne sert qu'à vous montrer l'implémentation complète d'une fonction en GLSL.

Quelques fonctions natives du GLSL

Le langage GLSL offre par défaut de nombreuses fonctions. Parmi ces fonctions, on retrouve beaucoup de fonctions mathématiques qui permettent de calculer à peu près tout et n'importe quoi, mais on retrouve aussi des fonctions indispensables effectuant des tâches bien précises propres aux shaders.

Tout d'abord, vous devez savoir que la plupart des fonctions du GLSL sont surchargées, ce qui facilite leur utilisation, qui devient alors intuitive et un vrai jeu d'enfant. Pour faire simple, je vous préviens d'avance : toutes les fonctions que je vais vous présenter sont surchargées, donc utilisez-les à volonté et dans toutes les circonstances. De plus, l'usage des fonctions prédéfinies du GLSL est fortement recommandé dans la mesure où la plupart de celles-ci sont directement implantées dans les cartes graphique, ce qui vous permet de tirer parti de toute la puissance de vos cartes et ainsi gagner en performance.


Fonctions de manipulation de vecteurs



Pour comprendre la plupart des fonctions que nous allons étudier ici, je vous recommande la lecture du chapitre annexe sur les vecteurs.

Normalisation de vecteurs



Le langage GLSL offre une fonction permettant de normaliser un vecteur. Cette fonction s'appelle normalize().
Elle prend un paramètre (un vecteur) et renvoie ce même vecteur, mais normalisé.
Voici un code pour illustrer la normalisation d'un vecteur v :

Code : Autre
1
2
3
vec3 v = vec3(0.2, 0.4, 0.6);

v = normalize(v);


Produit scalaire



Pour calculer le produit scalaire de deux vecteurs en GLSL, rien de plus simple : appelez la fonction dot(). Cette fonction prend deux paramètres. Ces paramètres sont les deux vecteurs dont on veut connaître le produit scalaire. dot() renvoie un flottant qui n'est autre que le résultat du produit :

Code : Autre
1
2
3
vec3 v1 = ..., v2 = ...;
...
float res = dot(v1, v2);


Produit vectoriel



Là encore, une fonction existe, il s'agit de cross(). Elle prend deux vecteurs en paramètres, et renvoie un vecteur qui est le résultat du produit vectoriel de ses deux paramètres :

Code : Autre
1
2
3
vec3 v1 = ..., v2 = ...;
...
vec3 res = cross(v1, v2);


Longueur d'un vecteur



Pour connaître la longueur d'un vecteur simplement, utilisez la fonction length() :

Code : Autre
1
float longueur = length( vec3(2.0, 0.8, 1.6) );


Distance entre deux vecteurs



Bien que cette solution soit envisageable :

Code : Autre
1
2
3
vec3 a, b;
...
float d = length( a - b );


Il en existe une plus explicite : utiliser la fonction distance() :

Code : Autre
1
2
3
vec3 a, b;
...
float d = distance( a, b );


Et voilà, ça sera tout pour les fonctions de manipulation de vecteurs :)

En ce qui concerne les additions/soustractions de vecteurs, rappelez-vous le précédent chapitre : les opérateurs en GLSL sont surchargés, par conséquent, vous n'aurez qu'à placer l'opérateur de votre choix entre deux vecteurs, ou entre un vecteur et une valeur.

Code : Autre
1
2
3
4
vec3 v1 = ..., v2 = ...;

vec3 add = v1 + v2;  // add = le resultat de l'addition des vecteurs v1 et v2
vec3 mul = v1 * 2.0; // chaque composante de mul = chaque composante de v1 * 2



La fonction ftransform()



Voici une fonction qui est souvent utilisée par les programmeurs pour... se faciliter la vie. Notez bien qu'elle n'est utilisable qu'au sein d'un vertex shader. Souvenez-vous lorsque vous avez créé votre premier vertex shader lors du précédent chapitre. Vous aviez attribué à la variable de sortie gl_Position le résultat de la multiplication de la position du sommet par les matrices modelview et projection combinées. Voici quel était le code final du vertex shader :

Code : Autre
1
2
3
4
void main(void)
{
    gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}

Ce code est plutôt lourd et long à coder.
Il est cependant remplaçable par celui-ci, qui a l'avantage d'être beaucoup plus léger :

Code : Autre
1
2
3
4
void main(void)
{
    gl_Position = ftransform();
}

Vous me demanderez sans doute quel est l'intérêt de la première méthode, ce à quoi je vous répondrai : quel est l'intérêt de la seconde vous voulez dire ? En fait la fonction ftransform() a pour effet de vous rendre la position finale du sommet comme si il avait été traité par le FFP.


Encore quelques fonctions



Je vous montre encore quelques fonctions, et après c'est bon, je vous aurai montré le principal (fonctions les plus utilisées).

Nous allons voir trois fonctions très simples, mais très pratiques :
  • min() ;
  • max() ;
  • clamp().


min()



Cette fonction renvoie la plus petite valeur entre deux valeurs fournises :

Code : Autre
1
2
3
float a = 0.2, b = 0.5;

float res = min(a, b);

Ici, res = 0.2

Notez que cette fonction peut être remplacée par l'instruction suivante (comme en C) :

Code : Autre
1
(a < b) ? a : b;

Tout comme de nombreuses fonctions, min() est surchargée, vous pouvez donc lui envoyer des vecteurs, elle vous renverra le plus court.

max()



Exactement l'inverse de min(), max() vous renvoie son plus grand paramètre :

Code : Autre
1
int res = max(2, 4); // res = 4

Elle est bien évidemment elle aussi surchargée.

clamp()



La fonction clamp() est un mélange des deux fonctions vues ci-dessus, elle prend trois paramètres :

Code : Autre
1
T clamp(T var, T minimum, T maximum);

L'emploi de 'T' représente juste un type quelconque.


et renvoie ceci :

Code : Autre
1
min(max(var, minimum), maximum);


Euh, j'ai rien compris, c'est normal ?

Oui, rassurez-vous :D

En fait, la fonction clamp() vous renvoie une valeur qui se situe forcément entre minimum et maximum.
clamp() renvoie var si sa valeur est située entre minimum et maximum, sinon elle renvoie la valeur la plus proche de var (minimum ou maximum).

Nous pouvons programmer clamp() comme ceci :

Code : Autre
1
2
3
4
5
6
if(minimum > var)
    return minimum;
else if(up < var)
    return maximum;
else
    return var;



Spécifications du GLSL



La version du langage étudié dans ce tutoriel possède des spécifications que vous trouverez sur le site d'OpenGL. Ceci est la documentation de référence et votre meilleur guide dans l'avenir pour l'apprentissage du GLSL.

Q.C.M.

Suis-je autorisé à utiliser le mot clé if dans un vertex shader ?
L'instruction switch existe-t-elle en GLSL ?
Que dois-je faire pour surcharger une fonction ?
Que fait la fonction normalize() ?
Comment créer un pointeur en GLSL ?

Statistiques de réponses au QCM

Je reconnais que ce chapitre avait un aspect "bourrage de crâne", mais il vous sera sûrement plus utile que vous ne le pensez. En effet, mine de rien nous avons appris beaucoup de choses très pratiques :
  • les instructions de contrôle, et les incompatibilités avec le C ;
  • les fonctions et la surcharge des fonctions ;
  • quelques fonctions du GLSL, que nous utiliserons fréquemment.

Maintenant que vous connaissez le langage GLSL, vous devriez être aptes à comprendre un code source quelconque, sauf bien sûr si celui-ci comporte des fonctions du GLSL qui vous sont inconnues.
Chapitre précédent Sommaire Chapitre suivant

Partager

Il n'y a pas encore de commentaire pour ce tuto.