MOS8.5 - Centrale Lyon

TP3 - Eclairage direct


Maintenant que la base fonctionne pour tout le monde, on va pouvoir tout casser...


Partie 1.

Terminez le tp précédent, notamment l'intersection rayon / triangle.
Prévoyez également une structure Scene qui abstrait la représentation des objets et permet de séparer le calcul de l'image de la représentation des objets.

remarque : vous pouvez décomposer les modifications de votre programme en plusieurs étapes : 1. créer la structure scene en gardant le plan et les spheres, 2. utilisez les fonctions intersect() et visible() de scene dans le reste de votre programme. 3. ajouter les triangles à la scène. 4. enlever les spheres et le plan.

Si vous pensez être trop en retard vous pouvez utiliser ces éléments de correction ou comparer à votre solution :
struct Hit
{
    int triangle_id;
    float t;
    float u, v;
   
    Hit( ) : triangle_id(-1), t(FLT_MAX), u(0), v(0) {}       // init par defaut, pas d'intersection
    Hit( const int _id, const float _t, const float _u, const float _v ) : triangle_id(_id), t(_t), u(_u), v(_v) {}
   
    operator bool( ) const { return (triangle_id != -1); }    // renvoie vrai si l'intersection est initialisee...
};

struct Triangle
{
    Point p;
    Vector e1, e2;
    int id;
   
    Triangle( const Point& a, const Point& b, const Point& c, const int i ) : p(a), e1(Vector(a, b)), e2(Vector(a, c)), id(i) {}
   
    /* cf Optimizing Ray-Triangle Intersection via Automated Search
        https://perso.univ-lyon1.fr/jean-claude.iehl/Public/educ/M1IMAGE/kensler_triangle.pdf
       
        equations 2b, page 3, colonne de droite
    */
    Hit intersect( const Ray& ray, const float tmax ) const
    {
        Vector op= Vector(ray.o, p);
        float V=  dot( cross(e1, -e2), ray.d);
        float Vp= dot( cross(e1, -e2), op);
        float V1= dot( cross(op, ray.d), -e2);
        float V2= dot( cross(op, ray.d), e1);
       
        float t= Vp / V;
        if(t < 0 || t > tmax) return {};
       
        float u= V1 / V;
        if(u < 0 || u > 1) return {};
       
        float v= V2 / V;
        if(v < 0 || u+v > 1) return {};
       
        return Hit(id, t, u, v);
    }
};

struct Scene
{
    std::vector<Triangle> triangles;
   
    Scene( const char *file )
    {
        // lire le fichier et construire les triangles
    }
   
    Hit intersect( const Point&o, const Vector& d, const float tmax );  // ou un rayon
    bool visible( const Point& p, const Point& q );                     // ou un rayon
};

exercice 2 : chargement d'un fichier wavefront / .obj
utilisez read_positions( const char *file ) pour lire un fichier au format .obj qui décrit un ensemble de triangles. Ce format est assez limité mais est reconnu par tous les logiciels 3d comme Blender, par exemple et est très simple à analyser et à relire. Ce qui permet de convertir des objets très facilement et bien sur de les importer dans votre programme.

Il ne reste plus qu'à parcourir les triangles stockés dans le fichier pour construire leur représentation géométrique et les ajouter à la structure scene. cf la doc en ligne.

quelques scènes de tests sont disponibles : cf doc en ligne.
Pour commencer, il faudra se contenter des 3 premières scènes 3d. Les autres comportent beaucoup plus de triangles et il faudra construire une structure accélératrice pour les manipuler en temps raisonnable.

pour les trop rapides : cf la doc en ligne pour construire un arbre simple ou un arbre optimisé.


exercice 3 :
Vérifiez que le calcul de l'éclairage ambiant est toujours correct.

remarque : il faudra très probablement déplacer les objets devant la camera... par exemple, pour la cornell box, une translation par (0, -1, -2) suffit à voir la totalité de la scène dans l'image. Utilisez la classe Transform et Translation(), Rotation(), etc pour placer et orienter la scène, cf doc en ligne.

remarque : le plus simple est de pre-transformer les positions des sommets des triangles, ie au moment de la création des triangles.




remarque : vous pouvez aussi construire l'équivalent d'une projection perspective avec les transformations, cf Perspective( ). et utiliser son inverse (!!) pour générer les rayons passant par chaque pixel. cf "principe du lancer de rayon" dans la doc en ligne.



exercice 4 : couleur !
chargez les matières des triangles avec read_materials( ).
lorsque l'on trouve une intersection avec un triangle, il suffit de renvoyer la couleur diffuse de la matière associée au triangle. Ajoutez une fonction à la structure scene, comme suggéré dans le tp1 :
const Material& material( const Hit& hit ); // renvoie la matiere au point d'intersection
Color diffuse( const Hit& hit );            // renvoie la couleur diffuse au point d'intersection

utilisez cette couleur dans le calcul de la lumière réfléchie vers le pixel.




pour les curieux :
et avec des reflets ? Vous pouvez écrire une fonction Color Fr( const Material& material, const Vector& o, const Vector& n, const Vector& l ) qui renvoie l'évaluation de la couleur de la matière dans la configuration paramétrée par la normale, la direction vers la source ainsi que la direction vers l'observateur.


Partie 2 : éclairage direct

Reprenez la formulation de l'équation de rendu sur les aires des sources de lumière, écrivez l'estimateur Monte Carlo.
Comment construire la variable aléatoire ? Elle est composée de 2 "parties" indépendantes, la source de lumière et un point à la surface de la source sélectionnée.

exercice 1 :
Modifiez le chargement de la scène : il faut trouver les sources de lumière. Il suffit de parcourir les triangles, de récupérer leur matière et de conserver les triangles dont la matière émet de la lumière, ie Material::emission sera non nul, cf doc en ligne.
Ajoutez un ensemble de triangles émissifs dans la description de la scène.


exercice 2 : 1 source
On commence par considérer que seule la première source éclaire la scène.
Que faut-il faire pour évaluer l'estimateur ?

regardez le GI Compendium pour trouver un générateur de point dans un triangle. Ou cette solution plus géométrique :
"A Low-Distortion Map Between Triangle and Square", E. Heitz, Unity, 2019

quelle sera la densité de probabilité d'un point sur un triangle ?




exercice 3 : toutes les sources !
Utilisez une sélection uniforme de la source / du triangle émissif. Comment évolue la variable aléatoire de l'estimateur ? Quelle est sa densité de probabilité ?



reamrque : au lieu de passer du temps à régler l'intensité des sources pour éviter de saturer l'image, il est bien plus rapide d'enregistrer l'image dans un format  HDR qui permet de régler l'exposition a posteriori. Par exemple, en utilisant Tev dispo sur github.





pour les rapides : et si les sources ne sont pas toutes de la même taille ? Comment construire une variable aléatoire permettant de sélectionner une source proportionnellement à son aire, par exemple ? au flux émis par la source ? à l'angle solide occupé ?
vous pouvez tester avec emission.obj prévue pour tester ce cas précis...

pour les curieux : oui, la formulation sur les aires de l'équation de rendu introduit une singularité, mais ce n'est généralement un problème que lorsque les sources sont posées sur / touchent une surface... cf "Illumination in the Presence of Weak Singularities", T. Kollig, A. Keller, 2004

pour les curieux : une solution pour éviter ce problème consiste à générer uniformément des directions vers les sources. C'est simple lorsque la source est une sphère, il suffit de générer des rayons dans un cone, cf GI Compenium. Mais c'est plus délicat pour une source carré ou rectangulaire, cf "An Area-Preserving Parametrization for Spherical Rectangles", C. Urena, 2013. et oui, bien sur, ça revient à ne pas utiliser la formulation de l'équation de rendu sur les aires des sources.