2. Mise en place des shaders dans un programme▲
2-A. Utilisation des shaders (Documentation OpenGL 2.1)▲
Comme pour tout élément qui s'intègre dans OpenGL (textures, buffers…) il faut créer des objets OpenGL pour accueillir l'élément voulu (n'oubliez pas que, pour un programme shader, nous avons deux sources : une pour le vertex shader; une pour le fragment shader, donc nous avons besoin de deux objets).
- GLuint glCreateShader (GLenum shaderType)
shaderType : doit contenir GL_VERTEX_SHADER pour créer un vertex shader.
retourne l'identifiant du shader.
-
GLuint glCreateShader (GLenum shaderType)
shaderType : doit contenir GL_FRAGMENT_SHADER pour créer un fragment shader.
retourne l'identifiant du shader.
Lecture des fichiers, le contenu est passé à (encore une fois, il faut faire deux fois l'appel à la commande) :
- void glShaderSource (GLuint shader, GLsizei count, const GLchar ** string, const GLint * length)
shader : contient l'identifiant du shader, retourné par glCreateShader().
Count : indique le nombre d'éléments dans string et length.
string : est un pointeur sur le code source de notre ou nos shaders.
length : indique la taille de notre ou nos shaders.
Comme tout programme, nous compilons le code (deux appels nécessaires, encore) :
- void glCompileShader (GLuint shader)
shader : contient l'identifiant de notre shader.
La vérification des erreurs est très importante, sans quoi, vous ne savez pas si votre compilation fonctionne ou pas :
- void glGetShader (GLuint shader, GLenum pname, GLint * params)
shader : contient l'identifiant de notre shader.
pname : contient un identifiant définissant la valeur qui va être retournée. Ici nous utilisons GL_COMPILE_STATUS.
params : contiendra la valeur retournée par OpenGL.
Et pour connaître l'erreur (si elle est présente) :
- void glGetShaderInfoLog (GLuint shader, GLsizei maxLength, GLsizei * length, GLchar * infoLog)
shader : contient l'identifiant de notre shader.
maxLength : contient la taille que vous avez allouée pour recevoir le message d'erreur.
length : contiendra la taille du message retourné.
infoLog : la chaîne de caractères qui contiendra le message d'erreur.
Maintenant que nos deux (vertex et fragment) shaders sont compilés il faut créer le programme. Encore une fois, il nous faut un objet OpenGL et son identifiant :
- GLuint glCreateProgram (void)
retourne l'identifiant du Program Shader.
Une fois l'ID créé, vous devez dire à OpenGL quel shader vous voulez mettre dans le programme (opération de liage ('linking' en anglais)) :
- void glAttachShader (GLuint program, GLuint shader)
program : contient l'identifiant du Program Shader auquel nous voulons attacher le shader.
shader : contient l'identifiant du shader que nous voulons attacher.
Nos deux shaders liés au programme vont être mis ensembles pour former le programme. Cette dernière étape se fait avec la commande :
- void glLinkProgram(GLuint program)
program : contient l'identifiant du Program shader que nous voulons lier.
Enfin, le shader devrait être prêt à être utilisé. Mais je conseille vivement de vérifier les erreurs avec :
- void glGetProgram (GLuint program, GLenum pname, GLint * params)
program : contient l'identifiant du Program Shader.
pname : contient un enum déterminant ce que nous voulons en retour. Ici nous utilisons GL_LINK_STATUS.
params : contiendra la valeur retourné par OpenGL.
Si une erreur est rapportée, vous pouvez en connaître la cause avec :
- void glGetProgramInfoLog (GLuint program, GLsizei maxLength, GLsizei * length, GLchar * infoLog)
program : contient l'identifiant du Program Shader.
maxLength : contient la taille que vous avez allouée pour recevoir le message d'erreur.
length : contiendra la taille du message retourné.
infoLog : la chaîne de caractères qui contiendra le message d'erreur.
Maintenant, vous pouvez utiliser le shader avec :
- void glUseProgram (GLuint program)
program : contient l'identifiant du Program Shader.
Si vous ne voulez plus utiliser de shader, vous pouvez appeler cette commande en passant 0.
Une fois que vous avez fini d'utiliser avec vos shaders, vous allez devoir les détruire.
Première chose à faire, nous arrêtons de les utiliser :
- void glUseProgram (GLuint program)
program : contient l'identifiant du Program Shader. Ici nous mettons 0 pour ne plus utiliser de shader.
Après, nous détachons les shaders du programme :
- void glDetachShader (GLuint program, GLuint shader)
program : contient l'identifiant du Program Shader.
shader : contient l'identifiant du shader à détacher du programme.
Puis nous détruisons le programme :
- void glDeleteProgram (GLuint program)
program : contient l'identifiant du Program Shader.
Puis les deux shaders :
- void glDeleteShader (GLuint shader)
shader : contient l'identifiant du shader à détruire.
Sachez que :
- si des shaders sont encore liés à un programme, l'appel à glDeleteShader() va mettre les shaders en attente pour suppression et ils seront supprimés lorsqu'ils seront détachés (glDetachShader()) ;
- si le programme est encore utilisé, l'appel à glDeleteProgram() va mettre le programme en attente pour suppression, et il sera supprimé une fois inutilisé (glUseShader(0)) ;
- le fait de supprimer un programme détache automatiquement les shaders qui lui sont attachés.
En prenant en considération tout ce qui vient d'être dit, il est conseillé de :
- supprimer les shaders dès que vous les avez attachés (si vous ne voulez pas les attacher une autre fois, bien sur) ;
- supprimer le programme une fois qu'il n'y en a plus besoin. La suppression du programme détachera les shaders, ce qui fera qu'ils seront supprimés tout de suite avec le programme.
2-B. Lecture d'un fichier texte pour l'intégrer comme shaders▲
Comme nous avons pu le voir, la commande glShaderSource() accepte un tableau de chaines de caractères et non le fichier directement. Ceci est fait pour que le code d'OpenGL soit le plus portable possible (par exemple nous pouvons mettre directement notre code source dans une variable dans le code de notre programme). Ici, nous allons voir comment nous pouvons lire un fichier pour OpenGL.
La première chose à faire sur notre fichier est de récupérer la taille du fichier. Une méthode consiste à mettre le pointeur du fichier directement sur la fin est à récupérer sa position :
long
getFileSize
(
FILE*
pFile)
{
long
length =
0
;
fseek
(
pFile,0
,SEEK_END);
length =
ftell
(
pFile);
//
Ne
pas
oublier
de
mettre
le
fichier
à
son
début,
sinon
on
ne
peut
pas
le
lire
fseek
(
pFile,0
,SEEK_SET);
return
length;
}
Ensuite, il faut récupérer le contenu du fichier en lui même :
char
*
fileContent =
(
char
*
)malloc
(
fileLength+
1
); //
+1
pour
terminer
la
chaine
proprement
if
(
fileContent =
=
NULL
)
{
return
-
3
;
}
fread
(
fileContent,fileLength,1
,pFile);
//
Termine
le
tableau
qui
contient
le
shader
fileContent[fileSize] =
'
\0
'
;
//
Utilisation
avec
glShaderSource()
free
(
fileContent);
Vous pouvez voir que l'on réutilise la taille du fichier trouvé plus tôt.
Vous pouvez trouver le code que j'utilise pour charger des shaders, ici. Ci-dessous, nous allons voir les sections les plus importantes de ce code. Afin de simplifier le code, je n'ai pas fait une vérification minutieuse des erreurs.
//
Pour
plus
de
simplicité,
j'ajoute
une
fonction
qui
vérifie
la
compilation
des
shaders
char
checkShaderCompilation
(
GLuint shaderID)
{
GLint compilationStatus =
0
;
//
Vérification
de
la
compilation
pour
le
vertex
shader
glGetShaderiv
(
vertexID, GL_COMPILE_STATUS, &
compilationStatus);
if
(
compilationStatus !
=
GL_TRUE )
{
//
Nous
savons
que
la
compilation
a
échoué,
donc
nous
devons
savoir
pourquoi
//
Nous
devons
récupéré
la
taille
du
log
GLint logLength =
0
;
GLchar*
log =
NULL
;
glGetShaderiv
(
shaderID, GL_INFO_LOG_LENGTH, &
logLength);
//
Maintenant
que
nous
avons
la
taille,
nous
pouvons
alloué
la
mémoire
suffisante
pour
ce
log
log =
(
GLchar*
)malloc
(
logLength);
if
(
log =
=
NULL
)
{
fprintf
(
stderr,"
Erreur
d
'
allocation
de
mémoire
pour
le
log
de
la
compilation
du
shader
\n
"
);
return
0
;
}
glGetShaderInfoLog
(
shaderID, logLength, &
logLength, log);
//
On
peut
afficher
le
message
fprintf
(
stderr,"
Erreur
de
compilation
:
\n
%
s
"
,log);
//
Et
on
n'oublie
pas
de
libéré
la
mémoire
free
(
log);
return
0
;
}
return
1
; //
Pas
d'erreur
}
Le code qui suit charge et compile nos shaders :
void
loadShader
(
)
{
//
Lecture
des
fichiers
//
Certaines
personnes
aiment
avoir
le
fichier
du
vertex
shader
avec
l'extension
.vert
//
et
le
fichier
du
fragment
shader
avec
l'extension
.frag
GLchar*
vertexSource =
(
GLchar*
)readFile
(
"
data
/
simple
.
vert
"
);
GLchar*
fragmentSource =
(
GLchar*
)readFile
(
"
data
/
simple
.
frag
"
);
GLint programState =
0
;
GLint vertexSize =
0
;
GLint fragmentSize =
0
;
//
Création
des
IDs
vertexID =
glCreateShader
(
GL_VERTEX_SHADER);
fragmentID =
glCreateShader
(
GL_FRAGMENT_SHADER);
//
Vérification
des
fichiers
if
(
!
vertexSource |
|
!
fragmentSource )
{
//
Ici,
il
faudrait
faire
en
sorte
que
le
programme
s'arrête
deleteShader
(
); //
Nettoyage
return
;
}
//
Chargement
des
sources
dans
OpenGL
vertexSize =
strlen
(
vertexSource);
fragmentSize =
strlen
(
fragmentSource);
glShaderSource
(
vertexID, 1
, (
const
GLchar*
*
)(
&
vertexSource), &
vertexSize);
glShaderSource
(
fragmentID, 1
, (
const
GLchar*
*
)(
&
fragmentSource), &
fragmentSize);
//
Compilation
du
vertex
shader
glCompileShader
(
vertexID);
glCompileShader
(
fragmentID);
//
Vérification
des
erreurs
if
(
!
checkShaderCompilation
(
vertexID) |
|
!
checkShaderCompilation
(
fragmentID))
{
deleteShader
(
);
return
;
}
//
Creation
de
l'ID
pour
le
programme
programID =
glCreateProgram
(
);
//
On
attache
les
shaders
ensemble
glAttachShader
(
programID, vertexID);
glAttachShader
(
programID, fragmentID);
//
On
peut
enfin
passer
aux
liage.
glLinkProgram
(
programID);
//
Et
encore
une
fois
on
vérifie
si
tout
se
passe
bien
glGetProgramiv
(
programID , GL_LINK_STATUS , &
programState);
if
(
programState !
=
GL_TRUE)
{
//
On
récupère
la
taille
du
log
GLint logSize =
0
;
GLchar*
log =
NULL
;
glGetProgramiv
(
programID, GL_INFO_LOG_LENGTH, &
logSize);
//
On
peut
allouer
la
mémoire,
une
fois
que
l'on
a
la
taille
du
log
log =
(
GLchar*
)malloc
(
logSize);
if
(
log =
=
NULL
)
{
fprintf
(
stderr,"
Erreur
d
'
allocation
de
mémoire
pour
le
log
de
la
compilation
du
programme
\n
"
);
deleteShader
(
);
return
;
}
//
Et
on
récupère
le
log
glGetProgramInfoLog
(
programID, logSize, &
logSize, log);
//
On
affiche
fprintf
(
stderr,"
Erreur
lors
du
liage
du
shader
:
\n
%
s
"
,log);
free
(
log);
deleteShader
(
);
return
;
}
//
Voilà,
nous
sommes
prêt
glUseProgram
(
programID);
}
Et pour notre premier essai, nous utilisons le vertex shader ci-dessous. Ce vertex shader reproduit le comportement du pipeline fixe afin de définir la position du sommet. La deuxième ligne copie la couleur du sommet au fragment shader.
void
main (
void
)
{
gl_Position =
ftransform
(
);
gl_FrontColor =
gl_Color;
}
Le fragment shader suivant copie la couleur attribuée aux sommets dans les pixels.
void
main
(
void
)
{
gl_FragColor =
gl_Color;
}
Si vous lancez le programme, vous remarquez que rien de spectaculaire ne se passe. Il n'y a que le cube qui tourne et il a les couleurs comme indiquées dans le programme OpenGL. En plus, si vous supprimez tout le code lié au shader, ou même, plus simple, si vous enlevez le code glUseShader() de la fonction loadShader(), vous aurez exactement le même résultat :
Vous pouvez télécharger le projet (Code::Blocks / Windows) en cliquant iciProgramme OpenGL basique pour travailler avec les shaders
Voilà, nous avons le premier programme OpenGL utilisant des shaders en état de marche. Certes le résultat n'est pas spectaculaire, mais cela est un très bon début.
Maintenant, vous allez remplacer votre vertex shader, par le suivant :
void
main (
void
)
{
gl_Position =
ftransform
(
);
gl_Position =
vec4
(
gl_Position.xyz *
0
.3
,gl_Position.w);
gl_FrontColor =
gl_Color;
}
Voilà, notre cube est devenu tout petit. Effectivement, le vertex shader que j'ai fait rétrécit les dimensions des vertex :
Vous pouvez reprendre le premier vertex shader que je vous ai présenté, mais remplacer le fragment shader par le suivant :
void
main
(
void
)
{
gl_FragColor =
vec4
(
1
.0
,0
.0
,0
.0
,1
.0
);
}
Maintenant, notre cube est tout rouge alors que je n'ai touché aucune ligne de code du programme OpenGL :
Mais, sachez que les shaders ne permettent pas uniquement de changer la taille ou la couleur des objets, ils permettent beaucoup plus et nous allons le voir dans la suite de ce tutoriel.