M2 Image

TP1 - prise en main



Partie 1 :  installez gKit

cf la rubrique installation de la doc.

modifiez un des tutos, par exemple le tuto6 (version plutot "c") ou le tuto7 (version plutot "c++"), pour afficher plusieurs objets, par exemple des cubes disposés sur une grille :

rappel : transformations standards et matrices model, view, projection, et viewport. c'est la matrice model qui permet de placer et d'orienter un objet dans le monde.

indications : si tout ça est un peu flou, relisez :





affichez le temps cpu et gpu (dérivez de la classe AppTime, au lieu de la classe de base App).

quelles remarques peut on faire sur le temps cpu et le temps gpu en fonction du nombre d'objets affichés (faites plusieurs essais en modifiant le nombre d'objets affiché, ainsi que le nombre de lignes et de colonnes de la grillle) ?

est ce que le temps d'affichage varie selon la position de la camera ? et le temps cpu ? gpu ?

au lieu d'afficher plusieurs fois le même objet, chargez des objets différents, par exemple ceux de quaternius :



mêmes questions : est ce que le temps cpu et/ou gpu changent ?

Partie 2 : et avec openGL ?

modifiez votre programme pour dessiner les objets directement avec openGL.

Le mieux est de faire les modifications progressivement, en suivant les explications de tuto9_shader pour dessiner un objet avec des shaders :
    1. utilisez le shader minimaliste du tuto9 pour dessiner un cube / un objet, au lieu de laisser draw() le faire à votre place,
    2. modifiez le shader pour changer la couleur de l'objet,
        que faut-il modifier dans le shader ? dans l'application ?

Maintenant que vous avez une idée de comment les shaders s'executent, il reste encore à transférer les triangles sur la carte graphique pour utiliser explicitement openGL pour dessiner. Les explications sont dans tuto9_buffer.
    3. créez les buffers et configurez le vertex array object et dessinez directement avec openGL.

et avec plusieurs objets ?
affichez plusieurs objets, chacun d'une couleur différente. que faut-il modifier ? le shader ? l'application ? les 2 ?

si vous affichez une route (ou une ville) composée des blocs modulables de quaternius (cf les exemples de routes et d'intersections au dessus) que va-t-il se passer ? combien de draw() sont necessaires pour afficher 10 blocs composés chacun de 2, 3 ou 4 matieres ? est ce que ce nombre de draw() pose un probleme ? peut-on restructurer tout ça pour faire mieux (reduire le nombre de draw()) ?

indication : on suppose que tous les blocs utilisent le meme ensemble de matieres (par exemple : trottoir, route, eau, etc.)


pour les curieux : comment décrire plusieurs objets avec les mêmes attributs de sommets (par exemple : position + normale) ? un vertex array object et un vertex buffer par objet ? ou est-ce qu'une autre organisation permettrait de gagner du temps ? pourquoi ? comment faire ?

indication : il existe 2 interfaces pour configurer un vao, la version openGL 3.3, cf glVertexArrayPointer() et la version openGL 4.3, cf glVertexArrayFormat() / glBindVertexBuffer(), relisez les détails, si nécessaire (la section se trouve à la fin de la page).

Partie 3 : animation et déformations

On souhaite modifier le shader de l'exercice precedent pour appliquer une transformation à chaque objet avant de le dessiner. Bien sur, on peut utiliser une transformation standard et modifier la matrice model comme dans la partie precedente, mais on n'utilise pas vraiment les shaders...

Modifiez votre shader pour ajouter un parametre supplémentaire : un vecteur de translation. comment modifier le shader ? faut-il modifier l'application ? comment ?

Partie 4 : plusieurs matières

assets de quaternius

L'objectif de cette partie est d'afficher les objets en fonction de leurs matières.

Chaque triangle d'un Mesh est associé à une matière, cf la classe Material. cf Mesh::materials() pour recuperer l'ensemble de matieres et la description de la classe Materials.
les indices des matieres des triangles sont egalement stockes dans Mesh, utilisez const std::vector<unsigned>& Mesh::material_indices() pour recuperer tous les indices, ou Mesh::triangle_material_index() pour connaitre l'indice de la matiere d'un triangle.

Il existe bien sur plusieurs solutions pour dessiner les triangles en tenant compte de leur matiere.

exercice 1 :
Modifiez votre shader pour ajouter un parametre supplementaire : une couleur. comment modifier le shader ? faut-il modifier l'application ? comment ?

bien sur, il est possible de recuperer la matiere de chaque triangle et de dessiner l'objet triangle par triangle, mais ce n'est vraiment pas efficace...

une solution courante et plus rapide consiste à trier les triangles de l'objet par matière, puis à afficher chaque groupe de triangles avec les bons parametres.
std::vector<TriangleGroup> Mesh::groups() trie les triangles d'un Mesh par matiere et renvoie les groupes de triangles à dessiner.

TrangleGroup est une structure toute simple :
 struct TriangleGroup
 {
     int material_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.

exemple :
init( ):
        m_objet= read_mesh("data/robot.obj");     // charge le fichier .obj et le .mtl associe (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
        const Materials& materials= m_objet.materials();
       
        // dessine chaque groupe de triangle, 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.material_index].diffuse_color;
           
            // parametrer le shader pour dessiner avec la couleur
            glUseProgram(m_program);
            { ... }
           
            // dessiner les triangles du groupe
            glDrawArrays(GL_TRIANGLES, group.first, group.n);
        }
           

exercice 2 :
est-il réellement nécessaire de faire plusieurs affichages ?

peut-on ré-organiser les "informations" différemment pour afficher tous les triangles et leurs matieres, en une seule fois / avec le même fragment shader ?
testez votre solution et comparez.

la solution à ce problème est classique et très simple : il suffit d'utiliser une indirection simple, ou une indirection double...

avec openGL, on peut faire la même chose en declarant les tableaux dans le fragment shader, et utiliser gl_PrimitiveID comme indice de triangle.
mais il y a une contrainte à respecter. les tableaux d'uniforms sont statiques. il faut déclarer leur taille et au total, les uniforms déclarés par un shader ne peuvent pas occupper plus de 32Ko... du coup, il est plus raisonnable d'utiliser la version avec une seule indirection qui n'utilise qu'un (petit) tableau de matieres. par contre, il va falloir trouver une autre solution pour obtenir l'indice de la matiere du triangle dans le fragment shader.

une solution directe est d'utiliser un attribut de sommet. il faudra utiliser glVertexAtttribIPointer() pour configurer le vertex array au lieu du glVertexAttribPointer() habituel.
il est egalement assez simple d'utiliser un shader storage buffer (opengl 4.3) pour stocker plus de valeurs dans les tableaux du shader (leur taille est limitee par la memoire de la carte graphique).

remarque : vu la limite sur le nombre de matieres, il est raisonnable d'utiliser un GL_UNSIGNED_BYTE, un entier non signe sur 8bits, comme attribut de sommet (et oui, ca limite le nombre de matieres à 256. mais stocker 256 descriptions de matières occuppe une bonne partie des 32Ko disponibles...)


indications :
    le fragment shader connait l'indice de la primitive qu'il dessine., cf gl_PrimitiveID
    on peut utiliser des tableaux d'uniforms,
    on peut ajouter des attributs aux sommets des triangles : pour configurer un attribut entier (l'indice de la matière par exemple), il faut utiliser glVertexAtttribIPointer(), au lieu de glVertexAttribPointer()
    on peut utiliser des tableaux de textures 2d : cf GL_TEXTURE_2D_ARRAY


bonus :
peut on utiliser la même stratégie avec les matières de plusieurs objets ? quelles seraient les contraintes à respecter ?
    on peut dessiner plusieurs objets avec un seul glDraw(), cf glDrawArraysInstanced() et glMultiDrawArraysIndirect()...
    qui gère les ressources openGL ? chaque objet ? la technique d'affichage ? faut-il créer une gestion centralisee ou peut-on laisser chaque objet décider ?


Partie 5 : des vrais matières

exercice 1 : que faut-il modifier dans les shaders pour utiliser le modele de matiere de Lambert ?
en gros, on calcule le cosinus de l'angle entre la normale au point p et la direction vers la source de lumiere. comment obtenir ces informations dans le vertex shader ? dans le fragment shader ? faut-il modifer l'application ?


exercice 2 : meme question pour les reflets. que faut-il modifier pour utiliser le modele de matière de Blinn-Phong ?
meme question, quels sont les parametres du modele, quelles informations faut-il connaitre dans les shaders ?


rappel : tous les calculs sur des points ou des directions doivent se faire avec des coordonnees dans le meme repere. celui de la scene ou de la camera, par exemple.

exercice 3 : et avec une texture couleur en plus ?
relisez la section sur les textures dans le tuto9 si necessaire.

Partie 6 : un peu de lecture

pour préparer la suite du projet, lisez ces explications sur les limites du rendu classique et comment contourner le probleme...
que faut-il savoir manipuler dans openGL pour programmer ces techniques ?


Partie 7 : une scène de test

"exterior" (1.2Go)  extrait de la scene Bistro :
version allégée de la scène "exterior-small" (200Mo 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

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


init( ) :

        m_objet= read_mesh("bistro/exterior.obj");
        
        if(m_objet.materials().count() == 0)
            // pas de matieres, pas d'affichage
            return -1;
        
        Materials& materials= mesh.materials();
        m_textures.resize(materials.filename_count(), 0);
        // charge toutes les textures
        {
            printf("%d materials.\n", materials.count());
            
            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);
            }
        
            printf("%d textures.\n", materials.filename_count());
        }

render( ) :
        // ensemble de matieres
        const Materials& materials= m_objet.materials();
      
        // dessine chaque groupe de triangle, 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 material_color= materials[group.material_index].diffuse_color;

            // et recuperer la texture couleur
            int texture_id= materials[group.material_index].diffuse_texture;
            GLuint material_texture= m_textures[texture_id];
          
            // parametrer le shader pour dessiner avec la couleur et une texture, cf program_use_texture() par exemple...
            glUseProgram(m_program);
            { ... }
          
            // dessiner les triangles du groupe
            glDrawArrays(GL_TRIANGLES, group.first, group.n);
        }


les textures sont assez grosses, sur un gpu integré, il est probable que ce soit trop gros, il suffit de réduire la résolution des images avant de construire les textures :
            
init( ) :
        // charge toutes les textures et reduit leur taille...
        int n= 0;

        int size= 0;
        for(int i= 0; i < materials.filename_count(); i++)
        {
            // charge l'image
            ImageData image= read_image_data(materials.filename(i));
            
            // resize
            ImageData level= mipmap_resize(image);
            size+= level.pixels.size();
            
            // cree la texture sur l'unite 0
            m_textures[i]= make_texture(0, level);
            
            // 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);
 
            n++;
        }
        
        printf("%d textures. %dMB\n", n, size / 1024 / 1024);

et la fonction resize :
 
ImageData mipmap_resize( const ImageData& image )
{
    assert(image.size == 1);
    
    ImageData level= ImageData(image.width/2, image.height/2, image.channels, image.size);
    int w= level.width;
    int h= level.height;
    int c= level.channels;
    
    for(int y= 0; y < h; y++)
    for(int x= 0; x < w; x++)
    for(int i= 0; i < c; i++)
    {
        int m= 0;
        m= m + image.pixels[image.offset(2*x, 2*y, i)];
        m= m + image.pixels[image.offset(2*x +1, 2*y, i)];
        m= m + image.pixels[image.offset(2*x, 2*y +1, i)];
        m= m + image.pixels[image.offset(2*x +1, 2*y +1, i)];
        
        level.pixels[level.offset(x, y, i)]= m / 4;
    }
    
    return level;
}