gKit2 light
Loading...
Searching...
No Matches
éclairer un objet avec plusieurs matières

tous les objets ne sont pas composés d'une seule surface avec une seule couleur / apparence / matière. même le robot utilise plusieurs matières de couleurs différentes :

on a vu comment dessiner le robot et l'éclairer correctement dans premières lumières, éclairer un objet, sRGB et gamma. que faut-il modifier pour gérer plusieurs matières ?

comme d'habitude, il y a plusieurs solutions... soit on fait bosser l'application, soit on utilise les shaders.

c'est l'application qui fait le boulot

la solution intuitive consiste à dessiner chaque triangle avec les bons paramètres de couleur / matière. mais dessiner les triangles un par un n'est vraiment pas une bonne idée. une application ne peut pas vraiment espérer faire plus de ~10000 draws et rester temps réel à 60 images par seconde, ce qui limite un peu trop la quantité de géométrie que l'on peut dessiner. allez voir mesure du temps cpu et gpu pour vous convaincre. une bien meilleure solution consiste à grouper les triangles par matière et à dessiner chaque groupe avec les bonnes couleurs / paramètres de matières.

comment ça marche ?

lorsque l'on charge un objet avec read_mesh(), par exemple, un ensemble de matières, cf Materials, est aussi chargé, et chaque triangle connait l'indice de sa matière. il ne reste plus qu'à trier les triangles en fonction de cet indice de matière. les triangles associées à la matière 1 se trouvent au début, suivis par les triangles de la matière 2, etc. c'est exactement ce que fait Mesh::groups(), la fonction trie les triangles et renvoie la position du premier triangle ainsi que le nombre de triangles de chaque groupe :

// mesh.h
std::vector< TriangleGroup > Mesh::groups();
std::vector< TriangleGroup > groups()
renvoie les groupes de triangles de meme matiere. re-organise les triangles. permet d'afficher l'obje...
Definition mesh.cpp:291

ces informations sont représentées par une structure toute simple TriangleGroup :

{
int index; // indice de la couleur / matiere
int first; // position du premier triangle du groupe
int n; // nombre de triangles du groupe
};
representation d'un ensemble de triangles de meme matiere.
Definition mesh.h:103

il suffit de dessiner groupe par groupe :

glUseProgram(program);
for(auto const& group : groups)
mesh.draw(group.first, group.n, program);

ou directement avec openGL :

glBindVertexArray(vao);
glUseProgram(program);
for(auto const& group : groups)
glDrawArrays(GL_TRIANGLES, group.first, group.n);
// ou pour les maillages indexés :
// glDrawElements(GL_TRIANGLES, group.n, GL_UNSIGNED_INT, (void *) (group.first * sizeof(unsigned)));
Note
il y a 2 manières de représenter un objet / un ensemble de triangles. soit chaque triangle est décrit par les indices de ses 3 sommets, et on dit que le maillage est indexé ou qu'il partage ses sommets, soit chaque triangle stocke directement ses 3 sommets.
read_mesh( ) renvoie un maillage ou chaque triangle stocke ses 3 sommets, et on dessine avec glDrawArrays( ... );
read_indexed_mesh( ) renvoie un maillage indexé avec des sommets partagés ou chaque triangle stocke 3 indices, et on dessine avec glDrawElements(GL_TRIANGLES, count, GL_UNSIGNED_INT, (void *) (first * sizeof(unsigned)));.

et les couleurs ?

ou sont les couleurs ? dans Materials qui est chargé en même temps que la géométrie par read_mesh() / read_indexed_mesh(). on récupère cet ensemble de matières avec Mesh::materials(). après le tri par matière avec Mesh::groups(), il ne reste plus qu'à parcourir les groupes et TriangleGroup::index indique quelle matière utiliser pour dessiner les triangles. il suffit de récupérer la couleur de chaque matière, ie Material::diffuse dans Material et de paramétrer le shader avant de dessiner. au final, c'est plutôt direct :

// init
Mesh mesh= read_mesh( "..." );
std::vector<TriangleGroup> groups= mesh.groups();
GLuint vao= mesh.create_buffers( USE_POSITION | USE_NORMAL );
GLuint program= read_program( "..." );
program_print_error(program);
// render
glBindVertexArray(vao);
glUseProgram(program);
const Materials& materials= mesh.materials();
for(auto const& group : groups)
{
const Material& material= materials(group.index);
program_uniform(program, "color", material.diffuse);
glDrawArrays(GL_TRIANGLES, group.first, group.n);
// ou mesh.draw(group.first, group.n, program);
}
representation d'un objet / maillage.
Definition mesh.h:121
Mesh read_mesh(const char *filename)
charge un fichier wavefront .obj et renvoie un mesh compose de triangles non indexes....
Definition wavefront.cpp:14
@ USE_POSITION
inclut l'attribut position dans les buffers.
Definition mesh.h:112
@ USE_NORMAL
inclut l'attribut normale dans les buffers.
Definition mesh.h:114
GLuint read_program(const char *filename, const char *definitions)
Definition program.cpp:218
Color diffuse
couleur diffuse / de base.
Definition materials.h:17
std::vector< Material > materials
description des matieres.
Definition materials.h:47

code complet dans tuto9_groups.cpp et tuto9_groups.glsl

c'est les shaders qui font le boulot

la solution précédente semble pas mal intuitive, mais on peut faire encore plus direct en utilisant les shaders...

quel est le problème ? il suffit de connaître l'indice de la matière et d'avoir un tableau de couleurs pour utiliser la bonne couleur pour chaque triangle. ces infos sont dans l'application, mais pas dans les shaders... est ce qu'il est possible de les transmettre aux shaders ?

le fragment shader connait l'indice du triangle qu'il est en train de dessiner, ie gl_PrimitiveID, cf shaders et GLSL, il suffit de déclarer dans le shader un tableau d'indice de matière par triangle + un tableau de matière, et hop c'est fini ! pour dessiner ~1000 triangles, ça marche, mais la taille des tableaux uniforms déclarés dans le shader est limitée à 32Ko au total, soit 8000 ints, c'est pas énorme (et en plus il faut déclarer leur taille, ce qui n'est vraiment pas pratique). on pourrait créer des buffers, cf openGL 3.3 : uniform buffers, mais ce ne serait pas aussi simple à manipuler...

que reste-t-il comme moyen de transmettre des infos aux shaders ? les vertex buffers ! il suffit d'ajouter une information aux sommets : l'indice de la matière du triangle qui utilise le sommet. Mesh le fait automatiquement, il suffit de déclarer l'attribut de sommet dans le shader, en respectant la convention de Mesh, cf première application avec des shaders. il suffit de transmettre l'indice au fragment shader, et hop !

mais c'est un peu bizarre d'interpoler un indice... ie interpoler un entier entre les sommets du triangle et l'intérieur du triangle ? euh, si. mais c'est prévu dans GLSL, on peut indiquer que l'on ne veut pas interpoler une sortie du vertex shader, avec le mot clé flat.

// vertex shader
layout(location= 4) in uint material; // type uint, cf unsigned int en c++
flat out uint vertex_material;
// décoration flat : le varying est un entier, donc pas vraiment interpolable... il faut le déclarer explicitement
void main( )
{
gl_Position= ...;
vertex_material= material;
}
// fragment shader
flat in uint vertex_material; // !! decoration flat
void main()
{
...
}
Note
ça ne fonctionne que parce que read_mesh() / read_indexed_mesh() dupliquent les sommets partagés par plusieurs matières.

pour les curieux : comme la valeur n'est pas interpolée, et pour éviter les surprises lorsque les sommets d'un triangle sont associés à des valeurs entières différentes, le pipeline choisit la valeur du premier sommet du triangle et ignore les autres. petite blague, selon l'api, direct3d ou opengl, ou vulkan ou metal, le "premier sommet" n'est pas le même... cf les conventions "provoking vertex" des différentes apis.

comment ça marche ?

on a enfin l'indice de la matière dans le fragment shader, comment on retrouve la couleur ? avec un (petit) tableau de couleurs, une par matière ! il suffit de le préparer dans l'application. et connaissant l'indice de la matière, il n'y a plus qu'à récupérer la couleur dans le tableau pour finir d'éclairer le point...

// vertex shader
layout(location= 4) in uint material; // type uint, cf unsigned int en c++
flat out uint vertex_material; // !! decoration flat
void main( )
{
gl_Position= ...;
vertex_material= material;
}
// fragment shader
flat in uint vertex_material; // !! decoration flat
uniform vec4 colors[16];
void main()
{
// recupere la couleur de la matière
vec4 color= colors[ vertex_material ];
// eclaire le point...
float cos_theta= max( 0, dot( ... ) );
gl_FragColor= color * cos_theta;
}
Point max(const Point &a, const Point &b)
renvoie la plus grande composante de chaque point { max(a.x, b.x), max(a.y, b.y), max(a....
Definition vec.cpp:35
float dot(const Vector &u, const Vector &v)
renvoie le produit scalaire de 2 vecteurs.
Definition vec.cpp:181
vecteur generique 4d, ou 3d homogene, utilitaire.
Definition vec.h:192

et le code de préparation dans l'application va ressembler à :

// init
Mesh mesh= read_mesh( "..." );
unsigned count= mesh.vertex_count();
GLuint vao= mesh.create_buffers( USE_POSITION | USE_NORMAL );
GLuint program= read_program( "..." );
program_print_error(program);
// copier les matieres utilisees
std::vector<Color> colors(16); // le fragment shader declare un tableau de 16 matieres
const Materials& materials= mesh.materials();
assert(materials.count() <= colors.size());
for(int i= 0; i < materials.count(); i++)
colors[i]= materials.material(i).diffuse;
// render
glBindVertexArray(vao);
glUseProgram(program);
program_uniform(program, "colors", colors);
glDrawArrays(GL_TRIANGLES, 0, count);
// ou mesh.draw(program);
int count() const
nombre de matieres.
Definition materials.h:102
const Material & material(const int id) const
renvoie la ieme matiere.
Definition materials.h:110

code complet dans tuto9_materials.cpp et tuto9_materials.glsl

Note
dans ce cas tout simple, avec une seule couleur pour décrire une matière, on aurait aussi pu déclarer le tableau colors dans le vertex shader, lire la couleur et ne transmettre que la couleur au fragment shader, ce qui éviterait la manipulation des flats. les matières vont utiliser de plus en plus de paramètres, et au final il est plus efficace de ne transmettre que l'indice au lieu de tout faire interpoler...

pour les curieux : si vous trouvez que dupliquer l'indice de la matière sur chaque sommet d'un triangle n'est pas très élégant, vous avez en partie raison. mais allez voir Mesh::create_buffers() et Mesh::update_buffers() : l'indice de matières est un entier 8bits, et au final cette solution utilise moins de place qu'un indice entier 32bits. mais on est effectivement limité à 256 matières. en pratique, c'est rarement un problème.