M2 Image

TP3 - lancer de rayon



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 la scène toute simple cornell.gltf dans un premier temps, elle est composée de peu de triangles et permet de construire un code départ simple sans construire de bvh...


relisez principes du lancer de rayon, si nécessaire...

Partie 1 : lancer de rayons et visibilité

Pour chaque pixel de l'image résultat, déterminez les coordonnées de l'origine et de l'extrémité du rayon dans le repère de la scène. 
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 et ne conservez que l'intersection valide la plus proche de l'origine du rayon.
doc de GLTFScene


indications : votre programme devrait ressembler à :
main
    charger un fichier gltf / un maillage et ses matières (+ camera + sources de lumière)
    creer une image

    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 :
            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 rouge dans l'image

        enregistrer l'image


étape 1 : récupérer les triangles de la scène

il faut parcourir les noeuds, ce qui donne la transformation pour placer les triangles dans la scène et l'indice du maillage. il ne reste plus qu'à parcourir tous les triangles du maillage, et transformer les positions de ses sommets.

rappel : les sommets des triangles sont indexés. un triangle est un triplet d'indices consécutifs dans le tableau indices d'une primitive (et un maillage est est un ensemble de primitives), cf doc de GLTFScene
const GLTFPrimitives& primitives= ...;
for(unsigned i= 0; i +2 < primitives.indices.size(); i+= 3)
{
    // indice des sommets du triangle
    int a= primitives.indices[i];
    int b= primitives.indices[i+1];
    int c= primitives.indices[i+2];
   
    // position des sommets
    vec3 pa= primitives.positions[a];
    vec3 pb= primitives.positions[b];
    vec3 pc= primitives.positions[c];
    ...
}

indication : vous pouvez utiliser une structure qui ressemble à :
// triangle / gltf
struct Triangle
{
    Point p;            // sommet a du triangle
    Vector e1, e2;      // aretes ab, ac du triangle
    int mesh_id;        // indexation du triangle
    int primitive_id;
    int triangle_id;
   
    Triangle( const vec3& a, const vec3& b, const vec3& c, const int _mesh_id, const int _primitive_id, const int _id ) :
        p(a), e1(Vector(a, b)), e2(Vector(a, c)),
        mesh_id(_mesh_id), primitive_id(_primitive_id), triangle_id(_id) {}
   
    Hit intersect( const Ray &ray, const float htmax ) const;
};

remarque : oui, on duplique tous les triangles instanciés, mais pour le moment, ce n'est pas grave...


étape 2 : récupérer la caméra de la scène

la caméra est définie par les transformations view et projection. les paramètres aspect, fov, znear et zfar permettent de construire la matrice de projection avec Perspective() (cf mat.h).
mais les matrices sont déjà construites, il suffit de les récupérer, cf GLTFCamera

bonus : vous pouvez aussi créer une image en respectant les proportions largeur / hauteur définies par la camera, cf GLTFCamera::aspect


étape 3 : générer les rayons

pour chaque pixel de l'image, il faut calculer la position de l'origine et de l'extremité du rayon...

repassez dans la doc, si nécessaire


étape 4 : calculer les intersections

utilisez Triangle::intersect() pour tester les intersections et ne conserver que l'intersection valide la plus proche de la camera.
// triangle / gltf
struct Triangle
{
    Point p;            // sommet a du triangle
    Vector e1, e2;      // aretes ab, ac du triangle
    int mesh_id;
    int primitive_id;
    int triangle_id;
   
    Triangle( const vec3& a, const vec3& b, const vec3& c, const int _mesh_id, const int _primitive_id, const int _id ) :
        p(a), e1(Vector(a, b)), e2(Vector(a, c)),
        mesh_id(_mesh_id), primitive_id(_primitive_id), triangle_id(_id) {}
   
    /* calcule l'intersection ray/triangle
        cf "fast, minimum storage ray-triangle intersection"
        https://cadxfem.org/inf/Fast%20MinimumStorage%20RayTriangle%20Intersection.pdf

       
        renvoie faux s'il n'y a pas d'intersection valide (une intersection peut exister mais peut ne pas se trouver dans l'intervalle [0 tmax] du rayon.)
        renvoie vrai + les coordonnees barycentriques (u, v) du point d'intersection + sa position le long du rayon (t).
           
        convention barycentrique : p(u, v)= (1 - u - v) * a + u * b + v * c
     */
    Hit intersect( const Ray &ray, const float htmax ) const
    {
        Vector pvec= cross(ray.d, e2);
        float det= dot(e1, pvec);
       
        float inv_det= 1 / det;
        Vector tvec(p, ray.o);
       
        float u= dot(tvec, pvec) * inv_det;
        if(u < 0 || u > 1) return Hit();
       
        Vector qvec= cross(tvec, e1);
        float v= dot(ray.d, qvec) * inv_det;
        if(v < 0 || u + v > 1) return Hit();
       
        float t= dot(e2, qvec) * inv_det;
        if(t < 0 || t > htmax) return Hit();
       
        return Hit(t, u, v, mesh_id, primitive_id, triangle_id);
    }
};

vous aurez aussi besoin de représenter une intersection, par exemple avec la structure Hit :

// intersection / gltf
struct Hit
{
    float t;            // p(t)= o + td, position du point d'intersection sur le rayon
    float u, v;         // p(u, v), position du point d'intersection sur le triangle
    int instance_id;    // indice de l'instance
    int mesh_id;        // indexation globale du triangle dans la scene gltf
    int primitive_id;
    int triangle_id;
    int pad;
   
    // intersection non definie
    Hit( ) : t(FLT_MAX), u(), v(), instance_id(-1), mesh_id(-1), primitive_id(-1), triangle_id(-1) {}

   
    // touche !

    Hit( const float _t, const float _u, const float _v, const int _mesh_id, const int _primitive_id, const int _id ) : t(_t), u(_u), v(_v),
        instance_id(-1), mesh_id(_mesh_id), primitive_id(_primitive_id), triangle_id(_id) {}
   
    operator bool ( ) { return (triangle_id != -1); }   // renvoie vrai si l'intersection est definie / existe
};

étape 5 : construisez l'image

modifiez la couleur de chaque pixel de l'image en fonction de l'intersection trouvée. le plus simple est de construire une couleur avec les coordonnées barycentriques du point d'intersection



pour enregistrer l'image en png, utilisez write_image( image, "fichier.png" ); cf doc


Partie 2 : matières et lumière

si vous avez du mal à organiser votre code, regardez les codes d'exemples :
  • tuto_ray_gltf : charge une scène gltf, récupère tous les triangles et calcule toutes les intersections, c'est la version gltf du code de départ dans "principes du lancer de rayons"

  • tuto_bvh2 : construit un bvh à 2 niveaux, charge un mesh .obj et crée quelques instances "à la main",
  • tuto_bvh2_gltf : idem mais charge une scène gltf,
  • tuto_bvh2_gltf_brdf : idem + utilitaires pour récupérer les normales, les coordonnées de textures, les paramètres des brdfs, etc...


étape 1 : normale interpolée de l'intersection

maintenant que l'on sait quel triangle est visible pour chaque pixel, il faut finir les calculs, mais il faut connaitre la normale au point d'intersection...
on peut tout simplement calculer la normale géométrique du triangle :
Vector n= normalize( cross(triangle.e1, triangle.e2) );
mais on utilise en général la normale interpolée au point d'intersection... comment faire ? il faut récupérer les normales des sommets du triangle, les interpoler et transformer la normale dans le repère de la scène (rappel : l'objet à subit une transformation pour etre placé dans le repère de la scène)

les coordonnées barycentriques pour l'interpolation sont fournies par l'intersection, cf u et v (et w= 1 - u - v). quelles sont les normales des sommets du triangle ?
connaissant l'indice du triangle dans une primitive d'un maillage, il faut récupérer ces informations, cf GLTFScene

quelle est la transformation qui à placé le triangle dans la scène ? celle du noeud (ou de l'instance) cf GLTFNode::model

transformez la normale dans le repère de la scène et utilisez la pour évaluer un modèle diffus / lambert, en utilisant une direction vers la camera comme direction de la source (cf tp précédent)


étape 2 : ou sont les sources ?

maintenant que l'on connait la normale, il reste à savoir ou se trouve la lumière pour calculer la couleur du pixel.

en pre-traitement, on va construire l'ensemble des sources de lumière de la scène. il y a des sources simples (point, direction, etc) qui sont stockées directement dans la scène gltf, mais les objets peuvent également émettre de la lumière... il suffit de parcourir les triangles des objets de la scène, de récupérer la matière associée et de conserver les triangles dont la matière à une emission non nulle. cf la structure GLTFMaterial

indication : les matières sont associées aux primitives dans la scène gltf. un triangle connait les indices de GLTFMesh et de GLTFPrimitives, il ne reste plus qu'à récupérer l'indice de la matière... cf GLTFPrimitive::material_index

utilisez le centre de chaque triangle émettant de la lumière comme source de lumière.


étape 3 : ou sont les ombres ?

que faut-il faire pour savoir que le point d'intersection est éclairé ou à l'ombre d'une source ?

indication : s'il n'y a pas d'intersection entre le point d'intersection et la source, le point est éclairé...


étape 4 : matières, la suite...

quels sont les paramètres de la matière associée au triangle ? utilisez au moins la couleur de base de la matiere pour le modèle diffus.
les détails sont dans la doc / section "c'est quoi le modèle ?" et dans la structure GLTFMaterial


indication : vous pouvez vous inspirer ? ou re-utiliser Brdf du code d'exemple : tuto_bvh2_gltf_brdf.cpp


pour les curieux : et les textures transparentes ?? par exemple les feuilles des arbres ou le sillage des bateaux ?
dans l'intersection rayon triangle, il faut verifier que la texture associee au triangle est opaque avant d'accepter l'intersection comme valide.


Partie 3 : intégration numérique


exercice 1 : éclairage ambiant

reprenez le cours étape par étape...






exercice 2 : et les pénombres ?


c'est toujours pareil, on commence par écrire / formuler ce que l'on veut calculer, ensuite on ecrit l'estimateur E(f/p) et il n'y a plus qu'à trouver la bonne recette de cuisine pour générer
les points / directions, etc. évaluer leur densité et finir le calcul...

recueil de recettes : GI Compendium
pour les curieux, les explications sur les transformations sont dispo dans le chapitre 13 de PBRT, notamment les sections "transforming between distributions" ainsi que "the inversion method".




Annexe : code de départ


si vous avez du mal à démarrer, regardez tutos/tuto_ray_gltf.cpp
mais essayez de construire votre code sans copier l'intégralité du code proposé, vous gagnerez du temps au final...