gKit2 light
Loading...
Searching...
No Matches
première application avec des shaders

cf tuto9.cpp + tuto9_color.glsl

glDraw( ) et la famille 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, ... ), comme dans les exemples précédents, cf premiers shaders avec shader_kit. 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 (leurs attributs) dans le shader.

repassez dans shaders et GLSL, si nécessaire (et peut être introduction api 3d, openGL et pipeline graphique).

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() fourni 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:218
int program_print_errors(const GLuint program)
affiche les erreurs de compilation.
Definition program.cpp:446

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 paramètres ? 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 diffèrent... 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:169
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition vec.h:192

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'occupe 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
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:67

oui c'est moche... les paramètres sont identifiés par une chaîne 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 détectées 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 caméra, il faut savoir ou se trouve l'objet dans le monde, ou se trouve la caméra 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:121

La caméra calcule les transformations view et projection, il suffit de les récupérer et de composer model, view, et projection pour obtenir une seule matrice qui enchaîne 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

remarque : si le fragment shader (ou le vertex shader) utilise d'autres paramètres, il ne faut pas oublier de leur affecter 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 :

  • attribut 0, vec3 position,
  • attribut 1, vec2 texcoord,
  • attribut 2, vec3 normal,
  • attribut 3, vec4 color,
  • attribut 4, uint material;

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

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 numéros ne changent pas, il suffit de copier les déclarations ci-dessus. par exemple pour utiliser position 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 ... ).

#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());
mesh.draw(program);
Color Red()
utilitaire. renvoie une couleur rouge.
Definition color.cpp:28

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", ...);
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

debugger un shader / recharger sans fermer l'application

arrêter et relancer l'application à chaque fois qu'un shader plante ou ne compile pas, n'est ni très pratique, ni très rapide... 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:46
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:40
int init(std::vector< const char * > &options)

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

et avec plusieurs matières sur le même objet ?

si plusieurs shaders, ou plusieurs paramètres (couleurs, matières, 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 :

  • d'abord charger la texture, cf GLuint texture= read_texture(0, fichier ); documenté dans texture.h,
  • vérifier que les sommets de l'objet à dessiner sont bien associés à des coordonnées de texture, cf Mesh::texcoord() pour décrire les attributs de sommet, ou Mesh::texcoord_buffer_size(), pour vérifier que les coordonnées de texture sont bien présentes,
  • déclarer l'attribut dans le vertex shader, cf layout(location= 1) in vec2 texcoord;
  • transmettre les texcoord au fragment shader,
  • lire la texture dans la fragment shader, aux bonnes coordonnées.

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(objet.texcoord_buffer_size() == 0)
return "erreur, pas de texcoords...";
// charge une texture sur l'unite 0
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, "color_texture", 0, texture); // le shader déclare: uniform sampler2D color_texture;
// go !
objet.draw(program);
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:133

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

unité de texture ? comme pour les buffers, pour manipuler plusieurs textures simultanément, openGL les numérote. l'unité de texture 0 décrit la première texture manipulée par le shader. l'unité 1, la seconde, etc. il y aura une unité configurée par texture manipulée, jusqu'à 16 ou 32, selon les cartes graphiques.

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(objet.texcoord_buffer_size() == 0)
return "erreur, pas de texcoords...";
// charge une texture sur l'unite 0
base= read_texture(0, "...");
// charge une texture sur l'unite 1
detail= 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, "color_texture", 0, base); // le shader déclare: uniform sampler2D color_texture;
// . utilisation d'une texture configuree sur l'unite 1, cf texture= read_texture(1, "...");
program_use_texture(program, "detail_texture", 1, detail); // le shader déclare: uniform sampler2D detail_texture;
// go !
objet.draw(program);

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