I. L'article original▲
Cet article est une adaptation en langue française de 1. GLSL Setup, de Donald Urquhart.
II. Introduction▲
Pour démarrer cette série de tutoriels sur GLSL, on repartira du code d'un tutoriel précédent, une rotation lisse. Pour ceux qui n'ont aucune idée de ce qu'est GLSL, cette introduction donnera également une vue d'ensemble assez brève de ce que les shaders peuvent faire, notamment en comparaison de DirectX.
Pour ceux qui ne savent rien à propos de GLSL, un programme shader est composé de soit un vertex shader et un fragment shader, soit un vertex shader et un geometry shader et un fragment shader, cette dernière possibilité n'étant supportée que sur les cartes graphiques NVIDIA 8000 et plus récentes.
Un vertex shader permet de manipuler des sommets et donne accès à leur normale, le pipeline d'éclairage, les matériaux des objets et d'autres fonctions.
Un fragment shader permet de manipuler les pixels qui sont affichés à l'écran, ils sont utilisés pour créer des effets tels que l'éclairage, les textures, le brouillard, etc.
Un geometry shader (qui sera utilisé plus loin) permet de créer et de supprimer des sommets à l'envi. Cette liberté est très intéressante pour gérer un niveau de détail dynamique, pour créer des polygones plus flous, exploser des formes et d'autres effets.
Ce tutoriel est prévu pour OpenGL 2 et plus récents, GLSL ne faisant partie d'OpenGL qu'à partir de la version 2. Toutes les versions précédentes devront utiliser les extensions ARB.
III. Fichier shader.h▲
Ce premier tutoriel risque d'être assez long, au vu du nombre de choses à préparer. Rien n'est vraiment compliqué et le code final est assez court, mais, avec la variété de choses à faire, il ne peut être que long.
Il utilisera également une approche orientée objet pour les shaders GLSL, il se finira sur une classe réutilisable pour chaque programme shader.
Pour démarrer la déclaration de la classe, on crée un nouveau fichier d'en-tête C++, shader.h. Puisque l'on souhaite créer une nouvelle classe, on y insère quelque chose comme ceci :
#ifndef __SHADER_H
#define __SHADER_H
#if ( (defined(__MACH__)) && (defined(__APPLE__)) )
#include
<stdlib.h>
#include
<OpenGL/gl.h>
#include
<GLUT/glut.h>
#include
<OpenGL/glext.h>
#else
#include
<stdlib.h>
#include
<GL/glew.h>
#include
<GL/gl.h>
#include
<GL/glut.h>
#include
<GL/glext.h>
#endif
#include
<string>
class
Shader {
public
:
Shader();
~
Shader();
private
:
}
;
#endif
Il s'agit là d'un squelette assez propre sur lequel tout viendra s'ajouter. Du code un peu plus long est mis dans les inclusions, avec une condition, de telle sorte que les utilisateurs d'OS X puissent l'utiliser.
Maintenant, on veut ajouter quelques méthodes publiques à la classe. On souhaite que tout utilisateur de cette classe puisse créer un shader en utilisant le constructeur ou une méthode d'initialisation. Les deux prendront en paramètres les noms de fichier du vertex shader et du fragment shader.
On ajoute donc les lignes suivantes :
Shader(const
char
*
vsFile, const
char
*
fsFile);
void
init(const
char
*
vsFile, const
char
*
fsFile);
La prochaine étape est d'utiliser ces shaders. On aura besoin de trois méthodes : bind et undind pour l'activer ou le désactiver, ainsi que id pour récupérer le numéro associé au shader, afin de le manipuler par après :
void
bind();
void
unbind();
unsigned
int
id();
Tout ce qu'il reste à faire est de créer trois variables privées : une pour le programme shader, une pour le vertex shader et une pour le fragment shader.
unsigned
int
shader_id;
unsigned
int
shader_vp;
unsigned
int
shader_fp;
Tout mis ensemble, cela donne le contenu suivant, qui est assez simple mais fait le nécessaire.
#ifndef __SHADER_H
#define __SHADER_H
#if ( (defined(__MACH__)) && (defined(__APPLE__)) )
#include
<stdlib.h>
#include
<OpenGL/gl.h>
#include
<GLUT/glut.h>
#include
<OpenGL/glext.h>
#else
#include
<stdlib.h>
#include
<GL/glew.h>
#include
<GL/gl.h>
#include
<GL/glut.h>
#include
<GL/glext.h>
#endif
#include
<string>
class
Shader {
public
:
Shader();
Shader(const
char
*
vsFile, const
char
*
fsFile);
~
Shader();
void
init(const
char
*
vsFile, const
char
*
fsFile);
void
bind();
void
unbind();
unsigned
int
id();
private
:
unsigned
int
shader_id;
unsigned
int
shader_vp;
unsigned
int
shader_fp;
}
;
#endif
IV. Fichier shader.cpp▲
On passe alors au fichier shader.cpp. Il contiendra le code de toutes les méthodes esquissées plus haut, en plus d'une nouvelle.
Autant commencer depuis le début, avec les constructeurs :
Shader::
Shader() {
}
Shader::
Shader(const
char
*
vsFile, const
char
*
fsFile) {
init(vsFile, fsFile);
}
Après, la méthode d'initialisation. Les deux premières lignes demanderont à OpenGL de créer un vertex shader et un fragment shader et assigneront leurs identifiants aux variables privées correspondantes.
shader_vp =
glCreateShader(GL_VERTEX_SHADER);
shader_fp =
glCreateShader(GL_FRAGMENT_SHADER);
Les deux lignes d'après liront les fichiers des shaders. Il s'agit simplement de fichiers texte, peu importe leur extension. La méthode textFileRead() est une méthode qui retourne le contenu d'un fichier de shader comme un char*. Le code de ce tutoriel comprend une implémentation, mais elle ne sera pas commentée, étant donné qu'une connaissance du C++ est attendue.
const
char
*
vsText =
textFileRead(vsFile);
const
char
*
fsText =
textFileRead(fsFile);
Ensuite, on effectue une petite vérification, pour s'assurer que les deux fichiers ont bien été lus. Si une erreur est survenue, on passe juste la méthode, OpenGL continuera normalement, sans les shaders.
if
(vsText ==
NULL
||
fsText ==
NULL
) {
cerr <<
"Either vertex shader or fragment shader file not found."
<<
endl;
return
;
}
Les deux lignes d'après passent le contenu des fichiers de shader à OpenGL pour qu'il les attache à ses variables.
glShaderSource(shader_vp, 1
, &
vsText, 0
);
glShaderSource(shader_fp, 1
, &
fsText, 0
);
Les deux suivantes demandent à OpenGL de compiler les shaders.
glCompileShader(shader_vp);
glCompileShader(shader_fp);
Finalement, on crée le shader_id comme un programme shader. On attache alors le vertex shader et le fragment shader au programme shader et on lie le programme.
shader_id =
glCreateProgram();
glAttachShader(shader_id, shader_fp);
glAttachShader(shader_id, shader_vp);
glLinkProgram(shader_id);
Maintenant que l'on peut lire un fichier de shader et le compiler, on regarde les autres méthodes pour l'utiliser.
La première, id(), retourne l'identifiant du programme, shader_id. Il est utilisé pour lier le shader ou pour lui passer des variables.
unsigned
int
Shader::
id() {
return
shader_id;
}
La seconde, bind(), attache simplement le shader : tout ce qui est dessiné après utilisera ce shader, jusqu'à ce qu'il soit détaché ou qu'un autre shader soit activé.
void
Shader::
bind() {
glUseProgram(shader_id);
}
La troisième est unbind(), qui lie le shader 0, un identifiant de shader réservé à OpenGL, ce qui désactive le shader courant.
void
Shader::
unbind() {
glUseProgram(0
);
}
Que reste-t-il, à présent ? On peut créer un shader, l'utiliser. Il faut encore nettoyer le shader une fois qu'il n'est plus nécessaire.
On le fait dans le destructeur du shader, qui ne devrait être appelé qu'une seule fois, à la fin de l'application ; sinon, il faudra le recréer, ce qui est assez coûteux.
Le destructeur n'utilise que cinq appels à OpenGL. Les deux premiers ne font que détacher les deux shaders, tandis que les trois suivants suppriment tous les shaders puis le programme shader.
glDetachShader(shader_id, shader_fp);
glDetachShader(shader_id, shader_vp);
glDeleteShader(shader_fp);
glDeleteShader(shader_vp);
glDeleteProgram(shader_id);
C'est là tout ce qu'il faut pour créer un programme shader. Ce n'est pas très difficile ou délicat et tout est logique quand on y repense.
#include
"shader.h"
#include
<string.h>
#include
<iostream>
#include
<stdlib.h>
using
namespace
std;
static
char
*
textFileRead(const
char
*
fileName) {
char
*
text;
if
(fileName !=
NULL
) {
FILE *
file =
fopen(fileName, "rt"
);
if
(file !=
NULL
) {
fseek(file, 0
, SEEK_END);
int
count =
ftell(file);
rewind(file);
if
(count >
0
) {
text =
(char
*
)malloc(sizeof
(char
) *
(count +
1
));
count =
fread(text, sizeof
(char
), count, file);
text[count] =
&
#8216
;\0
';
}
fclose(file);
}
}
return
text;
}
Shader::
Shader() {
}
Shader::
Shader(const
char
*
vsFile, const
char
*
fsFile) {
init(vsFile, fsFile);
}
void
Shader::
init(const
char
*
vsFile, const
char
*
fsFile) {
shader_vp =
glCreateShader(GL_VERTEX_SHADER);
shader_fp =
glCreateShader(GL_FRAGMENT_SHADER);
const
char
*
vsText =
textFileRead(vsFile);
const
char
*
fsText =
textFileRead(fsFile);
if
(vsText ==
NULL
||
fsText ==
NULL
) {
cerr <<
"Either vertex shader or fragment shader file not found."
<<
endl;
return
;
}
glShaderSource(shader_vp, 1
, &
vsText, 0
);
glShaderSource(shader_fp, 1
, &
fsText, 0
);
glCompileShader(shader_vp);
glCompileShader(shader_fp);
shader_id =
glCreateProgram();
glAttachShader(shader_id, shader_fp);
glAttachShader(shader_id, shader_vp);
glLinkProgram(shader_id);
}
Shader::
~
Shader() {
glDetachShader(shader_id, shader_fp);
glDetachShader(shader_id, shader_vp);
glDeleteShader(shader_fp);
glDeleteShader(shader_vp);
glDeleteProgram(shader_id);
}
unsigned
int
Shader::
id() {
return
shader_id;
}
void
Shader::
bind() {
glUseProgram(shader_id);
}
void
Shader::
unbind() {
glUseProgram(0
);
}
V. Fichier main.cpp▲
On regarde à présent le fichier main.cpp du projet. Comme la plupart de son contenu provient d'un tutoriel précédent, on n'expliquera que les parties liées à l'utilisation de GLSL et à l'implémentation de la nouvelle classe.
La première chose à faire est d'inclure le fichier d'en-tête :
#include
"shader.h"
Maintenant que l'on a accès à la classe de shader, on déclare un objet Shader. On l'appelle shader pour le moment, faute d'avoir un terme plus approprié dans ce tutoriel.
Shader shader;
Avant de faire quoi que ce soit avec les shaders, on doit appeler glewInit(). Ceci initialise GLEW, de telle sorte que l'on peut utiliser les extensions requises pour les shaders. Ce code ira sous l'appel glutCreateWindow() de la méthode principale.
glewInit();
Jusqu'ici, les choses sont simples. On veut maintenant initialiser le shader. Dans la méthode init(void), on ajoute la ligne suivante :
shader.init("shader.vert"
, "shader.frag"
);
Pour ce tutoriel, les programmes shader s'appelleront shader.vert et shader.frag et se situeront dans le même dossier que l'application compilée.
Tout ce qu'il reste encore à faire est d'utiliser le shader. Dans la méthode display(void), on veut appliquer le shader au cube, de telle sorte que, avant de dessiner le cube, on lie le shader et on le délie après coup.
shader.bind();
cube();
shader.unbind();
#if ( (defined(__MACH__)) && (defined(__APPLE__)) )
#include
<stdlib.h>
#include
<OpenGL/gl.h>
#include
<GLUT/glut.h>
#include
<OpenGL/glext.h>
#else
#include
<stdlib.h>
#include
<GL/glew.h>
#include
<GL/gl.h>
#include
<GL/glut.h>
#include
<GL/glext.h>
#endif
#include
"shader.h"
Shader shader;
GLfloat angle =
0.0
; // Angle de rotation
void
init(void
) {
glEnable(GL_DEPTH_TEST);
glDepthFunc(GL_LESS);
shader.init("shader.vert"
, "shader.frag"
);
}
void
cube (void
) {
glRotatef(angle, 1.0
, 0.0
, 0.0
); // Rotation sur l'axe X
glRotatef(angle, 0.0
, 1.0
, 0.0
); // Rotation sur l'axe Y
glRotatef(angle, 0.0
, 0.0
, 1.0
); // Rotation sur l'axe Z
glColor4f(1.0
, 0.0
, 0.0
, 1.0
);
glutWireCube(2
);
}
void
display (void
) {
glClearColor (0.0
, 0.0
, 0.0
, 1.0
);
glClear (GL_COLOR_BUFFER_BIT |
GL_DEPTH_BUFFER_BIT);
glLoadIdentity();
gluLookAt (0.0
, 0.0
, 5.0
, 0.0
, 0.0
, 0.0
, 0.0
, 1.0
, 0.0
);
shader.bind();
cube();
shader.unbind();
glutSwapBuffers();
angle +=
0.1
f;
}
void
reshape (int
w, int
h) {
glViewport (0
, 0
, (GLsizei)w, (GLsizei)h);
glMatrixMode (GL_PROJECTION);
glLoadIdentity ();
gluPerspective (60
, (GLfloat)w /
(GLfloat)h, 1.0
, 100.0
);
glMatrixMode (GL_MODELVIEW);
}
int
main (int
argc, char
**
argv) {
glutInit(&
argc, argv);
glutInitDisplayMode(GLUT_DOUBLE |
GLUT_RGBA |
GLUT_DEPTH;
glutInitWindowSize(500
, 500
);
glutInitWindowPosition(100
, 100
);
glutCreateWindow("A basic OpenGL Window"
);
glewInit()
glutDisplayFunc(display);
glutIdleFunc(display);
glutReshapeFunc(reshape);
init();
glutMainLoop();
return
0
;
}
VI. Source du vertex shader▲
Un vertex shader permet de manipuler les sommets d'un objet. Le principal avantage se montre lors de la manipulation d'un groupe de sommets d'une certaine manière. Par exemple, de l'eau pourrait être créée avec un plan assez tesselé et un vertex shader serait bien plus rapide pour calculer les positions de ces sommets que le mode immédiat.
Ce shader, comme tous les shaders, dispose d'une fonction principale. Il est possible de définir d'autres fonctions dans un shader, mais OpenGL ne cherchera et n'appellera que la fonction principale.
Dans cette fonction, on définit la position des sommets à la même valeur que si l'on n'avait pas utilisé de shader. On multiplie la matrice de projection par le sommet courant pour le placer où il devrait être à l'écran.
La variable gl_Position est utilisée pour définir la position du sommet courant. Une autre manière de le positionner sans les shaders est de définir gl_Position à ftransform, une variable OpenGL qui fait la même chose que la multiplication glModelViewProjectionMatrix * glVertex.
Tous les vertex shaders sont appelés pour chaque sommet individuellement.
Chaque vertex shader doit avoir la ligne
gl_Position =
*
something*
;
parce que l'objectif d'un vertex shader est de définir la position de sommets.
void
main
(
) {
// Définit la position du sommet courant.
gl_Position
=
gl_ModelViewProjectionMatrix *
gl_Vertex;
}
VII. Source du fragment shader▲
Le fragment shader requiert une fonction principale, comme le vertex shader. Par contre, contrairement au vertex shader, le fragment shader est appelé pour chaque pixel à l'écran.
Tous les fragment shaders doivent se terminer avec une ligne
gl_FragColor =
vec4
(*
something*
);
parce que leur objectif est de définir la couleur actuellement écrite à l'écran.
Dans le fragment shader, gl_FragColor sera la couleur à afficher à l'écran. Ici, la valeur est la couleur blanche, de telle sorte que tous les pixels de la forme seront blancs.
GLSL dispose d'une série de types, il est recommandé de regarder le guide de référence rapide de GLSL pour se faire une idée.
gl_FragColor prend la valeur de vec4(), un vecteur de quatre nombres en virgule flottante. On peut l'écrire vec4(red, green, blue, alpha) ou même passer un autre vec4 au constructeur. GLSL fournit même le raccourci vec4(1.0) pour mettre toutes les composantes à l'unité. Cela donne la couleur blanche.
void
main
(
) {
// Définit la couleur de sortie du pixel courant.
gl_FragColor =
vec4
(
1
.0
);
}
VIII. Remerciements▲
Merci à Alexandre Laurent pour son aide à la traduction, ainsi qu'à Maxime Gault et Winjerome pour leur relecture orthographique !