M2 - Images




TP 4 - approximations espace image



mise à jour gKit 11/10/17

mesh_viewer, mesh_data, mesh_buffer et material_data dans tutos + tuto lecture etaffichage de scenes, cf modules / lire un fichier wavefront dans la doc.




L'objectif de ce projet est de continuer à manipuler le pipeline openGL, d'écrire des shaders un peu plus intéressants, d'acquérir quelques notions de lancer de rayons (sur un cas particulier), ainsi que les notions fondamentales de l'intégration numérique.

Un bon exercice qui couvre toutes ces notions est de réaliser un prototype de "stochastic screen space reflections", présenté par DICE en 2015 :
(cours Advances in real-time rendering in games, siggraph 2015, et page interne DICE)

pour les curieux, les archives du cours depuis 2009 sont dispo.


l'idée de cette technique est d'ajouter, a posteriori, le reflet des objets voisins, visibles dans la direction du reflet :


(à gauche, la scène "brute", tirée de la présentation de DICE, mirror's edge catalyst, à droite, le résultat avec les reflets)


(et ce qui a été ajouté en utilisant la technique)



Le principe est très simple : pour chaque fragment dessiné, il suffit de construire la direction réfléchie et de calculer l'intersection le long de cette direction avec les objets déjà dessinés et stockés dans le framebuffer.

Partie 1 : prise en main des framebuffer objects.

repassez dans le tuto sur la création et l'utilisation de textures (cf tuto5GL) et de framebuffer objects (cf tuto_framebuffer).

écrivez un programme permettant de charger et d'afficher une scène. modifiez le pour stocker la couleur et la profondeur des fragments visibles dans un framebuffer.

vous pouvez utiliser la scène sponza version Crytek (cf M. McGuire : Meshes), pour vos tests.


Partie 2 : reflets spéculaires et lancer de rayons.

les post traitements sont réalisés après avoir dessiné la scène et stocké la couleur et la profondeur de chaque fragment visible dans 2 textures. pour appliquer le traitement, il faut exécuter un autre shader sur tous les pixels de l'image à traiter.


exercice 1 : fragment shader et post traitements

modifiez votre programme pour exécuter un fragment shader sur tous les pixels du framebuffer. pour vérifier que tout est correct, vous pouvez écrire un shader qui applique une modification simple à la couleur de chaque pixel de l'image de la première étape (remplacez la couleur par son niveau de gris moyen, par exemple).

rappel : pour lire un pixel dans une texture, connaissant ses coordonnées dans ([0..largeur] x [0..hauteur]), le plus simple est d'utiliser texelFetch(sampler2D, ...) de GLSL.

rappel : la variable gl_FragCoord contient les coordonnées du fragment en cours de traitement (dans le repère image).


indications :
vérifiez que le framebuffer est bien rempli en affichant directement la profondeur. par exemple :
#ifdef FRAGMENT_SHADER

uniform sampler2D zbuffer;
out vec4 fragment_color;

void main( )
{
    float z= texelFetch(zbuffer, ivec2(gl_FragCoord.xy), 0).r; // !! texture() et texelFetch() renvoient un vec4(z, 0, 0, 0) !!

    z= pow(z, 256);                                            // la valeur de Z n'est pas lineaire dans le repere image...
    fragment_color= vec4(z, z, z, 1);
}
#endif



testé sous linux, driver nvidia et mesa 17.3 / intel hd iris 6100


exercice 2 : lancer de rayon 3d

La méthode repose sur un outil de base qu'il faut construire : trouver quel objet est visible dans la direction du reflet pour chaque fragment de l'image. Connaissant la position de la camera, le point et sa normale, on peut calculer la direction réfléchie ainsi que le point d'intersection avec le volume observe par la camera. Ces 2 points définissent un segment dans l'espace et il ne reste plus qu'à trouver si une partie du segment est cachée par les objets de la scène. Une solution raisonnable consiste à tester plusieurs points entre l'origine et l’extrémité du segment. Pour chaque point, il faut calculer ses coordonnées (x, y, z) dans le repère image et les comparer au zbuffer(x, y) construit par la première étape. Si le ième point testé se trouve derrière la géométrie déjà dessinée, l'intersection se trouve entre ce point et le précédent.

quelques détails :
soit a la position du fragment, l'origine du segment, et b, son extrémité. on peut décrire l'ensemble des points appartenant au segment en interpolant linéairement les positions de a et b : p(t) = (1-t) * a + t *b ou p(t)= a + t * (b - a). t varie entre 0 et 1, avec p(0)= a et p(1)= b.
si on veut tester N points le long du segment, il suffit de choisir N valeurs de t entre 0 et 1.

pour comparer la profondeur du point p(t) avec le zbuffer, il faut transformer le point dans le repere image. comment ?

comment choisir N ? le zbuffer ne contient pas une infinité de pixels... il est donc suffisant de tester une fois chaque pixel sur la projection du segment ab... peut on le faire exactement ?


écrivez le fragment shader de la 2ieme passe qui calcule l'intersection. si l'intersection existe, utilisez la couleur de l'intersection comme une source de lumière et re-évaluez le reflet en fonction de la matière.

quelles sont les informations nécessaires à ce calcul ? comment les obtenir ? modifiez votre programme en conséquence.



indication :
GLSL dispose de la fonction reflect() pour calculer la direction réfléchie. mais attention à l'orientation des vecteurs...

indication : si vous devez stocker des positions ou des directions dans une texture et un framebuffer, utilisez un format adapté, GL_RGBA32F, par exemple. cf tuto_framebuffer




(tiré de la présentation de DICE, utilise la scène sponza version Crytek)


pour les curieux :
lancer de rayon discret

une autre solution travaille directement dans le repère image, après la projection. mais il faut régler 2 détails : comment tracer une droite en 2d (qui est la projection du segment 3d dans le repère du monde) pour pouvoir interpoler la position et la profondeur de chaque point (en 3d) pour la comparer au zbuffer ?

l'idée du tracé de droite discret est le même que dans le cas 3d. il faut tester plusieurs points le long du segment, en s'éloignant de l'origine et ne visiter qu'une fois chaque pixel. c'est à dire en choisissant la longueur et la direction du pas, noté d. un point sur le segment s'exprime toujours : p(i)= a + i * d, mais avec i entier entre 0 et N le nombre de pas, et d le pas, la direction du rayon (b - a)

pour un segment plutôt horizontal, qui traverse l'écran de gauche à droite, de a.x= 10 jusqu'à b.x= 200, par exemple. il faut N= 190 pas pour visiter tous les pixels, il ne reste plus qu'à calculer d.x et d.y pour interpoler a pour t= 0 et b pour t= N-1.
si la droite est orientée verticalement, de bas en haut, de a.y= 10 jusqu'à b.y= 100, il faudra 90 pas et calculer d.

l'algorithme complet doit traiter les 4 cas, en fonction des signes de d.x et d.y, mais il est très simple de se ramener à 2 cas (segment plutôt horizontal ou plutôt vertical).

dernière étape pour le tracé de droite discret, comment calculer la profondeur du point dans le repère image pour le comparer directement au zbuffer ?

indication : que peut-on interpoler linéairement dans le repère image homogène ? cf gamasutra : software rasterizer and interpolating in screen space.


écrivez un shader qui calcule la direction réfléchie pour chaque fragment, calcule les extrémités du rayon et trouve l'intersection avec le zbuffer. si l'intersection existe, utilisez la couleur de l'intersection comme une source de lumière et re-évaluez le reflet en fonction de la matière.
quelles sont les informations nécessaires à ce calcul ? comment les obtenir ? modifiez votre programme en conséquence.


pour les curieux : est-il possible de passer à travers la surface sans détecter d'intersection ? dans quels cas ? que peut-on faire ?
comment garantir que tous les pixels traversés par le segment sont testés ?



à droite dans la figure ci-dessus, le pixel en jaune est traversé par la droite, mais il n'est pas "visité", ni testé, par l'algorithme. comment inclure ce pixel ?

l'idée est de tester un point intermédiaire, entre p(i) et p(i+1) : p(i + 0.5)... pourquoi ? la droite sort du pixel en traversant soit le coté supérieur soit le coté droit du pixel. le point intermédiaire permet d'évaluer la position de la droite lorsqu'elle sort du pixel : elle est soit au dessus (prochain pixel en haut), soit au dessous (prochain pixel à droite) du coin superieur droit du pixel...

il suffit de tester systématiquement les 2 points, à chaque itération, pour obtenir la figure de gauche. cet algorithme à été publié par J.E. Bresenham en 1962 (cf wikipedia). les processeurs de l'époque ne pouvaient pas faire de calculs sur des réels de manière efficace et l'algorithme est construit uniquement sur des calculs entiers, ce qui à l'avantage d'etre exact, mais très pénible. il faut traiter les 8 orientations de droites possibles, et au final l'algorithme n'est pas très adapté aux processeurs graphiques (remarque : les calculs sur des réels sont plus rapides que sur des entiers...)

au final, tracer une droite discrete se résume à :
vec3 a= vec3( ... );
vec3 b= vec3( ... );
vec3 d= { ... };
int n= { ... };

vev4 color= vec4( ... );
for(int i= 0; i < n; i++)

{
    vec3 p= a + i * d;              // p(i)
    vec3 pm= a + (i + 0.5) * d;     // p(i+.5), point intermédiaire / midpoint
    pixel(floor(p.x), floor(p.y))= color;
    pixel(floor(pm.x), floor(pm.y))= color;
}
d'autres solutions adaptées aux gpu sont publiées, par exemple : "Efficient GPU Screen-Space Ray Tracing", M. McGuire, M. Mara, 2014


Partie 3 : éclairage ambiant et intégration numérique.

maintenant que l'on peut récupérer la couleur de l'objet dans une direction quelconque, il est possible de calculer plus de choses. l'éclairage indirect (cf L2, chemins de longueur 2), bien sur, ou une approximation courante : l'occultation ambiante. dans les 2 cas, l'idée est de trouver les points visibles dans N directions et de sommer leur contribution.

exercice 1 : utilisez la spirale de fibonacci pour choisir N directions. comment transformer ces directions dans le repère du monde ou de la camera ?

indication : vous aurez besoin d'une normale... laquelle : géométrique (celle du triangle) ou interpolée (celle des sommets) ?


exercice 2 : indirect L2 : sommez la couleur du fragment visible dans chaque direction. quelle est la la formulation complète ?
qu'obervez-vous sur les matieres diffuses ? sur les matieres reflechissantes ?


exercice 3 : occultation ambiante.
c'est un peu lent non ? vous ne pourrez probablement pas utiliser plus d'une dizaine de directions en temps "interactif". le lancer de rayon discret est plus rapide, mais sera toujours trop lent. quelle approximation supplémentaire peut-on utiliser pour gagner du temps ?

une idée est de ne tester que l'extrémité d'un rayon sans le tracer complètement, au lieu de trouver le point réellement visible. par contre, il faut adapter la formulation : au lieu de sommer les couleurs des points visibles pour un ensemble de directions, il faut sommer la visibilite (0 ou 1) de l'extrémité du rayon.
il faut également modifier la génération des directions, pour générer aussi une longueur et ne tester que la profondeur de l'extremite avec le zbuffer. les points plus proches que le zbuffer sont considérés visibles.

quelle est la formulation complète ?
indication : quelle serait la formulation de l'éclairage direct pour une matière diffuse éclairé par un ciel uniforme ?

un peu de lecture :



Partie 4 : matière réfléchissante et intégration numérique.

à suivre...


Partie 5 : intégration numérique et réduction de variance.

à suivre...


Bonus : c'est un peu lent, non ?

à suivre...