Aller au menu - Aller au contenu

Notions supplémentaires


Informations sur le tutoriel

Avatar
Auteur : Yno
Visualisations : 5 072
Licence : Creative Commons BY-SA


Plus d'informations Plus d'informations
Allez courage, plus qu'un dernier chapitre et vous en saurez suffisemment sur le langage GLSL en lui même :)

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 ;)

Allons-y pour le dernier chapitre dans le genre "rebutant" :p
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.

Ça fait pas très sérieux...

Le GLSL n'est pas un langage aussi rigoureux que le C, c'est plutôt un langage "jouet", il suffit d'enchaîner quelques instructions dans un main() et hop, 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 :D :

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. Ça fait parti des petits plus de ce langage par rapport au C :)

Le préprocesseur



Du préprocesseur en GLSL ?? o_O

Eh bien oui ! :D
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" :D , 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



Alors là, je vous préviens tout de suite : les pointeurs en GLSL, ça n'existe pas. Pas la peine d'aller chercher plus loin :D De ce fait, les chaînes de caractères n'existent pas non plus.

L'instruction switch



L'instruction switch n'existe pas non plus en GLSL.


___________________________________________


Cette "liste" n'est sûrement pas exhaustive, mais elle présente tout de même le strict minimum pour ne pas mélanger C et GLSL, ce qui est important.


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
5
6
7
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.


Où déclarer une fonction ?

Tout comme 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
12
13
14
15
16
17
18
19
20
21
float my_func(void);



void main(void)

{

    ...

}



float my_func(void)

{

    ...

}

Un prototype prend un point-virgule à la fin, comme en C.


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. Personnellement, le choix est vite fait ;)

Le principe de fonctionnement global est pareil qu'en C.

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
9
10
11
12
13
14
15
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.

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
9
10
11
12
13
14
15
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 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 qu'on va attribuer un seul nom de fonction à plusieurs fonctions.

Hein ??

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
6
7
8
9
vec2 a, b;

vec3 x, y;



float resultat1 = produitScalaire(a, b);

float resultat2 = produitScalaire(x, y);


La classe hein ? :p
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



Bien, maintenant que vous avez compris à quoi peut servir la surcharge, nous allons voir comment mettre cela en pratique :)

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
11
12
13
14
15
16
17
18
19
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;

}


Les parenthèses dans le calcul du produit scalaire sont facultatives, 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
11
12
13
14
15
16
17
18
19
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
3
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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
// 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 ;)

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
4
5
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
4
5
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
4
5
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
4
5
vec3 a, b;

...

float d = length( a - b );


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

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

...

float d = distance( a, b );


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

Hé, on a pas vu les additions/soustractions de vecteurs, comment on fait ça ?

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
5
6
7
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 :-° Il n'y a pas grand chose à dire sur cette fonction, si ce n'est qu'elle n'est utilisable qu'au sein d'un vertex shader.

Que fait cette fonction ?

Vous vous souvenez quand vous avez créé votre premier vertex shader lors du précédent chapitre ? Vous avez 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. Bien sûr que vous vous en souvenez, c'était à l'exercice :-°
Bon, voici quel était le code final du vertex shader :

Code : Autre
1
2
3
4
5
6
7
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
5
6
7
void main(void)

{

    gl_Position = ftransform();

}

Quoi ?! Mais alors quel est l'intérêt de la première méthode ?

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
7
8
9
10
11
if(minimum > var)

    return minimum;

else if(up < var)

    return maximum;

else

    return var;



Liste des fonctions du GLSL



Comme cela a déjà été fait, je n'allais pas m'amuser à re-énumérer toutes les fonctions du GLSL :-° Je ne vous ai présenté que les plus utiles et les plus fréquemment utilisées.

Ainsi je vous renvoie vers cet excellent PDF en français, qui, en plus de proposer une liste de toutes (ou presque ?) les fonctions du GLSL, offre un tuto couvrant quasiment tous les aspects du langage :
http://www.g-truc.net/article/glsl.pdf

Et n'oubliez pas les spécifications du langage 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


Nous y voilà enfin :)

Je reconnais que ce chapitre était un peu "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, vous verrez qu'elles sont très pratiques ;)


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.

À présent, nous allons aller à la découverte des fonctionnalités du GLSL et des choses qu'il permet de faire.

Et voilà, c'est fini pour cette partie du tutoriel. Je vous invite à présent à venir voir ce qui vous attend dans la seconde partie, vous allez voir : on va beaucoup plus s'y amuser, nous allons enfin commencer à réaliser de vrais shaders :)
Prêts pour débuter une nouvelle experience ? Allons-y ;)
Chapitre précédent Sommaire Chapitre suivant

Informations sur le tutoriel

Retour en haut Retour en haut

Créé : Le 06/04/2007 à 02:56:27
Modifié : Le 08/11/2008 à 15:21:06
Avancement : 100%

Commentaires