gKit2 light
Loading...
Searching...
No Matches
premiers shaders avec shader_kit

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 :

  • vertex shader : applique les transformations aux sommets des objets pour les dessiner,
  • fragment shader : calcule les couleurs des pixels pour dessiner la forme des objets dans l'image.

les shaders d'openGL s'écrivent en GLSL, c'est un langage très proche du C/C++ avec quelques différences quand même, 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 minimum, on souhaite dessiner les objets pour le point de vue de la camera... il faut bien déterminer si les objets sont devant, derrière, 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 caméra. 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 géométrie avec toutes les informations associées, normale, couleur, matière, etc... et plusieurs fragments peuvent se projeter sur le même pixel, le fragment le plus proche de la caméra 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 différence 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 contrôle 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 paramètre
{ ... }
}
// remarque : il existe ~10 versions de GLSL... il faut préciser 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.

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

autre différence, 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 écrire :

#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:192

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 première application avec des shaders.

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 fenêtre :

#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 repère projectif de la caméra). 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'exécute est dans le paramètre (initialisé par le pipeline graphique) gl_VertexID.

et sans shader_kit ? il suffit de créer un Mesh vide, sans géométrie et d'utiliser Mesh::draw(first, count, program) qui permet d'indiquer les indices des sommets.

init( ):
mesh= Mesh(GL_TRIANGLES);
program= read_program("shader.glsl");
render( ):
glUseProgram(program);
mesh.draw(0, 3, program); // dessine 3 sommets en partant de 0 : ie 0, 1, 2
representation d'un objet / maillage.
Definition mesh.h:121
GLuint read_program(const char *filename, const char *definitions)
Definition program.cpp:218
int init(std::vector< const char * > &options)

et avec des coordonnées en pixels ?

le vertex shader doit produire des coordonnées dans le repère projectif de la caméra. 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 paramètre mat4 viewportInvMatrix est affecté 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( ):
mesh= Mesh(GL_TRIANGLES);
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
mesh.draw(0, 3, program); // dessine 3 sommets en partant de 0 : ie 0, 1, 2
int window_height()
renvoie la hauteur de la fenetre de l'application.
Definition window.cpp:27
int window_width()
renvoie la largeur de la fenetre de l'application.
Definition window.cpp:23
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
representation d'une transformation, une matrice 4x4, organisee par ligne / row major.
Definition mat.h:21

et avec une animation ?

il suffit d'utiliser time qui donne le temps écoulé depuis le démarrage de shader_kit en millisecondes. on peut par exemple déplacer 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:152

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

init( ):
mesh= Mesh(GL_TRIANGLES);
program= read_program("shader.glsl");
render( ):
glUseProgram(program);
program_uniform(program, "time", global_time()); // affectation du parametre
mesh.draw(0, 3, program); // dessine 3 sommets en partant de 0 : ie 0, 1, 2
float global_time()
renvoie le temps ecoule depuis le lancement de l'application, en millisecondes.
Definition window.cpp:126

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:169

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);
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 reçoit 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( ):
mesh= Mesh(GL_TRIANGLES);
program= read_program("shader.glsl");
render( ):
glUseProgram(program);
mesh.draw(0, 3, program);