Développement d'un Raytracer

Chapitre II : Que la lumiére soit !

Introduction

La semaine derniére nous avons introduit le raytracing, et nous avons découvert deux primitives de base : la sphére et le plan. Nous avons également implémenté une primitive de sphére dans un moteur de rendu de base qui était en réalité un scan line renderer (cf Chapitre introductif). Le résultat graphique était donc un cercle blanc sur fond noir (en fait une sphére sans éclairage). Pas vraiment épatant.
Dans ce chapitre notre sphére va enfin pouvoir prendre vie, et ne pas se limiter à un cercle blanc. En effet nous allons introduire les materiaux, ainsi qu'un type d'éclairage de base : l'omni light, sans oublier la gestion des ombres ! Bref nous allons faire un grand pas dans le monde du raytytracing.
Mais avant de rentrer dans le vif du sujet, étudions l'algorithme de base d'un raytracer (en fait toujours un scanline renderer, du fait que l'on appelle pas encore récursivement la méthode de Raytrace, mais nous y viendrons la semaine prochaine; patience).

Algorithme de base d'un raytracer

La semaine derniére notre programme se contentait de retourner la couleur BLANC pour le pixel, si le rayon envoyé à travers ce pixel rentrait en collision avec un objet (une sphére dans notre cas), et la couleur NOIR si il n'y avait pas d'interesection. Cet algorithme éxtrémement simple, nous affichait donc la silhouette de l'objet.

L'algorithme qui suit prend en compte de nombreux points :
- Le fait qu'il faille retourner l'intersection avec l'objet le plus proche. En effet un rayon peut toucher plusieurs objets sur son chemin, cependant les objets situés dérriére le premier objet touché, sont cachés par ce dernier (sauf dans le cas de la transparence, mais nous étudierons ce cas plus tard).
- Le calcul de la couleur exacte du pixel si il y à intersection, plutôt que de juste retourner BLANC. Ce calcul prendra en compte les sources de lumiére et le material appliqué à l'objet.
- La gestion des ombres simples.
Cet algorithme n'est néammoins pas complet, il correspond juste aux points que nous allons étudier dans ce chapitre, et chaque semaine nous donnerons une nouvelle version de l'algorithme.

Methode RayTrace(Rayon R) retourne Couleur
   Initialise DISTANCE à une valeur maximale
   Initialise le pointeur OBJET à NULL

   // On recupére l'objet qui rentre le premier en contact avec le rayon ainsi que la position d'intersection
   Pour chaque objet de la scéne faire
       Calculer la distance parcourue par le rayon entre son origine et l'intersection avec l'objet (1.1)
       Si la distance < DISTANCE alors
            Mettre à jour DISTANCE avec la nouvelle distance
            Mettre à jour le pointeur OBJET avec l'objet touché
       fsi
   Objet suivant

   // Si il y a eu intersection (donc au moins un objet touché), on procéde à l'éclairage et au rendu des ombres.
   Si OBJET != NULL alors
      Initialise POSITION avec la position de l'intersection du rayon avec l'objet

      Initialise la couleur finale C à NOIR (couleur de fond)

      Pour chaque lumiére L de la scéne faire
          Pour chaque objet O de la scéne faire
             Si O != OBJET alors
                 Si O n'est pas sur le chemin de la source lumineuse venant de la lumiére jusq'au point d'intersection de l'objet alors (1.2)
                     Calculer la lumiére percue au niveau de l'intersection actuelle [METHODE GetLightAt]
                     Ajouter ce montant à la couleur finale C 
                 fsi
             fsi
          Objet suivant
      Lumiére suivante
   fsi

   Retourne la couleur finale C
Fin Methode RayTrace.
        
      

Schéma 1. Exemple de scéne



Le schéma 1 ci-dessus, va vous permettre de mieux comprendre l'algorithme qui est cependant dèja trés clair.
La caméra est placée en C, notre point light se situe au point L, et nous avons 3 sphéres respectivemment annotées 1, 2 et 3 dans notre scéne.
Concernant le point (1.1) de notre algorithme, prenons par exemple le rayon visuel rouge du milieu. Ce rayon touche la sphére 1 et la sphére 2 sur son chemin. Cependant la sphére 1 n'étant pas transparente, il nous est impossible de voir la sphére 2 avec ce rayon. De plus prendre le point d'intersection du rayon avec la sphére 2 serait inccorect. Il nous faut donc retourner le point d'intersection le plus proche entre la camera et les objets. Ici c'est donc le point d'intersection avec l'objet 1 qui est renvoyé, bien que deux objets soient touchés par le rayon.
Dans le cas des deux autres rayons visuels, les rayons ne touchent que la sphére 2 et la sphére 3 sur leur parcours, le probléme ne se pose donc pas.
Pour visualiser le point (1.2) 
de l'algorithme, on étudie les rayons lumineux bleus émis par la lampe L. Ici on voit bien que le rayon de lumiére du haut, e
entre directement en contact avec l'objet 1, sans être arrêté par aucune autre sphére. De même pour le rayon du milieu. Le rayon du bas lui entre en contact avec l'objet 3, ce dernier est donc éclairé. Cependant quand il faudra éclairer un point situé dans la zone rayée en jaune, par exemple un point de la sphére 2 dans cette zone, l'éclairage sera nul, puisque la sphére 3 bloque le passage du rayon lumineux.
Normalement tout devrait être clair à ce point. Tout du moins je l'éspére :)

Attelons nous maintenant à l'implémentation de cet algorithme dans notre raytracer.

Les materiaux

Avant de parler de lumiére il faut parler de matériaux. En effet sans prendre en compte les matériaux, il est impossible de calculer l'éclairage d'un objet. (quand on croit ne pas utiliser les matériaux pour le calcul de l'éclairage d'un objet, on en utilise néammoins sans le savoir :p).
Un material (nous utiliserons ce terme anglais comme singulier de "matériaux"), est en fait constitué d'une série de valeurs qui vont définir comment la lumiére va réagir avec la surface.
Pour vous donner un exemple concret le marbre, le bois, le plastique, la laine ... sont des types de matériaux. En effet la lumiére sera beaucoup plus réfléchie sur un material de type plastique que la laine. Cela va de soi.
Beaucoup de débutants confondent souvent materiaux et textures. Je me permet donc de préciser à ce niveau, qu'un material n'est PAS une texture, mais ils sont néammoins couplés (ce qui est à l'origine de la confusion). En effet pour reprendre l'exemple du marbre, appliquer le material MARBRE à l'objet fera que la lumiére réagira d'une certaine maniére au contact de l'objet, cependant sans texture l'objet paraitra nu. La texture est appliquée en plus du matérial pour donner un aspect visuel réel à l'objet. De la même maniére une texture seule sans le material MARBRE n'aura que peu d'intérêt, du fait que l'objet ressemblera en effet à du marbre, mais il ne brillera pas comme du marbre, ce qui enléve une part de la réalite. Il ne faut donc pas confondre TEXTURE et MATERIAL, mais bien les distinguer comme deux entités bien distinctes qui sont néammoins complémentaires. Nous étudierons les textures dans un chapitre plus avancé.
Un material comme nous l'avons dit plus haut, posséde des attributs qui vont determiner comment la lumiére va réagir avec la surface de l'objet.
Voici ces principaux attributs :

char* mName; // Le nom du material.
CColor mSpecularColor; // La couleur Specular (rattaché à la brillance de l'objet).
CColor mDiffuseColor; // La couleur Diffuse (éclairage diffus).
CColor mAmbientColor; // La couleur Ambient (éclairage ambient).
CColor mSelfIllumColor; // La couleur de Self Illum (objets éméttant eux même de la lumiére).
float mShininess; // La "brillance" du material (utilisé pour la réfléxion et couplé avec la couleur Specular).
float mShinestrength; // La puissance de brillance (coefficient couplé avec la valeur précédente).
float mTransmittivity; // Le coefficient de  transmission (utilisé pour la réfraction).
float mReflectivity; // Le coefficient de réfléxion (utilisé pour la réfléxion).
bool mPermanent; // Permet de savoir si le material doit rester en memoire ou est temporaire.


Il est bon de noter que nous n'utiliserons pas tout ces attributs dans ce chapitre, cependant autant les implémenter dès maintenant pour la suite.
Dans ce chapitre, notre implémentation utilisera uniquement l'atrribut souligné en italique de la liste, soit la diffuse color. Rappellons au passage que la classe CColor dans notre implémentation, représente les valeurs RGB par des réels compris entre 0 et 1, et que la conversion en valeur RGB comprises entre 0 et 255 se fait au dernier moment. Tout ceci pour dire qu'en fait les quatres couleurs représentées dans le matérial, correspondent en fait à des coefficients multiplicateurs qui seront affectés à la couleur de la lumiére. Par exemple un matérial avec pour valeur de Diffuse color RGB(1, 1, 1) laissera la lumiére émise telle quelle. Si on mettait un matérial à (0, 0, 0) en diffuse color, il ne laissera pas passer du tout de lumiére diffuse.
Vous comprendrez sûrement mieux ce concept dans la partie suivante : Que la lumiére soit !


Que la lumiére soit !

Nous avons désormais tous les éléments nécéssaires sous la main, pour pouvoir étudier l'éclairage. Nous étudierons juste l'éclairage diffus dans ce chapitre (correspondant à l'attribut mDiffuseColor du material). La méthode de calcul pour le specular est différente et nous l'étudierons dans le chapitre suivant.
Commencons donc avec un éclairage de base : l'éclairage Omni. (ou point light).
Ce type d'éclairage est en fait representé par un point dans l'espace, point qui émet autant de lumiére dans toutes les directions autour de lui.
Tout les objets recevront donc de la lumiére d'une point light, sauf dans le cas ou le rayon de lumiére est stoppé par un objet (ce qui donnera donc une ombre). Je viens d'intoduire un nouvel élément : le rayon de lumiére. Un rayon de lumiére sera implémenté exactement de la même maniére que nos rayons visuels dans le raytracer, d'ailleurs ils utiliseront également la classe CRay, ce qui facilitera grandemment les choses au niveau de la gestion des intersections rayon de lumiére/objet, puisque nos méthodes d'intersections sont déja implémentées dans la classe CRay.
Nous allons ici etudier l'algo de calcul de lumiére venant d'une point light. Ce calcul est appellé par la méthode GetLightAt qui est présente dans tous les types d'éclairage du raytracer, et qui est appellé durant le raytracing comme vous pouvez le constater dans l'algo exposé en début de ce même chapitre.
Etudions donc la méthode GetLightAt, qui prend en paramétre la position du point d'intersection courant (le point correspondant à l'intersection du rayon visuel avec l'objet le plus proche), la normale en ce point, ainsi que le material de l'objet intersecté.
Cette méthode retourne la couleur finale, correspondante à l'éclairage de l'omni.

Methode GetLightAt(Vector3D normal, Vector3D intersection, Material matl) retourne Couleur
    Calculer le vecteur LIGHTVECTOR correspondant au vecteur partant de la source de lumiére jusqu'au point d'intersection avec l'objet (2.1)
    Normaliser LIGHTVECTOR 

    Calculer l'angle de frappe ANGLE du rayon lumineux avec la surface en utilisant la normale au point d'intersection (2.2)
    Si ANGLE <= 0 Alors
        COULEURFINALE = Couleur de fond
    Sinon
        COULEURFINALE = mDiffuseColor(mat) * couleur lumiére * ANGLE; (2.3)
    fsi

    Retourne COULEURFINALE

Fin Methode GetLightAt.


L'algorithme est assez clair et simple et ne devrait pas vous poser de problémes d'implémentation.
Le point (2.3) qui est le calcul de l'éclairage nécéssite cependant une petite explication rapide, appuyée par le schéma suivant.

 
Schéma 2. Eclairage.


Sur ce schéma on peut voir une sphére S, éclairée par une source lumineuse (point light) L.
Les rayons rouges correspondent aux rayons lumineux émis, les fléches vertes correspondent au normales au point d'intersection avec le rayon.

L'angle A correspond à l'angle formé par la normale et le rayon lumineux.(dans notre implémentation on utilise le dot product, on obtient donc le cosinus de l'angle et non pas l'angle lui même).
Prenons le deuxiéme rayon en partant du bas. Ce rayon entre en collision avec la sphére de maniére frontale directe. Le point d'intersection recevra donc le maximum de lumiére. Le premier rayon en partant du bas recevra moins de lumiére car l'angle est plus important.
C'est de cette maniére que l'on obtient un degradé de couleur cohérent.

Profitons-en pour effectuer quelques rappels mathématiques sur les vecteurs (qui sont trés utilisés dans un raytracer, du fait que les rayons sont traités comme des vecteurs).

Un peu de maths ne fera pas de mal

Nous allons voir des mathématiques de base sur les vecteurs 3D.

Pour calculer le vecteur AB partant du point A vers le point B, on soustrait tout simplement la position de B à A soit :
    AB = B - A.(Math 1)
C'est le calcul de base des mathématiques vectorielles. Attention néammoins, notez bien que l'on soustrait toujours le point d'arrivée au point de départ.

Pour calculer la longueur d'un vecteur (utile pour la distance d'intersection) on utilise la formule :
   |AB| = sqrt( X^2 + Y^2 + Z^2 ). (Math 2)
sqrt correspond à la racine carré.
Un vecteur unitaire est un vecteur qui à pour longueur 1 : |AB| = 1.

Normaliser un vecteur consiste un rendre un vecteur d'une longueur quelquonque en vecteur unitaire.
Pour cela rien de plus simple, il nous suffit de diviser chaque composante du vecteur (x, y, z), par la longueur du vecteur.
Pour normaliser le vecteur A par exemple :
   A = ( Ax/|A|, Ay/|A|, Az/|A| ).(Math 3)

Pour calculer l'angle formé par deux vecteurs A et B (utile dans le calcul de l'éclairage), on utilise le Dot Product.
La formule du Dot Product (produit scalaire) est la suivante :
   A dot B = |A||B|cos(A,B)= Ax*Bx + Ay*By + Az*Bz. (Math 4)
La plupart du temps les deux vecteurs A et B sont normalisés avant le calcul, ce qui permet d'éliminer la premiére partie de l'équation et de ne garder que : cos(A,B)= Ax*Bx + Ay*By + Az*Bz.

Le cross product (produit vectoriel) permet quand à lui de calculer un vecteur perpidenculaire aux deux autres (en fait perpendiculaire au plan formé par les deux autres vecteurs). Le vecteur résultant peut avoir deux directions différentes. C'est en fait l'ordre de l'opération qui determine la direction du vecteur final (on utilise souvent la technique du pouce pour détérminer la direction du vecteur résultant).
La formule du cross product  est la suivante :
   A*B = ( Ay*Bz-By*Az , Az*Bx-Bz*Ax , Ax*By-Bx*Ay ) (Math 5)

Implémentation

Voyons désormais en pratique l'implémentation de ce que nous avons étudié dans ce chapitre.
Tout d'abord les materiaux. Il va nous falloir créer une classe CMaterial, possédant les attributs cités ci-dessus, ainsi que des opérateurs, permettant d'ajouter deux matériaux (par interpolation), de multiplier des matériaux ...
Pour l'éclairage, le plus pratique est de créer une classe de base CLight qui contiendra juste une position et une couleur, ainsi qu'une méthode virtuele pure GetLightAt qui devra être implementée par les classes derivées. Tous nos types de lampes, comme la point light que nous avons étudié ici, seront dérivés de cette classe de base.
Rien de bien plus compliqué ici, étudions donc maintenant l'implémenation des différents algorithmes étudiés, tout du moins les formules pouvant poser probléme.
Concernant l'algorithme du raytracer, le calcul (1.1) est tout simplement effectué en utilisant la formule mathématique (Math 2) sur le vecteur ayant pour origine l'origine du rayon (position de la camera), et pour destination la position d'intersection (vecteur calculé grâce à (Math 1)).
Pour le calcul suivant
(1.2) on va, comme on l'a dit, creer un rayon de lumiére, qui sera traité comme un rayon visuel normal. Pour teste si le rayon rentre en contact avec un objet on appelle tout simplement la même méthode que celle utilisée pour l'intersection d'un objet avec un rayon visuel.
La rayon lumineux sera initialisé de la maniére suivante :
   - L'origine du rayon O, sera la position de la source lumineuse (ici note point light).
   - Le vecteur de direction sera le vecteur unitaire partant de l'origine, pour aller vers l'intersection avec l'objet traité.


Vient ensuite la méthode du calcul de la lumiére à l'intersection, dont certains points concernant l'implémentation sont exposés ci-dessous.
La partie (2.1) du calcul dans l'algorithme, ou il nous faus calculer le vecteur partant de la source de lumiére jusq'au point d'intersection avec l'objet est trés simplement réalisé en utilisant la formule (Math 1), vue précédemment.
   LIGHTVECTOR = INTERSECTION - POSITION LUMIERE.
La partie suivante(2.2) du calcul consiste à calculer l'angle entre le vecteur normal de la surface au point d'intersection, et le rayon émis de la source lumineuse. Pour ce faire, on va utiliser la formule du Dot Product (Math 4), en ayant pris soin de normaliser les deux vecteurs concernés avant d'effectuer le calcul.

Et voila !
Si vous avez bien implementé tout ce que nous avons vu dans ce chapitre, vous devriez vous retrouver avec non plus un cercle blanc représentant la silhouette de la sphére, mais bien avec une sphére éclairée, qui à la limite peut projeter son ombre sur un plan (si vous avez réussi à implémenter le plan du chapire I).
Vous pouvez bien entendu recupérer le code complet commenté de ce deuxiéme chapitre, qui est en fait une extension du code du premier chapitre. Si vous executez le code vous vous retrouverez avec une sphere eclairée par deux omnilights de couleurs différentes, et qui projete son ombre sur un plan situé sous elle
.


Le rendu final de ce chapitre. Joli non ?

 
Conclusion

Nous avons vu dans ce chapitre comment implementer un type d'éclairage de base dans un raytracer : la point light. Nous avons également découvert les matériaux et leur utilité. Désormais notre sphére prend réellement vie, et dévoile tout ses atouts. Cependant il nous reste encore de nombreux points à implémenter pour obtenir un rendu encore plus satisfaisant, notamment de l'anti aliasing, qui va permettre d'éviter les effets "d'escaliers" rencontrés au niveau des extremités des surfaces, nous verrons également comment utiliser l'attribut mSpecularColor du material ce qui donnera des surfaces avec le "spot" de la lumiére qui se reflete sur la surface. Nous verrons également comment traiter la réfléction entre objets (pour les materiaux specular). Finalement nous redesignerons notre moteur afin qu'il tourne sur une base de plugins, ce qui nous donnera un moteur puissant et facile à entretenir.
Bref que du bon.
A bientôt dans le chapitre III de cette série passionnante :)
Tchao !


Downloader le fichier zip contenant les sources documentées : RayTracerTutChap2.zip.

© Benoît Lemaire alias DaRkWoLf 2002. All Rights Reserved. Unauthorized modification/distribution forbidden.