Le
GLSL, acronyme d'
OpenGL Shading Language, fait partie, au même titre que le
HLSL de Direct3D ou le
Cg de nVidia,
des langages de programmation des vertex et pixel processeurs de la carte graphique. Les vertex et pixel processeurs sont des unités
de traitement, situés dans le GPU (Graphics Processing Unit), qui agissent respectivement sur les vertices et sur les pixels.
Afin de ne pas dupliquer ce que j'ai déjà dit sur la présentation des shaders programmables, je vous renvoie à la partie 3 du tutoriel
sur les
cartes graphiques.
Pour aborder sérieusement la programmation des vertex et pixel shaders en GLSL, il est fortement conseillé de se munir de
l'Orange Book, le livre de référence en matière de GLSL (de Randi Rost - ISBN: 0-321-19789-5):
Fig. 1 - Le livre de référence sur le langage GLSLNous allons, dans la suite de ce tutoriel, nous servir de la plateforme
Demoniak3D pour
intégrer et tester nos vertex et pixel shaders écrits en GLSL. Il vous faudra aussi une carte 3D supportant les shaders. Toutes les cartes
nVidia Geforce FX 5200 et supérieures ou ATI Radeon 9600 et supérieures supportent le GLSL. Bien sûr, la dernière version
des pilotes graphiques des constructeurs (Forceware pour nVidia et Catalyst pou ATI) doit être installée.
Petite mise en garde: la programmation des vertex et pixel shaders est globalement une programmation de bas
niveau. Il y a donc très souvent, malheureusement, des comportements différents entre les différentes cartes 3D. L'idéal,
pour coder en GLSL et s'assurer que son code fonctionnera partout, est d'avoir 2 machines avec dans l'une une carte nVidia
Geforce et dans l'autre une ATI Radeon... ça promet!
Avant d'aller plus loin, clarifions le vocabulaire. Un vertex shader est une
portion de code (un programme) qui va être compilé puis exécuté dans le vertex processeur. Idem pour le pixel shader. Un shader (aussi appelé
shader program) est le terme général pour nommer l'ensemble formé par un vertex shader et un pixel shader.
Dans la terminologie OpenGL, le vertex shader se nomme
vertex program et le pixel shader se nomme
fragment program. Dans la
suite, j'utiliserai plus souvent vertex shader et pixel shader, qui sont d'après moi les termes les plus courants pour qualifier les shaders programs.
Les différents langages de shading (GLSL, Cg et HLSL) sont assez proches les uns des autres. Aussi, l'apprentissage de l'un permet de
passer rapidement à l'autre. Pour ma part, je consulte souvent des sources en HLSL avant de les convertir/adapter à mes besoins en GLSL. Ceci étant dit, nous allons passer
au codage de notre premier shader.
Pour notre premier shader, je vous propose quelque chose d'extrêmement simple. Le but recherché est de voir la structure d'un programme en GLSL et surtout
de comprendre le fonctionnement de base d'un shader. Une fois cette étape passée, la porte vers le monde merveilleux des vertex et pixel shaders
vous est ouverte!
Un shader program est composé de deux parties:
- le code du vertex shader
- le code du pixel shader
En fonction des plateformes de dévéloppement, ces 2 codes peuvent se trouver dans deux fichiers distincts, ou dans le même.
Dans le cas de Demoniak3D, un shader program (vertex shader + pixel shader) est codé dans un seul fichier. Mais, à ce niveau , Demoniak3D
offre une plus grande souplesse, puisque l'on peut directement coder notre shader dans le script XML, nous évitant ainsi la gestion
d'un grand nombre de fichiers lors des projets volumineux.
La fonction de ce premier shader program est de colorier de manière uniforme un simple mesh plan composé de 4 vertices. La figure 2
montre le rendu que l'on va obtenir en appliquant ce shader au mesh.
Fig. 2 - Le rendu de notre premier shaderAfin de comprendre la suite, je vous conseille de télécharger le projet d'accompagnement et de le charger
dans Demoniak3D. Le téléchargement se situe en bas de cette page.
Avant d'entrer dans le détail du code du shader, il faut comprendre ce que
appliquer un shader veut
dire.
Tous les objets 3D dans Demoniak3D sont dotés d'au moins un matériau. Le matériau est fondamental puisqu'il
intervient dans les calculs d'éclairage, peut posséder des textures et enfin, et c'est le plus important pour ce
tutoriel, le matériau permet de faire la liaison entre un shader program et un objet 3D.
Donc, en un mot, pour qu'un shader program puisse agir sur un mesh, il faut affecter ce shader à l'un des matériaux
qui composent le mesh. Dans notre cas, le mesh est composé d'un unique matériau (appelé plane_mat dans le script XML).
L'affectation du shader au matériau se fait de la manière suivante:
<material name="plane_mat" shader_program_name="simple_color_shader" />
où simple_color_shader est le nom de notre shader. On retrouvera ce nom dans le noeud
shader_program.
Ce noeud est le noeud central pour la création d'un shader program.
2.1 - Le vertex shader
Passons maintenant à l'analyse du vertex shader. Ce code est le suivant:
[Vertex_Shader]
void main(void)
{
gl_Position = ftransform();
}
La première ligne, [Vertex_Shader], marque le debut du vertex shader. Attention: il n'y a aucun standard à ce niveau.
Chaque environnement de développement 3D, chaque moteur 3D, a sa propre façon de marquer le début d'un
vertex ou pixel shader. Le moteur oZone3D, qui est tapi dans l'ombre de Demoniak3D, a opté pour cette façon là qui semblait la plus
simple, vu que le même fichier contient à la fois le code du vertex shader et celui du pixel shader.
La syntaxe GLSL s'inspire, comme beaucoup de langages actuels, du vénérable langage C. A ce titre, le point d'entrée
du vertex shader est la fonction
main(). C'est la première fonction qui sera exécutée, c'est aussi simple que ça.
Le corps de la fonction main, situé entre les deux incolades, est réduit au strict minimum:
gl_Position = ftransform();
Cette ligne de code d'apparence anodine est pourtant fondamentale. En fait elle est d'ailleurs obligatoire et
tous les vertex shaders doivent au minimum contenir cette fonctionnalité. Mais que fait-elle vraiment?
Pour y repondre, il nous faut quelques informations complémentaires.
Un vertex shader traite un vertex à la fois. Donc s'il y a 10000 vertices dans notre mesh, le vertex processor
va exécuter 10000 fois le vertex shader. Un vertex est composé de plusieurs attributs (vertex attrib): position, coordonnées
de texture, normale et couleur pour les plus courants. L'attribut de position, ou simplement la position, est le plus important.
Les coordonnées (x, y et z) de la position du vertex entrant sont celles qui ont été données par l'artiste 3D lors de la
modélisation. La position du vertex est définie dans le repère local du mesh (ou repère objet).
On peut maintenant répondre à la question précédente: le but de ce vertex shader est de transformer la position du vertex
du repère local vers le repère 2D de la caméra. C'est seulement sous cette forme que la position du vertex pourra être utilisée
dans les étages suivants du pipeline 3D. De manière plus détaillée, le vertex shader va multiplier la position du vertex
entrant par la matrice 4x4 model_view_projection. Cette matrice est la concaténation des trois matrices suivantes:
- la matrice de transformation de l'objet
- la matrice de transformation de la caméra
- la matrice de projection de la caméra
Le code suivant est tout aussi valable:
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
Théoriquement, il est cencé faire la même chose. Mais il peut y avoir des artefacts de rendu dus aux différentes
implémentations du standard OpenGL dans les cartes graphiques. La fonction ftransform() garantie que le rendu sera le même
quelque soit l'implémetation OpenGL.
gl_Vertex est un mot-clé du langage GLS. Plus exactement c'est une des nombreuses variables prédéfinies
dans GLSL, qui permet d'accéder directement aux différents attributs des vertices et pixels entrants. Il en est de même
pour
gl_Position. gl_Vertex représente la position du vertex entrant, tandis que gl_Position est celle du vertex sortant
transformé.
Un vertex processor ne traite, comme je l'ai dit plus haut, que des vertices. Ceci est important à comprendre car même
si un polygone est composé de 3 ou plus vertices, le vertex processeur n'en saura jamais rien. Il ignore même que le
vertex entrant fait partie d'un polygone. Il est donc impossible pour le vertex shader d'effectuer des opérations comme
le back-face culling car cette opération nécessite la connaissance de tous les vertices composants une face ou de la
normale de cette face.
Nous verrons dans un prochain tutoriel, l'utilisation des autres attributs d'un vertex (normal, couleur,
coordonnées de texture, ...).
2.2 - Le pixel shader
Attaquons maintenant l'analyse du pixel shader. Le code est le suivant:
[Pixel_Shader]
void main (void)
{
gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
}
La structure du pixel shader est la même que celle du vertex shader. Le début du pixel shader est marqué par
[Pixel_Shader] et le point d'entrée est défini par la fonction main(). Ce pixel shader ne fait qu'une chose: écrire
le vecteur 4D <1.0, 0.0, 0.0, 1.0> dans la variable
gl_FragColor.
gl_FragColor fait partie des variables prédéfinies du GLSL, tout comme gl_Vertex et gl_Position. Le seul but
d'un pixel shader est de calculer la valeur qui sera écrite dans gl_FragColor. gl_FragColor représente la couleur
finale du pixel dans le frame buffer. Mais attention: certains tests se situant après le pixel processeur, comme
l'alpha-test, peuvent modifier cette affirmation. En effet, si le pixel sortant du pixel processeur ne passe pas le test
alpha, il ne sera tout simplement pas écrit dans le frame buffer.
vec4 est un des nombreux types de données disponibles dans GLSL. vec4 représente un vecteur 4D. gl_FragColor
n'est rien d'autre qu'un vecteur 4D qui contient les 4 composantes de couleur d'un pixel: Red, Green, Blue et Alpha.
Pour accéder aux coordonnées du vecteur gl_FragColor, rien de plus simple comme le montre le bout de code suivant:
gl_FragColor.r = 1.0;
gl_FragColor.g = 0.0;
gl_FragColor.b = 0.0;
gl_FragColor.a = 1.0;
Ce code a exactement le même effet que le code originel. Nous aurions aussi pu écrire la chose suivante:
vec4 final_color = vec4(1.0, 0.0, 0.0, 1.0);
gl_FragColor.r = final_color.x;
gl_FragColor.g = final_color.y;
gl_FragColor.b = final_color.z;
gl_FragColor.a = final_color.w;
GLSL nous offre une grande liberté d'expression!
Que d'explications pour ce simple shader! Peut-être commencez-vous à comprendre le mythe qui dit que la
programmation des shaders est complexe. Elle est complexe, c'est un fait. Mais elle nous oblige à véritablement comprendre
ce qui se passe sous le capot de nos chères (et onéreuses!) cartes graphiques. C'est seulement à ce prix que vous
pourrez concevoir des shaders évolués et laisser libre cours à votre imagination...
Voici, pour terminer, une liste de liens utiles:
- 3DLabs Fixed Functionality Shader Tool: ce petit utilitaire développé par 3DLabs vous permettra de générer des vertex
et pixel shaders GLSL qui reproduisent le fonctionnement des unités fixes de Transform & Lighting et de Texturing. Vraiment intéressant pour comprendre le pipeline 3D...
- nVidia SDK: Ce SDK comporte des dizaines d'exemples sur la prog des shaders GLSL. A avoir dans sa trousse à outils de développeur 3D!
- ATI SDK: Le SDK d'ATI est maintenant vraiment cool et bien présenté (genre nVidia!). Il y a un grand nombre de code sources et de bons whitepapers. A avoir également dans sa trousse à outils de développeur 3D!