M2 - Images

TP 1 - Lancer de rayons




Travail à rendre : pour la semaine du 14/12/2009

    La première partie à rendre concerne la réalisation du lancer de rayon minimal, ainsi la prise en compte des sources de lumières et des matières. Vous rendrez vos sources et un makefile ainsi qu'un rapport de quelques pages.

    Le document décrivant votre travail sur cette première partie s'attachera à décrire précisement les repères (et les transformations associées) dans lesquels vous avez choisi de travailler, ainsi que la représentation de matière et les calculs d'éclairement que vous avez choisis.

    Vous apporterez un soin particulier à expliquer votre méthode de génération des rayons (dans quel espace, homogène ou réel, transformations associées, etc.), ainsi qu'aux calculs liés aux modèles d'éclairements.
 
   
    Vous rendrez votre rapport en pdf et une archive .tar.gz de vos sources (ainsi qu'un makefile).
    Le rapport et l'archive porterons le nom des personnes associées au tp. Vous transmettrez le rapport et l'archive en utilisant le mail de l'université en précisant vos noms et numéros d'étudiants.





Ce TP propose de construire un lancer de rayons, élément par élément.

Partie 1 : intersections primitive - rayon

    Une des premières choses à faire est de décrire un objet. Pour un lancer de rayons, un objet est principalement défini par sa position dans l'espace et sa fonction d'intersection (qui permettra de le visualiser).

    Pour avancer rapidement vers un "système" fonctionnel, commencons par un type d'objet simple comme une sphère, un cube, etc.

    Q1: représentez une sphère de centre (0, 0, 0, 1)T et de rayon R.
    Q2. écrivez une fonction d'intersection avec un rayon passé en paramètre capable de renvoyer l'existence de l'intersection ainsi que son abscisse, notée t.

    Un rayon se définit par une origine et une direction. Un point p( t ) le long du rayon se définit par : p( t ) = origine + t . direction
   
    exemple : int sphere_intersecter( const float *origine, const float *direction, float *t )


Partie 2 : placer des objets dans la scène

    Maintenant que nous avons des objets, il faut construire une scène à visualiser. Il suffit de positionner le repère local de chaque objet dans le repère global de la scène.

    Q1: attachez un changement de repère à votre objet afin de le positionner dans la scène.
    Q2: représentez un ensemble de primitives : la "scène", écrivez la fonction d'intersection entre un rayon et la scène :

    exemple : int scene_intersecter( const SCENE *scene, const float *origine, const float *direction, float *t )

    rappel : lorsque plusieurs objets de la scène intersectent le rayon, seule l'intersection la plus proche de l'origine du rayon est renvoyée.

Partie 3 : définir une caméra

    Le dernier élément d'un lancer de rayon est la caméra.

    Q1. Une camera est placée et orientée dans le repère global de la scène, comment représenter cette transformation ?
    Q2. Une camera doit également définir sa projection : le passage du reprère global de la scène, en 3D, à un plan image, en 2D. Comment représenter cette transformation ?
    Q3. Un point 3D placé dans son repère local doit subir plusieurs transformations avant d'être projetté par la caméra, lesquelles ?
    Q4. Quelles les caractéristiques de l'espace projectif de la caméra ? Comment retrouver des coordonnées exploitables ?
    Q5. Il manque, a priori, une dernière étape : passer du repère projectif de la caméra au plan image permettant de visualiser le résultat final. Définissez cette dernière transformation.

    Q6. Combien de matrices sont utilisées pour représenter les transformations nécessaires ?

Partie 4 : générer un rayon

    Nous pouvons presque commencer à calculer l'image. Il ne reste plus qu'à construire un rayon pour chaque pixel de l'image. Le plus simple est de le créer dans le repère projectif de la camera, puis de le passer dans le repère global de la scène.

    Q1: Ecrivez une fonction qui génère un rayon pour le pixel (x, y) dans le repère de la scène.
        exemple : void camera_generer_rayon(const CAMERA *camera, float x, float y, float *origine, float *direction)

    Q2. Modifiez la fonction d'intersection d'un rayon et la scène (cf. Partie 2) afin de transformer le rayon pour  l'exprimer dans le repère local de chaque l'objet, ce qui vous permettra d'utiliser les fonctions d'intersection de la Partie 1.

Partie 5 : ... un peu d'ombre

    Les différents éléments du lancer de rayon sont maintenant disponibles, il ne reste plus qu'à calculer une image. L'information la plus importante pour "comprendre" la structure d'une scène 3D est la présence d'ombres qui permettent de situer les objets les uns par rapport aux autres.

    Il faut donc, en premier lieu, définir une source de lumière, un point placé dans le repère global, pour déterminer si un objet est éclairé ou à l'ombre.

     Q1. ajoutez une source de lumière à la description de votre scène. Pour un lancer de rayon, une source se définit principalement par une position et sa "capacité" à éclairer un point :

       float source_eclairer( const SOURCE *source, const float *point );

     Q2. écrivez la fonction source_eclairer() pour une source ponctuelle définie par une position dans le repère de la scène, cette fonction renvoie 1 si le point est éclairé et 0 sinon. Il suffit de tester la visibilité d'un point de la source et du point à éclairer en lançant un rayon, s'il y a une intersection entre les deux points, ils ne sont pas visibles et le point est à l'ombre.

     Q3. vous allez constater que les résultats sont "bruités", certains pixels sont à l'ombre alors qu'ils devraient être éclairés, et vice versa. Une solution simple consiste à décaler légèrement l'origine et l'extrémité du rayon. L'origine est "poussée" le long de la normale de l'objet et l'extrémité est éloignée du point sur la source (en direction du point). Pour obtenir la normale de la surface sous le point "éclairé", vous devrez modifier les fonctions d'intersection pour renvoyer également cette information :

     int sphere_intersecter( const float *origine, const float *direction, float *normale, float *t )
   int scene_intersecter( const SCENE *scene, const float *origine, const float *direction, float *normale, float *t )

    remarque : utilisez vec3_add_const_mul(point, point, k, direction) de sdlkit qui réalise le cacul nécessaire p= p + k*direction

Partie 6 : un peu de lumière ...

    Les images obtenues jusqu'à présent sont relativement pauvres, les objets ont tous le même aspect et l'influence de la lumière se limite à du noir ou du blanc.

    La physique qui régit le comportement de la lumière permet d'ajouter facilement quelques "subtilités" : selon l'orientation de la surface sur laquelle il est posé un point ne reçoit pas la même quantité d'énergie : elle est maximale lorsque la normale du point éclairé est orientée vers la source de lumière et décroit proportionnellement au cosinus de l'angle (entre la normale et la direction de la source) dans les autres cas.

    La quantité de lumière réçue par un point p est simplement : N . L, pour N, la normale de la surface au point éclairé, et L, la direction de la source de lumière observée par le point. N et L doivent être des vecteurs unitaires afin d'évaluer le cosinus de l'angle entre les deux vecteurs.

    Q1. modifiez la fonction source_eclairer pour calculer l'énergie reçue :

       float source_eclairer( const SOURCE *source, const float *point, const float *normale );

    remarque : utilisez les fonctions vec3_sub( ), vec3_norm( ) et vec3_dot( ) de sdlkit pour faire les calculs.

   Encore plus de lumière

    Comment "gérer" plusieurs sources de lumière ? La encore, la physique fournit une réponse simple : si un point est éclairé par plusieurs sources de lumière, il reçoit la somme des énergies émises par chaque source.

    Q2. ajoutez un tableau de source de lumières à la description de votre scène et écrivez la fonction :

       float scene_eclairer( const SCENE *scene, const float *point, const float *normale );

    qui renvoie la somme des énergies reçues par 'point' orienté selon 'normale'.

    Un problème va apparaître lors de la visualisation des images utilisant plusieurs sources de lumières, que se passe-t-il lorsque l'énergie recue par un point dépasse 1 (ou 255, pour une image RGB classique) ? Les appareils photos et les caméras ont le même problème : certaines zones peuvent être sous exposées ou sur-exposées !
    Le plus simple est de limiter la somme d'énergie reçue à la valeur affichable. Pour faire mieux, il faut utiliser une reproduction de tons qui converti une image en dynamique complète (ou image HDR, avec une énergie associée à chaque pixel, au lieu d'une couleur) en une image faible dynamique affichable sur un écran.

    Des lumières différentes

    Pour l'instant toutes les sources de lumière émettent la même quantité d'énergie, il suffit d'ajouter la quantité d'energie émise e dans la description de la source de lumière :

    Q3. stockez la quantité d'énergie e émise par la source et modifiez les calculs de point_eclairer : remplacer (N . L) par e * (N . L)


Partie 7 : de la couleur

    Votre lancer de rayon doit être maintenant capable de produire des images présentant plusieurs objets éclairés par plusieurs sources de lumières différentes. Il manque quand même, un élément essentiel : la couleur. Une approximation grossière, mais courante, consiste a manipuler 3 bandes d'énergies, une pour le rouge, une autre pour le vert et une dernière pour le bleu.

    Q1. modifiez la description des sources de lumières, au lieu de ne stocker qu'une quantité d'énergie émise, il faut en stocker trois : le rouge, le vert et le bleu.

    Q2. modifiez les fonctions scene_eclairer() et source_eclairer(). Au lieu de renvoyer une énergie, elles doivent maintenant renvoyer 3 énergies.


Partie 8 : des matières variées, diffuses, spéculaires, réfléchissantes, ...

    Pour obtenir des images plus variées, la prochaine étape consiste à simuler des aspects de surface différents selon les objets afin d'obtenir des surfaces diffuses, réfléchissantes ou miroirs. L'aspect d'une surface est représentée par une fonction, notée BRDF(oeil, p, source), qui représente la fraction d'énergie réfléchie entre les directions oeil et source pour le point p (posé sur une surface de normale N). Les directions oeil et source sont des vecteurs unitaires dont l'origine est p.

    Q1. comment représenter les différents comportements ? diffus, réfléchissant, miroir ?
        Comment décrire une matière qui est 30% diffuse et 60% réflechissante ?
        Proposez une structure MATIERE que vous ajouterez à la structure primitive (qui permet de placer un objet dans le repère de la scène).

    Q2. écrivez la fonction :
        float matiere_eclairer( const MATIERE *matiere, const float *oeil, const float *p, const float *N, const float *source );

       Faut-il modifier quelquechose pour tenir compte d'une couleur de matière ?

    Q2. modifiez source_eclairer() et scene_eclairer() afin d'utiliser matiere_eclairer().



Annexes

Calculs avec vec.h et mat44.h

    Les fonctions de manipulation de vecteurs se trouvent dans vec.h et celles travaillant sur les matrices dans mat44.h.

    Opérations sur les vecteurs :

    Il y a plusieurs types de vecteurs : 2D, 3D et 4D. Chaque groupe de fonctions est prefixé par la taille des vecteurs manipulés, vec2 pour les vecteurs 2D, vec3 pour les vecteurs 3D, etc. Les vecteurs sont représentés par des tableaux de float, vous pouvez utiliser les types prédéfinis VEC2, VEC3 et VEC (pour 4D).

    Les fonctions prefixées par vecX_* existent pour les différentes tailles de vecteurs.

    Création :
    Addition :
    Produit :
    Autres :

    Opérations sur les matrices homogènes :

    Seules les matrices homogènes 4x4 sont définies. Elles sont représentées par un tableau de 16 float.

    Création :
    Addition :
    Produit :
    Produit par un vecteur :
    Autres :
    Transformation géométriques (construit une matrice A représentant une transformation) :
    Les explications sur les différentes transformations sont dans le "openGL Programming Guide" / Chapitre 4. Les matrices construites par ces fonctions sont au format openGL, il est possible de les charger directement avec glLoadMatrixf().



Annexe : enregistrer une image HDR


  installez imgtoy dans un répertoire
    cd XXX
    make
    make install

    vous pouvez utiliser ~/local/bin/viewhdr pour visualiser les images hdr et les convertir en tga.

    exemple de création, remplissage et écriture d'une image HDR :

    #include <assert.h>
    #include "img.h"
    #include "hdr.h"

    void img_set_pix( IMG *img, const int x, const int y,
        const float r, const float v, const float b )
    {
        assert(img != NULL);
        assert(x >= 0 && x < img->largeur);
        assert(y >= 0 && y < img->hauteur);
   
        const unsigned int pix= (img->hauteur -1 - y) * img->largeur * img->channels + x * img->channels;
        img->dataf[pix]= r;
        img->dataf[pix +1]= v;
        img->dataf[pix +2]= b;
    }

    ... quelque part dans le programme :

    /* creer l'image resultat */
    IMG *img= new_img_datafloat(largeur, hauteur);

    /* ecrire des pixels dans l'image */
    int y, x;
    for(y= 0; y < hauteur; y++)
       for(x= 0; x < largeur; x++)
          img_set_pixel(img, x, y, 3.0f, 2.0f, 1.0f);

    /* sauver l'image sous image.hdr */
    hdr_write(img, "image.hdr");

    free_img(img);