Lancer de rayons - Les 12 travaux
Avant propos - Le code
Plusieurs classes sont fournies afin que vous n'ayez à vous concentrer que sur les parties graphiques du TP. La plupart des classes auront à être complétées, d'autres ne seront utilisées complètement qu'à la fin du TP (Material
). Parcourez le code rapidement pour en
comprendre la structure:
- Viewer: le visualiseur/débuggeur OpenGL. Possère un
RayTracer
et connait laScene
. - RayTracer: chargé de lancer les rayons pour créer l'image.
- Ray: un rayon dans la scène.
- Hit: regroupe toutes les informations sur l'intersection d'un rayon avec un objet.
- Color: une triplet RGB avec toutes les opérations utiles.
- Material: la définition complète du matériau d'un objet.
- Scene: regroupe tous les objets qui composent la scène.
- Camera: la caméra de la scène d'où on va créer l'image.
- Object: classe abstraite dont dérivent les classes d'objets de la scène.
- Sphere: dérive de
Object
et représente une sphère. - Light: classe abstraite dérivée en 3 types de lampes.
Partie I - Les sphères
Dans un premier temps, notre scène 3D ne sera composée que de sphères. Celles-ci sont définies par leur position, leur rayon et leur couleur et définies dans la classeSphere
.
Cette classe dérive de la classe abstraite
Object
qui regroupe les méthodes communes à
tous les types d'objets de la scène. En particulier chaque objet possède un repère
(Frame
) qui définit sa position et son orientation dans l'espace ainsi qu'un matériau
(Material
) représentant un matériau complexe, dont on utilisera pour le moment que la
diffuseColor()
.
Les fichiers
sphere.{h|cpp}
contiennent une implémentation de la classe
Sphere
. Complétez la méthode draw()
de cette classe qui dessinera la
sphère en OpenGL.
Modifiez la matrice de
GL_MODELVIEW
via un glMultMatrixd(frame().matrix())
pour vous placer dans le repère local de l'objet. Pensez à sauvegarder/restaurer la matrice
avant/après votre affichage (glPushMatrix()
) pour revenir au repère du monde. Changez
ensuite la couleur en utilisant un glColor3fv(material().diffuseColor())
. Dessiner
enfin la sphère en utilisant la méthode gluSphere()
dont vous chercherez la
documentation.
Testez ces méthodes en instanciant des sphères dans le viewer, et en remplaçant le dessin de la spirale par des appels aux méthodes
draw()
des sphères définies. Il faut temporairement
ajouter un #include "sphere.h"
pour cela.
Partie II - Gestion de la scène
La scène 3D sera représentée par une classeScene
qui contiendra principalement une
liste d'Object
, sur laquelle elle appliquera des méthodes génériques définies dans
Object
. Ces méthodes seront déclarées virtuelles pures dans Object
et
auront donc à être ré-implémentées et spécifiées par les différentes classes d'objets.
Le viewer aura lui un pointeur vers cette
Scene
. Cette séparation des données permet par
exemple d'avoir deux viewers de la même Scene
, ou d'appeler les méthodes de la
Scene
via un autre programme, qui pourra par exemple ne pas faire d'affichage et être
paramétré via une ligne de commande (batch processing).
Lancez la commande
assistant
pour afficher l'aide de Qt. Dans l'index, regarder comment
définir et utiliser un QPtrList<Object>
(QList
avec Qt4) pour
définir une liste d'objets. Cette liste sera déclarée privée et seule la Scene
pourra
la parcourir.
Définissez ensuite la méthode
addObject(Oject* o)
qui permet d'ajouter un objet à cette
liste. Faites une méthode draw()
qui parcourt le tableau et appelle la méthode
draw
de chaque objet de la scène.
Complétez les deux méthodes
radius
et center
permettant d'avoir une
estimation du centre et du rayon de la scène. Pas la peine d'être exact, ça ne sert qu'au viewer
pour afficher l'intégralité de la scène chargée.
Utiliser pour cela la méthode
boundingRadius()
de Object
, qui donne une
estimation du rayon d'un objet. La méthode position()
de Frame
vous permet
d'avoir la position de l'objet. Les méthodes de la classe Vec
(norme, somme,
division...) vous seront alors pratiques pour calculer ces valeurs.
Partie III - Chargement de la scène 3D
Le format XML
Les scènes seront décrites par des fichiers au format XML. Ce type de fichiers permet de représenter une très grande variété de données dans un formalisme identique et rigoureux. Il permet également de facilement extraire et transformer cette information (voir XSLT). Un autre avantage de cette structure est qu'on peut ignorer certaines parties du fichier, le reste restant syntaxiquement valide. C'est ce que nous ferons dans ce TP, où nous n'utiliserons au début qu'une sous-partie des informations des fichiers de scène.La syntaxe des fichiers se comprend aisément en lisant un exemple minimaliste:
<?xml version="1.0" encoding="UTF-8"?> <Scene> <Sphere radius="0.4"> <Material> <DiffuseColor red="0.1" green="0.1" blue="0.9" /> </Material> <Frame> <position x="0.1" y="0.2" z="0.0" /> <orientation q0="0.0" q1="0.0" q2="0.0" q3="1.0" /> </Frame> </Sphere> </Scene>La méthode
loadScene()
du viewer ouvre une fenêtre pour demander un nom de fichier
(remarquez que Qt permet de le faire en une ligne de code). Elle délèguera ensuite le chargement du
fichier à la Scene
via une méthode loadFromFile(const QString& filename)
que vous implémenterez dans Scene
.
Dans notre structure de fichier, les différents objets de la scène sont définis les uns à la suite des autres, comme fils du noeud racine
Scene
. À terme la scène pourra contenir des
objets de différent type (lumières, cylindre, plans, ...) et leur tagName()
permettra
de savoir quel type d'objet créer à partir du fichier. Nous nous restreignons pour le moment aux
sphères.
Chargement d'une sphère
Il existe deux méthodes principales pour lire un fichier XML: DOM et SAX. La première transfère l'intégralité des informations dans une structure arborescente qu'il suffit de parcourir ensuite, tandis que la seconde fait appel à des méthodescallback
lors du parcours du fichier.
La première est très simple tandis que la seconde est rapide et pratique pour les fichiers
volumineux.
Nous allons utiliser l'interface DOM fournie par Qt pour lire le fichier de scéne. Regardez dans
assistant
la documentation de QDomDocument
. Regardez en particulier le
code donné en exemple qui affiche les noms des noeuds fils du noeud racine. Copier ce code pour
parcourir les différents fils du noeud racine dans loadFromFile()
. Affichez les noms
des tagName()
rencontrés pour vérifier.
Lorsque le
tagName()
est Sphere
, on créera une Sphere
, on
l'initialisera via la méthode initFromDomElement()
de la classe Sphere
, et
on la placera dans la liste des objets de la scène. La plupart des classes possèdent une méthode
initFromDomElement()
qui permet de les initialiser à partir d'un
QDomElement
.
C'est en particulier le cas pour les
Object
. Regarder le code de
Object::initFromDomElement()
(et celui d'autres classes) pour mieux en comprendre le
fonctionnement. Puisqu'une Sphere
est un Object
, on peut décider que le
code XML représentant une Sphere
est celui d'un Object
auquel on a
ajouté des informations en XML. C'est tout à fait cohérent avec la notion d'héritage. Ceci
permet à un Objet
de s'initialiser correctement à partir du QDomElement
correspondant à une Sphere
, en n'utilisant que les champs (tagName()
)
reconnus.
La première chose à faire dans
Sphere::initFromDomElement()
est donc un simple appel à
Objet::initFromDomElement()
, qui initialise le repère et le matériau. Il reste ensuite
à initialiser le rayon de la sphère. Cette valeur est indiquée comme attribut du noeud
Sphere
passé en paramètre. Les fonctions hasAttribute()
et
attribute
de la classe QDomElement
(cf assistant
) permettent
de récupérer ces valeurs. Le résultat est une chaine de caractères QString
qui peut
être convertie à l'aide de la méthode statique toFloat()
de QString
.
Pour la suite, pensez à rendre votre code de lecture XML robuste en gérant les problèmes de fichiers ne respectant pas la syntaxe (ce qui arrivera lorsque vous les modifierez à la main). Détectez les attributs et fils manquants, en arrêtant avec un message d'erreur précis ou en donnant des valeurs par défaut. La méthode
Scene::loadFromFile()
sera complétée sur le même principe au fur
et à mesure que la définition de notre scène se complexifiera.
Le viewer est maintenant capable d'afficher la scène via la méthode
draw()
de
Scene
. Testez votre code sur le fichier troisSpheres.scn
, puis sur des
variantes de ce fichier pour vérifier que tous les paramètres sont correctement initialisés.
Partie IV - Premier rayon
Nous allons lancer nos premiers rayons dans la scène. Le principe est simple (voir les diapos de cours du MIT) : pour chaque pixel, lancer un rayon depuis l'oeil vers la scène. Chercher quel objet est le premier intersecté par ce rayon et donner au pixel la couleur correspondante.Un rayon est défini par un point de départ et une direction, que l'on normera. La classe
Ray
représente un tel rayon (à l'aide de deux Vec
). Ajoutez-lui une
méthode draw()
permettant de l'afficher en OpenGL (un simple GL_LINES
).
Pour tester notre algorithme de lancer de rayon, on souhaite pouvoir lancer facilement un rayon dans la scène et suivre son trajet. Lorsqu'on clique avec le bouton gauche de la souris en maintenant la touche
Shift
enfoncée, le QGLViewer
appelle alors la méthode
select(const QPoint& point)
qui permet normalement de sélectionner un objet de la
scène (voir la documentation de select()
). Nous allons surcharger cette méthode afin
qu'elle lance un rayon partant de la position courante de la caméra et passant par le pixel choisi.
La
camera()
associée à tout QGLViewer
possède justement une méthode
convertClickToLine()
qui donne le bon résultat. Ajoutez un membre Ray
à
votre viewer, initialisez-le dans select
et affichez-le dans draw()
.
Testez votre méthode: crééz un rayon puis déplacez la caméra pour vérifier qu'il est bien dessiné au
bon endroit. Désactivez le GL_LIGHTING
pour cet affichage et les suivants où la normale
n'est pas définie (regardez ce qui se passe sinon).
L'affichage de la position de la caméra se fait en sauvant la position
(Alt+F1 à F12)
d'où
l'on lance le rayon, puis en activant l'affichage des positions de caméras avec C
(voir
l'onglet Keyboard
de l'aide, obtenue par H
).
Partie V - Intersection rayon-scène
L'étape suivante consiste à trouver l'intersection (éventuelle) la plus proche du rayon avec les objets de la scène. On va pour cela utiliser une méthodebool intersect(const Ray& ray, Hit&
hit) const
dans la classe Object
. Elle renvera vrai ssi une intersection a été
trouvée avec le rayon. La classe Scene
possèdera la même méthode qui se contentera
d'appeler successivement cette méthode sur tous les objets de la scène.
Le paramètre
hit
de la méthode précédente va contenir toutes les informations
d'intersection nécessaires pour la suite. La classe Hit
stocke en particulier un
"temps", correspondant à la distance de l'intersection à l'origine du rayon (d'où l'intérêt d'avoir
normalisé la direction du rayon). Ce temps sera toujours positif (sinon cela représente une
intersection avec un objet situé derrière l'origine du rayon) et il sera mis à jour par chaque objet
qui intersecte le rayon à un temps inférieur au temps courant. Il est initialisé à une très grande
valeur correspondant à une intersection à l'infini.
En plus du temps,
Hit
stocke la position dans le repère du monde du point
d'intersection correspondant au temps courant. Les informations stockées dans la classe
Hit
n'ont évidemment de signification que si intersect()
renvoie vrai.
Le
Material
associé à un Hit
est le matériau de l'objet qui a provoqué
cette intersection. Chaque objet teste donc s'il intersecte le rayon, et si c'est à un temps
inférieur à celui actuellement stocké dans le Hit
, le hit
est modifié en
conséquence.
Visualisez le résultat de votre code en appelant
intersect
lorsque vous cliquez sur un
pixel avec Shift
(i.e. dans la méthode select
). Si une intersection est
trouvée, affichez le point d'intersection (utilisez GL_POINTS
et
glPointSize()
). Vérifiez qu'il est bien toujours au bon endroit, en particulier dans
les cas complexes (plusieurs sphères alignées intersectant le rayon). Il se peut que le point ne
s'affiche pas à cause de problèmes de précision numérique : il est sur la sphère, ce qui
n'est pas vraiment défini le Z buffer risque de le considérer comme dedans et donc caché. Réglez ce
problème, par exemple en décalant très légèrement le point ou en désactivant le test de profondeur.
Partie VI - Définition d'une caméra
Nous allons maintenant ajouter une caméra à notre scène. Cette caméra sera fournie dans le ficher.scn
de définition de scène, comme un objet 3D. Voici un exemple de XML définissant une
telle caméra:
<Camera fieldOfView="0.7854" xResolution="400" yResolution="400"> <Frame> <position x="1.0" y="-1.0" z="0.5" /> <orientation q0="0.0" q1="0.0" q2="0.0" q3="1.0" /> </Frame> </Camera>
Chargement
La classeCamera
définit un tel objet et fournit une méthode
initFromDOMElement()
. Modifiez le loadFromFile
de la scène pour prendre en
compte les objets de type Camera
.
Attention, cette
Camera
n'a pas de rapport avec la caméra qui affiche la scène dans le
QGLViewer
, et qui permet de visualiser et de vérifier le bon déroulement de
l'algorithme. Néanmoins la méthode initFromScene()
, appelée au chargement de la scène,
fait correspondre la première position clef de la caméra QGLViewer avec celle définie dans la scène.
Appuyez sur F1
pour faire correspondre les deux caméras, ce qui vous permet d'avoir une
représentation OpenGL de la scène qui va être rendue (faire CTRL+S
pour sauver une
telle image, pour comparer).
Affichage de la caméra
Ecrivez la méthodedraw(float radius) const
de la classe Camera
et
appelez-la dans le draw
du viewer. Cette méthode devra afficher le frustrum
(pyramide) correspondant à la caméra ainsi qu'une grille représentant les pixels. Le paramètre
radius
permet d'adapter la taille de l'affichage à celui de la scène (utilisez un
glScalef()
).
Le
fieldOfView()
de la caméra est donné en radians et correspond à l'ouverture
verticale totale (comme dans gluPerspective()
). L'ouverture horizontale
est déduite du raport xResolution/yResolution
pour obtenir des pixels carrés.
Le plus simple pour dessiner est de se placer dans le repère de la caméra (avec un simple
glMultMatrixd(frame().matrix())
). L'axe des Z négatifs est alors le long de la
direction de vue (convention OpenGL), les axes X et Y étant horizontaux et verticaux.
Notez l'ajout d'un plan semi-transparent pour mieux visualiser le plan de l'écran.
Affichage des rayons
L'étape suivante consiste à créer des rayons passant par le centre de chaque pixel. Les coordonnées de ces rayons doivent être exprimées dans le repère du monde, où se font tous les calculs. Dans la classeCamera
, la méthode Ray getRay(float x, float y) const
doit créér
un rayon passant par le pixel (x,y). (0,0) correspond au pixel supérieur gauche de l'image,
(xResolution()-1, yResolution()-1) à celui en bas à droite. Vous verrez l'intérêt des
float
pour les coordonnées par la suite.
La classe
Frame
possède toutes les méthodes permettant de convertir une position ou une
direction exprimée dans le repère du Frame
vers le repère du monde. La méthode
position()
renvoie en particulier la position de l'origine du repère dans le repère
monde. Comme d'habitude, vérifiez vos résultats en affichant l'ensemble des rayons. Vérifiez en
particulier que les rayons passent bien par les centres des pixels, et que celui
correspondant à (0,0) est bien en haut à gauche.
Partie VII - Première image
Ajouter un attributbackgroundColor
de type Color
à la classe
Scene
. Chargez-le depuis le fichier de scène s'il y est présent. Il représentera la
couleur de fond de la scène et donc des images.
Les images seront générées par un objet de la classe
RayTracer
. C'est le viewer qui
possède cet objet, et il est initialisé pour avoir un pointeur sur la scène (voir le
init()
de viewer.cpp
).
La méthode
renderImage()
du RayTracer
parcourt tous les pixels de l'image,
calcule leur couleur et la stocke avec un setPixel()
(voir la documentation de
QImage
). Notez que cette méthode peut directement prendre une Color
en
paramètre grâce à l'opérateur QRgb
de cette classe.
Pour calculer la couleur du pixel,
renderImage
utilise la méthode
rayColor
. Celle-ci se contente pour le moment d'utiliser la méthode
intersect
de la Scene
, en renvoyant la diffuseColor
de
l'objet intersecté (ou la couleur de fond de la scène s'il n'y a pas d'intersection).
Appuyer sur
S
pour lancer les rayons et sauvegarder l'image du résultat.
Shift+S
fait de même, mais depuis le point de vue courant de la caméra QGLViewer au
lieu de celle de la scène. C'est pratique pour chercher un point de vue intéressant et faire une
image depuis ce point de vue.
L'image est ici agrandie quatre fois pour voir les pixels. Vous pourrez par la suite ajouter un
QProgressDialog
à renderImage
pour voir la progression des calculs longs.
Partie VIII - Éclairage
Normales
Lors de l'intersection d'un rayon avec un objet (les sphères dans notre cas), on va désormais également stocker dans leHit
la normale à la surface au point d'intersection. Ajoutez
ce code (dans Hit
ainsi que dans la méthode intersect
). Pour le tester,
vous pouvez afficher une normale au point d'intersection (lorsque vous lancez un rayon avec
Shift+Click
).
Vous pouvez également affecter à chaque pixel une couleur représentant sa normale (x devient rouge, y vert et z bleu). Attention, la classe
Color
attend des valeurs comprises entre 0 et
1.
Lampes
Il faut ensuite définir dans notre scène une ou plusieurs lampes qui l'éclaireront. On considère trois types de sources : ambiantes, directionnelles et ponctuelles. On négligera l'atténuation de leur intensité avec la distance à la source. La classe abstraiteLight
est dérivée en
trois classes : AmbientLight
, DirectionalLight
et PointLight
représentant ces trois types de sources.
Ajoutez un vecteur de pointeurs sur
Light
dans la scène et initialisez-le d'après le
fichier de scène. Selon le tagName()
rencontré, il faudra créer un objet de la classe
adéquate. Vous pouvez utiliser la méthode draw()
de Light
pour afficher
(sans fioritures) les lampes de la scène pour vérifier votre chargement.
Calculs d'éclairage
UneAmbientLight
représente la couleur cambient d'une lampe virtuelle qui
éclaire toute la scène de façon uniforme. Cette lampe, également présente en OpenGL, n'a pas de
signification physique et sert à "déboucher" les zones sombres.
La couleur d'un objet est alors le produit de sa couleur diffuse multipliée par la couleur ambiante:
cpixel = cambiente * cdiffuse objet
L'éclairage diffus a quand à lui une intensité qui dépend du produit scalaire entre la normale à la surface et la direction de la lumière :
cpixel = SOMME source i [ (Li . N) * csource i * cdiffuse objet ]
N est la normale au point intersecté sous le pixel, et Li la direction depuis ce point vers la lumière. Le produit scalaire Li . N doit être positif. Le mettre à 0.0 sinon pour que les surfaces qui ne font pas face à la lampe ne soient pas éclairées. La direction Li est constante pour une
DirectionalLight
et dirigée vers la lampe pour une PointLight
.
Ecrire une méthode
Color illuminatedColor(Hit&) const
dans Scene
. Cette
méthode prend en entrée la position, la normale et le matériau d'un point, regroupés dans le
Hit
. Elle renvoie la nouvelle couleur de ce point, éclairé par les différentes lampes
de la scène. Cette méthode se contente de sommer les résultats fournis par chacune des lampes, via
une méthode virtuelle du même nom que vous coderez dans Light
et ses classes dérivées.
Ajoutez l'éclairage de Phong à votre calcul d'éclairage. Il prendra en compte la
specularColor()
et le specularCoefficient()
du matériau. Il va falloir
donner un paramètre supplémentaire à la méthode illuminatedColor()
de
Light
.
Partie VIII - Les ombres
Pour générer des ombres, il suffit de vérifier que chaque source lumineuse est bien visible depuis le point d'intersection. Une source ambiante est toujours visible. Sinon, lancer un rayon depuis le point d'intersection en direction de la lumière, et voir s'il y a intersection. Pour lesPointLight
, il faut de plus comparer le temps d'intersection avec la distance à la
lampe: s'il est inférieur, un objet bloque la lumière. Si c'est le cas, la contribution de la lampe
doit être ignorée.
Ajoutez à la classe
Light
une méthode virtuelle bool visibleFrom(const
qglviewer::Vec& pos, const Scene* const scene) const
et réimplémentez-la dans chaque classe
dérivée. Il faut passer la scène en paramètre pour pouvoir lui faire des requêtes d'intersection.
Vous allez probablement obtenir des images bruitées. En effet, les imprécisions numériques font qu'il peut exister une intersection entre un rayon partant de la surface d'un objet et l'objet lui-même. Pour les corriger, contraignez le temps d'un
Hit
à être légèrement supérieur
à 0.0 grâce à un epsilon.
Partie IX - Plusieurs rebonds
Un des grands intérêt du lancer de rayons est que la gestion des surfaces réflechissantes se fait via un simple appel récursif. Il suffit d'ajouter à la couleur d'un point celle d'un rayon lancé depuis ce point dans la direction mirroir à celle d'arrivée (par rapport à la normale).Il suffit donc de rappeler
RayTracer::rayColor()
pour obtenir le résultat. La couleur
du rayon mirroir est pondérée par la reflectiveColor()
du matériau. Après un certain
nombre de rebonds ou lorsque le rayon n'intersecte plus d'objets, il a une contribution nulle
(couleur 0,0,0).
En revanche, lorsqu'un rayon partant de l'oeil n'intersecte aucun objet, on souhaite lui donner la
backgroundColor
définie dans la scène. Pour différencier ces deux cas, on
ajoute un paramètre booléen fromEye
à la méthode rayColor()
du
RayTracer
.
Pour vérifier votre algorithme, ajouter au
RayTracer
un QValueVector
(QVector
avec Qt4) de Segment
, qui va représenter un chemin lumineux. Un
Segment
est une classe privée du RayTracer
qui contient les deux
extrémités d'un segment ainsi que la normale au point d'arrivée.
La méthode
rayColor()
concatène dans le chemin le segment correspondant au
Ray
qu'elle est en train de traiter. Une méthode drawRayPath
affiche le
chemin lumineux ainsi stocké.
Conseils : un constructeur de
Segment
pourra prendre un Ray
et un
Hit
en paramètres. Définissez une méthode draw()
dans
Segment
. N'oubliez pas de vider le chemin lumineux au départ du rayon. C'est
rayColor
et non plus intersect
qu'il faut appeler dans le
select()
du viewer.
Partie XI - Textures
Textures dans le Matériau
On souhaite désormais texturer nos objets. La classeMaterial
gère les textures, et
peut charger la plupart des formats de fichiers grâce à Qt. Une texture est définie par un nom de
fichier, au chemin défini par rapport à l'endroit d'où vous lancez l'éxecutable.
Material
permet également de définir un textureMode()
qui indique comment
combiner la diffuseColor()
d'un matériau avec celle de sa texture. Les trois modes
possibles sont MODULATE
, BLEND
et REPLACE
(voir le code et la
documentation de glTexEnv()
pour les détails). Enfin un paramètre
textureScale[U/V]
permet de régler l'échelle de la texture sur l'objet.
La méthode
diffuseColor(float u, float v)
de Material
prend en compte tous
ces paramètres et donne la couleur voulue. Les coordonnées u
et v
sont
quelconques, les valeurs hors de [0,1] étant ramenées dans cet intervalle. Si aucune texture n'est
définie, elle renvoie la diffuseColor
classique.
Coordonnées de textures
Ajoutez dans la classeHit
deux float u,v
pour stocker les coordonnées de
texture du point d'intersection (ainsi que les méthodes associées).
Mettez ces valeurs à jour dans la méthode
intersect()
de Sphere
. On
utilisera la longitude et la latitude comme coordonnées u et v, en les ramenant dans l'intervalle
[0,1]. La fonction atan2
vous sera probablement utile ici. Vous pouvez régler la
couleur d'un pixel en fonction des u,v du point d'intersection pour vérifier votre calcul (cf image).
Enfin, utilisez la nouvelle fonction
diffuseColor(float u, float v)
à la place de la
précédente lors du calcul d'éclairage dans les classes Light
pour prendre en compte la
couleur de la texture. J'ai choisi pour réduire le code d'ajouter à la classe Hit
une
méthode diffuseColor()
qui renvoie la diffuseColor()
du matériau, aux
coordonnées u
et v
stockées dans le Hit
.
Partie XII - Anti-aliassage
Un zoom sur les discontinuités de l'image obtenue dévoile les problèmes d'aliassage de notre méthode. Pour les réparer, il suffit de lancer plusieurs rayons pour chaque pixel et de moyenner leurs couleurs.Modifier la méthode
renderImage
de RayTracer
pour qu'elle lance plusieurs
rayons, par exemple à travers une grille régulière nxn, placée dans chaque pixel. Les images
suivantes (grossies 3 fois) montrent les résultats pour n=1, 2, 3 et 4.