gKit2 light
premiers shaders

maintenant que construire ou charger des objets et les afficher est plus clair, cf première application avec gKit, que peut-on faire de plus ?

écrire des shaders !

comment ça marche ?

un shader est une fonction assez classique exécutée par les processeurs de la carte graphique, il y a 2 types de shaders :

les shaders d'openGL s'écrivent en GLSL, c'est un langage très proche du C/C++ avec quelques différences quand meme, et une librairie de fonctions standards pour faire des calculs sur des points, des matrices, des vecteurs, des couleurs, etc...

pourquoi transformer les sommets ?

au miminum, on souhaite dessiner les objets pour le point de vue de la camera... il faut bien déterminer si les objets sont devant, derriere, etc, et surtout on a besoin de savoir sur quels pixels de l'image ils se projettent pour pouvoir ensuite dessiner la forme de chaque objet en coloriant tout ces pixels.

lorsque l'application dessine un objet, que se passe-t-il ? ou sont les shaders ?

pour l'instant, ils sont cachés... pour simplifier la prise en main, gKit construit un shader en fonction du contenu du mesh qu'il dessine. mais la seule chose à retenir est que dessiner \( n \) triangles, provoque l'exécution de \( 3 n \) vertex shaders qui transforment les sommets des triangles vers le repère projectif de la camera. ensuite, la carte graphique détermine sur quels pixels dessiner chaque triangle. un fragment shader est exécuté pour calculer la couleur de chacun de ces pixels (mais on ne connait pas le nombre de fragment shaders exécutés à l'avance...)

rappel : la surface des objets est triangulée pour être dessinée par la carte graphique...

fragment ou pixel ? les pixels sont les éléments de l'image et les fragments sont les morceaux des triangles qui couvrent / remplissent un pixel. un fragment est un bout de geometrie avec toutes les informations associées, normale, couleur, matiere, etc... et plusieurs fragments peuvent se projetter sur le meme pixel, le fragment le plus proche de la camera sera le seul visible dans l'image à la fin du dessin / calcul.

chaque shader a besoin de paramètres pour s'exécuter (les matrices de transformation, par exemple, ou une couleur de base), comme n'importe quelle fonction de l'application. une grosse difference existe par contre : ce n'est pas l'application qui appelle / exécute directement la fonction, c'est le pipeline graphique / la carte graphique. pourquoi ? tout ça est fait en parallèle automatiquement sur les processeurs de la carte graphique, et l'application ne controle pas ce mécanisme.

du coup, on ne peut pas écrire vertex_shader( Translation(0, 2, 0) ); pour déplacer tous les sommets le long de l'axe Y... vu que l'appel de la fonction n'est pas immédiat, l'application affecte une valeur à chaque paramètre (avec un mécanisme particulier), et enfin l'application utilise une fonction draw() pour dessiner les triangles. au final, c'est le pipeline graphique qui exécute les shaders et leur passe les paramètres.

pour les curieux : les shaders par défaut de gKit sont construits par DrawParam::create_program() dans draw.cpp.

à quoi ca ressemble ?

les shaders s'écrivent en GLSL, cf shaders et GLSL, et ils utilisent une convention particulière : la fonction se déclare void main( ) et les paramètres se déclarent comme des variables globales, par exemple :

#version 330
uniform float param;
void main( )
{
// utiliser le parametre
{ ... }
}
// remarque : il existe ~10 versions de GLSL... il faut preciser la version du langage utilisée par le shader
// celle d'openGL 3.3 est la version 330...

les résultats (obligatoires...) calculés par les shaders utilisent aussi une convention particulière, ce sont aussi des variables globales déjà déclarées, vec4 gl_Position pour un vertex shader, et vec4 gl_FragColor pour un fragment shader.

resultats obligatoires ? les shaders ont un role précis dans le pipeline graphique, chaque étape du pipeline doit produire des resultats qui sont traités par les étapes suivantes. le role du vertex shader est de produire les coordonnées de chaque sommet dans le repère projectif de la camera, et le role du fragment shader est de produire la couleur du fragment...

autre difference, il y a 2 types de paramètres, les uniform et les autres, que gKit affecte automatiquement. les uniform sont les paramètres classiques. par exemple, on peut déclarer une matrice de transformation 4x4 avec le type mat4 utilisé dans un vertex shader :

#version 330
uniform mat4 mvp; // parametre du vertex shader, transformation vers le repere projectif de la camera
void main( )
{
gl_Position= mvp * position_du_sommet;
}

pour un fragment shader, on peut ecrire :

#version 330
uniform vec4 color; // parametre du fragment shader, affecte par l'application
void main( )
{
gl_FragColor= color;
}
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition: vec.h:168

il faut ensuite compiler ces 2 shaders et les linker pour obtenir un shader program utilisable pour dessiner des objets.

par ou on commence ?

par lire shaders et GLSL qui fournit plus d'informations sur GLSL et les paramètres des shaders.

attention : la doc inclut aussi pas mal de renvois sur le fonctionnement et l'utilisation d'openGL, il n'est pas nécessaire de les lire pour démarrer.

ensuite, le plus simple est d'utiliser shader_kit pour écrire et tester les premiers shaders. lorsqu'ils fonctionnent on peut les intégrer à une application en suivant les explications de tuto9.cpp.

quelques exemples avec shader_kit...

si ce n'est pas déjà fait, lisez comment utiliser shader_kit.

dessiner un triangle rectangle en haut à droite de la fenetre :

#version 330
#ifdef VERTEX_SHADER
void main( )
{
vec4 positions[3]= vec4[3]( vec4(0, 0, 0, 1), vec4(1, 0, 0, 1), vec4(0, 1, 0, 1) );
gl_Position= positions[gl_VertexID];
}
#endif
#ifdef FRAGMENT_SHADER
void main( )
{
gl_FragColor= vec4(1, 1, 1, 1);
}
#endif
// executer avec
bin/shader_kit shader.glsl

les coordonnées des sommets visibles / dessinés sont entre -1 et 1 sur x, y, z (dans le repere projectif de la camera). shader_kit dessine 1 triangle, donc 3 vertex shader sont exécutés pour transformer les sommets d'indice 0, 1, et 2. l'indice du sommet pour lequel le vertex shader s'execute est dans le paramètre (initialisé par le pipeline graphique) gl_VertexID.

et sans shader_kit ? comme il n'y a pas de (coordonnées de) sommets à stocker dans l'application (le shader produit les sommets tout seul), on ne peut pas utiliser Mesh et Mesh::draw(), mais on peut bien sur utiliser directement une fonction draw() d'openGL. et comme ces shaders n'ont pas de paramètres non plus, il suffit de dessiner un triangle / 3 sommets pour que le pipeline graphique exécute les shaders.

init( ):
program= read_program("shader.glsl");
render( ):
glUseProgram(program);
glDrawArrays(GL_TRIANGLES, 0, 3);
GLuint read_program(const char *filename, const char *definitions)
Definition: program.cpp:204
int init(std::vector< const char * > &options)
Definition: shader_kit.cpp:92

et avec des coordonnées en pixels ?

le vertex shader doit produire des coordonnées dans le repère projectif de la camera. si on veut décrire la position des sommets d'un triangle avec les coordonnées des pixels de l'image, il faut transformer ces coordonnées du repère image vers le repère projectif... avec l'inverse de la matrice viewport. ce parametre mat4 viewportInvMatrix est affecte par shader_kit.

#version 330
#ifdef VERTEX_SHADER
uniform mat4 viewportInvMatrix;
void main( )
{
vec4 positions[3]= vec4[3]( vec4(0, 0, 0, 1), vec4(400, 0, 0, 1), vec4(0, 400, 0, 1) );
gl_Position= viewportInvMatrix * positions[gl_VertexID];
}
#endif
#ifdef FRAGMENT_SHADER
void main( )
{
gl_FragColor= vec4(1, 1, 1, 1);
}
#endif
// executer avec
bin/shader_kit shader.glsl

et sans shader_kit ? cf l'exemple précédent. mais dans ce cas, il faut aussi affecter une valeur au paramètre mat4 viewportInvMatrix avant l'appel de draw():

init( ):
program= read_program("shader.glsl");
render( ):
Transform viewport= Viewport(window_width(), window_height()); // passage repere projectif vers image
Transform viewport_inv= Inverse(viewport); // inverse
glUseProgram(program);
program_uniform(program, "viewportInvMatrix", viewport_inv); // affectation du parametre
glDrawArrays(GL_TRIANGLES, 0, 3);
int window_height()
renvoie la hauteur de la fenetre de l'application.
Definition: window.cpp:29
int window_width()
renvoie la largeur de la fenetre de l'application.
Definition: window.cpp:25
Transform Inverse(const Transform &m)
renvoie l'inverse de la matrice.
Definition: mat.cpp:197
Transform Viewport(const float width, const float height)
renvoie la matrice representant une transformation viewport.
Definition: mat.cpp:357
void program_uniform(const GLuint program, const char *uniform, const std::vector< unsigned > &v)
affecte un tableau de valeurs a un uniform du shader program.
Definition: uniforms.cpp:94
representation d'une transformation, une matrice 4x4, organisee par ligne / row major.
Definition: mat.h:21

et avec une animation ?

il suffit d'utiliser uniform float time; qui donne le temps écoulé depuis le démarrage de shader_kit en millisecondes. on peut par exemple deplacer les sommets du triangle en fonction du temps... mais pas trop vite...

#version 330
uniform float time;
#ifdef VERTEX_SHADER
void main( )
{
vec2 positions[3]= vec2[3]( vec2(0, 0), vec2(1, 0), vec2(0, 1) ); // coordonnées en 2d
// interpole entre 2 points o et e en fonction du temps
vec2 o= vec2(-1, -1);
vec2 e= vec2(1, 1);
float t= mod(time / 1000, 1); // t entre 0 et 1...
vec2 p= (1 - t) * o + t * e;
gl_Position= vec4(p + positions[gl_VertexID], 0, 1);
// vec4( p, 0, 1 ) ??
// vec4( p.x, p.y, 0, 1 ), idem mais plus long à écrire...
}
#endif
#ifdef FRAGMENT_SHADER
void main( )
{
gl_FragColor= vec4(1, 1, 1, 1);
}
#endif
// executer avec
bin/shader_kit shader.glsl
vecteur generique, utilitaire.
Definition: vec.h:131

et sans shader_kit ? cf l'exemple précédent. mais dans ce cas, il faut aussi affecter une valeur au paramètre float time avant l'appel de draw():

init( ):
program= read_program("shader.glsl");
render( ):
glUseProgram(program);
program_uniform(program, "time", float(global_time())); // affectation du parametre
glDrawArrays(GL_TRIANGLES, 0, 3);
float global_time()
renvoie le temps ecoule depuis le lancement de l'application, en millisecondes.
Definition: window.cpp:128

et avec un mesh et une camera ?

il faut déclarer les attributs de sommets en respectant la convention de shader kit / Mesh et récupérer la matrice de transformation entre le repère objet et le repère projectif, toujours en fonction des paramètres gérés par shader_kit, cf uniform mat4 mvpMatrix.

#version 330
#ifdef VERTEX_SHADER
layout(location= 0) in vec3 position;
uniform mat4 mvpMatrix;
void main( )
{
gl_Position= mvpMatrix * vec4(position, 1);
}
#endif
#ifdef FRAGMENT_SHADER
void main( )
{
gl_FragColor= vec4(1, 1, 1, 1);
}
#endif
// executer avec
bin/shader_kit shader.glsl data/robot.obj
vecteur generique, utilitaire.
Definition: vec.h:146

et sans shader_kit ? dans ce cas, il y a bien des coordonnées de sommets à stocker, on va pouvoir utiliser Mesh comme prévu. quels sont les paramètres des shaders ?

init( ):
program= read_program("shader.glsl");
mesh= read_mesh("mesh.obj");
render( ):
// transformations
Transform model= { ... };
Transform mvp= camera().projection() * camera().view() * model;
glUseProgram(program);
program_uniform(program, "mvpMatrix", mvp);
mesh.draw(program, /* use position */ true, /* use texcoord */ false, /* use normal */ false, /* use color */ false,
/* use material index */ false);
void draw(const GLuint program, const bool use_position, const bool use_texcoord, const bool use_normal, const bool use_color, const bool use_material_index)
dessine l'objet avec un shader program.
Definition: mesh.cpp:768
Transform projection(const int width, const int height, const float fov)
fixe la projection reglee pour une image d'aspect width / height, et une demi ouverture de fov degres...
Definition: orbiter.cpp:47
Transform view() const
renvoie la transformation vue.
Definition: orbiter.cpp:40
Mesh read_mesh(const char *filename)
charge un fichier wavefront .obj et renvoie un mesh compose de triangles non indexes....
Definition: wavefront.cpp:14

et avec des couleurs ?

les shaders précédents donnent la même couleur à tous les pixels, ce qui permet de dessiner une forme simple mais devient vite très limité lorsque l'on charge un objet 3d...

en reprenant le premier exemple, rien n'empêche d'associer une couleur à chaque sommet du triangle... mais le vertex shader ne peut pas écrire de couleur dans un pixel, c'est le rôle du fragment shader. il faut donc transmettre la couleur au fragment shader en déclarant une sortie dans le vertex shader, cf out vec3 vertex_color; et l'entrée associée dans le fragment shader, cf in vec3 vertex_color;. ce paramètre s'appelle un varying, cette valeur n'existe que pendant l'exécution du pipeline graphique, elle n'est pas accessible à l'application...

#version 330
#ifdef VERTEX_SHADER
out vec3 vertex_color;
void main( )
{
vec4 positions[3]= vec4[3]( vec4(-0.5, -0.5, 0, 1), vec4(0.5, -0.5, 0, 1), vec4(0, 0.5, 0, 1) );
vec3 colors[3]= vec3[3]( vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1) );
gl_Position= positions[gl_VertexID];
vertex_color= colors[gl_VertexID];
}
#endif
#ifdef FRAGMENT_SHADER
in vec3 vertex_color;
void main( )
{
gl_FragColor= vec4(vertex_color, 1);
}
#endif

ce résultat est quand même un peu surprenant, non ? pourquoi la couleur est-elle interpolée entre les sommets ? chaque fragment recoit un mélange de la couleur des 3 sommets (le 1er sommet est rouge, le second vert et le dernier bleu...)

ce mécanisme d'interpolation des varyings, des sorties du vertex shader, fait parti du fonctionnement du pipeline graphique.

et sans shader_kit ? comme dans le tout premier exemple...

init( ):
program= read_program("shader.glsl");
render( ):
glUseProgram(program);
glDrawArrays(GL_TRIANGLES, 0, 3);