M2 Image

TP2 - rendu indirect / deferred


Partie 1 : mise en place

chargez la scène de test : rungholt.zip

il y a 2 modèles : la version complète assez volumineuse, et une seule maison, plus rapide à charger et plus simple à manipuler.

toutes les textures sont regroupées dans un seule image (un atlas de textures... rungholt-RGBA.png) ce qui est très simple à manipuler : il suffit de charger l'image et de fabriquer une texture openGL avec, cf read_texture() dans texture.h.

rappel : utilisation de textures dans les shaders, cf doc en ligne.


   

exercice 2 : et la camera ?
placez une camera dans la rue et à la hauteur de la tête d'un personnage minecraft, ie 2 cubes de haut (ou 1.5 pour les yeux).
comment peut-on déplacer la camera dans la scène ?


exercice 3 : on ne marche pas sur les pelouses !
comment vérifier que la camera / le personnage ne se déplace que dans la rue et ne passe pas à travers les murs, par exemple ?

indication : vous pouvez simplement vérifier l'altitude du cube sur lequel se trouve la caméra.
et il n'est pas trop difficile de pré-calculer les cubes sur lesquels on peut placer la camera lors du chargement de la scène.

autre solution : on pourrait également lancer un rayon pour vérifier l'existence de géométrie, comme dans le projet de l'année précédente...


exercice 4 : et les fenetres ?
les textures des faces de certains cubes sont semi-transparentes. les texels opaques ont une valeur alpha= 1, et les texels parfaitement transparents alpha= 0.
il suffit de tester la valeur d'alpha et d'utiliser l'instruction discard du fragment shader pour "dessiner" les parties transparentes.

c'est aussi le bon moment pour utiliser une matière diffuse sur les cubes opaques. utilisez une source directionnelle vers la camera, par exemple, pour éclairer la scène.


Partie 2 : lumières !


256 sources ponctuelles animées. rendu direct.


exercice 1 : plus de lumière ?
initialisez un tableau de sources de lumière ponctuelles. il suffit de tirer aléatoirement la position des sources dans l'englobant de la scène.

rappel : Mesh::bounds() calcule l'englobant d'un mesh.


exercice 2 : shader !
comment déclarer un tableau de sources de lumières dans le shader ? lequel ?
comment écrire le calcul de l'éclairage direct dans le shader ?
comment transférer les données entre l'application et le shader ?

rappel : il faut diviser l'emission d'une source ponctuelle par le carré de la distance au point éclairé.
(oui, les physiciens font des trucs bizarres parfois.)

faites varier le nombre de sources pour obtenir un rendu un peu trop long (~10 images par seconde) mais pas completement figé non plus...

Partie 3 : rendu indirect / deferred

 
256 sources ponctuelles animées. mêmes conditions. rendu indirect / deferred.


relisez la description de pourquoi le rendu normal / direct est aussi lent dans ce cas la, ie beaucoup de fragments par pixel et pas mal de sources de lumières accumulées par fragment... cf doc en ligne.
mais bien sur, on peut éviter de faire tout ces calculs inutiles.

exercice 1 : stockage
quelles informations stocker par pixel ? au minimum, la position (ou la profondeur), la couleur diffuse et la normale.

quelle format de texture utiliser ? on peut bien sur utiliser des floats 32bits, cf GL_RGB32F, mais c'est aussi le format le plus volumineux et ce sera donc le plus long à copier en mémoire. il est possible d'utiliser des formats plus légers, mais un peu moins précis comme des floats 16bits, GL_RGB16F, ou de quantifier sur 8bits, GL_RGB8 pour des valeurs non signées (comme des couleurs), ou GL_RGB8_SNORM pour des valeurs signées (comme des coordonnées de normales). les différents formats existants sont résumés dans la doc sur la création des textures, cf doc en ligne.

voups pouvez utiliser les utilitaires de texture.h pour créer les textures au format souhaité, ils sont la pour ça, par exemple :
GLuint make_vec3_texture(/* unit */,  /* widh */, /* height */, /* texel format */ );


exercice 2 : un peu de technique
il ne reste plus qu'à apprendre comment configurer un framebuffer : cf doc en ligne.

écrivez le fragment shader qui va écrire les informations dans les textures associées au framebuffer.
comment déclarer des sorties dans le fragment shader ? comment les associer aux sorties du framebuffer ? dans quelle texture sera écrite la sortie du shader ?


exercice 3 : lumière !
il ne reste plus qu'à écrire le fragment shader qui va faire le calcul par pixel.

comment provoquer l'exécution du fragment shader sur tous les pixels de l'image ?
il suffit de dessiner de la géométrie qui couvre tous les pixels de l'image. la forme la plus simple que l'on peut dessiner est un quad ou un grand triangle.

dans quel repère connait-on les coordonnées x et y des sommets du quad / triangle plein-écran ?
quelle valeur utiliser pour z afin d'être certains que tous les fragments passent le test de profondeur ? (on veut vraiment exécuter un fragment shader par pixel...)

indication :
ou se trouve le frustum dans le repère projectif ?
ou: quelles sont les coordonnées des points du repère projectif qui sont associés à des pixels de l'image ?

pour les curieux : comment dessiner le quad ou le triangle plein-écran sans créer de vertex / index buffers ?


rappel : votre application va ressembler à quelque chose comme ça :
int w, h;
GLuint framebuffer;
GLuint depth;
GLuint color;
GLuint normal;

init( ): 
    // exercice 1

    depth= make_depth_texture( 0, w, h, /* texel format */ );
    color=  make_vec3_texture( 0, w, h, /* texel format */ );
    normal= make_vec3_texture( 0, w, h, /* texel format */ );
    ...
   
    // exercice 2
    glGenFramebuffer(1, &framebuffer);
    glBindFramebufer(GL_DRAW_FRAMEBUFFER, framebuffer);
   
    glFramebufferTexture(GL_DRAW_FRAMEBUFFER,  /* attachment */ GL_COLOR_ATTACHMENT0, /* texture */ color, /* mipmap level */ 0);
    glFramebufferTexture(GL_DRAW_FRAMEBUFFER,  /* attachment */ GL_COLOR_ATTACHMENT1, /* texture */ normal, /* mipmap level */ 0);
    glFramebufferTexture(GL_DRAW_FRAMEBUFFER,  /* attachment */ GL_DEPTH_ATTACHMENT, /* texture */ depth, /* mipmap level */ 0);
   
    // associer les sorties du fragment shader aux textures attachees au framebuffer
    GLenum buffers[]= { /* sortie / location 0 */ GL_COLOR_ATTACHMENT0, /* sortie / location 1 */ GL_COLOR_ATTACHMENT1 };
    glDrawBuffers(2, buffers);   
   
    // nettoyage...
    glBindFramebufer(GL_DRAW_FRAMEBUFFER, 0);
   
   
render( ):
// etape 1 : construire le GBuffer   // exercice 2
    glBindFramebufer(GL_DRAW_FRAMEBUFFER, framebuffer);
    glViewport(0, 0, w, h);
    glClear( ... )
   
    glUseProgram(program_draw):
    ...
    glDraw( ... );    // dessiner les objets et construire le GBuffer
   
// etape 2 : evaluer le GBuffer   // exercice 3
    glBindFramebufer(GL_DRAW_FRAMEBUFFER, 0);
    glViewport(0, 0, window_width(), window_height());
    glClear( ... )
   
    glUseProgram(program_deferred):
    ...
    glDraw( ... );    // evaluer le GBuffer sur chaque pixel


exercice 4 : quelle est l'influence du format des textures utilisées pour stocker le G-Buffer ?
constatez-vous une différence de performance entre les différents formats de texture proposés dans l'exercice 1 ?

même question pour stocker ou calculer la position (à partir du zbuffer et de gl_FragCoord) ?


pour les très curieux : quel est l'impact du visibility buffer ? ie ne stocker que l'id du triangle sur chaque pixel, mais comment construire le G-Buffer ?
alternative : au lieu de re-calculer l'intersection pixel / rayon / triangle pour tous les pixels, on peut aussi interpoler, cf "deferred attribute interpolation for ... deferred shading" 2015.

pour les trop rapides : comment éviter de faire les calculs sur les sources qui influencent peu un fragment ?
à lire sur le sujet : GDC 2015 "Advancements in Tiled-Based Compute Rendering"


Partie 4 : ne pas...

il est aussi très long de transformer les sommets de toute la scène (13.5M quand même...) alors que l'on ne voit qu'une toute petite partie de la géométrie (la camera est au niveau du sol et l'essentiel de la scène est probablement cachée derrière un batiment...).

une idée est de découper la scène en blocs et de vérifier qu'un bloc est visible par la camera (ie dans son frustum) avant de le dessiner.


exercice 1 :
comment déterminer qu'un point est visible par la camera ? ie qu'il appartient à son frustum ?

indication :
quelles conditions vérifient les coordonnées des points du frustum ? dans le repère de la scène ? dans le repère de la camera ? dans le repère projectif ? dans le repère image ?


exercice 2 :
même question pour un cube aligné sur les axes ?
comment déterminer que le cube est, au moins en partie, visible ? serait-il plus simple de vérifier que le cube n'est pas visible ?
proposez un test qui vérifie qu'un cube ne peut pas être visible. l'idée est de trouver un plan qui sépare le frustum et le cube...

indication : c'est la même idée que pour dessiner un triangle, si tous les sommets d'un objet sont de l'autre coté du plan défini par chaque face du cube englobant, les 2 objets sont séparés par ce plan...




sur le schema, il suffit de vérifier que les sommets rouges (ie les sommets du frustum transformés dans le repère de la scène) sont tous à droite de l'englobant. il suffira de tester les 6 plans associés aux 6 faces...

rappel : il faut connaitres les coordonnées des sommets des objets à tester dans le même repère. celui de la scène par exemple.
il suffit de transformer les coordonnées des sommets de l'objet par la matrice model et de construire l'englobant, ie les min et max sur les 3 axes.
et il faut aussi obtenir les coordonnées des sommets du frustum dans le repère de la scène. on connait ces coordonnées dans le repère projectif, il suffit de les mulitplier par view et projection inverse.


exercice 3 : le test précédent est-il suffisant ?
que se passe-t-il dans ce cas :




comment modifier le test pour vérifier que les 2 objets sont bien séparés ?

indication : tous les sommets de l'englobant sont du même coté d'une face du frustum...
indication : dans quel repère le frustum est-il un cube aligné sur les axes ? serait-il possible... de faire le test dans ce repère ?




écrivez le test complet. est-il nécessaire de faire les 2 tests dans tous les cas ?


exercice 4 :
découpez la scène en blocs.

une solution très simple est de "plonger" la scène dans une grille et de déterminer dans quelle cellule de la grille se trouve le centre de chaque triangle. puis de construire la liste des triangles de chaque cellule de la grille ainsi que l'englobant des triangles.

Mesh::groups() peut trier les triangles automatiquement et construire les groupes correspondant à chaque cellule. il suffit de passer en paramètre un std::vector avec un entier par triangle. l'entier sera tout simplement un identifiant unique de la cellule (ie l'indice linéaire par exemple) dans laquelle se trouve le triangle. il ne restera plus qu'à construire l'englobant de chaque groupe de triangles.

remarque : vous pouvez bien sur utiliser n'importe quelle autre solution pour réaliser ce découpage...


exercice 5 :
utilisez le test précédent et ne dessinez que les blocs visibles.

quelle taille choisir pour un bloc ? ou combien de triangles par bloc ? quelle influence peut avoir ce paramètre ?


pour les curieux : sans tests de visibilité on pouvait dessiner toute la scène avec un seul draw, on vient de passer à un draw par bloc, ce qui peut être génant lorsque beaucoup de blocs sont visibles... comment utiliser glMultiDrawIndirect() pour ne faire qu'un seul draw pour dessiner tous les blocs visibles en une seule fois / avec un seul draw ? cf doc en ligne.

remarque : vous aurez besoin de modifier efficacement le contenu d'un buffer, cf doc en ligne.


alors c'est mieux ? ou pas ?