I. Introduction▲
Cet article fait suite à l'affichage d'une sphère. Nous allons repartir de ces sources pour ajouter des sources de lumières, des ombres, et de l'anti-aliasing.
Vous pouvez trouver les sources de cet article en cliquant sur ce lien.
II. Lumières : Révision du calcul de luminosité▲
II-1. La lumière ambiante▲
Nous allons maintenant introduire les sources de lumières. Il y a diverses sources possibles : projecteurs, boules de feu, lumière éloignée (soleil)... On peut aussi souhaiter que la lumière décroisse lorsque l'on s'éloigne de la source.
Nous allons, de façon tout à fait arbitraire, introduire les sources lumineuses suivantes :
- Une lumière ambiante, qui sera appliquée à tous les objets. On fait le choix que la partie réfléchie reçue sera multipliée par le coefficient kitxmlcodeinlinelatexdvp\frac{1}{ad^2 + bd + c}finkitxmlcodeinlinelatexdvp où les coefficients a, b et c pourront être librement choisis, et d est la distance de l'observateur à l'objet. Cela signifie que plus l'observateur sera loin, moins la luminosité sera intense. Une lumière ambiante n'a pas de position précise dans l'espace, donc pas de coordonnées.
- Les lumières directionnelles. Tous les rayons sont parallèles, et « proviennent de l'infini ». C'est une bonne approximation des rayons du soleil. On dira qu'une telle lumière ne s'atténue pas. Une lumière directionnelle n'a pas non plus de position dans l'espace.
- Les spots, ou projecteurs. Ce sont des sources de lumières directionnelles en forme de cône. Plus on est dans l'axe, plus la zone est éclairée. L'angle du cône de lumière sera nécessairement aigu (afin de rendre l'implémentation plus simple). On disposera de trois coefficients a, b et c pour multiplier le résultat par kitxmlcodeinlinelatexdvp\frac{1}{ae^2 + be + c}finkitxmlcodeinlinelatexdvp où e est la distance du projecteur à l'objet.
- Les sources omnidirectionnelles. Ce sont des sortes de boules de lumière qui éclairent dans toutes les directions. À nouveau, ces lumières disposeront d'une atténuation fonction de leur distance.
Vous remarquerez que pour toutes les lumières disposant de trois coefficients (a, b, c) pour contrôler l'atténuation, le choix (0, 0, 1) nous donne une lumière non atténuée.
Tout comme pour nos objets, une lumière sera constituée de ses caractéristiques, et d'une fonction de calcul de luminosité. Mais puisque la seule caractéristique commune à nos lumières est leur couleur, et que l'on n'en a besoin que pour calculer la luminosité, on n'a pas besoin de la stocker.
module
Lights (
Light (Light),
Attenuation,
computeAttenuation,
buildAmbiantLight,
buildDirectionalLight,
buildOmnidirectionalLight,
buildSpotLight,
) where
import
Tools
import
Objects
data
Light =
Light (Intersection ->
Scene ->
Color)
-- Les facteurs a, b et c de l'aténuation, qui est un polynome en d
type
AConstant =
RealRep
type
ALinear =
RealRep
type
ASquare =
RealRep
-- Le triplet (a, b, c) utilisé pour calculer 1/(ad^2 + b*d + c)
type
Attenuation =
(ASquare, ALinear, AConstant)
computeAttenuation ::
Attenuation ->
Distance ->
RealRep
computeAttenuation (a, b, c) d =
1
/
(a *
d ^
2
+
b *
d +
c)
buildAmbiantLight =
undefined
buildDirectionalLight =
undefined
buildOmnidirectionalLight =
undefined
buildSpotLight =
undefined
Toutes les fonctions exportées serviront à construire les différents types de lumières listés quelques paragraphes plus tôt. Commençons par le plus simple : la lumière ambiante.
Il nous faut pouvoir multiplier deux à deux les coordonnées d'un vecteur. On rajoute donc une fonction utilitaire.
vectorProduct ::
Vector ->
Vector ->
Vector
vectorProduct (a, b, c) (x, y, z) =
(a *
x, b *
y, c *
z)
On calcule maintenant la lumière ambiante réfléchie au niveau de l'intersection.
buildAmbiantLight ::
Attenuation ->
Color ->
Light
buildAmbiantLight attenuation lightColor =
Light lightFunction
where
lightFunction intersection scene =
(computeAttenuation attenuation distance) *!
light
where
light =
lightColor `vectorProduct` objectColor
Material objectColor =
material
(_
, distance, Object _
material _
) =
intersection
Afin de pouvoir tester petit à petit nos lumières, et les voir naître sous nos yeux, on va maintenant mettre en place l'application des lumières aux objets de la scène avant de poursuivre.
II-2. Calcul de la lumière▲
Nous nous retrouvons confrontés à un souci de dépendances cycliques. On voudrait le type objet dans Object.hs, le type Light dans Light.hs, et le type Scene dans Raytracer.hs. Seulement Scene dépend de Light et Object. Comme ghc ne supporte pas les dépendances cycliques, nous avons deux solutions. L'une utilise un système de « bootstraping », l'autre consiste à placer tous les types dans un Type.hs. Pour des raisons de simplicité et maintenabilité nous choisissons la seconde.
module
Types (
Red,
Green,
Blue,
Color,
Camera (Camera),
Scene (Scene),
Intersection,
Object (Object),
Light (Light),
Material (Material),
) where
import
Tools
--Représentation de l'observateur
data
Camera =
Camera Location Distance PlanSize
--Une scène
data
Scene =
Scene Camera [Object]
--Une intersection
type
Intersection =
(Location, Distance, Object)
--On object raytracable
data
Object =
Object Location Material (Ray ->
Scene ->
[Distance])
--Une lumière
data
Light =
Light (Intersection ->
Scene ->
Color)
--Couleur, 0.0 signifie "pas de couleur" et 1.0 "toute la couleur".
type
Red =
RealRep
type
Green =
RealRep
type
Blue =
RealRep
type
Color =
(Red, Green, Blue)
-- Une matière
data
Material =
Material Color
Nous ajoutons maintenant une liste de lumières :
data
Scene =
Scene Camera [Object] [Light]
On met à jour les diverses apparitions du constructeur Scene, en ajoutant un troisième argument (_ s'il s'agit de décomposer une scène, ou [] s‘il s'agit de construire une scène). Dans computeItersection, on doit récupérer les lumières pour construire scene.
computeIntersection ray object scene =
-- ...
-- ...
Scene camera objects lights =
scene
-- ...
scene'
=
Scene camera'
objects lights
Il nous faut maintenant revoir computeColor pour utiliser cette liste de lumières. On commence par ajouter une fonction pour sommer les couleurs provenant d'une liste de couleurs. N'oublions pas de l'exporter. Comme on se doute que l'on va devoir sommer beaucoup de valeurs, on ne veut pas construire une liste de thunk qui seront longs à évaluer, et l'on préfère donc utiliser foldl' plutôt que foldl.
import
Data.List (foldl')
-- ...
vectorSum ::
[Vector] ->
Vector
vectorSum =
foldl'
(+!
) (0
, 0
, 0
)
On revoit maintenant computeColor pour qu'elle calcule chacune des lumières sur l'intersection, puis somme les couleurs produites. Cela implique de changer son type.
--Construit la couleur à partir d'une intersection
computeColor ::
Intersection ->
Scene ->
Color
computeColor intersection scene =
color
where
colors =
map (\ (Light f) ->
f intersection scene) lights
color =
vectorSum colors
Scene _
_
lights =
scene
Il nous faut donc modifier les appels. Par chance, il n'y en a qu'un.
getPixel ::
Scene ->
PixelLocation ->
[Word8]
getPixel scene pixelLocation =
case
color of
Nothing
->
[0
, 0
, 0
, 255
]
Just
pixels ->
pixels
where
intersection =
return pixelLocation
>>=
computeRay
>>=
computeIntersections
>>=
getClosestIntersection
color =
fmap intersectionToPixel (intersection scene)
intersectionToPixel intersection =
colorToPixel . computeColor intersection $
scene
Scene _
_
lights =
scene
Si l'on lance maintenant notre raytracer, on n'obtient qu'un écran noir. C'est que l'on doit ajouter des lumières à notre scène pour y voir quelque chose. On importe Lights dans Main.hs, et on modifie donc dans tableData la ligne suivante :
scene =
Scene camera objects [buildAmbiantLight (0
, 1
, 0
) (1
, 1
, 1
)]
On retrouve donc exactement la lumière que l'on avait dans l'article précédent (car kitxmlcodeinlinelatexdvpa = 0finkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvpb = 1finkitxmlcodeinlinelatexdvp, kitxmlcodeinlinelatexdvpc = 0finkitxmlcodeinlinelatexdvp, donc on multiplie par kitxmlcodeinlinelatexdvp\frac{1}{0.d^2+1.d+0) = \frac{1}{d}finkitxmlcodeinlinelatexdvp).
II-3. Exposition▲
On peut s'amuser à ajouter plus de lumières. Ajoutons un effet torche avec une lumière ambiante jaune (1, 1, 0) ainsi qu'une lumière ambiante blanche (1, 1, 1) "plate" sur une sphère blanche.
lights =
[buildAmbiantLight (0
, 0
, 1
) (1
, 1
, 1
), buildAmbiantLight (0
, 1
, 0
) (1
, 1
, 0
)]
objects =
[buildSphere 1
.0
(0
, 0
, -
2
) (Material (1
.0
, 1
.0
, 1
.0
))]
scene =
Scene camera objects lights
Nom d'un phoque ! D'où sort ce bleu ? C'est bien sur un overflow dû au fait que l'on multiplie 255 par une valeur supérieure à 1 dans colorToPixel, elle-même appelée par getPixel. En fait, on voulait garantir que computeColor ne produise jamais des valeurs supérieures à 1.
Il y a deux solutions simples pour gérer cette situation. D'abord, on pourrait diviser par la somme de toutes les lumières de la scène. Ça fonctionnerait, mais on obtiendrait une image très sombre, surtout dans les zones où seulement une partie des lumières participent. Une autre solution, simple et efficace, est d'appliquer une fonction avec une courbe écrasée et tendant vers 1 en plus l'infini. Il faut aussi qu'elle soit linéaire vers les valeurs proches de 0, et qu'elle vaille 0 en 0 (en d'autres termes, on veut qu'elle soit équivalente à kitxmlcodeinlinelatexdvp\lambda*xfinkitxmlcodeinlinelatexdvp en 0). Une fonction qui répond à nos attentes est kitxmlcodeinlinelatexdvp1 - e^{-\lambda*x}finkitxmlcodeinlinelatexdvp. Le choix de lambda dépend des scènes, donc on laissera cette information dans la caméra, sous le nom "d'exposition".
On rajoute donc le champ dans le type Camera
--Représentation de l'observateur
data
Camera =
Camera Location Distance PlanSize Exposition
--L'exposition
type
Exposition =
RealRep
On s'occupe de modifier tous les appels au constructeur de Camera. Dans buildDefaultCamera, on mettra une exposition de 1. Ensuite, on peut revoir la fonction computeColor.
--Construit la couleur à partir d'une intersection
computeColor ::
Intersection ->
Scene ->
Color
computeColor intersection scene =
vectorApply (\x ->
1
-
exp(-
exposition*
x)) color
where
colors =
map (\ (Light f) ->
f intersection scene) lights
color =
vectorSum colors
Scene camera _
lights =
scene
Camera _
_
_
exposition =
camera
Le résultat obtenu est bien plus convainquant :