gKit2 light

- Main Page
- Related Pages
- Modules
- Classes
- Files

compiler et linker un shader program

cf tuto2GL.cpp

un shader program est la même chose que d'habitude : le code exécutable crée par l'édition de liens des binaires obtenus après la compilations des shaders.

et comme d'habitude, il peut y avoir des erreurs de compilation et des erreurs d'édition de liens.

la démarche est très classique :

- lire le fichier source,
- créer un vertex shader
- lui donner le source,
- compiler,
- recommencer pour le fragment shader,
- créer un shader program
- associer les 2 shaders au program,
- linker
- utiliser / exécuter, si tout c'est bien passé.

la partie la plus longue est finalement la vérification et la récupération des erreurs, sans grande surprise...

rappel : les objets openGL sont manipulés à travers des identifiants numériques, openGL définit le type GLuint spécialement pour ça. vous pouvez repasser dans interface C openGL, si nécessaire.

## lire les sources

le plus simple en C++, renvoie une chaine de caractères std::string :

std::string read( const char *filename )
{
std::stringbuf source;
std::ifstream in(filename);
// verifie que le fichier existe
if(in.good() == false)
// affiche une erreur, si le fichier n'existe pas ou n'est pas accessible
printf("[error] loading program '%s'...\n", filename);
else
printf("loading program '%s'...\n", filename);
// lire le fichier, jusqu'au premier separateur,
// le caractere '\0' ne peut pas se trouver dans un fichier texte, donc lit tout le fichier d'un seul coup
in.get(source, 0);
// renvoyer la chaine de caracteres
return source.str();
}

## créer et compiler les shaders

GLenum type= GL_VERTEX_SHADER; // ou GL_FRAGMENT_SHADER...
GLuint shader= glCreateShader(type);

il faut ensuite préparer les sources à compiler. la fonction glShaderSource() veut un tableau de chaines de caracteres. il faut donc le construire.

std::string source= read( "vertex_shader.glsl" );
const char *strings[]= { source.c_str() };
glShaderSource(shader, 1, strings, NULL);

puis compiler le shader :

glCompileShader(shader);

## vérifier les erreurs de compilation du shader

un objet shader openGL maintient un état de compilation ainsi que les messages d'erreurs (et sonsource également). il suffit de l'interroger avec glGetShader( ) :

GLint status;
glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
if(status == GL_TRUE)
// pas d'erreurs de compilation
else
// erreurs

si la compilation à échoué, il faut récupérer et afficher les messages, cf glGetShaderInfoLog( ). comme on ne connait pas a priori la longueur des messages, il faut la demander, allouer une chaine de caractères de la bonne taille et enfin récupérer les messsages...

// recuperer la longueur des messages d'erreurs
GLint length;
glGetShaderiv(shader, GL_INFO_LOG_LENGTH, &length);
// allouer une chaine de caracteres, length compte le 0 a la fin de la chaine
char *message= new char [length];
// recuperer les messages d'erreurs
glGetShaderInfoLog(shader, length, message, NULL);
// afficher
printf("[errors]\n%s\n", message);
// nettoyer
delete [] message;

si tout c'est bien passé pour le premier shader, on peut passer au 2ieme, seul le type de shader change : ce sera GL_FRAGMENT_SHADER au lieu de GL_VERTEX_SHADER.

## créer et linker le program

la démarche est la même que pour les shaders, il faut indiquer au program quels shaders linker ensemble avec glAttachShader( ), puis vérifier les erreurs,

GLuint program= glCreateProgram();
glAttachShader(program, vertex_shader);
glAttachShader(program, fragment_shader);
glLinkProgram(program);
GLint status;
glGetProgramiv(program, GL_LINK_STATUS, &status);
if(status == GL_TRUE)
// pas d'erreur de link, le program est pret a etre utilise !
else
// erreurs ...

et éventuellement récupérer et afficher les erreurs...

// recuperer la longueur des messages d'erreurs
GLint length;
glGetProgramiv(program, GL_INFO_LOG_LENGTH, &length);
// allouer une chaine de caracteres, length compte le 0 a la fin de la chaine
char *message= new char [length];
// recuperer les messages d'erreurs
glGetProgramInfoLog(program, length, message, NULL);
// afficher
printf("[link errors]\n%s\n", message);
// nettoyer
delete [] message;

voila, rien de bien compliqué, mais c'est un peu long.

un exemple minimaliste est dispo tuto2GL.cpp

une fois que le program est correctement compilé et linké, avant de dessiner avec glDraw( ), il faut donner une valeur à tous ses uniforms et associer un buffer à ses attributs cf afficher plusieurs triangles, modifier les paramètres uniform d'un shader program et configurer un format de sommet, vertex array object. il est aussi possible, pour vérifier que l'on a rien oublié, de demander au shader program quels sont les uniforms déclarés, leur nom, leur type, leur valeur, etc. pour vérifier que l'initialisation est correcte. cf récupérer les uniforms et les attributs utilisés par un shader program.

## formatage des erreurs

par contre les messages d'erreurs sont très succints et n'affichent qu'un numéro de ligne sans aucun contexte, ce qui est assez pénible pour corriger l'erreur.

exemple: la compilation du shader suivant, qui comporte une erreur :

#version 330
const float dx= 0.0;
const float dy= 0;
const float dz= 0;
void main( )
{
// intialiser les coordonnees des 3 sommets
vec3 positions[3]= vec3[3]( vec3(-0.5, -0.5, 0), vec3(0.5, -0.5, 0), vec3(0, 0.5, 0) );
// recuperer le sommet a traiter
vec3 p= positions[i];
// calculer le resultat
vec4 r;
r.x= p.x + dx;
r.y= p.y + dy;
r.z= p.z + dz;
r.w= 1;
// renvoyer le sommet transforme
gl_Position= r;
}

renvoie les erreurs :

[errors]
0(15) : error C1008: undefined variable "i"

ce qui n'est pas vraiment utilisable...

program.h fournit une fonction utilitaire qui analyse le message d'erreur, retrouve les numéros de lignes sur lesquels se sont produit les erreurs, et les insère dans le source du shader :

GLuint program= ... ;
program_print_errors(program);

ce qui donne :

0001 #version 330
0003
0004
0005 const float dx= 0.0;
0006 const float dy= 0;
0007 const float dz= 0;
0008
0009 void main( )
0010 {
0011 // intialiser les coordonnees des 3 sommets
0012 vec3 positions[3]= vec3[3]( vec3(-0.5, -0.5, 0), vec3(0.5, -0.5, 0), vec3(0, 0.5, 0) );
0013
0014 // recuperer le sommet a traiter
0015 vec3 p= positions[i];
error C1008: undefined variable "i"
0016
0017 // calculer le resultat
0018 vec4 r;
0019 r.x= p.x + dx;
0020 r.y= p.y + dy;
0021 r.z= p.z + dz;
0022 r.w= 1;
0023
0024 // renvoyer le sommet transforme
0025 gl_Position= r;
0026 }

vous pouvez compiler les shaders comme vous voulez et utiliser uniquement program_print_errors() ou program_format_errors().

pour les curieux : le source est dispo program.cpp, fonctions print_line( ) et print_errors( ).

## utilitaires : program.h

program.h fournit une fonction permettant de compiler plusieurs shaders écrits dans le même fichier, cf read_program( ).

GLuint program= read_program( "tutos/intro1.glsl" );

les shaders sont compilés séparement en utilisant le pré processeur, avec #ifdef VERTEX_SHADER, #ifdef FRAGMENT_SHADER, etc. un fichier .glsl ressemble à ça :

#version 330
#ifdef VERTEX_SHADER
void main( )
{
// intialiser les coordonnees des 3 sommets
vec3 positions[3]= vec3[3]( vec3(-0.5, -0.5, 0), vec3(0.5, -0.5, 0), vec3(0, 0.5, 0) );
// recuperer le sommet a traiter
gl_Position= positions[gl_VertexID];
}
#endif
#ifdef FRAGMENT_SHADER
void main( )
{
// remplir le triangle avec une couleur uniforme
gl_FragColor= vec4(0.8, 0.4, 0, 1);
}
#endif

## utilitaires : shader_kit

autre solution, shader_kit.cpp peut être utilisé comme compilateur de shader, il affiche les erreurs sous cette forme un peu plus lisible, et permet de recharger et de recompiler le shader en appuyant sur la touche R. ça permet de corriger interactivement le source du shader en le modifiant dans un éditeur.

shader_kit est un 'bac à sable' permettant d'écrire et de tester des shaders assez facilement. il permet aussi de charger un objet .obj, des textures et permet de déplacer la camera à la souris.

comme il n'est pas possible d'écrire une solution totalement générique, shader_kit à des limites :

- 1 seul shader program, écrit dans un seul fichier .glsl, avec les #ifdef VERTEX_SHADER, etc, voir plus haut,
- il n'est pas obligatoire de charger un objet, dans ce cas, il fonctionne comme shadertoy.com,
- charge 1 seul objet,
- plusieurs textures 2d rgba8, jusqu'à la limite d'openGL, 16 ou 32 selon les machines :
  - uniform sampler2D texture0;
  - uniform sampler2D texture1; etc, en fonction du nombre d'images chargées,

il définit également les valeurs de plusieurs uniforms :

- uniform float time; le temps en milli secondes,
- uniform vec3 mouse; la position x, y de la souris dans la fenêtre, et l'état du bouton gauche, mouse.z (!= 0 enfoncé, == 0 pas enfoncé),
- uniform vec3 motion; le déplacement x, y de la souris, et l'état du bouton gauche, motion.z (!= 0 enfoncé, == 0 pas enfoncé),
- uniform vec2 viewport; les dimensions de l'image / de la fenêtre,
- ainsi que les transformations standards et leurs inverses :
  - uniform mat4 mvpMatrix, mvpInvMatrix; la transformation standard pour passer du repère local au repère projectif,
  - uniform mat4 mvMatrix; la composition de model et view, repère local vers le repère caméra,
  - uniform mat4 normalMatrix; même transformation que mv, mais pour les normales des sommets,
  - uniform mat4 modelMatrix, modelInvMatrix;
  - uniform mat4 viewMatrix, viewInvMatrix;
  - uniform mat4 projectionMatrix, projectionInvMatrix;

malgré ces limites, on peut écrire des choses interressantes :

- un shader qui éclaire un objet texturé :
- un lancer de rayons classique :
- un lancer de rayons sur champ de distance :

cf tuto2GL.cpp