Développement d'un Raytracer

Chapitre I : Les bases du raytracing

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.