M2 Image

TP3 - lancer de rayon
et Monte Carlo



L'objectif de ce tp est de mettre en place les éléments nécessaires pour une simulation "correcte" de l'éclairage direct et indirect basé sur les méthodes de Monte Carlo.

Utilisez les scenes de test suivantes, elles sont composées de très peu de geométrie (triangles), ce qui permet d'éviter de construire une structure accélératrice pour limiter les calculs de visibilité (au moins dans un premier temps ...) :

Partie 1 : lancer de rayons et visibilité

exercice 1 : visualisation par lancer de rayons

Pour chaque pixel de l'image résultat, déterminez les coordonnées de l'origine du rayon dans le repère de la scène, ainsi que celles de son extrémité. 
Dans quel espace est-il le plus simple de calculer l'origine et l'extrémité du rayon ?

Calculez l'intersection du rayon avec tous les triangles de la scène (cf. la fonction intersect() dans tuto_ray.cpp) et ne conservez que l'intersection valide la plus proche de l'origine du rayon.


remarques : 

indications : votre programme devrait ressembler à :
main
    creer une image
    charger un maillage et ses matières

    extraire les sources de lumiere (trouver les triangles dont la matière émet de la lumiere)
    
    pour chaque pixel de l'image :
        générer un rayon dans le repere de la scene

        // trouver le point de la scène visible pour le rayon
        pour chaque triangle :
            transformer le rayon dans le repere local de l'objet
            calculer l'intersection du rayon avec le triangle,
            conserver l'intersection si elle est valide et plus proche que la dernière trouvée

        si une intersection valide existe
            ecrire un pixel blanc dans l'image

    sauver l'image

exercice 2 : matière

Récupérez la couleur de la matière du triangle touché par chaque rayon et copiez cette couleur dans l'image.

indications : hit.object_id contient l'indice du triangle, et Mesh::triangle_material( id ) renvoie la matière du triangle d'indice id.



exercice 3 : caméra

Utilisez Orbiter::read_orbiter() pour charger la description d'un point de vue de la scène. Générez les rayons en fonction des différentes transformations définies par Orbiter.

Utilisez shader_kit pour charger votre objet, choisir un point de vue et enregistrer l'orbiter (appuyez sur C pour copier/relire un fichier, sur V pour coller / enregistrer un fichier).
./bin/shader_kit data/shaders/mesh.glsl mesh.obj

exercice 4 : ombres et éclairage direct

Choisissez un point sur chaque source de lumière et vérifiez que chaque point visible par la caméra est également visible par au moins une des sources de lumière. 
Comment déterminer l'origine et la direction des rayons permettant de faire ce test ? 
Les points non éclairés seront noirs.


exercice 5 : pénombres et éclairage direct

utilisez N points sk générés aléatoirement à la surface des sources de lumières pour évaluer l'éclairage direct.


rappel : Lr(p,o)=Le(p,o)+Le(s,p)fr(s,p,o)V(s,p)cosθcos θsd2(s, p)ds

Lr(p,o)Le(p,o)+1Nk=1k=NLe(sk,p)fr(sk,p,o)V(sk,p)cosθcosθskd2(sk,p)1pdf(sk)L_r(p, o) \approx L_e(p,o) + \frac{1}{N} \sum_{k=1}^{k=N} L_e(s_k, p) f_r(s_k, p, o) V(s_k, p) \frac{\cos \theta \cos \theta_{sk}}{d^2(s_k,p)} \frac{1}{pdf(s_k)}

L_r(p, o)= L_e(p, o) + \int L_e(s, p) f_r(s, p, o) V(s, p) \frac{\cos \theta \, \cos \theta_s}{d^2(x, y)} ds

theta est l'angle entre la normale en p et la direction p vers s,
theta_s est l'angle entre la normale en s et la direction s vers p.

cf GI compendium eq 18 pour générer des points dans un triangle,
ou cette solution plus récente (et mieux expliquée) : "A Low-Distortion Map Between Triangle and Square", E. Heitz, 2019




Partie 2 : occultation ambiante



utilisez N directions vk générées aléatoirement pour évaluer l'occultation ambiante :

Lr(p,o)=1πV(p,v)cosθdv

L_r(p, o)= \int \frac{1}{\pi} V(p, \vec{v}) \cos \theta dvLr(p,o)1Nk=1k=N1πV(p,vk)cosθk1pdf(vk)L_r(p, o) \approx \frac{1}{N} \sum_{k=1}^{k= N} \frac{1}{\pi} V(p, \vec{v_k}) \cos \theta_k \frac{1}{pdf(\vec{v_k})}
les directions vk sont uniformes sur l'hemisphère, cf GI compendium eq 34, ou distribuées selon cosθπ\frac{\cos \theta}{\pi}, cf GI compendium eq 35.

V(p, vk) = 1 s'il n'y a pas d'intersection / de géométrie dans la direction vk, et 0 sinon (le point p est à l'ombre)

remarque : les directions aléatoires sont générées dans un repère local, il faut donc les transformer pour connaitre leurs coordonnées dans le repère du monde. On peut construire le changement de repère avec la normale du point p et un autre vecteur (+ 2 produits vectoriels pour construire 2 directions orthogonales). Il y a quelques subtilités dans cette construction, autant en utiliser une qui est robuste et rapide :

cf "Generating a consistently oriented tangent space",  Frisvad, 2012,
et "Building an Orthonormal Basis, Revisited", Pixar, 2017

struct World
{
    World( const Vector& _n ) : n(_n) 
    {
        float sign= std::copysign(1.0f, n.z);
        float a= -1.0f / (sign + n.z);
        float d= n.x * n.y * a;
        t= Vector(1.0f + sign * n.x * n.x * a, sign * d, -sign * n.x);
        b= Vector(d, sign + n.y * n.y * a, -n.y);        
    }
    
    // transforme le vecteur du repere local vers le repere du monde
    Vector operator( ) ( const Vector& local )  const { return local.x * t + local.y * b + local.z * n; }
    
    // transforme le vecteur du repere du monde vers le repere local
    Vector inverse( const Vector& global ) const { return Vector(dot(global, t), dot(global, b), dot(global, n)); }
    
    Vector t;
    Vector b;
    Vector n;
};



Partie 3 : mais c'est trop lent !!

construisez un arbre BVH pour accélérer les calculs et pouvoir calculer l'exemple de la partie 2. les différentes constructions sont expliquées dans la doc. On peut aussi construire des arbres plus efficaces à parcourir en utilisant une des solutions présentées ici.


Partie 4 bonus: et les textures ?

vous pouvez charger et manipuler les textures en utilisant la classe Image
connaissant les coordonnées de texture, vous pouvez utiliser

attention : les coordonnées de texture de la scène bistro se repètent à la surface des objets, elles peuvent > 1 (ou négatives). il faut en tenir compte, sinon segfault... Image::texture() n'est pas robuste à ce type d'erreur. le pixel est sensé exister... une solution simple est de calculer le modulo des coordonnées de textures par rapport aux dimensions de l'image.


et pour les billboards / textures semi transparentes ?

pendant le parcours du bvh, lorsqu'une intersection avec un triangle est trouvée, il faut vérifier si une texture transparente est "plaquée" sur le triangle et vérifier la valeur du canal alpha / transparence pour les coordonnées de texture du point d'intersection.

si alpha < 0.5 la texture indique qu'il n'y a pas de géométrie à cet endroit, il faut donc poursuivre le parcours du bvh...

remarque : votre intersecteur rayon / bvh doit donc avoir accès au bvh (noeuds + feuilles + triangles, bien sur), mais aussi aux matières et textures du mesh pour récupérer ces informations.
(faites attention à la gestion mémoire des images / textures, il y a 1Go de textures compressées dans la scène... évitez de charger plusieurs fois les images)