M1-Images

TP pipeline et shaders


L'objectif du tp est de comprendre un élément essentiel de l'écriture des shaders. vous pourrez ensuite afficher les Mesh de votre projet en utilisant vos shaders plutot que ceux par défaut.

rappel : les tutos de gkit... et surtout le tuto9 : utiliser un shader program avec draw(mesh)...


Partie 1 : paramètres uniforms

les shaders sont des fonctions classiques, la seule différence est qu'elles sont exécutées en parallèle par les processeurs de la carte graphique. Avant d'appeler une fonction qui prend un ou plusieurs paramètres, il faut leur affecter une valeur. pour un shader c'est la même chose, mais l'affectation ne s'écrit pas avec a= 10; ou f(10)...

pourquoi ?

la variable a de l'exemple, se trouve quelquepart dans la mémoire de la carte graphique... pas dans la mémoire accessible directement par l'application. openGL fournit un mécanisme en deux temps :

  1. récupérer un identifiant de la variable déclarée par un shader,
  2. affecter une valeur à la variable désignée par l'identifiant.

c'est le même principe que d'affecter une valeur à la variable désignée par un pointeur :

#include "program.h"

GLuint program;

// init( ) :
    // creer un shader program (qui declare des parametres uniforms)
    program= read_program( ... );
    prog
ram_print_errors(program);

// draw( ) :
    // recuperer l'identifiant de la variable "a"
    program GLint id= glGetUniformLocation(program, "a");
    // affecter une valeur
    glUniform(id, 10);

openGL est une librairie C, sans surcharge, glUniform est une famille de fonctions dont le nom se termine par le type de la valeur à affecter :

  1. affecter 1 int : glUniform1i(id, v),
  2. affecter 1 float : glUniform1f(id, v),
  3. affecter un Point (3 float) : glUniform3f(id, x, y, z),
  4. affecter une Color (4 float) : glUniform4f(id, r, g, b, a),
  5. affecter un Point par pointeur : Point p; glUniform3fv(id, &p);
  6. affecter une Color par pointeur ; Color c; glUniform4fv(id, &c);

plus de détails dans le tuto3 dans la section "uniforms et shader program".

si vous trouvez tout ça fastidieux, "uniforms.h" fournit les surcharges les plus courantes, par exemple :

#include "program.h"
#include "uniforms.h"

GLuint program= { ... };

program_uniform(program, "a", int(1));


exercice 1 : lisez le tuto9

exercice 2 : mettez à jour gKit : git pull et compilez tuto10.

modifiez tuto10 pour changer la couleur du cube. vous pouvez le faire en changeant la constante dans le source du shader, mais l'idée est que la couleur soit un paramètre (uniform) du fragment shader. il faudra donc l'initialiser dans l'application.

exercice 3 : modifiez tuto10 pour changer la position du cube et affichez un autre cube d'une autre couleur à un autre endroit. la position et la couleur sont des paramètres des shaders...
rappel : les transformations classiques sont fournies dans mat.h


Partie 2 : paramètres varyings

le pipeline interpole les varyings, les sorties du vertex shader, avant d'exécuter les fragments shaders qui déterminent la couleur des pixels de l'image du triangle.

exercice 1 : remplacez votre shader précédent par celui ci, tuto10_uv.glsl

#version 330

#ifdef VERTEX_SHADER

uniform mat4 mvpMatrix;

layout(location= 0) in vec3 position;

out vec3 vertex_uv;    // declare une sortie du vertex shader (un paramètre varying), mot clé : out

void main( )
{  
    gl_Position= mvpMatrix * vec4(position, 1);

    // coordonnees barycentriques des sommets abc d'un triangle
    const vec3 uv[3]= vec3[3](vec3(1, 0, 0), vec3(0, 1, 0), vec3(0, 0, 1));
   
    // exporte les coordonnees barycentriques du sommet traite par le shader
    vertex_uv= uv[gl_VertexID%3];
}
#endif


#ifdef FRAGMENT_SHADER

in vec3 vertex_uv;    // recupere la sortie du vertex shader (un parametre varying), mot-clé : in
// meme type et meme nom que la sortie du vertex shader

out vec4 fragment_color;

void main( )
{
    fragment_color= vec4(vertex_uv, 1);
}
#endif


que se passe-t-il ? pourquoi ?



exercice 2 : afficher les aretes ou les sommets d'un triangle.
il est assez simple de savoir que le fragment, le point sur le triangle, est proche d'une arete ou d'un sommet, connaissant ses coordonnées baycentriques.

vous devez obtenir quelquechose qui ressemble à ça, c'est à dire une "distance" au bord :



quelle est la valeur maximum de cette distance ? à quel point du triangle cela ce produit-il ? quelles sont les coordonnées de ce point ?

il ne reste plus qu'a choisir un seuil et décider d'utiliser une couleur "bordure" ou une couleur "interieur" :




pour les curieux : dFdx(), dFdy() et fwidth() (cf opengl.org reference pages) permettent de connaitre la valeur de n'importe quelle variable évaluée pour le fragment voisin, ce qui permet de controller l'epaisseur en pixels de la bordure.

exercice 3 : remplacez les constantes controllant l'épaisseur de la bordure, la couleur de la bordure et de l'intérieur par des paramètres. lequels ? comment leur affecter une valeur ?

exercice 4 : les fragments peuvent être détruits avant d'être écrits dans l'image résultat, ce qui permet de faire des "trous" dans les triangles. utilisez "discard" pour détruire un fragment, et utilisez cette instruction pour détruire les fragments à l'intérieur du triangle.




quelques idées pour debugger un shader :

il n'y a pas d'outil pour inspecter les valeurs des variables locales d'un shader, il faut donc tricher un peu.

une solution courante renvoie la valeur d'une variable comme une couleur, mais il faut ensuite interpreter le résultat.
(attention : l'image n'affiche que des valeurs positives comprises entre 0 et 1, il faudra normaliser les valeurs des variables dans certains cas, et/ou utiliser abs()).

une autre idée permet de faire l'equivalent d'un assert( ) : si la condition est fausse, renvoyez une couleur improbable, violet/magenta, par exemple, vec4(1, 0, 1, 1).
(vous pouvez aussi utiliser discard pour détruire les fragments pour lesquels la condition est vraie).

quelques erreurs classiques :