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, eentre 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.
|