M2 Image

TP1 - prise en main


Partie 1 : gKit

installez gkit, cf la doc.

compilez le code de départ, make tp, comme indiqué dans la doc. vérifiez que tout fonctionne sans surprises :

bin/tp


n'oubliez pas la doc en ligne qui reprend tous les éléments nécessaires à la réalisation des tps :


Partie 2 : prise en main

les exercices suivants permettent de manipuler chaque étape, ie passer d'un code simpifié avec gKit vers un code openGL direct.

exercice 1 : modifez le code de départ (cf projets/tp.cpp) pour charger 1 objet supplémentaire, data/cube.obj, ou data/bigguy.obj, par exemple, et affichez-le. le code de départ charge déja data/robot.obj.
on souhaite dessiner bigguy plus petit pour que sa taille soit comparable à celle du robot. comment faire ?

si on souhaite dessiner bigguy posé sur un cube, comment faire ? quelles transformations utiliser pour dessiner les 2 objets 3d ?

relisez "transformations et affichage" cf doc, si nécessaire.

indication : Mesh::bounds( pmin, pmax ) renvoie les points extrêmes de l'englobant d'un mesh.






finissez de lire la doc sur les transformations et créez une camera ! ou utilisez l'orbiter de gkit, cf Orbiter et update_camera() de app_camera.h qui permet de déplacer / manipuler l'orbiter.

exercice 2 : écrivez un couple de shaders qui dessine les objets avec une couleur uniforme / constante.

utilisez shader_kit pour écrire et tester les shaders, sans modifier l'application.
(ou est la doc de shader_kit ?)

dans un premier temps, la couleur sera une constante dans le fragment shader.
que faudra-t-il modifier (dans l'application et dans le shader) pour dessiner un objet rouge et un autre objet vert, par exemple ?


exercice 3 :
et avec les normales ?

dessiner un objet avec une couleur uniforme est un peu limité... pour faire mieux, il faut une matière de base et une source de lumière.
la matière la plus simple est une constante, ca tombe bien, ce n'est pas difficile à calculer...

par contre, il faut quand même calculer le cosinus de l'angle entre la normale d'un fragment / d'un point de la surface de l'objet et la direction vers la source de lumière.

le cosinus d'un angle entre 2 vecteurs se calcule directement avec le produit scalaire de ces 2 vecteurs (s'ils sont de longueur 1). en c++ on pourrait écrire :

Color fragment_shader( const Vector& n, const Vector& l )
{
    float cos_theta= dot(normalize(n), normalize(l));
    return White() * cos_theta;
}

écrivez le shader en utilisant shader_kit. quels sont ses paramètres ?
ou se trouve la doc sur toutes les fonctions de calcul de GLSL ? ie comment calculer un produit scalaire, comment normaliser un vecteur, etc.

est ce que ce shader pourra fonctionner avec tous les objets 3d ? pourquoi ? ou pas ?

indication : quelle direction utiliser pour la source de lumière ? vers la camera, par exemple, fonctionne correctement. c'est tout simplement l'axe Z (0, 0, 1) dans le repere camera.

rappel : calculs sur les points et les vecteurs, toutes les coordonnées doivent être dans le même repère, sinon c'est faux et souvent très bizarre... et oui, il faudra sans doute transformer les coordonnées de certains points ou vecteurs...

shader_kit initialise a peu près toutes les matrices pour transformer les points et leur normales dans à peu près n'importe quel repère.
ou se trouve la doc de shader_kit ?


faudra-t-il modifier l'application pour que les shaders obtiennent ces informations ?


exercice 4 : et avec un peu de style ?

modifiez votre shader précédent, toujours dans shader_kit, pour réaliser un effet "toon shading", avec des aplats de couleurs plutôt qu'un rendu plus lisse comme dans l'exercice précédent.

   

indications :
il suffit de modifier la couleur calculée en fonction du cosinus entre n et l : ie comment passer d'une valeur continue à une valeur par palier / constante par morceau. (et régler le nombre de paliers... 4 dans l'exemple de gauche et 2 dans l'exemple de droite)


exercice 5 :
modifiez votre application pour dessiner avec les shaders de l'exercice précédent.
relisez la doc, si necessaire.

si ces derniers shaders sont trop compliqués à utiliser, commencez par celui de l'exercice 2.

solution alternative : Mesh peut créer les buffers et configurer le format de sommet / vao pour vous : cf GLuint Mesh::create_buffers(...). le paramètre est une combinaison de flag associé aux attributs gérés par Mesh, cf doc.
il ne reste plus qu'à affecter une valeur aux uniforms des shaders et on peut dessiner directement avec openGL, à peu près comme ça :

GLuint program= 0;
GLuint vao= 0;
unsigned n= 0;

init( ):
    Mesh mesh= read_mesh( /* fichier.obj */ );
    vao= mesh.create_buffers( USE_POSITION | USE_NORMAL );
    n= mesh.vertex_count();
   
    program= read_program( /* fichier.glsl */ );

render( ):
    glBindVertexArrayObject( vao );
    glUseProgram( program );
   
    program_uniform( program, /* uniform */, /* valeur */ );
   
    glDrawArrays(GL_TRIANGLES, 0, n);

pas de panique !!

si vous avez besoin de reprendre tout ça calmement, essayez les tutos :


Partie 3 : plusieurs couleurs

tous les objets ne sont pas constitués d'un seul groupe de triangles de même couleur, par exemple data/robot.obj, comment peut on dessiner ces triangles avec la bonne couleur ?

bien sur, il y a plusieurs solutions, les exercices suivants proposent de réaliser et de comparer 2 solutions.





exercice 1 / solution 1 :
on peut regrouper les triangles de même couleur et les dessiner ensemble. S'il y a, par exemple, 4 couleurs différentes, il y aura 4 groupes de triangles qu'il faudra dessiner un par un avec la bonne couleur.

std::vector<TriangleGroup> Mesh::groups() trie les triangles d'un Mesh par couleur / matière et renvoie les groupes de triangles, ie les triangles avec la matière d'indice 1 sont rangés au début du tableau, puis les triangles avec la matière d'indice 2, etc.
remarque : comme (le tri) est un peu long, il faut le faire une seule fois dans init( ) et conserver le tableau de groupes de triangles...

TriangleGroup est une structure toute simple :
 struct TriangleGroup
 {
     int index;             // indice de la matiere dans Mesh::materials()
     int first;             // indice du premier sommet a dessiner
     int n;                 // nombre d'indices
 };

à quoi servent les valeurs first et n ? ce sont les parametres de glDrawArrays(GL_TRIANGLES, /* first */, /* n */ ) qui dessine des triangles avec n sommets (donc n/3 triangles) à partir du sommet d'indice first dans le tableau de sommets du Mesh, ce qui permet de dessiner le groupe de triangles.

plus précisement, après le tri, les triangles sont rangés par ordre croissant d'indice de matière. d'abord les triangles avec la matière d'indice 1, puis ceux pour la matière d'indice 2, etc. un triangle est simplement représenté par 3 sommets consécutifs. donc le premier triangle commence à l'indice 0 du tableau trié, le triangle suivant commence à l'indice 3, etc. TriangleGroup::first indique ou trouver le premier sommet du premier triangle du groupe. TriangleGroup::n indique le nombre de sommets des triangles du groupe.

connaissant l'indice de la matière, il ne reste plus qu'à récupérer la couleur avant de dessiner le groupe de triangles. Les informations de couleurs, textures, etc. l'ensemble des matières est stocké dans le  Mesh, on peut le récupérer avec Mesh::materials(). cette fonction renvoie un ensemble de matières décrit par Materials. chaque matière est décrite par une structure Material.

exemple d'utilisation :
Mesh m_objet;
std::vector<TriangleGroup> m_groups;

init( ):
        m_objet= read_mesh("data/robot.obj");     // charge le fichier .obj et la description des matieres (fichier .mtl), s'il existe...
        if(m_objet.materials().count() == 0)
            return -1;     // pas de matieres, pas d'affichage
        
        // trie les triangles par matiere et recupere les groupes de triangles utilisant la meme matiere.
        m_groups= m_objet.groups();

render( ):
        // ensemble de matieres de l'objet
        const Materials& materials= m_objet.materials();
       
        // dessine chaque groupe de triangles, avec sa matiere
        for(unsigned i= 0; i < m_groups.size(); i++)
        {
            const TriangleGroup& group= m_groups[i];

            // recuperer la couleur de la matiere du groupe
            Color color= materials[group.index].diffuse;
           
            // parametrer le shader pour dessiner avec la couleur
            glUseProgram(m_program);
            { ... }
           
            // dessiner les triangles du groupe
            glDrawArrays(GL_TRIANGLES, group.first, group.n);
        }

astuce : on peut utiliser Mesh pour dessiner un groupe de triangles, cf Mesh::draw( first, n, program, ... ), on n'est pas obligés d'utiliser directement openGL.

exercice 2 / solution 2 : Mesh définit aussi un attribut de sommet supplémentaire : l'indice de la matière / la couleur du triangle. connaissant cet indice, comment le fragment shader peut-il récupérer la couleur du triangle ?

Mesh::draw() initialise un buffer avec l'indice de la matière de chaque triangle. il suffit de déclarer cet indice de matière dans le vertex shader en respectant la convention utilisée par Mesh, cf doc, section "et les attributs",

layout(location= 4) in uint material;
par contre, cet attribut n'est pas un réel, c'est juste un entier, et il n'est pas vraiment interpolable par le pipeline (entre la sortie du vertex shader et l'entrée du fragment shader), il faut prendre quelques précautions lors de la déclaration du varying. la décoration flat indique que le varying est constant à la surface du triangle :
// vertex shader
// attributs
layout(location= 0) in vec3 position;
layout(location= 2) in vec3 normal;
layout(location= 4) in uint material;
...

// sorties / varyings
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, le varying est marque explicitement comme non interpolable  !!
...

void main( )
{
    gl_FragColor= ...;
}


Reste une dernière étape : déclarer un tableau de couleurs dans le fragment shader et l'initialiser avec les couleurs des matières  (dans l'application). Connaissant l'indice de la matière dans le fragment shader, il ne reste plus qu'à récupérer la bonne couleur dans le tableau de couleurs et à finir le calcul, cf cos \theta, etc.

indication : relisez la description de GLSL pour déclarer un tableau, mais c'est sans surprise...

indication : relisez la section "uniforms et shader program" dans la doc gkit, ou la doc officielle openGL pour découvrir le nom de la fonction qui permet d'initialiser un tableau d'uniforms ou cherchez dans les fonctions utilitaires de uniforms.h

indication : oui, il faut déclarer la taille du tableau. le plus simple est de déclarer un tableau de 16 couleurs par exemple, seules les premières seront utilisées, il suffit d'initialiser les autres qui ne seront pas utilisées (normalement) avec une couleur par défaut bien visible, comme du magenta par exemple : Color(1, 0, 1).
pourquoi une couleur bien moche ? pour debugger facilement, s'il y a un problème avec les indices dans le shader...


exercice 3 : quelle est la meilleure solution ? pour l'application ? pour le gpu ? même question si on doit aussi changer de texture pour chaque matière ?

il va falloir mesurer les temps d'exécution cpu et gpu pour répondre, cf doc en ligne.

pas de panique !!

si vous avez besoin de reprendre tout ça calmement, essayez les tutos :


pour les curieux : textures

avec une texture plaquée sur l'objet, que faut-il modifier dans les shaders ? dans l'application ? peut on texturer n'importe quel objet chargé ?

indication : cf utilisation des textures et des shaders avec Mesh::draw(), doc section "et avec une texture ?"

et avec plusieurs groupes de triangles qui utilisent des textures différentes ? quelle semble être la meilleure solution ?
il existe des tableaux de textures (cf texture2DArray) en plus des textures classiques (texture2D), est ce que ca change le type de solution applicable ?