Utilisation des shaders avec OpenGL 3.x

Michaël Gallego

Ce tutoriel a pour objectif de vous présenter de manière synthétique l'utilisation des shaders (via le langage GLSL) avec les versions 3.0 et 3.1 d'OpenGL.

- Titre : Utilisation des shaders avec OpenGL 3.x

- Auteur : Michaël Gallego

- Parution : 24 juin 2009

- Licence :

Sommaire

- 0. Introduction

- I. GLSL 1.30 et GLSL 1.40 : nouveautés

- I-A. GLSL 1.30 : nouveautés

- I-B. GLSL 1.40 : nouveautés

- II. Création des shaders

- III. Ecriture des shaders

- III-A. Spécifier la version du shader

- III-B. Attributs génériques, entrées - sorties

- III-C. Variables uniform définies par l'utilisateur

- III-D. Qualificateurs d'interpolation

- III-E. Mot-clé « invariant »

- III-F. Qualificateurs de précision

- III-G. Sorties d'un fragment shader

- IV. Conclusion

- V. Liens

- VI. Remerciements

0 - Introduction

L'OpenGL Shading Language, plus connu sous le nom de GLSL, est un langage de shader de haut niveau introduit officiellement avec OpenGL 2.0, en version 1.00. OpenGL 3.0 a fait évoluer le langage en version 1.30, tandis qu'OpenGL 3.1 l'a logiquement fait évoluer en version 1.40. Au fil de ces évolutions, le langage de shader d'OpenGL s'est progressivement rapproché de celui d'OpenGL ES (la version d'OpenGL pour mobile), notamment sur la sémantique du langage, afin d'offrir un portage bien plus facile entre des applications OpenGL et OpenGL ES.

Dans ce tutoriel, j'assume que vous êtes déjà familier avec l'une des versions précédentes de GLSL, puisque l'objectif de ce tutoriel est plutôt de vous aider à prendre en main rapidement ces nouvelles versions du langage et ainsi d'écrire le plus rapidement possible des shaders dans un OpenGL moderne et évolutif. Comme tout le temps, je vous suggère de toujours avoir avec vous la documentation officielle de GLSL, dont les spécificités de la version 1.40 sont détaillées ici : http://www.opengl.org/registry/doc/GLSLangSpec.Full.1.40.05.pdf

I - GLSL 1.30 et GLSL 1.40 : nouveautés

Avant toute chose, et comme je l'avais écrit dans le tutoriel d'introduction, il est indispensable, pour profiter de GLSL 1.30 et GLSL 1.40, d'avoir à la fois les derniers drivers de votre carte graphique, une carte compatible avec OpenGL 3.x, et enfin de créer un contexte OpenGL 3.x valide (pour ceci, je vous renvoie au tutoriel d'introduction sur OpenGL 3.x.

Les deux nouvelles versions de GLSL, 1.30 et 1.40, sont relativement similaires et, en l'absence de drivers compatibles avec OpenGL 3.1 à l'heure où j'écris ces lignes, les exemples de ce tutoriel seront écrits en GLSL 1.30. Toutefois, les étendre à GLSL 1.40 est relativement aisé, comme nous allons le voir plus loin et, pour être honnête, c'est surtout GLSL 1.30 qui a apporté un changement sur la sémantique du langage. Voici, sans plus attendre, un tour d'horizon des nouveautés apportées par ces versions (tiré de la documentation officielle). Notez également que ce tutoriel n'a pas la prétention de vous présenter toutes les nouveautés apportées par ces deux versions !

I-A - GLSL 1.30 : nouveautés

Voici les nouveautés apportées par GLSL 1.30 :

- Support complet des entiers :

- Entiers signés et non signés, vecteurs d'entiers, et opérations sur ces vecteurs

- Décalages de bits, masques

- Indices de textures

- Variables uniforms, attributs, varying... pouvant être des entiers

- Fonctions built-in supportant les entiers (max, abs.)

- Modifications du système des textures

- Fonctions permettant d'accéder à la taille des textures

- Tableaux de textures

- Fonctions d'offsets

- Nouvelles fonctions pour accéder aux valeurs d'une texture

- Support des switch (switch/case/default)

- Nouvelles fonctions built-in : trunc(), round(), roundEven(), isnan(), isinf(), modf()

- Fonctions trigonométriques hyperboliques

- Nouvelles fonctionnalités du pré-processeur

- Nouvelle sémantique du langage concernant les attributs, les entrées/sorties des vertex/fragment shaders

- Compatibilité améliorée avec OpenGL ES

- Interpolation linéaire (sans perspective) grâce au mot-clé noperspective

- Nouvelle variable built-in accessible dans le vertex shader : gl_VertexID

Comme pour OpenGL 3.x, le langage de shader introduit également un système de dépréciation (qui n'est, finalement, qu'une conséquence logique des modifications apportées à l'API). Voici les fonctionnalités dépréciées avec GLSL 1.30 :

- Utilisation des mot-clés attribut et varying

- Utilisation de la variable gl_ClipVertex (utilisez gl_ClipDistance à la place)

- Utilisation de gl_FragData et gl_FragColor (utilisez le mot-clé out)

- Les attributs built-in (ceci comprend, par exemple : gl_Vertex, gl_Normal, gl_Color.). Il faut utiliser les attributs génériques (nous verrons ceci plus en détail dans le tutoriel suivant)

- Utilisez un shader dans un stage du pipeline (par exemple le vertex shader) et pas dans l'autre. Il faut utiliser un shader pour TOUS les stages du pipeline

- Tous les anciens noms d'accès aux textures (voir la documentation pour plus de détails)

- Utilisation des variables gl_FogFragCoord et gl_TexCoord

- La fonction ftransform()

- Utilisation de gl_MaxVaryingFloats (utilisez gl_MaxVaryingComponents à la place)

I-B - GLSL 1.40 : nouveautés

Voici à présent une liste des nouveautés apportées par GLSL 1.40 :

- Ajout du support pour les « uniform block » (nous les verrons dans un tutoriel ultérieur)

- Support des textures rectangulaires

- Support des « texture buffer »

- Ajout du mot-clé gl_InstanceID (utilisé pour l'instancing)

- Il n'est plus obligatoire d'écrire dans la variable built-in gl_Position

De la même manière que les fonctionnalités dépréciées d'OpenGL 3.0 étaient supprimées dans OpenGL 3.1, les fonctionnalités dépréciées de GLSL 1.30 sont supprimées dans GLSL 1.40 :

- Disparition du mot-clé gl_ClipVertex (utilisez gl_ClipDistance à la place)

- Disparition des attributs built-in (gl_Vertex, gl_Normal, gl_Color.)

- Ne pas fournir un shader pour TOUS les stages du pipeline est une erreur

- Disparition de la fonction ftransform()

Pour ceux qui souhaitent continuer à utiliser ces fonctionnalités dans les shaders, il suffit de passer par l'extension GL_ARB_compatibility, dont nous avons parlé dans le tutoriel d'introduction à OpenGL 3.x.

II - Création des shaders

Parlons tout d'abord de la création des shaders, du point de vue de l'application. Bonne nouvelle, cela n'a pas vraiment changé avec OpenGL 3.x. Voici un bout de code permettant de créer un shader :

unsigned int vertexID, fragmentID, programID ;

// Création des shaders
vertexID = glCreateShader (GL_VERTEX_SHADER) ;
fragmentID = glCreateShader (GL_FRAGMENT_SHADER) ;

// Chargement du code source des shaders
const char * vertexSource = Load ("monVertexShader.txt") ;
const char * fragmentSource = Load ("monFragmentShader.txt") ;

// Envoi du code source
glShaderSource (vertexID, 1, &vertexSource, NULL) ;
glShaderSource (fragmentID, 1, &fragmentSource, NULL) ;

// Compilation des shaders
glCompileShader (vertexID) ;
glCompileShader (fragmentID) ;

// Création du programme
programID = glCreateProgram () ;

// Attache des shaders au programme (on peut les détacher avec la fonction glDetachShader)
glAttachShader (programID, vertexID) ;
glAttachShader (programID, fragmentID) ;

// Linkage du programme
glLinkProgram (programID) ;

// Plus loin dans le programme, lorsque l'on souhaite utiliser le programme :
glUseProgram (programID) ;

Bien évidemment, il convient de nettoyer le tout dès que l'on n'a plus besoin des shaders :

// Destruction des shaders
glDeleteShader (vertexID) ;
glDeleteShader (fragmentID);

// Destruction du programme
glDeleteProgram (programID);

Il existe évidemment d'autres fonctions qui peuvent s'avérer utiles, notamment pour la gestion des erreurs mais, encore une fois, rien n'a changé depuis la version précédente, je vous suggère donc de lire la documentation d'OpenGL 3.0 ou 3.1 ou de lire un tutoriel plus complet sur le sujet.

Bref, d'un point de vue de l'application, rien n'a changé (enfin, si, l'apparition de nouvelles fonctions, mais nous verrons cela dans la suite du tutoriel). Afin d'offrir une plus grande compatibilité avec OpenGL ES, il est prévu qu'une version future d'OpenGL introduise la fonction glShaderBinary, qui permet de compiler à l'avance les shaders dans un format binaire, et ainsi d'éviter la couteuse opération de compilation directement dans l'application. Mais pour l'heure, cette fonctionnalité n'est pas disponible (et puis, compiler directement permet d'effectuer des optimisations spécifiques à la machine sur laquelle le code est exécuté).

III - Ecriture des shaders

Si, du côté OpenGL, l'envoi des shaders ne change pas, l'écriture de ces derniers, elle, subit quelques bouleversements. Afin d'illustrer tout ceci, voici un shader tout bête écrit en GLSL 1.20 (il effectue un éclairage diffus), et qui devrait vous paraître tout à fait familier :

// VERTEX SHADER
#version 120

varying vec3 EyeVector, Normal, LightDir;

void main ()
{
Normal = gl_NormalMatrix * gl_Normal;
vec3 Position = vec3 (gl_ModelViewMatrix * gl_Vertex);

LightDir = gl_LightSource[0].position.xyz - Position;
EyeVector = -Position;

gl_Position = ftransform ();
}

Et le fragment shader :

// FRAGMENT SHADER
#version 120

varying vec3 EyeVector, Normal, LightDir;

void main()
{
vec3 N = normalize (Normal);
vec3 L = normalize (LightDir);
float nDotL = max (dot (N, L), 0.0);

gl_FragColor = gl_LightSource[0].diffuse * gl_FrontMaterial.diffuse * nDotL;
}

Maintenant, oubliez tout ça : ces deux shaders contiennent déjà beaucoup de choses qu'il faut bannir pour écrire du code compatible avec GLSL 1.30 et, surtout, GLSL 1.40 (je rappelle en effet que ce code compilera sous GLSL 1.30 - mais émettra de nombreux warnings vous indiquant que vous utilisez des fonctions dépréciées - mais ne fonctionnera pas sous GLSL 1.40, à moins que vous utilisiez l'extension GL_ARB_compatibility bien sûr).

Voici maintenant le même shader, mais écrit en GLSL 1.30 :

// VERTEX SHADER
#version 130

// Attributs
in vec3 VertexPosition, VertexNormal;

// Uniform
uniform mat3 NormalMatrix;
uniform mat4 ModelViewMatrix, ModelViewProjectionMatrix;
uniform vec3 LightPosition;

// Sorties
smooth out vec3 EyeVector, Normal, LightDir;

invariant gl_Position;

void main ()
{
Normal = NormalMatrix * Normal;
vec3 Position = vec3 (ModelViewMatrix * vec4 (VertexPosition, 1.0));

LightDir = LightPosition - Position;
EyeVector = -Position;

gl_Position = ModelViewProjectionMatrix * vec4 (VertexPosition, 1.0);
}

Et le fragment shader :

#version 130

// Precision qualifier
precision highp float;

// Entrées
smooth in vec3 EyeVector, Normal, LightDir;

// Uniform
uniform vec3 LightDiffuse, MaterialDiffuse;

// Sortie
out vec4 Color;

void main()
{
vec3 N = normalize (Normal);
vec3 L = normalize (LightDir);
float nDotL = max (dot (N, L), 0.0);

Color = vec4 (LightDiffuse * MaterialDiffuse * nDotL, 1.0);
}

A première vue, ce code s'avère bien plus compliqué que le précédent et, effectivement, il l'est ! Toutefois, et c'est là la force de cette nouvelle version, le programmeur contrôle quasiment tout, de la gestion des matrices à l'envoi manuel des bons attributs. Dans les parties qui suivent, nous allons découper ce code afin qu'il vous paraisse plus clair, tout en détaillant certaines notions sémantiques de ces nouvelles versions.

III-A - Spécifier la version du shader

Commençons par le commencement, cette ligne :

#version 130

Beaucoup de personnes l'ignorent mais, par défaut, les shaders GLSL sont compilés en version 1.10. De ce fait, la première chose à écrire dans un vertex shader et un fragment shader, est cette ligne, qui spécifie que vous souhaitez utiliser la version 1.30 de GLSL (pour la version 1.40, il suffit de remplacer le 130 par 140). De ce fait, toute utilisation d'une fonction dépréciée vous sera signalée par un warning. Cette commande du pré-processeur vous permet également d'utiliser les fonctionnalités non présentes dans les versions précédentes du langage. Bref, une ligne à ne surtout pas oublier !

III-B - Attributs génériques, entrées - sorties

L'une des différences par rapport au code écrit en GLSL 1.20 est la disparition de la majorité des variables built-in, notamment gl_Vertex, gl_Normal . En effet, tous ces attributs, qu'on appelle des attributs de sommets (« vertex attributes ») parce qu'ils définissent des propriétés propres à chaque sommet d'une primitive (triangle, carré.), doivent dorénavant être utilisés avec des attributs génériques.

Je ne vais pas m'étendre sur les attributs génériques dans ce tutoriel, puisque ce sera l'objet du prochain (qui décrit justement l'utilisation des vertex buffer object avec ces nouvelles habitudes à prendre). Globalement, ce que vous devez retenir ici, c'est que vous ne devez plus utiliser les attributs built-in.

L'autre chose à noter est le mot-clé « in » qui précède les variables VertexPosition et VertexNormal. Ce mot-clé forme l'interface entre les différents stages du pipeline d'OpenGL. Les valeurs du stage précédent du pipeline sont donc copiées dans le stage courant chaque fois que le mot-clé in est rencontré. Dans le cas d'un vertex shader, le mot-clé in est utilisé pour les attributs de sommet. Dans le fragment shader, ceci remplace le mot-clé varying (qui est déprécié). Les variables de sorties du vertex shader (via le mot-clé out) sont copiées dans le fragment shader et deviennent ainsi des variables d'entrées du fragment shader (d'où le mot-clé in). Ce procédé est parfaitement illustré avec les variables EyeVector, Normal et LightDir.

La chose à retenir ici : il ne faut plus utiliser le mot-clé varying pour transmettre des valeurs du vertex shader au fragment shader, mais utiliser les mot-clés in et out. Les mot-clés qui précèdent in et out dans l'exemple ci-dessus seront abordés plus loin.

III-C - Variables uniform définies par l'utilisateur

Autre nouveauté de ce code, et non des moindres : la disparition de nos chers gl_ModelViewMatrix et autres gl_NormalMatrix. Eh oui, la gestion des matrices dans OpenGL ayant été dépréciée avec OpenGL 3.0, leur utilisation dans les shaders l'est tout autant. Ceci implique donc que toutes les matrices doivent être générées manuellement (il vous faut donc créer a la mano vos matrices de projections, notamment) et les envoyer à vos shaders sous forme de variables uniform.

Bien sûr, cette nouvelle manière de faire est parfois contraignante : écrire des classes pour générer des matrices, lorsqu'on ne souhaite afficher qu'un triangle à l'écran, est très peu pratique. Toutefois, ceci offre une grande flexibilité et également de meilleures performance (les cartes graphiques modernes étant conçues pour un « tout shader » et l'utilisation du pipeline fixe ne permet pas de les exploiter au maximum).

Notez que le fragment shader dispose également de variables uniforms, LightDiffuse et MaterialDiffuse, puisque les variables uniform gl_LightSource et gl_FrontMaterial sont également dépréciées (d'ailleurs, toute la gestion de l'éclairage par OpenGL est. dépréciée - vous commencez à bien saisir ! -). Il est bien évidemment possible, comme en GLSL 1.20, de déclarer des variables uniform dans le vertex shader et de ne pas les déclarer dans le fragment shader, et inversement. Par contre, deux variables uniform portant le même nom dans le vertex et fragment shader auront automatiquement la même valeur, il est impossible de spécifier des valeurs différentes pour des uniform du même nom.

Quant à l'envoi des variables uniform à partir de l'application, elle s'effectue exactement de la même manière qu'avec OpenGL 2.1. Il vous faut d'abord récupérer son emplacement avec la fonction glGetUniformLocation, puis envoyer les valeurs via l'une des fonctions glUniform. Pour plus de détails, je vous suggère donc de lire la documentation officielle ou de vous rendre sur un tutoriel traitant des shaders.

Pour terminer sur les variables uniform, sachez qu'OpenGL 3.1 a introduit le concept de « uniform block » (pour autant, l'envoi traditionnel des variables uniform n'est pas dépréciée et vous pouvez donc continuer à l'utiliser sans soucis), dont les valeurs sont envoyées non pas avec des appels à glUniform mais via un buffer (comme pour les VBO). Cette fonctionnalité sera l'objet d'un futur tutoriel, c'est pourquoi le sujet n'est qu'effleuré ici.

III-D - Qualificateurs d'interpolation

Reprenons le vertex shader et, en particulier, la ligne suivante :

// Sorties
smooth out vec3 EyeVector, Normal, LightDir;

Le « smooth » introduit devant le mot-clé out est appelé un qualificateur d'interpolation (« interpolation qualifier » en anglais). Il existe trois qualificateurs d'interpolation introduits depuis GLSL 1.30 : flat, smooth et noperspective. Ces qualificateurs doivent se placer devant le mot-clé out, et ne peuvent être utilisées qu'avec les sorties d'un vertex shader et les entrées d'un fragment shader (les qualificateurs doivent correspondre).

Auparavant, il existait le mot-clé varying. Ce mot-clé permettait de dire qu'une valeur dans le vertex shader doit être interpolée entre les trois sommets (dans le cas d'un triangle) dans le fragment shader. L'équivalent, en GLSL 1.30 et 1.40 du mot-clé varying, est le qualificateur d'interpolation smooth. Ainsi, un code qui ressemblait à ça auparavant :

// VERTEX SHADER
varying float maValeur ;

// FRAGMENT SHADER
varying float maValeur; // La valeur a été interpolée entre les trois sommets

Ressemblera désormais à ceci :

// VERTEX SHADER
smooth out float maValeur ;

// FRAGMENT SHADER
smooth in float maValeur; // La valeur a été interpolée entre les trois sommets

Quelques remarques : le qualificateur d'interpolation utilisée dans le vertex shader doit être le même dans le fragment shader ; de plus, il ne peut y avoir qu'un qualificateur d'interpolation par variable, ne vous amusez donc pas à écrire smooth flat float maValeur, ça ne marchera pas .

Parlons à présent des autres qualificateurs d'interpolation, puisque le langage nous offre plus de possibilité qu'auparavant. Tout d'abord, le qualificateur flat indique que la variable ne sera pas interpolée, et donc la variable aura la même valeur pour tous les fragments d'un triangle (ou de n'importe quelle autre primitive compatible). Le qualificateur smooth est l'équivalent du comportement par défaut du varying, et la valeur de la variable sera interpolée entre chaque sommet de manière à ce que la perspective soit correcte. Enfin, le qualificateur noperspective interpolera la valeur de la variable entre chaque sommet de manière linéaire dans l'espace de l'écran, sans tenir compte de la perspective.

III-E - Mot-clé « invariant »

Avec GLSL 1.20, il était conseillé d'utiliser la fonction ftransform() plutôt que d'écrire soi-même la ligne suivante :

gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex ;

L'un des arguments en faveur de la fonction ftransform était que celle-ci garantissait que la position soit toujours la même, quel que soit le shader, comme si on avait utilisé le pipeline fixe. En effet, il pouvait arriver, à cause de multiples raisons (optimisations, précisions des réels.), que le résultat de gl_Position sans utiliser ftransform soit légèrement différent lorsqu'on utilisait différent shader, d'où cette recommandation d'utiliser cette fonction. Et puis, ça nous faisait économiser quelques caractères.

Avec GLSL 1.30 et 1.40, cette fonction est dépréciée, et on doit donc de nouveau écrire nous même la multiplication (à la petite différence que l'on utilise plus les variables built-in mais nos propres variables) :

gl_Position = ModelViewProjectionMatrix * VertexPosition;

Apparaît alors un problème : on nous disait auparavant que le résultat stocké dans gl_Position pouvait ne pas être exactement le même en agissant ainsi, et on nous impose maintenant cette manière de faire ! La solution : le mot-clé invariant. Celui-ci nous garantit en effet que le résultat sera exactement le même quelque soit le shader. Pour spécifier qu'une variable est invariante (au passage, seules les variables de sortie d'un shader peuvent être déclarées avec le mot-clé invariant), il suffit de faire précéder le nom de la variable du mot-clé invariant, comme écrit plus haut. Il est également possible de faire en sorte que toutes les variables de sortie soit invariantes, en ajoutant, et UNIQUEMENT dans le vertex shader, la ligne suivante :

#pragma STDGL invariant(all)

Notez toutefois qu'il n'est pas conseillé d'utiliser ce pragma, car une variable invariante ne pourra pas être optimisée de la même manière qu'une variable non invariante, et il peut donc résulter un code final plus lent.

Prenons l'exemple de deux shaders. Pour qu'une variable soit garantie d'avoir la même valeur (par exemple, notre variable gl_Position), il faut :

- Que la variable soit déclarée invariant dans les DEUX shaders.

- Les valeurs d'entrées utilisées pour calculer la valeur de la variable invariante doivent être les mêmes (en d'autres termes, gl_Position aura la même valeur si la matrice de projection et la position du sommet sont les mêmes, ce qui est somme toute assez logique).

- Si la valeur de la variable invariante dépend d'une texture, il faut que la texture utilisée ait à la fois les mêmes valeurs et le même filtrage pour assurer le côté invariant de la variable.

Bref, une bonne pratique consiste à toujours déclarer la variable gl_Position comme invariante (et c'est d'ailleurs la pratique recommandée par les spécifications).

III-F - Qualificateurs de précision

Comme je l'ai dit en introduction, la version 1.30 de GLSL a rapproché la syntaxe de GLSL de celle d'OpenGL ES. Cette version supportant les qualificateurs de précision, ce système a également été ajouté pour la version standard de GLSL.

Il existe donc trois qualificateurs de précision : highp, mediump, et lowp. Ces qualificateurs permettent de contrôler la précision utilisée pour stocker et représenter les valeurs à la fois dans le vertex et le fragment shader. Avec OpenGL ES, la principale utilité de ces qualificateurs est d'un point de vue de l'optimisation, puisque ces qualificateurs peuvent influer sur le stockage nécessaire des variables. Toutefois, les spécifications de GLSL 1.30 et 1.40 précisent que l'ajout de ces qualificateurs n'est que pour la portabilité du code avec OpenGL ES, mais que, d'un point de vue fonctionnalité, ils n'ont aucun effet avec la version standard d'OpenGL. Toutefois, dans un but d'exhaustivité, voici quelques précisions supplémentaires (de plus, il y a un petit piège dans le fragment shader avec ces qualificateurs de précision !).

Pour définir la précision d'une variable, il suffit de faire précéder son type du qualificateur :

lowp float color ;
out medium vec2 P ;
lowp ivec2 foo (lowp mat3); // Prototype de function
highp mat4 m;

Pour agir de manière globale, la ligne suivante permet de spécifier le qualificateur de précision pour toutes les variables :

precision highp float; // Tous les float sont de precision "highp"
precision lowp int; // Tous les entiers sont de precision "lowp"

Par défaut, les entiers et les flottants ont une précision highp dans le vertex shader, tandis que dans le fragment shader, les entiers ont une précision par défaut mediump alors que les flottants n'ont... pas de précision par défaut ! Et c'est là que ça pose problème ! En effet, pour que des variables puissent être transmises du vertex shader au fragment shader, il faut qu'elles aient le même qualificateur de précision. Ainsi, le code suivant ne compile pas :

// VERTEX SHADER
smooth out vec3 monVecteur ;

// FRAGMENT SHADER
smooth in vec3 monVecteur;

Le message d'erreur suivant est généré : ERROR: '' : Declaration must include a precision qualifier or the default precision must have been previously declared. En effet, les qualificateurs de précision ne concordant pas : par défaut, le qualificateur pour les flottants est highp dans le vertex shader, et rien du tout dans le fragment shader. Résultat, ça ne correspond pas du tout ! Ce qui explique la présence de la ligne suivante dans le fragment shader :

// Precision qualifier
precision highp float;

A présent les qualificateurs de précision concordent, et on a donc plus de problèmes.

III-G - Sorties d'un fragment shader

Reste à élucider un dernier point dans le code de nos shaders. En GLSL 1.20, pour écrire dans le framebuffer, on utilisait la variable gl_FragColor. Lorsque l'on utilisait l'extension des framebuffer object, on pouvait écrire dans une texture spécifique en utilisant la variable gl_FragData[n]. Ces deux variables étant dépréciées, le mécanisme fonctionne un peu différemment.

Notez tout d'abord la ligne suivante dans le fragment shader :

// Sortie
out vec4 Color;

// Dans le main du fragment shader :
Color = quelque chose;

Comme vous ne voyez, on utilise plus gl_FragColor. Mais question : où est-ce que le résultat va s'écrire ? Dans le framebuffer, dans une texture ? A vrai dire, on ne peut pas vraiment savoir, puisque c'est encore au programmeur de tout gérer à la main !

Lorsque le programme de shader est lié, toutes les variables de sortie (déclarées avec out dans le fragment shader) sont automatiquement « attachées » à des points d'attache (!). Une fois le programme de shader lié, il faut utiliser la fonction glGetFragDataLocation, dont le prototype est :

int GetFragDataLocation( uint program, const char *name );

Le premier paramètre est l'identifiant du programme du shader, et le deuxième le nom de la variable de sortie du shader (dans notre exemple, c'est « Color »). Il est également possible de spécifier soi-même les points d'attaches, via la fonction glBindFragDataLocation, dont le prototype est le suivant :

void BindFragDataLocation( uint program, uint colorNumber, const char *name );

Le premier paramètre est, encore une fois, l'identifiant du programme de shader ; le second paramètre est l'indice du buffer de couleur (l'erreur GL_INVALID_VALUE est générée si ce paramètre est supérieur à la constante GL_MAX_DRAW_BUFFERS). Mais comment donc se servir de cette fonction ?

Avec OpenGL 2.1 et GLSL 1.20, il était possible de dessiner dans un buffer off-screen grâce à l'extension framebuffer_objects. On pouvait donc, par exemple, dessiner dans deux textures, en appelant la fonction glDrawBuffers, comme ceci :

glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,  GL_COLOR_ATTACHMENT0_EXT, GL_TEXTURE_2D, textureA, 0);
glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT,  GL_COLOR_ATTACHMENT1_EXT, GL_TEXTURE_2D, textureB, 0);

GLenum buffers [2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1} ;
glDrawBuffers (2, &buffers) ;

Les deux textures, textureA et textureB, ont été attachées au point GL_COLOR_ATTACHMENT0_EXT et GL_COLOR_ATTACHMENT1_EXT, respectivement. Dans le shader, l'instruction gl_FragData[0] et l'instruction gl_FragData[1]permettaient d'écrire dans les buffers de couleurs 0 et 1. Ainsi, ce qui était écrit dans gl_FragData[0] se dirigeait dans la textureA tandis que ce qui était écrit dans gl_FragData[1] se dirigeait dans textureB.

Avec OpenGL 3.x la procédure à suivre est un peu différente. Le programmeur doit en effet spécifier lui-même les points d'attaches. Par exemple, prenons le fragment shader suivant :

out vec4 TextureA;
out vec4 TextureB;

int main ()
{
TextureA = quelque chose ;
TextureB = quelque chose ;
}

Ce fragment shader dispose de deux variables de sortie. Il faut donc : soit récupérer les points d'attache spécifiés automatiquement par OpenGL, ou spécifier les nôtres, comme ceci :

// On décide, arbitrairement, de dire que le contenu de TextureA va aller au point d'attache n°1, et celui de TextureB au point d'attache n°3, mais
// on aurait très bien pu utiliser aussi GL_COLOR_ATTACHMENT1 et GL_COLOR_ATTACHMENT3
glBindFragDataLocation (programID, 1, "TextureA") ;
glBindFragDataLocation (programID, 3, "TextureB") ;

Résultat, on utilise par la suite ces points d'attache. Pour reprendre l'exemple, cela donne :

glFramebufferTexture2D (GL_FRAMEBUFFER, 1, GL_TEXTURE_2D, textureA, 0);
glFramebufferTexture2D (GL_FRAMEBUFFER,  3, GL_TEXTURE_2D, textureB, 0);

GLenum buffers [2] = {1, 3} ;
glDrawBuffers (2, &buffers) ;

Et voilà ! Notez également au passage la disparition du EXT derrière les fonctions et les constantes de l'extension framebuffer objects, puisque celle-ci passe, avec OpenGL 3.0, dans le core (via l'extension GL_ARB_framebuffer_objects), et est donc dorénavant acceptée par l'ARB.

IV - Conclusion

Ce premier « vrai » tutoriel consacré à OpenGL 3.x vous a présenté quelques spécificités de ces nouvelles versions du langage de shader de haut niveau GLSL. Bien sûr, il reste beaucoup à découvrir : de nombreuses autres choses ont été modifiées, ajoutées ou supprimées. Une bonne pratique pour écrire du code moderne et non déprécié est donc de toujours avoir la documentation officielle avec soi (les liens sont disponibles dans la section suivante), et de vérifier les parties correspondantes aux fonctionnalités que vous utilisez.

Dans le tutoriel suivant, nous verrons une partie que nous avons un peu laissé de côté : l'envoi des attributs de sommet du côté de l'application (souvenez-vous, les variables in du vertex shader). Un gros morceau qui nécessite bien une partie entière, compte tenu des changements apportés.

V - Liens

Documentation officielle de GLSL 1.30 : http://www.opengl.org/registry/doc/GLSLangSpec.Full.1.30.08.pdf

Documentation officielle de GLSL 1.40 : http://www.opengl.org/registry/doc/GLSLangSpec.Full.1.40.05.pdf

VI - Remerciements

Merci à IrmatDen pour sa relecture.