Introduction
Bienvenue dans ce premier chapitre d'une série d'articles sur
le raytracing. Si vous n'avez pas encore lu le chapitre introductif, je
vous conseille de le faire maintenant, du fait qu'il défini précisement
nos objectifs.
Dans ce premier chapitre nous allons découvrir les bases du raytracing,
notamment ce qu'est un rayon, mais nous allons également rentrer
dans le détail du fonctionnement d'un raytracer trés simple
(en fait un scanline renderer, le raytracing apparaitra avec les rayons
réfléchis dans le chapitre II). Nous étudierons également
deux primitives simples, pouvant être calculées par un raytracer
: la sphere et le plan.
Mais tout de suite, sans plus attendre, commencons par le commencement
...
Qu'est ce qu'un rayon ?
Un raytracer
est censé faire du "lancer de rayons". Mais qu'est ce
qu'un rayon ? Un rayon de vélo ? Un rayon mathématique considéré
comme étant la motiée du diamétre ? Le rayon fruit
et légumes du supermarché du coin de la rue ?
Bref il est clair qu'avant d'entrer en détail dans le raytracing,
nous devons expliquer ce qu'est un rayon dans un raytracer.
Vous pouvez tout simplement vous representer un rayon comme un rayon laser.
Un rayon laser à une origine (l'origine d'emission du rayon), il
à également une direction, et finalement quand il rentre
en contact avec une surface il est arretté (éventuellement
il est réfléchi et/ou refracté).
De même un rayon dans un raytracer posséde trois caractéristiques
:
- Une position O de départ (la source d'émission du rayon,
représentée par un vecteur).
- Un vecteur DIR de direction (vecteur unitaire représentant la
direction du rayon).
- Une variable 't' représentant en quelque sorte la distance parcourue
par le rayon.
Toutes ces caractéristiques se retrouvent dans l'équation
d'un rayon, la seule inconnue étant t.
ray
= O + DIR * t
L'équation d'un rayon une fois résolue donne donc une position
dans l'espace, correspondante la plupart du temps au point d'intersection
du rayon avec un objet.
C'est bien beau tout ca, mais j'en fais quoi de mon rayon ?
Ehe, c'est bien la qu'intervient tout l'art du raytracing.
Expliquons donc rapidement à quoi vont être utiles les rayons,
à travers l'algorithme le plus simple d'un raytracer (ou plutôt
dans ce cas précis, d'un scanline renderer (cf. l'article introductif)).
Imaginez votre scéne, avec tout vos objets, ici une sphére,
ici un cube et la un cône. Vous avez également une caméra
dans votre scéne, afin de la visualiser d'un certain point de vue,
avec une certaine focale.
Imaginez maintenant votre rendu final, disons qu'il est calculé
en 640X480. Il contient donc 640X480 pixels. Or dans un raytracer, comme
nous l'avons dit dans l'article introductif, nous allons calculer l'image
finale pixel par pixel, de gauche à droite et de haut en bas dans
notre cas. Au debut nous aurons donc une image toute vide, puis les couleurs
des pixels seront progressivemment calculées.
Cependant notre scéne est en 3D, et notre rendu final sur l'ecran
doit être en 2D (oui toujours pas d'écrans 3D c bien dommage
:p). Il faut donc representer la surface finale 2D, par une surface 3D
dans la scéne.
Revenons maintenant à nos rayons. Pour chaque pixel un rayon va
etre lancé dans la scéne. L'origine de ce rayon sera toujours
la position de la caméra (l'oeil), et la direction du rayon va
être calculée en imaginant que l'on trace un rayon depuis
l'origine, vers la scéne, en passant par le pixel courant, qui
se situe virtuellement devant la camera sur un plan correspondant à
l'image.
Bref c'est un peu compliqué comme ca à premiére vue,
mais en fait c'est tout à fait trivial. Pour le premier pixel qui
se situe en haut à gauche de l'image finale, nous allons tracer
un rayon depuis la position de la camera, en passant par la position calculée
correspondant au pixel en haut à gauche de l'image finale.
La caméra et le viewplane
En fait ce rectangle imaginaire situé devant la caméra et
correspondant à l'image finale, est appellé le viewplane.
Il est representé dans la classe CCamera, par trois attributs :
une largeur (viewplaneWidth), une hauteur (viewplaneHeight) et une distance
(viewplaneDist ... la distance correspondant à la distance entre
le viewplane et la caméra).
Ces valeurs dépdendent du fov (field of vision), de la caméra,
cependant nous allons assummer pour le moment que la largeur est de 0.35,
la hauteur de 0.5 et la distance de 1.0.
La position du coin haut gauche de ce viewplane dans l'espace, nous sera
trés utile, il est donc bon de la calculer lors de la création
de la caméra. Pour ce faire, on utilise la formule suivante :
viewPlaneUpLeft = camPos + ((vecDir*viewplaneDist)+(upVec*(viewplaneHeight/2.0f)))
- (rightVec*(viewplaneWidth/2.0f))
Cette formule est assez simple à comprendre, ici vecDir correspond
au vecteur directeur unitaire de la camera, upVec et rightVec sont respectivement
les vecteurs unitaires haut et droite de la caméra. Refechissez
bien à cette formule avant de continuer la lecture, elle est vraiment
triviale (mettez la clairemment sur un morceau de papier si vous avez
du mal à la lire online).
Grâce à cette position on à maintenant toutes les
informations nécéssaire pour calculer la position d'un pixel
de l'image finale sur le viewplane. Un pixel de l'image finale sera donc
situé dans l'espace, sur le viewplane à la position donnée
par la formule suivante :
viewPlaneUpLeft + rightVec*xIndent*x - upVec*yIndent*y
Où xIndent et YIndent sont respectivement calculés de la
facon qui suit :
xIndent = viewplaneWidth / (float)xRes;
yIndent = viewplaneHeight / (float)yRes;
xRes et yRes correspondant à la résolution finale de l'image
(par exemple 640x480).
x et y correspondent quand à eux, à la position 2D du pixel
sur l'image finale, dont on recherche la position dans l'espace sur le
viewplane.
On appelle donc ce calcul pour chaque pixel de l'image, qui nous retourne
le vecteur directeur non normalisé du rayon.
Il ne nous reste plus qu'à normaliser ce vecteur, pour obtenir
un vecteur directeur unitaire, et bingo !
Nous avons désormais l'origine du rayon (la position de la caméra),
le vecteur directeur de ce rayon pour chaque pixel, nous avons donc toutes
les informations nécéssaires pour commencer le raytracing
! Let the fun start !
Une primitive de base : la sphére
La sphére constitue une des primitives de base d'un raytracer,
c'est souvent la primitive la plus simple à calculer avec le plan,
et que l'on voit toujours dans des screenshots de raytracers, du fait
qu'elle est rapide et facile à calculer et qu'il n'y à rien
de plus ésthétique qu'une sphére :)
Nous cherchons donc, comme pour toutes les futures primitives du raytracer,
à lancer un rayon et à obtenir la valeur de la variable
t du rayon. Une fois la variable t calculée, nous la substituons
dans l'équation du rayon, et nous obtenons une position précise
d'intersection avec la primitive.
Vous connaissez sûrement tous l'équation d'une sphére
:
(X-Xc)^2 + (Y-Yc)^2 + (Z-Zc)^2 = r^2
Avec :
- X, Y, Z : un point quelquonque sur la spéere (l'inconnue).
- Xc, Yc, Zc : la position du centre de la sphére.
- r : le rayon de la sphere.
Maintenant remplacons tout simplement les inconnues de notre formule,
qui correspondent à un point sur la sphere, par l'équation
de notre rayon (qui correspond aussi à un point dans l'espace).
En gros il suffit donc de substituer O.x + DIR.x * t à X, O.y +
DIR.y * t à Y et finalement O.z + DIR.z * t à Z.
Bref en remplacant tout ca et aprés simplification, on arrive à
une équation à une inconnue du second degré de la
forme a*t^2 + b*t + c.
Et oui; connaissant l'origine du rayon émis et sa direction, il
ne reste plus qu'en effet comme seule inconnue, la variable t, comme prévu.
Ce qui est fort pratique, car maintenant en résolvant cette équation,
nous aurons cette inconnue t, que nous reinjecterons dans l'équation
du rayon, pour obtenir la position d'intersection exacte. Pratique n'est
ce pas ?
Ce principe est le même pour toutes les primitives de base (plans,
cones, torus ...) : vous récupérez l'équation de
la primitive et vous remplacez l'inconnue de position par l'équation
du rayon. Bateau non ?
Bref aprés simplification et identification vous arrivez finalement
à :
a = DIR.x^2 + DIR.y^2 + DIR.z^2
b = 2 * (DIR.x * (O.x - Xc) + DIR.y
* (O.y - Yc) + DIR.z * (O.z - Zc))
c = ((O.x - Xc)^2 + (O.y - Yc)^2 + (O.z - Zc)^2) - r^2
Bon maintenant que nous avons a, b et c, nous pouvons résoudre
l'équation.
Il faut donc trouver le déterminant (det) de l'équation,
donné par la formule b^2 - 4*a*c.
Trois cas se presentent alors :
det < 0 : pas de solution dans le domaine reel.
det = 0 : une et une unique intersection.
det > 0 : deux intersections.
Bref, des mathématiques de base, la résolution d'une équation
du second degré.
Donc dans le cas det > 0 (le plus courant), vous pouvez récuperer
les deux solutions suivant la formule :
t1 = (-b + sqrt(det)) / (2*a);
t2 = (-b - sqrt(det)) / (2*a);
Il nous faut alors récuperer l'intersection la plus proche. En
effet on ne s'intéresse qu'au premier point de la sphere touché,
le suivant étant caché.
Nous prenons donc tout simplement la plus petite valeur entre t1 et t2.
Dans le cas ou det = 0 nous n'avons pas ce probléme, étant
donné qu'il n'existe qu'une et unique solution.
Une fois que nous avons la variable t finale, nous la reinjectons dans
l'équation du rayon : O + DIR * t.
On connait l'origine du rayon, on connait sa direction et on connait desormais
t. On peut donc calculer tout simplement la position exacte de l'intersection.
Je dis à nouveau Bingo !
On reitére cette opération pour tous les pixels de l'image
finale, en lancant un rayon primaire par pixel comme expliqué auparavant.
Une petite optimisation est possible pour la sphére. En effet si
on à a = 1/4, dans le calcul du détérminant, le a
de l'équation disparait, ainsi que dans le calcul des résultats,
ce qui évite quelques calculs de trop, qui côutent souvent
cher en temps cpu (prenons de bonnes maniéres dès le départ
:p).
Bref en modifiant légérement l'équation, on arrive
à faire marcher cette astuce, qui est implémentée
dans le code (CSphere.cpp).
Une autre primitive : le plan
Une autre primitive de base, également trés simple à
calculer est un plan. Plus précisement un plan infini séparant
l'espace en deux parties.
L'équation d'un plan est :
Ax + By + Cz + D = 0;
(A, B, C) représentant la normale du plan.
Ici encore, x, y et z représentent un point du plan, ce sont nos
inconnues. Remplacons donc par l'équation du rayon, comme nous
l'avons fait avec la sphére.
Aprés simplification nous obtenons l'équation suivante :
t = - ( (A*X + B*Y + C*Z + D) / (A*DIR.x + B*DIR.y +
C*DIR.z) )
où (X Y Z) = (O.x-pointplaneX O.y-pointplaneY O.z-pointplane.Z).
pointplane est un point du plan que l'on connait (pour construire le plan
on lui passe un point sur le plan et la normale du plan).
Implémentation
Comme promis dans l'article introductif, chaque article
sera accompagné d'un code source documenté doxygen. Le code
source du chapitre II correspondra au code source du chapitre I amelioré,
le code source du chapitre III celui du chapitre II et ainsi de suite.
Bref notre code, et notre raytracer, s'ettayera au fil du temps.
Je vous conseille réellement de vous interesser au code joint.
En effet dans ce premier article nous n'avons pas réellement vu
de concepts évolués, cependant la plupart des classes de
base sont implementées dans le code joint, et il serait bon que
vous les assimiliez pour le chapitre suivant qui reprendra donc ce squelette
de code.
Essayez donc de bien comprendre ce code, et pourquoi pas d'essayer d'implementer
le support de la primitive du plan, seule la primitive de sphére
étant présente dans le code actuel (la primitive de plan
sera néammoins présente dans le code du chapitre II pour
correction :p).
Conclusion
Je tiens à profiter de cette conclusion pour m'excuser pour le
manque d'illustrations dans ce premier article, mais promis juré
craché, des schémas explicatifs, qui aident souvent énormémement
à la compréhension, seront présent dans les prochains
articles.
La semaine prochaine nous ferons un grand pas. Les images rendues vont
devenir de plus en plus belles rapidement. En effet nous travaillerons
sur la lumiére, et nos primitives 3D apparraitront alors dans toute
leur splendeur. Nous étudierons également les materiaux
ainsi que la réfléction des rayons. Bref beaucoup de choses
trés intéréssantes au programme !
Bonne chance à tous ! A trés bientôt !
Downloder le fichier zip contenant les sources documentées : RayTracerTutChap1.zip.
©
Benoît Lemaire alias DaRkWoLf 2002. All Rights Reserved. Unauthorized
modification/distribution forbidden.
|