M2 Image

TP1 - prise en main


Partie 1 : prise en main

installez gkit, cf la doc.

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

bin/tuto7_camera


avant de se lancer dans la manipulation directe d'openGL, il est recommandé de passer par quelques étapes :

il est aussi recommandé de passer un peu de temps à (re-) lire les explications sur le fonctionnement de tout ça : 


les exercices suivants permettent de manipuler chaque étape.


exercice 1 : modifez le code de départ pour charger 2 objets, data/cube.obj et data/robot.obj, ou data/bigguy.obj, par exemple, et affichez-les.
relisez "transformations et affichage" cf doc, si necessaire

comment afficher un cube rouge et un autre cube bleu, par exemple ? créer 2 objets différents n'est pas une option viable. on veut un parametre quelque part...

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

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 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 Color(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...

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 plutot 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 precedent.
relisez la doc, si necessaire.

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


exercice 6 : dessinez avec openGL, créez un ou plusieurs buffers, un vao, etc... la totale.
ou est la doc pour faire ça ?

astuce : Mesh peut créer les buffers et configurer le format de sommet / vao pour vous : cf GLuint Mesh::create_buffers(...).

Partie 2 : 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.
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.

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. les matières sont stockées par Mesh. const Materials& Mesh::materials() permet de les récupérer. cf Materials et Material pour la description d'une matière.

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 : Mesh peut aussi dessiner un groupe de triangles, cf Mesh::draw( begin, n, program, ... )


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

en dessinant avec Mesh::draw(), le vertex shader peut récupérer l'indice de la matière / couleur  : cf

        mesh.draw(program, /* use position */ true, /* use texcoord */ false, /* use normal */ true, /* use color */ false, /* use material index */ true);

il faut déclarer l'indice de la 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).

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 ?


Partie 3 : 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 ?


Partie 4 : openGL, buffers et VAO...

si ce n'est pas déjà fait, reprenez les exercices précédents et modifiez l'application correspondante pour dessiner directement avec openGL.

c'est à dire : créer les buffers nécessaires pour stocker les sommets et leurs propriétés, configurer un vertex array object (VAO) pour décrire comment sont stockés ces propriétés, puis au moment de dessiner : affecter une valeur à chaque paramètre uniform des shaders et enfin dessiner avec glDrawArrays(). 

comme précisé dans la doc, une application va ressembler à ça :

init( ) :

    chargement de l'objet 3d,
    création des buffers,
    configuration du vao,
    chargement et compilation des shaders,
    état minimal d'openGL (resolution, zbuffer et ztest, orientation des triangles et culling, couleur et z par défaut)

render( ):

    glBindVertexArray(vao);
    glUseProgram(program);
    glUniform( ... );
    ...
    glDrawArrays(GL_TRIANGLES, ... );


pour les curieux : temps de rendu



utilisez la classe AppTime au lieu de App pour construire l'application, le temps de dessin d'une image est mesuré et affiché pour le cpu et le gpu. modifiez votre application pour dessiner plusieurs objets, le robot disposé sur une grille par exemple. comment évolue le temps d'affichage lorsqu'on augmente le nombre d'objets ?

quelle conclusion peut on tirer de cette experience toute simple ? qui limite le nombre d'objet que l'on peut dessiner de cette maniere ? le cpu ou le gpu ?

mesure de temps cpu et gpu : cf la doc


scène de test

"exterior" (1.2Go)  extrait de la scene Bistro :
version allégée de la scène "bistro-small" (textures réduites, même géométrie)

 

le reste de la scène et les fichiers originaux sont dispo également.


indications : comment relire les textures de la scene avec Mesh, et Materials

        m_objet= read_mesh("bistro-small/exterior.obj");
        if(m_objet.materials().count() == 0)
            // pas de matieres, pas d'affichage
            return -1;
       
        printf("%d materials.\n", m_objet.materials().count());
       
        if(m_objet.has_texcoord() == false)
            // pas de texcoords, pas d'affichage
            return -1;
       
        Materials& materials= m_objet.materials();
        if(materials.filename_count() == 0)
            // pas de textures, pas d'affichage
            return -1;
       
        // charge les textures referencees par les matieres
        m_textures.resize(materials.filename_count(), 0);
        {
            for(unsigned i= 0; i < m_textures.size(); i++)
            {
                m_textures[i]= read_texture(0, materials.filename(i));
               
                // repetition des textures si les texccord sont > 1, au lieu de clamp...
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
                glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
                // meilleure qualite de filtrage...
                // glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 8);
            }
           
            printf("%d textures.\n", materials.filename_count());
        }
       
        // charge aussi une texture neutre pour les matieres sans texture...
        m_white_texture= read_texture(0, "data/grid.png");  // !! utiliser une vraie image blanche...
       
        // trie les triangles par matiere et recupere les groupes de triangles utilisant la meme matiere.
        m_groups= m_objet.groups();

sans oublier les déclarations :

    Mesh m_objet;
    std::vector<GLuint> m_textures;
    GLuint m_white_texture;


et pour dessiner ?
c'est sans surprise, en utilisant la solution 1, ie matiere par matiere, cf exercice 1 partie 2.
par contre, il faut faire attention : toutes les matieres n'utilisent pas une texture, il faut vérifier et utiliser une texture neutre / blanche à la place. ou écrire 2 versions du shader, mais ce n'est pas nécessaire.

        // configurer le pipeline
        glUseProgram(m_program);
       
        program_uniform(m_program, "mvpMatrix", mvp);
        program_uniform(m_program, "mvMatrix", mv);
       
        // afficher chaque groupe
        const Materials& materials= m_objet.materials();
        for(unsigned i= 0; i < m_groups.size(); i++)
        {
            const Material& material= materials(m_groups[i].index);
           
            //   couleur de la matiere du groupe de triangle
            program_uniform(m_program, "material_color", material.diffuse);
           
            //   texture definie par la matiere, ou texture neutre sinon...
            if(material.diffuse_texture != -1)
                program_use_texture(m_program, "material_texture", 0, m_textures[material.diffuse_texture]);
            else
                program_use_texture(m_program, "material_texture", 0, m_white_texture);
           
            // 1 draw par groupe de triangles...
            m_objet.draw(m_groups[i].first, m_groups[i].n, m_program, /* use position */ true,
                /* use texcoord */ true, /* use normal */ true, /* use color */ false, /* use material index*/ false);
        }