M2 Image

TP2 - affichage d'une scène détaillée

"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.

les indications pour charger les textures sont à la fin du tp de préparation.

version allégée des matières de la scène : si vous avez des problèmes pour récupérer les textures et à construire un texture2DArray, voila une description de matieres qui n'utilise que des textures 1024x1024 non constantes : export.mtl

il suffit de remplacer la ligne mtllib bistro.mtl par mtllib export.mtl dans bistro.obj pour les charger automatiquement avec read_mesh() ou read_indexed_mesh().
les textures constantes de taille variable sont toutes remplacées par une texture blanche 1024x1024 white.png, et les bonnes valeurs sont placées dans la description de la matière (diffuse, specular).

attention : les couleurs des reflets (cf Material::specular) utilisent une convention particulière : ce ne sont pas vraiment des couleurs, mais 3 valeurs entre 0 et 1. le canal rouge représente l'ambient occlusion, le canal vert la rugosité de la matière et le canal bleu permet de décrire les métaux. au minimum, il faut évaluer la concentration des reflets Blinn-Phong à partir de la rugosité : ns= (2 / rugosite**2 - 2)

voila la doc de l'exporter de blender qui a fait la conversion, cf section Metallic and Roughness

Partie 1 : ne pas dessiner ce que la camera ne peut pas voir

La première idée pour gagner du temps est de ne pas dessiner les objets / groupes de triangles en dehors de la région de la scène observée par la camera, comme c'est un peu long comme nom, on utilise souvent le mot frustum (def) pour désigner cette partie de la scène.

exercice 1 : comment déterminer qu'un point est dans le frustum ? ou à l'extérieur ?

exercice 2 : et pour une boite alignée sur les axes ?

Ce test serait très simple si l'on voulait déterminer que 2 boites alignées sur les axes se touchent (ou pas). Il suffit de vérifier que tous les sommets d'une boite se trouvent tous à droite de l'autre boite, ou à gauche, ou en bas, etc. (6 tests en tout)
Qu'est ce qui change si l'on veut tester qu'une boite touche (ou pas) le frustum ? Le frustum est un morceau de pyramide dans le repère de la scène, pas une boite alignée sur les axes... le test précédent n'est pas utilisable, par contre : c'est bien un cube aligné sur les axes dans le repère projectif...

une solution simple est donc de faire les tests dans le "bon" repère. Selon le cas, c'est un objet ou l'autre, qui est une boite alignée sur les axes :
  1. comparer la boite au frustum dans le repère projectif : le frustum est un cube aligné sur les axes, si tous les sommets (transformés) de la boite sont tous du même coté du frustum, les 2 objets sont séparés et ne se touchent pas,
  2. comparer le frustum dans le repère de la scène à la boite alignée sur les axes (du repère de la scène), si tous les sommets (transformés) du frustum sont tous du même coté de la boite, les 2 objets sont séparés et ne se touchent pas...

et oui, si le premier test ne permet pas de "séparer" la boite et le frustum, il faut faire le 2ieme test.

remarque : dans le repère projectif homogène, qu'est ce qui se passe avec w, le poids homogène des points projettés ?

question : quelles sont les coordonnées des sommets du frustum dans le repère projectif ? et dans le repère de la scène ? faut-il utiliser des transformations particulières ?


Partie 2 : ou sont les cubes ??

Il ne reste plus qu'à tester les objets de la scène avant de les dessiner. Mesh::bounds() calcule les points extrèmes de la boite englobante alignée sur les axes d'un objet.

exercice 1 : connaissant pmin et pmax, les points extrèmes d'une boite alignée sur les axes, comment calculer les coordonnées des 8 sommets de la boite ?

indications : pmin.x et pmax.x représentent les bornes de l'intervalle sur l'axe x, idem pour y et z.
autrement dit, 4 sommets se trouvent à x= pmin.x et 4 autres à x= pmax.x, idem pour y et z.

exercice 2 : vérifiez que votre test de visibité fonctionne correctement sur une scène de test, comme la grille de cubes du tp précédent.

exercice 3 : sauf que dans exterior.obj, il n'y a qu'un seul objet... comment faire pour découper la scène en plusieurs blocs ?

il y a bien sur plusieurs solutions, proposez-en 1 ou 2 et vérifiez avec les autres binomes, quelle est la meilleure solution ?

exercice 4 : réalisez le découpage du mesh en boites. testez-les avant affichage des triangles associés.

est-il plus rapide d'afficher toute la scène sans faire de tests de visibilité ? et en changeant la taille des boites ? que constatez-vous ?

indication : vous pouvez trier les triangles d'un objet avec Mesh::groups( const std::vector<unsigned>& properties ), en associant un entier à chaque triangle (par exemple un indice de cellule dans une grille...). groups() renvoie les triangles tries selon ce critere, par cellule de la grille... cf MeshGroup et tuto9_groups.cpp pour afficher les différents groupes.

Partie 3 : mais ou sont les sources ?

il y a ~20000 triangles qui émettent de la lumière dans exterior.obj. vous pouvez vous amuser à modifier un fragment shader pour évaluer la couleur du fragment en calculant la lumière réfléchie par chaque source et en sommant le tout. ca va etre un peu long, mais ça fonctionnera.

peut-on limiter le nombre de sources de lumière que l'on évalue dans le fragment shader ? sans introduire trop d'erreur dans le résultat...

indication : si la source est trop loin son influence est très faible.
rappel : le flux reçu par un point à une distance d de la source dépend du carré de d...

comment déterminer le sous ensemble de sources de lumière qui peut éclairer un fragment ? une région de la scène ? comment passer cette information au fragment shader ?

exercice 1 : modifiez votre fragment shader pour gérer N sources. comment transmettre cette information au shader ?
pour tester, créez N sources placées aléatoirement au dessus de la scène.

exercice 2 : on peut aussi utiliser le découpage spatial de la scène de la partie 2 pour sélectionner les sources qui peuvent éclairer chaque région. comment transmettre ces informations au shader ? comment le fragment shader peut-il retrouver l'ensemble de sources pouvant éclairer un fragment ?

question : serait-il interressant d'utiliser un découpage spatial plus fin, ie des régions plus petites, pour construire l'ensemble de sources associé à chaque région ?

Partie 4 : mais c'est trop lent !

peut-on éviter de faire des calculs inutiles ?

question : quels calculs sont inutiles ? comment éviter de les faire ?

combien de glDraw() utilisez-vous pour dessiner les régions visibles / dans le frustum ? peut-on réduire ce nombre ? est-ce a priori interressant ?
est-ce que le nombre de régions (cf partie 2) à une influence sur les performances ?

pensez vous qu'un rendu avec une Z prepass soit interressant dans ce cas ? ou un rendu différé ? pourquoi ?

exercice : modifiez votre programme pour tester une des 2 techniques. que constatez-vous ? avec plus de sources de lumière ? avec moins ?


indications :
parmi les choses inutiles, combien de vertex shaders s'exécutent si la scène est décrite par des triangles non indexés ? et avec des triangles indexés ?
rappel : il existe read_mesh() et read_indexed_mesh() pour vérifier (oui c'est récent...)

indications : on peut dessiner des objets différents (y compris des instances) avec un seul draw, cf glMultiDraw() et glMultiDrawIndirect()

indications : combien de positions uniques dans l'objet ? combien de sommets au total dans l'objet ? pourquoi ? peut-on ne dessiner que les positions uniques ?

Partie 5 : occlusion culling, bonus

parmi les choses inutiles, il y a : dessiner des objets entièrement cachés par d'autres. Oui, dessiner la scène avec une Z prepass ou du rendu différé limite le problème : les fragment shaders ne sont exécutés que sur les fragments visibles dans l'image. Mais, le gpu a quand même passé un temps non négligeable à exécuter les vertex shaders, à tester l'inclusion des pixels dans chaque triangle (cf unité de fragmentation / raterization) puis à exécuter (ou pas) les fragment shaders.

lisez "Practical, dynamic visibility for games", section 5
quel problème adresse cette méthode de rendu ?

la solution proposée semble-t-elle adaptée à ce tp ? pouvez-vous estimer la quantité de géométrie dessinée pour rien dans la scène de test (malgré les tests sur le frustum) ?
Combien de temps gpu pourrait-on gagner avec ce type de méthode ? même question avec des scènes différentes / pour quel type de scène ce test semble-t-il nécessaire ?

L'idée de la méthode est de vérifier qu'un objet n'est pas caché derrière un autre avant de le dessiner. Pour faire ce test, il faut construire le rectangle dans l'image qui englobe la projection image de l'objet, puis comparer (pour chaque pixel du rectangle) la profondeur de l'objet aux profondeurs des objets deja dessinés (ie le zbuffer). Si la profondeur de l'objet est supérieure aux profondeurs stockées dans le zbuffer, l'objet est caché par de la géométrie deja dessinée, et ce n'est pas la peine de le dessiner.

indication : pourquoi constuire le HZB / zbuffer hiérarchique ? combien de pixels faut-il tester sans la version hiérarchique ? et avec la version hiérarchique ?

Ce test est relativement simple à faire, mais il y a un problème : il faut dessiner des objets pour obtenir un zbuffer avant de faire les tests... Sinon, tous ces tests ne servent à rien.
L'autre idée proposée par cette méthode est de conserver la visibilité de chaque objet pour aider les tests de la prochaine image... il suffira de commencer par dessiner les objets visibles dans l'image précédente pour remplir le zbuffer, puis de tester les autres objets avant de les dessiner (ou pas), et de mettre à jour l'info caché / visible, pour aider le dessin de l'image suivante.


question :
dans quels cas ça ne fonctionnera pas ? quelle est l'hypothèse principale ? ou : pourquoi ça marche ?

indications :
pour relire le zbuffer depuis l'application, utilisez glReadPixels(), (ou regardez comment fonctionne screenshot()...)

    std::vector<float> zbuffer(width*height);
    glReadBuffer(GL_BACK);
    glReadPixels(0, 0, width, height, GL_DEPTH_COMPONENT, GL_FLOAT, zbuffer.data());

indication : construire les mipmaps, la version hiérarchique du zbuffer, se fait très facilement sur cpu (avec des shaders, c'est rigolo aussi, mais ce sera pour plus tard).





Annexe : texture arrays

creation d'un texture array à partir d'un ensemble d'images de mêmes dimensions :

GLuint make_texture_array( const int unit, const std::vector<ImageData>& images, const GLenum texel_format= GL_RGBA )
{
    assert(images.size());
    
    // verifie que toutes les images sont au meme format
    int w= images[0].width;
    int h= images[0].height;
    int c= images[0].channels;
    int s= images[0].size;
    int d= int(images.size());
    
    for(unsigned i= 1; i < images.size(); i++)
    {
        if(images[i].pixels.size() == 0) 
            continue;     // pas de pixels, image pas chargee ?
        
        if(images[i].width != w) 
            return 0;   //  pas la meme largeur
        if(images[i].height != h) 
            return 0;   // pas la meme hauteur
    }
    
    // alloue le tableau de textures
    GLuint texture= 0;
    glGenTextures(1, &texture);
    glActiveTexture(GL_TEXTURE0 + unit);
    glBindTexture(GL_TEXTURE_2D_ARRAY, texture);
    
    glTexImage3D(GL_TEXTURE_2D_ARRAY, /* mipmap */ 0,
        texel_format, w, h, d, /* border */ 0,
        GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
    
    // transfere les textures 
    for(unsigned i= 0; i < images.size(); i++)
    {
        if(images[i].pixels.size() == 0) 
            continue;
        
        // recupere les parametres de conversion...
        GLenum format;
        switch(images[i].channels)
        {
            case 1: format= GL_RED; break;
            case 2: format= GL_RG; break;
            case 3: format= GL_RGB; break;
            case 4: format= GL_RGBA; break;
            default: format= GL_RGBA; 
        }
        
        GLenum type;
        switch(images[i].size)
        {
            case 1: type= GL_UNSIGNED_BYTE; break;
            case 4: type= GL_FLOAT; break;
            default: type= GL_UNSIGNED_BYTE;
        }
        
        glTexSubImage3D(GL_TEXTURE_2D_ARRAY, /* mipmap */ 0,
            /* x offset */ 0, /* y offset */ 0, /* z offset == index */ i,
            w, h, 1,
            format, type, images[i].pixels.data());
    }
    
    // dimension max des textures 2d
    int max_2d= 0;
    glGetIntegerv(GL_MAX_TEXTURE_SIZE, &max_2d);
    int max_2d_array=0;
    // dimension max d'un tableau de textures 2d
    glGetIntegerv(GL_MAX_ARRAY_TEXTURE_LAYERS, &max_2d_array);
    printf("texture 2d maxsize %dx%d\n", max_2d,max_2d);
    printf("texture 2d array maxsize %dx%dx%d\n", max_2d,max_2d,max_2d_array);
    
    printf("texture array: %dx%dx%d %dMo\n", w, h, d, 4*w*h*d / 1024 / 1024);
    
    //mipmaps
    glGenerateMipmap(GL_TEXTURE_2D_ARRAY);
   
    return texture;
}

exemple d'utilisation :

init( ) :
    // charge un objet et ses matieres
    Mesh mesh= read_mesh( "..." );
    const Materials& materials= mesh.materials();

    // charge les textures
    std::vector<ImageData> images;
    for(int i= 0; i < materials.filename_count(); i++)
    {
        ImageData image= read_image_data(materials.filename(i));
        images.emplace_back(image);
    }
   
    // cree le texture 2d array
    GLuint texture_array= make_texture_array(0, images);

render( ):
    // selectionne le texture 2d array sur une unite de texture
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D_ARRAY, texture_array);

    glBindVertexArray(vao);
    // configure le shader
    glUseProgram(program);

    // indice de l'unite de texture associee au texture 2d array, cf le ActiveTexture() au dessus...
    program_uniform(program, "texture_array", 0);
   
    { ... } // autres parametres du shader

    // go !
    glDrawArrays(GL_TRIANGLES, ... );


fragment shader :
in vec2 vertex_texcoord;                 //< attribut de sommet exporte par le vertex shader

uniform sampler2DArray texture_array;    //< acces au tableau de textures...

vec4 fragment_color;
void main( )
{
    vec4 color= texture(texture_array, vec3(vertex_texcoord, <indice de la texture dans le tableau>));
    if(color.a < 0.3)
        discard;
   
    fragment_color= color * ... ;
}