TD1 - Recursive ray tracing

1. Rayons primaires

Le premier objectif de ce TD est de visualiser des sphères et des plans par la technique de lancer de rayons.

1.1. Génération des rayons

Les images seront générées en appelant la fonction render du fichier "main.cpp". Cette fonction devra parcourir tous les pixels de l'image, créer un rayon passant par le centre de chaque pixel et calculer la couleur le long de ce rayon.

Commencez à compléter l'implémentation de cette fonction. Plus précisément, effectuez le parcours de la grille de pixel (la classe Camera donne accès aux dimension de l'image) et, à l'aide de la position de la caméra et des vecteurs camX, camY et camF fournis, calculez pour chaque pixel l'origine (position) et la direction (vecteur unitaire) du rayon correspondant.

L'étape suivante consiste à trouver l'intersection (éventuelle) la plus proche entre le rayon et les objets de la scène. Implémentez la méthode Scene::intersect qui appelera pour chaque objet de la scene sa méthode :

bool Shape::intersect(const Ray &ray, Hit &hit) const
Celle-ci retournera true si une intersection entre l'objet et le rayon a été trouvée.

Le paramètre hit (référence C++) va servir à 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.

génération des rayons

1.2. Sphères

Une sphère de centre $\mathbf{c}$ et de rayon $r$ peut être représentée comme l'ensemble des points $\mathbf{p}$ tels que : $$||\mathbf{p}-\mathbf{c}||^2 -r^2 = 0$$

Substituez $\mathbf{p}$ dans l'équation implicite précédente par l'équation d'un rayon $\mathbf{r}(t) = \mathbf{o} + t \mathbf{d}$ avec $\mathbf{o}$ son origine et $\mathbf{d}$ sa direction (normalisée). Après développement de l'équation, vous obtenez une équation de second degré en $t$. Évaluer sont discriminant numériquement. S'il est négatif, il n'y a pas d'intersection. Sinon, gardez la racine postive la plus petite ; c'est l'intersection la plus proche de l'origine du rayon.

sphere

Implémentez ce calcul dans la méthode intersect de la classe Sphere définissant une sphère de centre m_center et de rayon m_rayon. Mettez à jour l'objet hit (vous utiliserez Hit::setT et Hit::setShape).

1.3. Première image

L'algorithme de lancer de rayon peut se résumer ainsi : tester pour chaque objet s'il intersecte le rayon et, si c'est à un temps inférieur à celui actuellement stocké, modifiez l'objet hit en conséquence. Implémentez cet algorithme dans Scene::intersect.

Pour calculer la couleur d'un pixel, la fonction render utilisera l'intégrateur de la scène. En particulier, pour chacun des rayons, il appellera la méthode :

Color3f Integrator::Li(const Scene *scene, const Ray &ray) const
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 ambiante de l'objet intersecté – ou la couleur de fond de la scène s'il n'y a pas d'intersection.

Dans la fonction render, pour stocker la couleur retournée dans l'image résultat, utilisez la méthode :

void ImageBlock::put(const Vector2f &_pos, const Color3f &value)

Appuyer sur la touche 'r' pour lancer les rayons et sauvegarder l'image résultat au format OpenEXR. Celle-ci s'affichera directement dans la fenêtre. Si vous fermez cette fenêtre, vous pouvez à nouveau visualiser l'image avec la ligne de commande :

$ ./ms3d_raytracer troisSpheres.exr
Vous devriez obtenir l'image suivante avec le fichier de scène "troisSpheres.scn" :

Flat

1.4. Plans

Un plan infini peut être décrit par un point $\mathbf{a}$ à sa surface et sa normale $\mathbf{n}$. Un point $\mathbf{p}$ appartient alors au plan si $(\mathbf{p} - \mathbf{a})\cdot \mathbf{n} = 0$, c'est-à-dire si leur produit scalaire est nul. Comme pour la sphère, substituez $\mathbf{p}$ par l'équation du rayon et résolvez l'équation linéaire en $t$. Quatre cas sont alors à gérer :

  • $t$ infini : le rayon est parallèle et distinct du plan
  • $t$ non-défini : le rayon est confondu avec le plan
  • $t < 0$ : l'intersection est derrière la caméra
  • $t > 0$ : intersection est devant la caméra
plan

Complétez la méthode Plane::intersect pour calculer l'intersection avec un plan. Testez votre implémentation avec le fichier de scène "petanque.scn" ; vous devriez obtenir l'image ci-dessous.

Petanque flat

2. Éclairage local

Le calcul d'intersection avec des sphères et des plans étant validé, vous ajouterez dans cette seconde partie du TD le calcul de l'éclairage local pour des sources lumineuses directionnelles (DirectionalLight) et ponctuelles (PointLight). Votre lanceur de rayon devra 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 (sphère ou plan), 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 les méthodes Sphere::intersect et Plane::intersect.

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 dans l'intervalle $[0,1]$ alors que les normales prennent leurs valeurs dans $[-1,1]$. Faites en sorte que l'intégrateur retourne la valeur absolue de la normale au point intersecté.

Pour tester, changer l'intégrateur du fichier de scène en :

<integrator type="normals"/>

Vous devriez obtenir une image comme celle-ci (en utilisant la valeur absolue et en désactivant la conversion en sRGB) :

Normales fausses couleurs
Normales en valeur absolue

2.2. Calcul de l'éclairage

Implémentez un nouvel intégrateur Direct qui calculera la couleur au point d'intersection en fonction de la normale $\vx{n}$ à la surface de l'objet intersecté, du matériau $\rho$ de cet objet et, pour toutes les lampes de la scène (LightList), de leur direction $\vx{l}_i$ et leur intensité $I_i$, selon l'équation :

$$R = \sum_{i} \rho \max \left(\cos(\theta),0 \right)I_i = \sum_{i} \rho \max \left(\langle \vx{l}_i \cdot \vx{n} \rangle,0 \right)I_i $$

Notez bien que si le produit scalaire $\langle \vx{l}_i \cdot \vx{n} \rangle$ est négatif, il est mis à zéro (d'où le $\max$) de telle sorte que les surfaces qui ne font pas faces à la lampe ne soient pas éclairées. La classe Material définie la méthode brdf à utiliser pour évaluer $\rho$.

Vous implémenterez la BRDF de Phong dans la classe du même nom. Dans le modèle de Phong, la luminance réfléchie en un point par une source lumineuse se décompose en une partie diffuse $\rho_d$ et une partie spéculaire $\rho_s$. Si $\vx{v}$ est la direction de vue, alors :

$$\rho_d = m_d$$

avec $m_d$ la couleur diffuse du matériau,

$$\rho_s = m_s (\cos \alpha)^s = m_s (\max(\vx{r} \cdot \vx{v}, 0))^s$$

avec $m_s$ la couleur spéculaire de l'objet, $\vx{r}$ le vecteur $\vx{l}$ réfléchi par rapport à la normale $\vx{n}$ et $s$ la brillance de l'objet.

La BRDF de Phong finale sera alors $\rho = \rho_d + \rho_s$.

eclairage

Les sources lumineuses dérivent toutes de la classe abstraite Light. Deux méthodes sont à implémentez dans les classes filles : direction (correspondant à $\vx{l}_i$) et intensity (correspondant à $I_i$). $\vx{l}_i$ et $I_i$ sont constantes pour une DirectionalLight tandis que $\vx{l}_i$ est dirigée vers la lampe et $I_i$ décroit de façon quadratique avec la distance pour une PointLight. Implémentez ces méthode dans la classe PointLight.

Pour tester, changer l'intégrateur du fichier de scène en :

<integrator type="direct"/>
Vous devriez obtenir ce résultat :

Phong

Testez également avec le fichier de scène "phong.scn" :

Phong

3. Rayons secondaires

3.1. Ombres portées

Pour ajouter les ombres portées à votre lanceur de rayon, il suffit de tester si chaque source lumineuse est 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. Modifiez l'intégrateur Direct pour y ajouter les ombres portées.

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 couleur 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 couleur 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.

Reflection

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 couleur retournée sera nulle. Attention, lorsqu'un rayon primaire n'intersecte aucun objet, vous devrez toujours retourner la couleur de fond de la scène.

Pour les fichiers de scène "phong.scn" et "petanque.scn", vous devriez obtenir les images ci-dessous avec l'intégrateur :

<integrator type="whitted">
   <integer name="maxRecursion" value="4"/>
</integrator> 

Phong   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 :

Color3f Material::diffuseColor(const Eigen::Vector2f& uv)
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 Vector2f pour stocker les coordonnées de texture du point d'intersection. Mettez cette valeur à jour dans la méthode intersect des plans et des sphères. Pour ces dernières, 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 :

Phong

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) :

<material type="phong">
   <color name="transmissivness" value="1.0, 1.0, 1.0"/>
   <float name="etaA" value="1"/>
   <float name="etaB" value="1.33"/>
</material>

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