gKit2 light
tuto9.cpp

cf tuto9.cpp + tuto9_color.glsl

draw( mesh, model, camera ) et les autres utilitaires de draw.h dessinent un objet avec un shader crée en fonction de la description de l'objet et des paramètres. Il est aussi très simple de continuer à utiliser Mesh pour décrire l'objet et de le dessiner avec un shader différent, cf Mesh::draw( program, ... ). Il faut par contre créer et configurer le shader avant de pouvoir dessiner l'objet, ainsi que respecter la même convention que Mesh pour décrire les sommets.

repassez dans introduction api 3d, openGL et pipeline graphique et shaders et GLSL, si nécessaire.

créer un shader program

les détails openGL sont dans compiler et linker un shader program, le plus simple est d'utiliser l'utilitaire read_program() fournit par program.h :

#include "program.h"
GLuint program= 0;
program= read_program("shader.glsl");
GLuint read_program(const char *filename, const char *definitions)
Definition: program.cpp:204
int program_print_errors(const GLuint program)
affiche les erreurs de compilation.
Definition: program.cpp:432

configurer un shader program

les shaders sont des fonctions comme les autres, pour produire leurs résultats, il faut d'abord affecter une valeur à leurs paramètres avant de pouvoir les exécuter. la syntaxe est inhabituelle, mais pas compliquée. les paramètres se déclarent avec le mot-clé uniform dans les fragment et vertex shaders. mais un vertex shader peut déclarer un autre type de paramètre : un ou plusieurs attributs de sommet comme la position, la couleur, la normale, etc...

pourquoi 2 types de parametres ? les shaders (et le reste du pipeline graphique) sont exécutés par les processeurs de la carte graphique au moment ou l'application utilise une fonction draw( ). La carte graphique transforme tous les sommets en parallèle : un vertex shader (cf l'exemple suivant) est exécuté par n threads. La matrice de transformation est la même pour tous les sommets (et tous les threads), mais bien sur ce n'est pas le cas pour les attributs de sommets... comme la position. Chaque thread qui exécute le vertex shader doit transformer un sommet different... et il faut bien "fournir" la position de chaque sommet à chaque thread.

#ifdef VERTEX_SHADER
uniform mat4 mvpMatrix; // la transformation à appliquer aux sommets, mot-clé : uniform
in vec3 position; // la position du ieme sommet, mot-clé : in
void main( )
{
gl_Position= mvpMatrix * vec4(position, 1);
}
#endif
vecteur generique, utilitaire.
Definition: vec.h:146
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition: vec.h:168

C'est pour cette raison que les paramètres mvpMatrix et position sont déclarés avec des mots-clés différents, uniform pour indiquer que tous les sommets utilisent la même transformation (la même matrice, c'est un paramètre "normal") et le mot-clé in pour indiquer que position est différent pour chaque sommet. Et bien sur, affecter une (seule) valeur à un uniform est différent d'affecter un ensemble de valeurs à un attribut. Mesh s'occuppe des attributs, le plus "complique", mais l'application est responsable des uniforms.

comment ça marche ?

il faut affecter une valeur à chaque paramètre uniform déclaré par le vertex shader et par le fragment shader. les fonctions d'openGL ne sont pas très pratiques à utiliser, du coup uniforms.h fournit plusieurs utilitaires simplifiés, qui permettent d'affecter des valeurs des types courants :

#include "uniforms.h"
// selectionner le shader program, si necessaire
// glUseProgram(program);
float f= { ... };
program_uniform(program, "facteur", f); // uniform float facteur; dans le shader
Transform m= { ... };
program_uniform(program, "matrice", m); // uniform mat4 matrice; dans le shader
Point p= { ... };
program_uniform(program, "point", p); // uniform vec3 point; dans le shader
Vector v= { ... }:
program_uniform(program, "vecteur", v); // uniform vec3 vecteur; dans le shader
Color c= { ... }:
program_uniform(program, "couleur", c); // uniform vec4 couleur; dans le shader
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 couleur (rgba) transparente ou opaque.
Definition: color.h:14
representation d'un point 3d.
Definition: vec.h:21
representation d'une transformation, une matrice 4x4, organisee par ligne / row major.
Definition: mat.h:21
representation d'un vecteur 3d.
Definition: vec.h:59

oui c'est moche... les paramètres sont identifiés par une chaine de caractères.

mais pourquoi ?? les variables / les paramètres des shaders sont stockés dans la mémoire de la carte graphique, pas dans la mémoire accessible normalement à l'application. il faut imaginer que l'application et les shaders fonctionnent sur 2 machines différentes connectées par un réseau.

il faut aussi, comme d'habitude, affecter une valeur du bon type. les erreurs ne sont detectés qu'à l'exécution de l'application, pas lors de la compilation. elles s'afficheront dans la console, qu'il faudra penser à surveiller en cas de comportement bizarre, image noire, etc.

remarque : afficher plusieurs triangles, modifier les paramètres uniform d'un shader program explique en détail comment affecter une valeur à chaque uniform déclaré dans les shaders directement avec les fonctions openGL.

les transformations standard

Pour calculer la transformation mvp, qui permet de projeter directement un sommet sur la camera, il faut savoir ou se trouve l'objet dans le monde, ou se trouve la camera et quelle est sa projection, c'est à dire les transformations / matrices model, view et projection. Jusqu'à présent afficher un objet ressemblait à ça :

#include "app_camera.h"
#include "mat.h"
#include "mesh.h"
#include "draw.h"
Mesh mesh= { ... };
Transform model= { ... }; // placer l'objet dans le monde
draw(mesh, model, camera());
// camera() renvoie la camera / Orbiter géré par AppCamera
representation d'un objet / maillage.
Definition: mesh.h:112

La camera calcule les transformations view et projection, il suffit de les recupérer et de composer model, view, et projection pour obtenir une seule matrice qui enchaine les 3 changements de repères :

Orbiter& camera= camera(); // la camera de AppCamera
// Orbiter camera= { ... }; // ou une autre camera / Orbiter
Transform view= camera.view();
Transform projection= camera.projection();
Transform model= { ... };
// composition des 3 changements de repères
Transform mvp= projection * view * model; // P(V(M*p))
glUseProgram(program);
program_uniform(program, "mvpMatrix", mvp);
representation de la camera, type orbiter, placee sur une sphere autour du centre de l'objet.
Definition: orbiter.h:17
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

remarque : si le fragment shader (ou le vertex shader) utilise d'autres parametres, il ne faut pas oublier de leur affecteur une valeur...

et les attributs ?

il faut respecter la convention utilisée par Mesh, qui numérote les attributs des sommets et impose un type qu'il faut respecter :

voila la déclaration à utiliser dans le vertex shader, en fonction des attributs nécessaires :

layout(location= 0) in vec3 position;
layout(location= 1) in vec2 texcoord;
layout(location= 2) in vec3 normal;
layout(location= 3) in vec4 color;
layout(location= 4) in uint material;
vecteur generique, utilitaire.
Definition: vec.h:131

par exemple, pour un vertex shader tout simple :

#version 330
#ifdef VERTEX_SHADER
layout(location= 0) in vec3 position; // parametre : position du ieme sommet
uniform mat4 mvpMatrix; // parametre : matrice de transformation des sommets
void main( )
{
// resultat obligatoire du vertex_shader : sommet transforme
gl_Position= mvpMatrix * vec4(position, 1);
}
#endif

et pour les autres attributs ?

les numeros ne changent pas, il suffit de copier les déclarations ci-dessus. par exemple pour utiliser postion et normale :

layout(location= 0) in vec3 position;
layout(location= 2) in vec3 normal;

dessiner

Une fois le shader program complètement paramétré, il ne reste plus qu'à dessiner l'objet avec Mesh::draw( program ... ). Un dernier détail, il faut indiquer quels attributs de sommets sont nécessaires à l'exécution des vertex shaders, cf les parametres bool :

#include "color.h"
#include "mesh.h"
#include "program.h"
#include "uniforms.h"
Mesh mesh= { ... };
GLuint program= { ... };
Transform mvp= { ... };
glUseProgram(program);
program_uniform(program, "mvpMatrix", mvp);
program_uniform(program, "color", Red());
// indiquer quels attributs de sommets du mesh sont necessaires a l'execution du shader.
// le shader de test n'utilise que position.
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:770
Color Red()
utilitaire. renvoie une couleur rouge.
Definition: color.cpp:57

cf tuto9.cpp pour un exemple complet qui utilise un uniform supplémentaire, qui permet de donner la même couleur aux pixels de l'objet.

Mesh::draw( ) vérifie (en mode debug, en tout cas) que les attributs déclarés dans le vertex shader sont disponibles dans le Mesh. par exemple, si le vertex shader déclare / utilise un attribut normale par sommet mais que ces valeurs ne sont pas dans le Mesh, le shader ne peut pas fonctionner...

résumé

la chose à retenir de tout ça est que lorsqu'un shader program, composé d'un vertex shader et d'un fragment shader déclare ses paramètres, par exemple :

#version 330
#ifdef VERTEX_SHADER
uniform mat4 mvpMatrix; // parametre 'mvpMatrix'
void main( )
{ ... }
#endif
#ifdef FRAGMENT_SHADER
uniform vec4 color; // parametre 'color'
void main( )
{ ... }
#endif

avant de pouvoir dessiner un objet / des triangles, il faut affecter une valeur aux paramètres uniforms déclarés :

Mesh mesh= read_mesh("objet.obj");
GLuint program= read_program("shader.glsl");
// calculer les valeurs des parametres, cf transformations standards, etc...
{ ... }
// parametres uniforms, c'est l'application qui affecte une valeur
glUseProgram(program);
program_uniform(program, "mvpMatrix", ...); // meme nom que dans le shader... sinon erreur
program_uniform(program, "color", ...);
// attributs de sommets, c'est Mesh::draw() qui affecte les valeurs
mesh.draw(program, /* use position */ true, /* use texcoord */ false, /* use normal */ false, /* use color */ false,
/* use material index */ false);
Mesh read_mesh(const char *filename)
charge un fichier wavefront .obj et renvoie un mesh compose de triangles non indexes....
Definition: wavefront.cpp:14

debugger un shader / recharger sans fermer l'application

relancer l'application à chaque fois qu'un shader plante, n'est pas très pratique, il est possible de recharger les shaders à la volée, sans quitter l'application, cf reload_program() de program.h.

#include "program.h"
#include "window.h"
init( ) :
program=read_program("tutos/tuto9_color.glsl");
draw( ) :
if(key_state('r'))
{
reload_program(program, "tutos/tuto9_color.glsl");
}
void clear_key_state(const SDL_Keycode key)
desactive une touche du clavier.
Definition: window.cpp:48
int key_state(const SDL_Keycode key)
renvoie l'etat d'une touche du clavier. cf la doc SDL2 pour les codes.
Definition: window.cpp:42
int init(std::vector< const char * > &options)
Definition: shader_kit.cpp:96

attention : les uniforms sont re-initialisés à 0 par la compilation d'un shader.

et avec plusieurs matières sur le meme objet ?

si plusieurs shaders, ou plusieurs paramètres (couleurs, matieres, etc) sont nécessaires pour dessiner les triangles de l'objet, une solution est de trier les triangles de l'objet par matière et de dessiner chaque groupe de triangles avec les bons paramètres : exemple complet dans tuto9_groups.cpp, tuto9_groups.glsl.

et avec openGL ?

cf tuto9_buffers.cpp pour un exemple complet qui alloue et initialise un buffer pour stocker les positions des sommets du Mesh et qui configure un format de sommet (un vertex array object) pour dessiner un objet directement avec openGL, sans utiliser les utilitaires draw( ).

et avec une texture ?

Il y a plusieurs étapes :

Lisez la section "écrire le fragment shader (et le vertex shader)" dans textures, samplers et pipeline pour comprendre comment les différents morceaux se connectent ensemble.

Dernière étape, configurer le pipeline pour utiliser la texture, vous pouvez lire les détails dans textures, samplers et pipeline ou utiliser l'utilitaire program_use_texture( );

#include "texture.h"
init( ) :
// verifie que l'objet a bien des coordonnées de texture
if(m_objet.texcoord_buffer_size() == 0)
return "erreur, pas de texcoords...";
// charge une texture sur l'unite 0
m_texture= read_texture(0, "...");
render( ) :
// selectionner le shader program, si necessaire
// glUseProgram(program);
// . parametres "supplementaires" :
// . utilisation d'une texture configuree sur l'unite 0, cf texture= read_texture(0, "...");
program_use_texture(program, "texture0", 0, m_texture);
// go !
// indiquer quels attributs de sommets du mesh sont necessaires a l'execution du shader.
// le shader utilise position et texcoord.
m_objet.draw(program, /* use position */ true, /* use texcoord */ true, /* use normal */ false, /* use color */ false, /* use material index */ false);
void program_use_texture(const GLuint program, const char *uniform, const int unit, const GLuint texture, const GLuint sampler)
configure le pipeline et le shader program pour utiliser une texture, et des parametres de filtrage,...
Definition: uniforms.cpp:198
GLuint read_texture(const int unit, const char *filename, const GLenum texel_type)
Definition: texture.cpp:148

cf tuto9_texture1.cpp pour un exemple complet qui charge une texture.

et avec plusieurs textures ?

C'est la même chose, par contre, il faut charger et configurer une unité de texture par image / texture à utiliser et déclarer un sampler2D par texture dans le shader.

#include "texture.h"
init( ) :
// verifie que l'objet a bien des coordonnées de texture
if(m_objet.texcoord_buffer_size() == 0)
return "erreur, pas de texcoords...";
// charge une texture sur l'unite 0
m_texture0= read_texture(0, "...");
// charge une texture sur l'unite 1
m_texture1= read_texture(1, "...");
render( ) :
// selectionner le shader program, si necessaire
// glUseProgram(program);
// . parametres "supplementaires" :
// . utilisation d'une texture configuree sur l'unite 0, cf texture= read_texture(0, "...");
program_use_texture(program, "texture0", 0, m_texture0); // le shader déclare: uniform sampler2D texture0;
// . utilisation d'une texture configuree sur l'unite 1, cf texture= read_texture(1, "...");
program_use_texture(program, "texture1", 1, m_texture1); // le shader déclare: uniform sampler2D texture1;
// go !
// indiquer quels attributs de sommets du mesh sont necessaires a l'execution du shader.
// le shader utilise position et texcoord.
m_objet.draw(program, /* use position */ true, /* use texcoord */ true, /* use normal */ false, /* use color */ false, /* use material index */ false);

cf tuto9_textures.cpp pour un exemple complet qui charge et utilise 2 textures.