TD1 - Recursive ray tracing
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.
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
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).
Shape :
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 :
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.
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).
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 :
RenderingWidget::render utilisera l'intégrateur de la scène. En particulier, il appellera sa méthode :
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 :
$ ./sire_raytracer troisSpheres.exr
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.
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).
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 :
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.
Testez également avec le fichier de scène
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.
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.
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
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 :
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 :
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 :
Vous pouvez également tester les scènes
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) :
Remerciements