samedi 11 février 2012

RayTracing temps réel... en Flash ! Partie 3


La gestion des intersections entre les rayons et les volumes étant réglée, il faut maintenant passer à l'étape suivante : l'introduction de la lumière . C'est à dire la gestion des ombres, de la quantité de lumière reçu par chaque "point" de la surface de la sphère, ...
En théorie une étape très consommatrice de ressources (à cause des nombreux calculs) et qui aurait pu mettre un point final à mon objectif de temps réel…

Cet Article fait partie d'une série. Il est fortement recommandé d'avoir lu les articles précédents pour y comprendre quelque chose…

Cette nouvelle étape est intimement liée à l'introduction d'une nouvelle entité : les sources lumineuses. En effet sans source de lumière, point d'image dans un RayTracer (dont les rayons sont finalement lancés uniquement à la recherche de ces sources lumineuses). On distingue en général 3 principaux types de sources :
- les sources directionnelles (sans position particulière mais éclairant dans une direction précise, un peu comme la lumière naturelle)
- les sources ponctuelles (positionnées à un point précis de l'espace 3D, celles-ci éclairent dans toutes les directions, comme le soleil au centre du système solaire)
- la lumière ambiante (venant de partout)

On peut aussi parler de projecteur (une position ET une direction), de lumières émissives (émise par l'objet lui-même), … mais dans un premier temps on va s'arrêter là. Je vais même me limiter aux lumières directionnelles. Je crée donc une classe dédiée.

Je vais donc m'attaquer dans cet article à deux problèmes : l'illumination directe et les ombres.

L'illumination directe
Je vais commencer par le plus simple en me focalisant sur l'éclairage provoqué directement par chaque source lumineuse sur chaque point de chaque sphère. Après avoir déterminé si un point est sur une sphère, je calcule donc la "quantité" de lumière qu'il reçoit de chaque source (dans mes exemples je me contente d'une seule source lumineuse directionnelle). En gros, si il est du "bon" coté de la sphère, il est éclairé, si il est du "mauvais" coté il reçoit peu/pas de lumière. Evidemment le résultat n'est pas binaire mais une valeur numérique (qui permet des dégradés) qui dépend de l'angle entre le rayon lancé (voir étape précédente) et les rayons de la lumière directionnelle. Pour calculer l'angle entre deux vecteurs (puisqu'un rayon peut être assimilé à un vecteur) on utilise le "produit scalaire", qui dit :

Soit deux vecteurs A et B, on peut écrire : ABcos(angle)= AxBx + AyBy + AzBz

"A" et B" désigne la longueur de ces vecteurs. On essaie en général de travailler sur des vecteurs dits "unitaires", c'est-à-dire avec une longueur de 1 (ça ne change rien à leur direction). On s'arrange donc pour "normaliser" les vecteurs au préalable, c'est à dire à les "transformer" en vecteur unitaire. Pour cela il suffit de les diviser par leur longueur. Pour rappel, la longueur d'un vecteur A se calcul ainsi :

longueur de A = racine carrée de(x 2 + Y 2 + Z 2 ).

La longueur du vecteur normal est égale au rayon de la sphère, ce qui évite des calculs…
Revenons à notre produit scalaire (appelé aussi dot product), entre deux vecteurs unitaires désormais. Cette formule donne un cosinus et non directement un angle, mais cela fera parfaitement l'affaire aussi. En fait on va calculer le cosinus entre la normale à la sphère au point d'impact du rayon lancé et le vecteur de la lumière. La normale à une sphère (idem pour les autres types de volume) est un vecteur orthogonal (=perpendiculaire) à l'objet en un point donné (perpendiculaire à une surface en fait). Pour une sphère le vecteur normal en un point passe par ce point et par le centre de la sphère, il est donc facile à calculer :

vecteur normal =(x-xc,y-yc,z-zc)

On a déterminé la valeur de z à l'étape précédente. On utilise donc le produit scalaire entre le vecteur normal au point et le vecteur de la lumière :

cos(angle)=vecteur normal x vecteur lumière

Le résultat obtenu est compris entre -1 et 1 (comme tout cosinus qui se respecte…). Personnellement j'utilise alors DIRECTEMENT cette valeur comme représentative de la quantité de lumière reçu par le point en la multipliant par chaque composante (rouge, vert, bleu) de la couleur de base de la sphère. Je limite quand même cette illumination pour ne pas avoir des parties complètement noires sur ma sphère (plus tard j'introduirai la notion de lumière ambiante pour justifier ça). En théorie je suis bien sagement les calculs "classiques" d'un Raytracer et pourtant j'obtiens des résultats fantaisistes, par exemple :


Ou encore :


Il faut imaginer les sphères en mouvement et leur surface se modifiant en même temps, offrant un rendu proche d'un effet plasma. C'est très psychédélique mais absolument pas ce que je veux… Je revois donc mes calculs… en testant le résultat du produit scalaire, qui me renvoi des valeurs non conformes à un cosinus… je remonte encore le fil de mes calculs (avec les différents plantages ça m'a pris un certain temps…) pour me rendre compte que tout vient d'un mauvais calcul du Z du point (à l'étape précédente). Une fois ce calcul corrigé, tout s'arrange et les choses commencent à ressembler à ce que j'attends :


Pour être sûr de mon rendu, je couple la direction de la lumière avec la position de la souris. L'animation en question pour vous amuser à votre tour (survolez avec la souris et bougez pour simuler la source lumineuse) :


Tellement content de moi, je ne cherche même pas à optimiser les calculs, je passe directement au problème suivant…

Les ombres
Le principe est là encore assez simple : pour chaque point d'une sphère (déterminé via l'article précédent) on relance un rayon en/selon la direction de chaque source lumineuse. Si ce rayon "percute" un objet (autre que celui du point en question) alors le point sera "dans l'ombre" de cet objet vis-à-vis de la source testée. Il faut donc tester à la fois chaque source et chaque volume ce qui promet de belles boucles de calculs…

On a donc une boucle sur chaque volume à l'intérieur d'une boucle sur chaque lumière :

Pour chaque lumière
         Pour chaque volume
                   Si le rayon passe par ce volume, le point est dans l'ombre

Plutôt que de détailler des calculs (qui ont déjà été en partie) je vais m'attarder sur un dilemme. En effet un même point peut être dans l'ombre de plusieurs objets (par rapport à la même source lumineuse ou pas); dans ce cas, faut-il "cumuler" ces ombres ou pas. Dans le premier cas, cela rajoute un certain "réalisme" qui ne me paraît pas si naturel que ça… Dans le second cas, on est à l'ombre ou pas, point. Et en plus ça permet de supprimer des calculs à l'occasion (dès qu'un point est déterminé à l'ombre d'un volume, on arrête de tester les autres volumes). J'opte pour la seconde solution, mais je pourrai facilement changer par la suite…

Je met temporairement en commentaire le code de calcul de l'illumination. L'animation en question pour vous amuser à votre tour (survolez avec la souris et bougez pour simuler la source lumineuse) :


Le résultat est correct (par rapport à toutes les simplifications et libertés que j'ai prises…), le FrameRate aussi. Je peux donc cumuler mes deux calculs (ombres portées + illumination directe). A vous de jouer :



Bon, on va penser transparence et réflexion maintenant…