Brett Ridel

About Me

-
Publications
-
Teaching
-
The Revealing Flashlight

TD1 - Recursive ray tracing

1. Rayons primaires
Le premier objectif est de visualiser une sphère par lancer de rayon. Sauf mention contraire, les illustrations suivantes ont été générées à partir du fichier de scène "troisSpheres.scn".
1.1. Premier rayon
Un rayon (classe Ray) est défini par un point de départ et une direction. Dans un premier temps, on souhaite pouvoir lancer un seul rayon dans la scène au clic de souris, et suivre son trajet en le dessinant en OpenGL, comme illustré ci-dessous.
Rayon
Un clic gauche de la souris en maintenant la touche 'Shift' enfoncée entraîne l'appel de la méthode :
'; highlight_xml($xml_str); ?>
Le vecteur point passé en paramètre contient les coordonnées du point cliqué en espace écran. Implémentez cette méthode afin qu'elle lance un rayon partant du centre de la caméra OpenGL et passant par le pixel cliqué.

La classe Camera possède une méthode convertClickToLine() qui donne le bon résultat. Initialisez l'attribut m_ray de la classe RenderingWidget dans la méthode select, puis affichez-le en OpenGL dans la méthode drawContents à l'aide du shader mFlatProgram et de la fonction Line::draw (définie dans le fichier glPrimitives.h).

Testez votre méthode : créez un rayon en cliquant puis déplacez la caméra pour vérifier qu'il est bien dessiné au bon endroit (presser 'h' pour dessiner la camera dans sa position initiale).

1.2. Intersection rayon - sphère
L'étape suivante consiste à trouver l'intersection (éventuelle) la plus proche entre le rayon et les objets de la scène. Vous utiliserez pour cela la méthode de la classe Shape :
'; highlight_xml($xml_str); ?>
Elle renverra vrai si une intersection avec le rayon a été trouvée. La classe Scene possède la même méthode ; elle se contentera d'appeler successivement Shape::intersect sur tous les objets de la scène.

La classe Sphere semble définir une sphère centrée en zéro. Néanmoins elle possède une matrice de transformation stockée dans sa classe mère Shape. Le centre est donc exprimé dans le repère local de l'objet après application de cette matrice. Lors du lancer de rayon, il vous faut donc prendre en compte cette matrice de transformation. Pour cela, vous avez deux solutions :

  • soit appliquer la transformation à l'objet – en l'occurrence au centre et au rayon de la sphère,
  • soit appliquer la transformation inverse au rayon – c'est-à-dire, à son origine et à sa direction.

Le paramètre hit de la méthode intersect va enregistrer les informations d'intersection nécessaires par la suite. La classe Hit stocke en particulier un "temps" t, correspondant à la distance parcourue depuis l'origine du rayon jusqu'à l'intersection (d'où l'intérêt d'avoir normalisé la direction du rayon). Il est initialisé à une très grande valeur correspondant à une intersection à l'infini. Ce temps doit toujours être positif – sans quoi il s'agirait d'une intersection avec un objet situé derrière l'origine du rayon. Il doit être mis à jour par chaque objet qui intersecte le rayon à un temps inférieur au temps courant.

L'algorithme peut donc se résumer ainsi : tester pour chaque objet s'il intersecte le rayon et, si c'est à un temps inférieur à celui actuellement stocké, modifier le hit en conséquence. La principale difficulté est de gérer correctement les transformations des objets. Comme discuté en cours, je vous conseille de transformer le rayon lancé par la transformée inverse de l'objet. Implémentez maintenant les fonctions Sphere::intersect et Scene::intersect.

Intersection

Visualisez le résultat de votre code en appelant Scene::intersect lorsque vous cliquez sur un pixel tout en maintenant la touche Shift. Si une intersection est trouvée, affichez le point d'intersection (utilisez Point::draw) comme illustré ci-dessus. Vérifiez qu'il est bien au bon endroit, en particulier dans des cas plus complexes (lorsque plusieurs sphères alignées intersectant le rayon, par exemple).

1.3. Première image
Les images seront générées en appelant la méthode RenderingWidget::render. Cette méthode doit parcourir chaque pixel de l'image, créer un rayon passant par le centre de ce pixel et calculer la radiance le long de ce rayon. Commencez par calculez l'origine et la direction de chacun des rayons dans le repère monde.

Comme précédemment, vérifiez vos résultats en affichant les rayons en OpenGL (dans la méthode drawContents). Vérifiez que les rayons passent bien par le centre des pixels, et que celui correspondant au pixel (0,0) est bien en haut à gauche de la grille. Comme dans l'image ci-dessous, définissez une caméra avec une très faible résolution (4x4 pixels, par exemple) en modifiant le fichier de scène ainsi :

'; highlight_xml($xml_str); ?>
Rayons
Pour calculer la couleur d'un pixel, RenderingWidget::render utilisera l'intégrateur de la scène. En particulier, il appellera sa méthode :
'; highlight_xml($xml_str); ?>
Complétez l'implémentation de l'intégrateur Flat qui se contentera d'utiliser la méthode Scene::intersect pour trouver l'intersection la plus proche, et retournera la couleur diffuse de l'objet intersecté – ou la couleur de fond de la scène s'il n'y a pas d'intersection. Pour stocker la radiance dans l'image résultat, utilisez la méthode :
'; highlight_xml($xml_str); ?>
Appuyer sur la touche 'r' pour lancer les rayons et sauvegarder l'image résultat au format OpenEXR. Vous pouvez ensuite visualiser cette image avec la ligne de commande :
$ ./sire_raytracer troisSpheres.exr
L'image est ici agrandie quatre fois pour bien voir les pixels (en désactivant la conversion en sRGB) :
Premiere
2. Éclairage local
Le calcul d'intersection avec des sphères étant validé, vous ajouterez dans cette seconde partie le calcul de l'éclairage local pour des sources lumineuses directionnelles et ponctuelles. La classe abstraite Light est donc dérivée en deux classes DirectionalLight et PointLight dont l'implémentation vous est fournie.

Vous devez supporter le modèle de BRDF de Phong qui comporte une composante diffuse et une composante spéculaire. Pour évaluer ce modèle, vous aurez besoin de la normale au point d'intersection.

2.1. Normales
Lors de l'intersection d'un rayon avec un objet (les sphères, pour l'instant), vous devrez désormais stocker dans le paramètre hit la normale à la surface au point d'intersection exprimé dans le repère monde. Ajoutez le code nécessaire dans la classe Hit ainsi que dans la méthode Sphere::intersect. Pour le tester, dessinez en OpenGL la normale au point d'intersection (lorsque vous lancez un rayon avec Shift+Click).
Normale
Implémentez également un nouvel intégrateur Normals qui affectera à chaque pixel une couleur représentant sa normale (x devenant rouge, y vert et z bleu). Attention, la classe Bitmap attend des couleurs positives alors que les normales prennent leurs valeurs dans\([-1,1]\). Pour tester, changer l'intégrateur du fichier de scène en :
'; highlight_xml($xml_str); ?>
Vous devriez obtenir une image comme celle-ci (en désactivant la conversion en sRGB) :
Normales fausse couleurs
2.2. Calcul de l'éclairage
Implémentez un nouvel intégrateur Direct qui calculera la radiance au point d'intersection en fonction de la normale \(N\) à la surface de l'objet intersecté, de la direction \(L_i\) depuis ce point vers la lumière, du matériau de l'objet \(\rho\) et des différentes lampes \(L_i\) de la scène, selon l'équation : $$R = \sum_{i} \rho \max \left(\langle L_i \cdot N \rangle,0 \right) $$ Attention, le produit scalaire \(\langle L_i \cdot N \rangle\) doit être positif. Dans le cas contraire, le mettre à zéro pour que les surfaces qui ne font pas face à la lampe ne soient pas éclairées. La direction \(L_i\) est constante pour une DirectionalLight et dirigée vers la lampe pour une PointLight. Complétez les méthodes PointLight::direction et PointLight::intensity ainsi que Phong::brdf.
Phong

Testez également avec le fichier de scène "phong.scn". Voud devriez obtenir ce résultat :

9 Sphères
3. Rayons secondaires


3.1. Ombres
Pour générer les ombres, il suffit de vérifier que chaque source lumineuse est bien visible depuis le point d'intersection. Pour cela, lancez un rayon depuis le point d'intersection en direction de la lumière, et testez s'il y a intersection. Pour les lampes ponctuelles, il faut en outre comparer le temps d'intersection avec la distance à la lampe : s'il est inférieur, un objet bloque la lumière. Auquel cas, la contribution de la lampe doit être ignorée.
Ombres

En pratique, vous allez peut-être obtenir des images bruitées en raison d'imprécisions numériques qui peuvent provoquer des intersections entre un rayon partant de la surface de l'objet et l'objet lui-même. Pour les éviter, décalez l'origine du rayon secondaire d'un epsilon (\(10^{-4}\)) dans la direction de la normale à la surface.

3.2. Rebonds multiples
L'un des grands intérêts du lancer de rayons est que la gestion des surfaces réfléchissantes se fait via un simple appel récursif. Il suffit en effet d'ajouter à la radiance du rayon primaire celle obtenue en relançant un rayon depuis le premier point d'intersection dans la direction miroir à celle d'arrivée (par rapport à la normale).

Implémentez un nouvel intégrateur nommé Whitted dans lequel vous appellerez récursivement la méthode Integrator::Li dans la direction réfléchie, et pondérez la radiance obtenue par la réflectivité du matériau. Celle-ci caractérise la fraction de la lumière incidente qui est réfléchie. En théorie, il faudrait calculer le coefficient de Fresnel du matériau qui dépend de son type (isolant ou conducteur) et du point de vue. En pratique, vous utiliserez une valeur constante par objet Material::reflectivity comme approximation.

Phong avec réflection

Vous utiliserez un nombre de rebonds maximum comme critère d'arrêt de la récursion (propriété maxRecursion dans les fichiers de scène). Pour cela, vous stockerez le nombre de rebonds courant dans l'attribut recursionLevel de la classe Ray. Lorsque le critère d'arrêt sera atteint, la radiance retournée devra être nulle. Attention, lorsqu'un rayon primaire n'intersecte aucun objet, vous devez toujours retourner la couleur de fond de la scène. Avec le fichier de scène "phong.scn", vous devriez obtenir l'image ci-dessous en dé-commentant l'intégrateur :

'; highlight_xml($xml_str); ?>
9 Sphères reflection
4. Bonus
Vous pouvez implémenter une de ces extensions. Ne le faites que s'il vous reste du temps et que vous avez terminé toutes les étapes précédentes. Le niveau de difficulté indiqué n'est qu'indicatif.
4.1. Textures (facile)
On souhaite enfin pouvoir texturer les objets de la scène. La classe Material gère déjà le chargement des textures. Elle permet également de définir un textureMode qui indique comment combiner la couleur diffuse d'un matériau avec celle de sa texture. Les deux modes possibles sont MODULATE et REPLACE. Enfin un paramètre textureScale[U/V] permet de régler l'échelle de la texture sur l'objet.

Implémentez la méthode :

'; highlight_xml($xml_str); ?>
qui doit prendre en compte tous ces paramètres et retrourner la couleur voulue. Les coordonnées u et v peuvent prendre des valeurs arbitraires, celle hors de \([0,1]\) doivent donc être ramenées dans cet intervalle. Si aucune texture n'est définie, la méthode retournera la couleur diffuse classique.

Ajoutez dans la classe Hit un Eigen::Vector2f pour stocker les coordonnées de texture du point d'intersection. Mettez ces valeurs à jour dans la méthode Sphere::intersect. Vous utiliserez la longitude et la latitude comme coordonnées u et v, en les ramenant dans l'intervalle \([0,1]\) (la fonction atan2 vous sera utile). Une fois encore, vous pouvez tester en écrivant un nouvel intégrateur Texcoords écrivant les coordonnées de texture sous forme de couleurs :

UV

Enfin, utilisez la nouvelle fonction Material::diffuseColor(uv) à la place de la précédente lors du calcul d'éclairage dans l'intégrateur Whitted. Vous devriez obtenir ce résultat :

Texture

Vous pouvez également tester les scènes "terre-lune.scn", "petanque.scn", "tasSpheres.scn" et "deuxPlans.scn" :

Terre-Lune    Pétanque tas de sphères    Deux plans
4.2. Réfractions (difficile)
La réfraction du rayon selon la loi de Snell-Descartes permet de simuler des objets transparents. En théorie, il faudrait utiliser le coefficient de Fresnel pour caractériser le degré de réfraction du matériau. En pratique, vous utiliserez à nouveau une valeur constante par objet Material::transmissivness comme approximation.

Il faut également définir l'indice de réfraction du matériau \(\eta\), et se souvenir si le rayon est à l'intérieur au non de l'objet, pour appliquer la réfraction inverse à la sortie du matériau. Pour vous simplifier la vie, je vous conseille de stocker dans le matériau les deux indices de réfraction de l'interface \(\eta_A\) et \(\eta_B\) (air / verre, par exemple) :

'; highlight_xml($xml_str); ?>
Quelle limitation ce stockage a-t-il ? Testez avec la scène "deuxSpheres.scn" :
Deux sphères
Les ombres portées opaques semblent étranges pour les objets transparents. Comment résoudre ce problème ?
Deux sphères
Comment obtenir non pas une bille de verre pleine, mais une "bulle" de verre ?
Deux sphères

Remerciements

Ce projet est basé sur celui de Pierre Bénard.