• Facile

Ce cours est visible gratuitement en ligne.

Vous pouvez être accompagné et mentoré par un professeur particulier par visioconférence sur ce cours.

J'ai tout compris !

Mis à jour le 13/03/2017

OpenGL Shading Language

Connectez-vous ou inscrivez-vous gratuitement pour bénéficier de toutes les fonctionnalités de ce cours !

Dans le chapitre précédent, nous avons vu ce qu'étaient les shaders et comment les implémenter au sein de nos applications. Nous avons même écrit, en quelque sorte, notre propre compilateur qui permet désormais de compiler absolument tous leurs codes sources. :p

Aujourd'hui, nous allons justement passer aux codes sources mêmes et étudier le langage de programmation avec lequel ils sont faits : l'OpenGL Shading Language. Nous nous concentrerons principalement sur sa syntaxe qui se rapproche fortement de celle du C mais qui contient tout de même quelques différences.

Si vous vous sentez prêts alors on peut commencer. :magicien:

Un peu d'histoire

L’histoire des shaders

La création

Ce qu'il faut savoir avec les shaders, c'est que leur intégration au sein des jeux-vidéo est une chose assez récente par rapport à leur création. En effet, ceux-ci ont été créés en 1988 par les studios d'animation Pixar qui avaient remarqué qu'il était possible de traiter tous les pixels un par un pour leur appliquer des instructions spécifiques. Cela leur permettait de manipuler leurs rendus non seulement pour les améliorer mais aussi pour les rendre plus réalistes sans surcharger les ressources des ordinateurs.

Le premier standard à utiliser les shaders est le RenderMan (RIS) développé par les studios du même nom. Depuis, ils se sont développés dans tous les domaines qui traitent la 3D tels que les films d'animation, le cinéma et évidemment les jeux-vidéo. Ils permettent aujourd'hui d'exploiter efficacement la carte graphique pour effectuer une multitude de calculs mathématiques. Leur plus grand intérêt est évidemment la réalisation d'effets réalistes que l'on retrouve aujourd'hui dans tous les jeux-vidéo à la mode. ;)

Au niveau de leur fonctionnement, vous savez maintenant que les shaders sont des petits programmes qui vont traiter les vertices et les pixels. Ils sont spécialement conçus pour faire ce genre d'opération, ils n'ont donc pas peur de traiter des millions de données à chaque seconde. :p

Les différents langages

Si les shaders sont des programmes alors ils possèdent forcément un code source pour savoir ce qu'ils doivent faire. Et évidemment, ces codes sources doivent être écrits avec un langage de programmation. Depuis 1988, plusieurs entreprises ont tenté de sortir leur propre langage soit pour s'imposer dans le domaine, soit pour un type d'utilisation spécifique. Certains sont plus ou moins connus et même plus ou moins compliqués à utiliser.

Il existe deux types de langage utilisables pour la programmation des shaders. Jusqu'à récemment, il fallait en utiliser certains qui étaient proches de l'Assembleur, ils étaient donc considérés comme étant de bas-niveau. Pour ceux qui ne savent pas ce qu'est l'Assembleur, sachez qu'il s'agit d'un langage de programmation extrêmement difficile à manier pour les non-initiés. Si vous voulez faire un jeu-vidéo 3D avec ça je vous conseille de vous lever très tôt chaque matin pour avancer dans le développement. :p

Mais heureusement pour nous, il existe aussi des langages dits de haut-niveau, donc plus faciles à utiliser, qui se rapprochent fortement de ceux que l'on connait aujourd'hui. Nous pouvons en citer principalement 3 :

  • OpenGL Shading Language (GLSL) : créé par le consortium Architecture Review Board qui gère OpenGL, il est évidemment compatible avec cet API et en est même le standard actuel

  • C for Graphics (Cg) : créé par Nvidia, il est compatible non seulement avec OpenGL mais aussi avec son concurrent Microsoft Direct3D

  • High Level Shader Language (HLSL) : créé par Microsoft, il est uniquement compatible avec Direct3D

Ces langages ne sont pas compatibles avec toutes les API 3D mais ils sont tous utilisés dans le monde du jeu-vidéo. Vu que nous, nous programmons avec OpenGL, nous allons donc utiliser l'OpenGL Shading Language (GLSL) pour développer nos shaders.

OpenGL Shading Language

Le GLSL est donc un langage de programmation créé par l'Architecture Review Board pour permettre aux programmeurs d'utiliser directement la puissance des cartes graphiques sans passer par l'ancien pipeline 3D. Cela permet de gagner en rapidité car on évite tous les calculs superflus qui existaient auparavant. On gagne également en flexibilité car on peut traiter chaque pixel individuellement.

L'avantage de ce langage c'est que d'une part il est multiplateforme, c'est-à-dire qu'il est supporté par la majorité des cartes graphiques, et d'autre part sa syntaxe se rapproche énormément de celle du C. Il est plus facile d'apprendre à l'utiliser lui plutôt que d'apprendre l'Assembleur. :p

Malgré sa notoriété, le GLSL n'est né que très récemment. Comparé à la création des shaders qui date de 1988, il fait office de bambin puisqu'il n'a été accepté officiellement qu'en 2004 avec la sortie d'OpenGL 2.0. Pour information, la première version d'OpenGL est sortie en 1992, ce qui fait 12 ans de décalage entre les deux ! :lol:

Cependant, durant sa courte histoire, le GLSL n'a pas chômé puisqu'il a fournit pas moins de 10 versions en l'espace de 8 ans. En voici quelques unes :

  • 1.20 : sortie avec OpenGL 2.1, c'est la version la plus connue à ce jour.

  • 1.30 : sortie avec OpenGL 3.0, elle introduit une nouvelle façon de programmer et déprécie certaines fonctionnalités (même s'il reste toujours possible de les utiliser).

  • 1.40 : sortie avec OpenGL 3.1. Cette fois-ci, les fonctionnalités dépréciées sont totalement supprimées, la nouvelle façon de programmer devient le nouveau standard.

  • 1.50 : sortie avec OpenGL 3.2, nous utiliserons celle-ci car c'est la plus haute version avec laquelle nous pourrons programmer de façon 'multiplateforme'. Au-delà, certains OS comme Mac OS X ne supportent plus le langage.

Contrairement à ce qu'on pourrait penser, il n'existe pas de version 1.60, 1.70 ... A partir d'OpenGL 3.3, le numéro des versions devient le même que celui de l'API. Pour OpenGL 3.3 par exemple, le GLSL passe en 3.30, pour la 4.0 il passe en 4.0, ...

Enfin, vous savez maintenant ce que sont les langages de programmation shader et vous avez même eu le droit à un petit cours d'histoire pour votre culture générale. :p

Pour la suite du tutoriel, nous utiliserons le GLSL dans sa version 1.50. C'est avec lui que nous serons bientôt capables de faire des effets réalistes que l'on pourra intégrer dans nos scènes 3D. D'ailleurs, nous allons tout de suite passer à l'étude de ce langage en commençant par sa syntaxe de base.

Les variables

Le code source minimal

Comme nous l'avons vu dans l'introduction, l'OpenGL Shading Language est fortement inspiré du C, vous n'avez donc pas à être effrayés par l'apprentissage d'un nouveau langage de programmation.

Dans cette partie, nous nous concentrerons principalement sur sa syntaxe avec la gestion des variables, les fonctions, etc. Je ferai en sorte de vous montrer des exemples à chaque fois et nous en profiterons pour faire quelques exercices.

Je n'ai malheureusement pas d'IDE à vous proposer pour coder vos shaders. Par habitude, j'utilise toujours celui pour coder les applications classiques et je ne connais que très peu les autres pour pouvoir vous les conseiller efficacement. Si vous voulez ne pas être embêtés à ouvrir plusieurs programmes, je vous propose de coder également sur le même IDE que vous utilisez en temps normal.

Enfin maintenant que l'on sait tout ce que l'on à savoir, nous pouvons enfin commencer l'étude du GLSL. La première chose que nous allons voir concerne le code source minimal demandé par un shader pour pouvoir être compilé. Celui-ci est assez simple mais mérite tout de même quelques explications :

void main()
{

}

Un peu cours n'est-ce pas ? :lol:

Le code minimal contient donc simplement une fonction qui main() qui ne prend et qui ne renvoie aucun paramètre. Remarquez que la syntaxe des fonctions est strictement identiques au C. Si vous essayez de compiler ce code vous n'aurez aucun message d'erreur, le shader l'acceptera sans problème mais il n'affichera pas grand chose.

La fonction main() a la même utilité qu'en C, c'est elle qui est appelée en première au lancement d'un programme. Nous placerons donc nos premières futures instructions à l'intérieur. Cependant, contrairement au C cette fonction n'a pas besoin de renvoyer une variable pour indiquer si tout s'est bien passé (le fameux return 0;).

En définitif, ce code constitue la source minimale pour compiler un shader, nous devrons donc implémenter la fonction main() à chaque fois. ;)

Les Variables

Les variables 'classiques'

Comme tout tutoriel sur les langages de programmation, nous allons nous attaquer en premier lieu aux variables.

Il existe plusieurs types de variable avec le GLSL dont certains sont assez déroutants car ils n'ont pas leur équivalent en C. On va commencer par voir ceux qu'on a l'habitude d'utiliser car ce sont les plus simples à comprendre :

  • bool : booléen pouvant prendre la valeur true ou false

  • int : les nombres entiers

  • uint : les nombres entiers non-signés

  • float : les nombres flottants (décimaux)

L'avantage des variables en GLSL c'est qu'elles se comportent exactement de la même façon qu'en C. Nous pouvons donc les additionner, les multiplier, leur affecter une valeur, etc. Le seul point auquel il faut faire attention c'est leur initialisation qui ne se fait non pas à l'aide de parenthèses mais avec le signe = .

Prenons un petit exemple en déclarant deux variables de type int, puis additionnons-les :

void main()
{
    // Déclaration de deux variables integer

    int mesChats = 5;
    int chatDuVoisin = 1;


    // Addition

    int resultat = mesChats + chatDuVoisin;
}

Ce code pourrait parfaitement servir à un programme normal. :p

Vous voyez ici que la déclaration et l'utilisation de variable se passent de la même manière qu'en C, chaque instruction se termine même par un point-virgule. Tout ce que vous connaissez sur les variables s'applique au GLSL, à savoir :

  • Les opérations arithmétiques

  • Les affectations

  • L'incrémentation et la décrémentation (++ et --)

  • ...

Vous avez peut-être même remarqué l'utilisation des commentaires dans le code source. Leur syntaxe reste aussi la même :

void main()
{
    // Petit commentaire


    /* Gros commentaire,
       Parce que j'ai beaucoup de chose à dire */
}
Les tableaux

Les tableaux, qui permettent de rassembler des données du même type en mémoire, sont aussi utilisables avec les shaders. Ils se comportent aussi de la même manière, ils possèdent donc une taille et ses cases sont accessibles à l'aide un indice.

Pour initialiser un tableau, il suffit de lui donner un nom ainsi qu'une taille entre crochets. Pour accéder ou affecter une valeur aux cases, on utilise également les crochets avec un indice qui peut commencer à 0 ou qui peut se terminer par la taille - 1.

Voici un petit exemple de l'utilisation d'un tableau de 3 flottants :

void main()
{
    // Déclaration d'un tableau

    float tableau[3] = {1.0, 2.0, 3.0};


    // Utilisation

    float premiereCase = tableau[0];
}
Les vecteurs

Les variables que nous venons de voir ont l'avantage d'être faciles à comprendre car nous avons l'habitude de les utiliser dans nos codes sources classiques. Cependant, il existe encore d'autres types propres au GLSL qui sont un peu spéciaux. D'ailleurs, je dois vous avouer que vous les connaissez déjà en fait. :p Ces derniers peuvent être comparés aux structures du C mais ce sont bel et bien des types de variable.

Ils ont été créés pour faciliter la vie aux développeurs car ils permettent de faire pas mal de calculs mathématiques relatifs à la 3D sans se prendre la tête. Si je vous dis que vous les connaissez déjà c'est parce que la librairie GLM que l'on utilise depuis le début du tuto est directement inspiré du GLSL et donc de ces types de variable. C'est à dire que l'on retrouve des types communs entre les deux, même s'il s'agit d'un objet d'un coté et d'une variable de l'autre.

Le premier de ces types concerne les vecteurs, il se décline en plusieurs versions :

  • vec2 : vecteur à 2 coordonnées

  • vec3 : vecteur à 3 coordonnées

  • vec4 : vecteur à 4 coordonnées

Le type vec3 devrait vous dire quelque chose il me semble. :p Et oui, l'objet vec3 qu'on utilise dans l'application C++ est inspiré directement du type de variable vec3.

Ces variables en GLSL se rapproche plus à des structures C car ils contiennent des sortes de "sous-variable". Par exemple, le type vec2 possède deux coordonnées (x, y) qui sont accessibles via l'utilisation du point '.' tout comme les structures :

void main()
{
    // Vecteur à 2 coordonnées

    vec2 monVecteur;


    // Accès aux coordonnées

    monVecteur.x = 1.0;
    monVecteur.y = 2.0;
}

Bien entendu, on peut affecter des valeurs à ces coordonnées. Elles n'auraient que bien peu d'intérêt sinon. :p Ces valeurs doivent être des float et non des int.

Les types vec3 et vec4 ont eux-aussi les coordonnées (x, y) sauf qu'ils en possèdent respectivement 1 (x, y, z) et 2 (x, y, z, w) en plus.

L'accès à ces nouvelles coordonnées se fait exactement de la même façon ben sûr :

void main()
{
    // Vecteurs à 3 et 4 coordonnées

    vec3 monVecteur;
    vec4 monGrandVecteur;


    // Accès aux coordonnées du premier vecteur

    monVecteur.x = 1.0;
    monVecteur.y = 2.0;
    monVecteur.z = 3.0;


    // Accès aux coordonnées du second vecteur

    monGrandVecteur.x = 1.0;
    monGrandVecteur.y = 2.0;
    monGrandVecteur.z = 3.0;
    monGrandVecteur.w = 4.0;
}

Vu que ces vecteurs sont des variables comme les autres, nous pouvons donc utiliser les opérateurs arithmétiques sur eux comme le +, -, *, etc :

void main()
{
    // Vecteurs 

    vec3 position, orientation;


    // Affectation de valeur

    position.x = 10.0;
    orientation.x = 5.0;
    ....


    // Opérations

    vec3 resultat = position + orientation;
    resultat *= 0.5;
}

Comme d'habitude avec les vecteurs, vous devez faire attention au nombre de coordonnées avant d'effectuer des opérations entre eux. Une variable vec2 ne peut pas être multipliée par un vec4 par exemple.

Petit bonus : il est possible de faire des tableaux de vecteur vu que ce sont des variables (j'espère vous l'avoir assez rabâché ^^ ) :

void main()
{
    // Vecteurs

    vec4 vecteur1, vecteur2;


    // Tableau

    vec4 montableau[2] = {vecteur1, vecteur2};
}
Les matrices

Aaaaah les matrices, je suis sûr que vous êtes tous contents de les retrouver. :p Surtout que vous pouvez l'être car le GLSL nous fournit très gentiment des types de variable qui devraient vous dire quelque chose.

Les matrices sont des outils mathématiques incontournables dans la 3D. Il était donc évident de les intégrer dans le langage de programmation des shaders, au même titre que les vecteurs. Elles sont tellement importantes qu'elles possèdent même plusieurs types de variable. Voici ceux que l'on utilisera le plus :

  • mat2 : matrice carrée d'ordre 2

  • mat3 : matrice carrée d'ordre 3

  • mat4 : matrice carrée d'ordre 4

Hum mat4, moi j'dis ça me rappellerait quelque chose. :-°

Les matrices en GLSL se déclarent simplement : il suffit d'utiliser un des types précédents suivi du nom qu'on veut donner.

void main()
{
    // Matrice carrée d'ordre 3

    mat3 matrice;
}

Leur principal avantage c'est qu'elles sont AUSSI considérées comme des variables tout comme les vecteurs. Il est donc possible de faire des opérations arithmétiques dessus :

void main()
{
    // Matrices

    mat3 matrice1, matrice2;


    // Multiplication

    mat3 resultat = matrice1 * matrice2;
}

Simple n'est-ce pas ?

Je ne vous parle pas de la façon d'accéder aux valeurs des matrices car elles fonctionnement d'une manière différente de celle que l'on connait. En fait, elles se lisent colonne par colonne plutôt que ligne par ligne. Nous verrons cela dans un prochain chapitre pour éviter de s'embrouiller. ;) En revanche, nous allons voir dans cette partie la façon de les initialiser.

Enfin, vous connaissez maintenant tous les types de variable qu'il existe en GLSL. Ou plutôt presque tous car il en existe encore plein d'autres comme les vecteurs d'entier (ivec), les matrices non-carrées (mat3x4), etc. Je ne vous ai présenté ici que ceux que nous utiliserons le plus, le reste est plus anecdotique.

Les constructeurs

L'initialisation simple

Les constructeurs sont des outils très puissants et très flexibles qui vont nous permettre de faire gagner pas mal à temps à nos shaders. C'est une des seuls points que l'on peut rapprocher au C++, même s'il ne s'agit pas de réel constructeur de classe car la notion d'objet n'existe pas en GLSL.

Leur utilité vient du fait qu'ils permettent d'initialiser les variables (vecteurs et matrices compris) en une seule ligne de code. Il devient alors inutile d'affecter toutes les valeurs à la main. Le nom du constructeur à utiliser correspond au type de variable à initialiser.

Par exemple, si on veut initialiser une variable vec3 avec les coordonnées (1.0, 2.0, 3.0), il suffit de faire :

void main()
{
    // Initialisation d'un vecteur à 3 coordonnées

    vec3 vecteur = vec3(1.0, 2.0, 3.0);
}

C'est exactement la même chose qu'avec GLM. Ce qui est normal d'ailleurs puisque les développeurs de cette librairie ont fait en sorte que le comportement C++ des objets se rapproche au maximum de celui des variables en GLSL. Même l'utilisation faite des constructeurs est recopiée.

Ce constructeur nous évite au final d'avoir à faire :

void main()
{
    // Initialisation d'un vecteur à 3 coordonnées

    vec3 vecteur;

    vecteur.x = 1.0;
    vecteur.y = 2.0;
    vecteur.z = 3.0;
}

Les constructeurs font très plaisir quand vous avez plusieurs vecteurs à initialiser dans votre shader, croyez-moi. :p

Petit bonus : Si vous voulez initialiser un vecteur nul, vous n'avez pas besoin de renseigner toutes les coordonnées. Seule la valeur 0.0 sera nécessaire, le constructeur saura qu'il doit tout initialiser avec :

void main()
{
    // Initialisation d'un vecteur nul

    vec4 vecteur = vec4(0.0);
}

Les vecteurs ne sont pas les seuls concernés par les constructeurs, les matrices aussi ont le droit d'en profiter. Voici, par exemple, comment initialiser une matrice carrée d'ordre 3 :

void main()
{
    // Initialisation d'une matrice

    mat3 matrice = mat3(0.0, 3.0, 6.0,
                        1.0, 4.0, 7.0,
                        2.0, 5.0, 8.0);
}

J'ai volontairement fait un retour à la ligne toutes les 3 valeurs pour que le code soit plus lisible, il est évidemment possible de tout mettre en une seule.

On remarque dans cet exemple la présence de 9 valeurs allant de 0.0 à 8.0 qui permettent d'initialiser la matrice. Si on avait utilisé un type mat4, alors il en aurait fallu 16. On remarque aussi que les valeurs sont arrangées en colonnes. Si vous suivez les valeurs des yeux dans l'ordre croissant, vous remarquerez cette particularité.

C'est compliqué d'utiliser les matrices comme ça, je vais devoir m'adapter à chaque fois que je vais m'en servir ?

Non pas vraiment car elles seront la plupart du temps déjà données. Nous n'aurons donc pas à jouer avec cette lecture en colonne. ;)

Petit bonus : pour initialiser une matrice nulle, c'est-à-dire qu'avec des valeurs égales à 0.0, il existe heureusement une astuce qui nous évite tous les petits soucis précédents. En fait, il suffit juste de mettre une seule valeur 0.0 dans le constructeur comme pour les vecteurs :

void main()
{
    // Matrice nulle

    mat4 matrice = mat4(0.0);
}

Plus simple que l'exemple précédent n'est-ce pas ? :p

Dernier bonus : si vous voulez initialiser une matrice d'identité, vous pouvez faire exactement la même chose qu'avec GLM en appelant le constructeur avec la valeur 1.0. Ce dernier saura automatiquement qu'il doit affecter cette valeur à la diagonale :

void main()
{
    // Matrice d'identité

    mat4 matrice = mat4(1.0);
}

Cette astuce ne fonctionne qu'avec les matrices. Pour les vecteurs, votre compilateur vous râlera dessus car il demandera la valeur des autres coordonnées.

L'initialisation à partir d'autres variables

L'utilité des constructeurs ne s'arrête pas avec des valeurs statiques, il est tout à fait possible d'initialiser une variable à partir d'autres variables pré-existantes. C'est même une chose que l'on fera très souvent dans nos codes sources. :p

Le gros avantage également c'est que vous pouvez initialiser des variables à partir de n'importe quelle autre variable quelque soit son type. Par exemple, vous pouvez très bien utiliser un vec2 pour construire un vec4 à partir de ses coordonnées :

void main()
{
    // Vecteur

    vec2 petitVecteur = vec2(1.0, 2.0);


    // Initialisation à partir d'un autre vecteur

    vec4 grandVecteur = vec4(petitVecteur);
}

Vous avez compris le principe ? Le constructeur vec4() va prendre automatiquement les coordonnées (x, y) du premier vecteur pour les copier dans le second. Nous aurions pu utiliser ce code pour faire la même chose :

void main()
{
    // Vecteur

    vec2 petitVecteur = vec2(1.0, 2.0);


    // Initialisation à partir d'un autre vecteur

    vec4 grandVecteur;

    grandVecteur.x = petitVecteur.x;
    grandVecteur.y = petitVecteur.y;
}

Alors bon, je dois vous avouer que ce code n'est pas totalement correcte. En fait, si nous le compilions nous nous retrouverions avec un beau message d'erreur car le constructeur ne sait pas comment initialiser les dernières coordonnées (z, w). Il faut donc leur affecter une valeur à elles-aussi.

Pour cela, il suffit simplement de rajouter 2 autres valeurs en paramètres :

void main()
{
    // Vecteur

    vec2 petitVecteur = vec2(1.0, 2.0);


    // Initialisation à partir de la variable

    vec4 grandVecteur = vec4(petitVecteur, 3.0, 4.0);
}

Cette fois, notre compilateur est content car toutes les coordonnées ont le droit à une valeur.

D'ailleurs, il est même possible d'inverser les trois paramètres en plaçant les valeurs 3.0 et 4.0 au début :

void main()
{
    // Vecteur position

    vec2 vecteur = vec2(1.0, 2.0);


    // Vecteur

    vec4 vecteur2 = vec4(3.0, 4.0, vecteur);
}

Si on fait ça en revanche, les données ne seront pas initialisées de la même façon. C'est-à-dire que la valeur 3.0 sera affectée à la coordonnée x et la valeur 4.0 à y. Pour z et w, le constructeur va prendre ce qu'il lui reste, à savoir respectivement les coordonnées (x, y) de la variable vec2.

Ce code revient à faire la chose suivante :

void main()
{
    // Vecteur

    vec2 petitVecteur = vec2(1.0, 2.0);


    // Autre vecteur

    vec4 grandVecteur;

    grandVecteur.x = 3.0;
    grandVecteur.y = 4.0;
    grandVecteur.z = petitVecteur.x;
    grandVecteur.w = petitVecteur.y;
}

Allez, on prend un dernier exemple pour être sûr que vous ayez compris. Si je vous montre le bout de code suivant, seriez-vous capable de trouvez l'équivalent en utilisant un constructeur ?

void main()
{
    // Vecteur

    vec2 petitVecteur = vec2(5.0, 8.0);


    // Autre vecteur

    vec4 grandVecteur;

    grandVecteur.x = 7.0;
    grandVecteur.y = petitVecteur.x;
    grandVecteur.z = petitVecteur.y;
    grandVecteur.w = 2.0;
}

J'ai volontairement changé les valeurs pour vous déstabiliser. :p

Vous avez trouvé ?

void main()
{
    // Vecteur

    vec2 petitVecteur = vec2(5.0, 8.0);


    // Autre vecteur

    vec4 grandVecteur = vec4(7.0, petitVecteur, 2.0);
}

Bien évidemment, je passe à coté de toute la subtilité offerte par le GLSL au niveau des variables. Cependant, nous en avons vu assez pour pouvoir travailler nos shaders efficacement. ^^

Les structures de contrôle

Les structures de contrôles

Nous avons vu le plus gros du chapitre avec la partie sur les variables, il y avait pas mal de petites notions à voir. Nous allons maintenant nous attaquer à tout ce qui concerne les structures de contrôle comme les conditions, les boucles, etc. Il n'y aura pas de surprise cette fois, vous connaissez déjà tout. :p

Les conditions

Les conditions permettent de déclencher une portion de code en fonction de la valeur d'une variable. Il y a deux manières de les utiliser :

  • Soit avec les instructions : if, else if, else

void main()
{
    // Variable

    int variable = 1;


    // Conditions

    if(variable == 0)
        ... ;

    else if(variable == 1)
        ... ;

    else
        ... ;
}
  • Soit avec les instructions : switch, case

void main()
{
    // Variable

    int variable = 1;


    // Conditions

    switch(variable)
    {
        // Cas 1

        case 0:
            ... ;
        break;


        // Cas 2

        case 1:
            ... ;
        break;


        // Sinon

        default:
            ... ;
        break;
    }
}
Les boucles

Les boucles permettent de répéter une portion de code en fonction d'une condition donnée. Ces conditions sont les mêmes que celles vues précédemment. Il y existe trois façons d'utiliser une boucle :

  • Soit avec l'instruction while

void main()
{
    // Variable

    int variable = 0;


    // Boucle 

    while(variable < 10)
        variable++;
}
  • Soit avec les instructions do, while

void main()
{
    // Variable

    int variable = 0;


    // Boucle 

    do
    {
        variable++;

    }while(variable < 10);
}
  • Soit avec l'instruction for

void main()
{
    // Variable

    int variable = 0;


    // Boucle 

    for(int i = 0; i < 10; i++)
        variable++;
}

Les fonctions

La déclaration

Les fonctions sont des fonctionnalités importantes avec les langages de programmation. Nous serions très malheureux sans elles puisqu'il faudrait tout coder au même endroit. Imaginez si nous devions réunir toutes les classes d'un projet C++ dans un seul fichier ! :lol:

Heureusement pour nous, ça ne sera pas le cas avec le GLSL car il permet la création et l'utilisation de fonctions. L'avantage en plus c'est qu'elles se comportent exactement comme en C il n'y a aucun différence. Nous retrouvons donc leurs caractéristiques principales :

  • Des paramètres

  • Un nom

  • Une variable retournée si besoin

Le prototype, qui représente la signature de la fonction, n'est pas obligatoire mais je vous conseille de l'utiliser pour simplifier la lecture de vos codes sources. Il se place juste avant la fonction main() pour que le compilateur puisse connaitre leur existence avant d'attaquer le programme.

Voici un exemple de prototype de fonction :

// Prototype

vec4 convertirVecteur(vec2 vecteur);


// Fonction main

void main()
{

}

L'implémentation se fait après la fonction main() de façon à ce que ce soit plus lisible :

// Implémentation de la fonction

vec4 convertirVecteur(vec2 vecteur)
{
    vec4 resultat = vec4(vecteur, 0.0, 0.0);

    return resultat;
}

Notez la présence du mot-clef return qui permet de retourner une variable.

Ce qui donnerait un shader final :

// Prototype

vec4 convertirVecteur(vec2 vecteur);


// Fonction main

void main()
{

}


// Implémentation

vec4 convertirVecteur(vec2 vecteur)
{
    vec4 resultat = vec4(vecteur, 0.0, 0.0);

    return resultat;
}
L'utilisation

Les fonctions s'utilisent également de la même manière qu'en C. Il suffit de les appeler en leur donnant les paramètres qu'elles demandent.

Si on reprend l'exemple précédent :

void main()
{
    // Vecteur

    vec2 monPetitVecteur = vec2(1.0, 2.0);


    // Appel à la fonction de conversion

    vec4 monGrandVecteur = convertirVecteur(monPetitVecteur);
}
Les surcharges

La surcharge de fonction est la deuxième notion du GLSL que l'on peut rapprocher au C++. Il est possible d'utiliser plusieurs fois le même nom pour des fonctions différentes. Les conditions qui permettent de faire cela sont les mêmes qu'avec le C++, à savoir que les paramètres doivent être différents en nombre ou en type.

On reprend une fois de plus l'exemple précédent en surchargeant la fonction convertirVecteur() afin qu'elle puisse prendre en paramètre des variables de type vec3:

// Prototype original

vec4 convertirVecteur(vec2 vecteur);


// Surcharge

vec4 convertirVecteur(vec3 vecteur);

Il faut bien évidemment faire une seconde implémentation car les deux prototypes représentent des fonctions différentes :

// Implémentation de la surcharge

vec4 convertirVecteur(vec3 vecteur)
{
    vec4 resultat = vec4(vecteur, 0.0);

    return resultat;
}

Pour utiliser cette surcharge, il suffit aussi de l'appeler par son nom. Le shader s'occupera tout seul de savoir s'il doit prendre celle-ci ou la fonction originale :

void main()
{
    // Vecteur

    vec3 monPetitVecteur = vec3(1.0, 2.0, 3.0);


    // Appel à la fonction de conversion

    vec4 monGrandVecteur = convertirVecteur(monPetitVecteur);
}
Les pointeurs

Malheureusement, les pointeurs n'existent pas en GLSL, et je ne vous parle même pas des références qui existent encore moins. Il n'est donc pas possible de modifier plusieurs variables dans une seule fonction, il faudra en utiliser plusieurs. Mais je vous rassure, nous n'aurions quasiment jamais eu besoin de pointeur de toute façon. ;)

Divers

Le préprocesseur

Le préprocesseur est un processus qui s'exécute juste avant le compilateur et qui permet de filtrer ou d'ajouter des instructions au code source original.

Sa particularité vient du fait qu'il ne lit pas les lignes de code 'normales' mais uniquement celles que l'on appelle des directives de préprocesseur. On les reconnait grâce à leur symbole #.

Avec le GLSL, il existe aussi un préprocesseur qui nous permet d'utiliser des directives comme #define, #ifdef, ... La plus importante pour nous va être celle qui permet de définir la version du GLSL avec laquelle nous coderons. Elle s'appelle #version et s'utilise de cette façon :

// Version du GLSL

#version 150 core


// Fonction main()

void main()
{

}

Elle permet de dire à OpenGL que notre code source utilisera la version 1.50 du GLSL ainsi que le profil core. Notez qu'il est possible de remplacer le mot-clef core par compatibility. ;)

Nous utiliserons cette directive au début de chacun de nos codes sources à partir de maintenant.

Les structures

Les structures sont un peu comme les ancêtres des objets. Ils possèdent des champs, que l'on peut rapprocher aux attributs, mais ne possèdent pas de méthodes propres. La bonne nouvelle c'est qu'on peut quand même les utiliser pour programmer nos shaders. :)

Leur déclaration se fait exactement comme en C, on utilise le mot-clef struct accompagné du nom souhaité. On inclut ensuite les champs à l'intérieur.

Prenons un petit exemple en déclarant une structure Camera. Structure qui ressemble étonnement à une classe que nous avons déjà codée. :p

// Structure Camera

struct Camera
{
    vec3 position;
    vec3 orientation;

    float rapidite;
};

L'avantage des structures en GLSL c'est qu'il n'y a pas besoin d'utiliser le mot clef typedef pour s'affranchir du mot-clef struct. Nous pouvons donc utiliser notre structure directement comme s'il s'agissait d'une variable normale :

void main()
{
    // Déclaration d'une structure Camera

    Camera maCamera;


    // Utilisation

    maCamera.position = vec3(2.0, 5.0, 2.0);
    maCamera.rapidite = 0.5;
}

Fonctions prédéfinies

Nous avons fait le tour de la syntaxe de notre nouveau langage, il n'y a pas grand chose à dire de plus dessus. Je vous réserve le reste pour le chapitre suivant. ;) Mais avant de passer à ça, j'aimerais vous montrer quelques fonctions prédéfinies qui nous serons utiles par la suite.

En effet, le GLSL a l'avantage d'intégrer nativement une bibliothèque mathématique assez complète qui permet d'aider le développeur dans ses calculs 3D. On retrouve ainsi plusieurs fonctions relatives à la trigonométrie, aux vecteurs, aux matrices, etc.

Les fonctions trigonométriques

Sinus, Cosinus et Tangente

Si je vous parle de trigonométrie vous devez forcément penser aux fonctions sin(), cos() et tan(). :p Elles permettent de calculer respectivement le sinus, le cosinus et la tangente d'un angle. Leur prototype est le même qu'en C++ :

float sin(float angle);
float cos(float angle);
float tan(float angle);

Voici un petit exemple qui permet de calculer le sinus d'un angle :

#version 150 core

void main()
{
    // Calcul du sinus d'un angle de 90°

    float sinus = sin(90.0);             /* La valeur du sinus sera de 1.0 */
}

Les autres fonctions s'utilisent de la même façon.

Convertir un angle en radian et inversement

Il existe des fonctions permettant de convertir un angle exprimé en degrés vers les radians, et inversement bien sûr. Elles sont très utiles dans certains cas, d'ailleurs on aurait bien aimé les avoir en C++ quand nous avons utilisé la trigonométrie. :p

Ces fonctions s'appellent respectivement radians() et degrees() :

float radians(float degrees);
float degrees(float radians);

Un petit exemple de conversion d'un angle exprimé en degrés :

#version 150 core

void main()
{
    // Conversion de l'angle 90° en radians

    float radians = radians(90.0);                /* La valeur de l'angle sera de 1/2 Pi soit environ 1.57 */
}

Les fonctions relatives aux vecteurs

Normalisation

Ah la normalisation ... Ça devrait vous rappeler quelques souvenirs. Surtout que nous avons eu l'occasion d'en faire plein avec GLM. :p

Vous devriez donc savoir que normaliser un vecteur revient à réduire sa norme (sa longueur) à 1.0. Cela permet de faire pas mal d'opérations mathématiques et de profiter de la trigonométrie.

La fonction qui permet de normaliser un vecteur s'appelle normalize() exactement comme celle de GLM :

vec normalize(vec vector);

Je ne vous fait pas d'exemple, je pense que vous avez compris le principe. ;)

Calculer la norme

Vu que l'on parle de la norme d'un vecteur, nous pouvons parler de la fonction qui permet de la calculer. Elle s'appelle length() :

float length(vec vector);
Calculer le produit vectoriel

Nous avons déjà utilisé le produit vectoriel pour faire des calculs dans la classe Camera qui nous permettait de trouver le vecteur orthogonal à l'orientation. La fonction permettant de faire la même chose en GLSL s'appelle aussi cross() :

vec cross(vec vector1, vec vertor2);

Cette fonction prend deux paramètres représentant les deux vecteurs à multiplier. Faites attention à utiliser le même type pour cette opération. Vous aurez une erreur de compilation si vous essayez d'en utilisez deux différents.

Autres fonctions

Il existe encore une multitude de fonctions prédéfinies dont je ne vous ai pas parlées mais il y en a tellement que mes pauvres doigts souffriraient trop si je devais toutes vous les énumérer. :lol:

Sachez qu'il existe aussi des fonctions permettant d'effectuer des calculs sur les matrices comme le calcul du déterminant, d'inversion, etc. Il en existe aussi qui permettent de faire des opérations 'classiques' comme les arrondis, les valeurs absolues, les modulos, etc. Le GLSL contient une véritable bibliothèque couteau-suisse pour les calculs mathématiques dont est inspirée la librairie GLM.

Dernier point à préciser : rappelez-vous que les shaders s'exécutent autant de fois qu'il y a de vertices/pixels, donc potentiellement des millions de fois par seconde. Il est donc préférable d'utiliser les fonctions prédéfinies à la place des nôtres quand on le peut.

Nous avons fait le tour du langage de programmation qu'est l'OpenGL Shading Language.

Nous connaissons les types de variables que l'on peut utiliser, les structures de contrôle comme les conditions, les boucles, etc. Nous avons vu en somme sa syntaxe globale; syntaxe qui se rapproche beaucoup de C/C++, les développeurs ont tout fait pour faciliter son apprentissage.

Nous nous servirons de ce langage pour coder tous nos futurs codes sources. D'ailleurs, nous allons commencer tout de suite dans le chapitre suivant en programmant notre premier shader ! :D

Exemple de certificat de réussite
Exemple de certificat de réussite