III. Lumières : les sources lumineuses▲
III-1. Les normales▲
Pour un bon éclairage, on a besoin de trois lumières : la lumière ambiante, la lumière diffuse, et la lumière spéculaire (cf http://en.wikipedia.org/wiki/Phong_shading). Pour la lumière ambiante, nous avons fait le choix de créer des sources de lumières particulières. Il nous reste à traiter les cas diffus et spéculaire. Ces deux cas dépendent de la direction de la source de lumière, et cette direction varie pour chacune de nos sources lumineuses. Pour les calculer, il faut connaître la normale à la surface. La normale est un vecteur unitaire qui indique la direction extérieure à la surface. Bien sûr, l'extérieur et l'intérieur d'une surface sont bien subjectifs. Par exemple, pour un plan, la normale est le long d'une droite orthogonale à celui-ci. Il faut que la normale à la sphère pointe "vers" la caméra, et donc selon le cas, c'est le vecteur normal que l'on veut avoir. On fera donc un petit ajustement au moment des calculs, pour choisir le bon vecteur. Modifions donc nos objets pour qu'ils fournissent une normale, à partir d'une intersection.
On commence par modifier nos intersections. Il faut bien différencier les coordonnées dans le repère local (centré sur) de l'objet (celles que l'on calculait jusque-là) et les coordonnées dans notre scène (qui pourront nous être utiles pour calculer la lumière).
--Représentation d'une position dans un repère local
type
LocalLoc =
Location
--Une intersection
type
Normal =
vector
type
Intersection =
Intersection Location LocalLoc Distance Object Normal
L'ajout de la normale nous servira plus tard. Pour le moment, on placera le vecteur nul dans les constructeurs. On utilise les messages du compilateur pour retrouver les utilisations du type Intersection et les modifier, et en exportant bien le constructeur de type.
distanceToIntersection ::
Object ->
Ray ->
Distance ->
Scene ->
Intersection
distanceToIntersection obj ray objectDistance scene =
Intersection intersectionLocation intersectionLocation objectDistance obj (0
, 0
, 0
)
where
Scene camera _
_
=
scene
Camera cameraLocation _
_
_
=
camera
intersectionLocation =
cameraLocation +!
(objectDistance *!
ray)
localIntersectionToGlobal ::
Intersection ->
Intersection
localIntersectionToGlobal intersection =
Intersection (objectLocation +!
localLoc) localLoc distance object normal
where
Intersection location localLoc distance object normal =
intersection
Object objectLocation _
_
_
=
object
En effet, à l'appel de distanceToIntersection, le repère local est le repère utilisé. On ajoute la possibilité, avec l'ancienne caméra, de recalculer la position de l'intersection.
--Calcule les intersections du rayon avec un objet.
computeIntersection ::
Ray ->
Object ->
Scene ->
[Intersection]
computeIntersection ray object scene =
fmap localIntersectionToGlobal intersections
where
intersections =
intersect ray >>=
mapM (distanceToIntersection object ray) $
scene'
Object objectLocation _
intersect =
object
Scene camera objects lights =
scene
Camera cameraLocation distance planSize exposition =
camera
camera'
=
Camera (cameraLocation -!
objectLocation) distance planSize exposition
scene'
=
Scene camera'
objects lights
On modifie le type Object pour ajouter le calcul de normal (et l'on modifie les appels du constructeur) :
--Un objet raytraçable
type
IntersectionFunction =
Ray ->
Scene ->
[Distance]
type
NormalFunction =
Object ->
LocalLoc ->
Vector
data
Object =
Object Location Material IntersectionFunction NormalFunction
Calculons les normales de la sphère :
--Détermine les normales de la sphère
sphereNormal ::
Object ->
LocalLoc ->
Vector
sphereNormal object localLoc =
normalise localLoc
buildSphere ::
Radius ->
Location ->
Material ->
Object
buildSphere radius loc mat =
Object loc mat (sphereIntersect radius) sphereNormal
En règle générale, quand un objet (comme la sphère) est défini de façon implicite, les normales s'obtiennent facilement en calculant le gradient. C'est-à-dire que l'on dérive l'équation par rapport à X, puis par rapport à Y, et enfin par rapport à Z. On obtient donc trois formules. Chacune nous donne une coordonnée du vecteur normal (respectivement la coordonnée en X, en Y et en Z). Il se trouve que pour la sphère, on trouve le vecteur kitxmlcodeinlinelatexdvp\begin{pmatrix} 2x \\ 2y \\ 2z \end{pmatrix}finkitxmlcodeinlinelatexdvp. Ce sont donc deux fois le vecteur coordonnées, et l'on peut alors directement normaliser l'intersection (en coordonnées locales).
Pour finir, on utilise la fonction de calcul de normales dans le calcul d'une intersection :
--Choisit la bonne direction de la normale, dans le repère local de l'objet
selectNormalSign ::
Camera ->
Normal ->
Normal
selectNormalSign camera normal =
if
aligned then
normal else
(-
1
) *!
normal
where
aligned =
(cameraLocation .!
normal) >=
0
Camera cameraLocation _
_
_
=
camera
--Compute intersection's location, from local rep
distanceToIntersection ::
Object ->
Ray ->
Distance ->
Scene ->
Intersection
distanceToIntersection object ray objectDistance scene =
Intersection intersectionLocation intersectionLocation objectDistance object (selectNormalSign camera normal)
where
normal =
(normalFunction object intersectionLocation)
Scene camera _
_
=
scene
Camera cameraLocation _
_
_
=
camera
intersectionLocation =
cameraLocation +!
(objectDistance *!
ray)
Object _
_
_
normalFunction =
object
Comme annoncé, on veut que la normale pointe vers la caméra, et on obtient cela grâce à selectNormalSign. De cette façon, pas de bug de lumière parce que la normale est dans le mauvais sens, et qu'elle vous fait dos alors qu'elle devrait vous faire face. La normale sera toujours dans le bon sens.
III-2. Lumière diffuse▲
Commençons par la fonction de calcul de la lumière diffuse. Celle-ci repose sur l'approximation selon laquelle la lumière est réfléchie équitablement dans toutes les directions, selon l'angle entre la normale à la surface et la direction de la lumière.
--Produit scalaire
(.!
) ::
Vector ->
Vector ->
RealRep
(.!
) (a, b, c) (a',
b',
c')
=
a *
a'
+
b *
b'
+
c *
c'
type
UnaryLightVector =
Vector
type
UnaryNormalVector =
Vector
-- Le type "type DiffuseFactor = Vector" sera placé dans Types.hs
-- Calcule la lumière diffuse à partir de deux rayons provenant de l'intersection et pointant respectivement
-- vers la source de lumière et vers "l'extérieur" de l'objet.
computeDiffuse ::
DiffuseFactor ->
UnaryLightVector ->
UnaryNormalVector ->
RealRep
computeDiffuse diffuseFactor unaryLightVector unaryNormalVector =
diffuseValue
where
diffuseValue =
diffuseFactor *
diffuseFactor
diffuseFactor =
max unaryLightVector .!
unaryNormalVector 0
Le calcul de lumière tient donc en une ligne : on pondère par le cosinus de l'angle entre le vecteur Objet -> Lumière, nommé unaryLightVector, et la normale de l'objet, nommé unaryNormalVector.
On ajoute un caractère diffus à nos objets. Remarquez que l'on ne garde le facteur diffuseValue (correspondant au cosinus de l'angle entre la source de lumière et la normale) que s‘il est positif (une lumière négative n'a pas de sens). Dans le main, on mettra la diffusion et la lumière spéculaire (les deux champs) de notre sphère à 1, histoire de pouvoir compiler.
-- Une matière
type
DiffuseFactor =
RealRep
type
SpecularFactor =
RealRep
data
Material =
Material Color DiffuseFactor SpecularFactor SpecularExponent
Ajoutons maintenant la source de lumière directionnelle.
type
LightDirection =
Vector
buildDirectionalLight ::
LightDirection ->
Color ->
Light
buildDirectionalLight lightDirection lightColor =
Light lightFunction
where
unaryLightVector =
normalise $
(-
1
) *!
lightDirection
lightFunction intersection scene =
(diffuseLightFactor +
specularLightFactor) *!
color
where
color =
lightColor `vectorProduct` objectColor
diffuseLightFactor =
computeDiffuse objectDiffuseFactor unaryLightVector normal
specularLightFactor =
0
Intersection _
_
_
object normal =
intersection
Object _
material _
normalFunction =
object
Material objectColor objectDiffuseFactor specularFactor specularExponent =
material
On a préparé l'emplacement pour le calcul de la lumière spéculaire, que l'on fixe pour le moment à 0.
Je vous propose une nouvelle scène, certes pas très esthétique, mais parfaite pour vérifier que nos calculs et nos lumières fonctionnent bien.
tableData ::
Int
->
Int
->
UArray (Int
,Int
,Int
) Word8
tableData width height =
listArray ((0
,0
,0
), (height-
1
, width-
1
, 3
)) $
pixels
where
pixels =
[(x, y) |
y <-
[0
..
height -
1
], x <-
[0
..
width -
1
]]
>>=
getPixel scene
camera =
buildDefaultCamera 600
width height
lights =
[buildAmbiantLight (0
, 0
, 2
) (1
, 1
, 1
), buildDirectionalLight (1
, -
1
, -
3
) (1
, 1
, 1
)]
objects =
[buildSphere 1
.0
(0
, 1
.2
, -
7
) (Material (1
.0
, 1
.0
, 1
.0
) 1
1
1
),
buildSphere 1
.0
(0
, -
1
.2
, -
7
) (Material (1
.0
, 0
.0
, 1
.0
) 1
1
1
),
buildSphere 1
.0
(1
.2
, 0
, -
7
) (Material (0
.0
, 1
.0
, 1
.0
) 1
1
1
),
buildSphere 1
.0
(-
1
.5
, 0
, -
7
) (Material (0
.0
, 0
.0
, 1
.0
) 1
1
1
),
buildSphere 0
.5
(0
, 2
, -
6
.6
) (Material (1
.0
, 1
.0
, 0
.0
) 1
1
1
)]
scene =
Scene camera objects lights
On passe maintenant à la lumière omnidirectionnelle.
buildOmnidirectionalLight ::
Attenuation ->
Location ->
Color ->
Light
buildOmnidirectionalLight attenuation lightLocation lightColor =
Light lightFunction
where
lightFunction intersection scene =
factor *!
color
where
factor =
attenuationFactor *
(diffuseLightFactor +
specularLightFactor)
unaryLightVector =
normalise (lightLocation -!
(intersectionLocation))
attenuationFactor =
computeAttenuation attenuation distance
color =
lightColor `vectorProduct` objectColor
diffuseLightFactor =
computeDiffuse objectDiffuseFactor unaryLightVector normal
specularLightFactor =
0
Intersection intersectionLocation _
distance object normal =
intersection
Object _
material _
normalFunction =
object
Material objectColor objectDiffuseFactor specularFactor specularExponent =
material
Voici les nouvelles lumières, pour tester :
lights =
[buildAmbiantLight (0
, 0
, 2
) (1
, 1
, 1
), buildOmnidirectionalLight (0
, 0
, 0
.8
) (0
, 0
, -
5
) (1
, 1
, 1
)]
Ajoutons maintenant notre spot lumineux :
--Un angle
type
RadianAngle =
RealRep
buildSpotLight ::
LightDirection ->
RadianAngle ->
Attenuation ->
Location ->
Color ->
Light
buildSpotLight lightDirection coneAngle attenuation lightLocation lightColor =
Light lightFunction
where
unaryLightDirection =
normalise $
(-
1
) *!
lightDirection
lightFunction intersection scene =
factor *!
color
where
unaryLightVector =
normalise (lightLocation -!
intersectionLocation)
spotAngle =
acos(unaryLightVector .!
unaryLightDirection)
halfConeAngle =
coneAngle /
2
spotFactor =
if
spotAngle <
halfConeAngle
then
cos(pi *
spotAngle /
coneAngle) -- = cos(2 * pi * spotAngle / halfConeAngle)
else
0
factor =
spotFactor *
attenuationFactor *
(diffuseLightFactor +
specularLightFactor)
attenuationFactor =
computeAttenuation attenuation distance
color =
lightColor `vectorProduct` objectColor
diffuseLightFactor =
computeDiffuse objectDiffuseFactor unaryLightVector normal
specularLightFactor =
0
Intersection intersectionLocation _
distance object normal =
intersection
Object _
material _
normalFunction =
object
Material objectColor objectDiffuseFactor specularFactor specularExponent =
material
Pour tester, vous pouvez utiliser les lumières suivantes :
lights =
[buildAmbiantLight (0
, 0
, 15
) (1
, 1
, 1
), buildSpotLight (1
, -
1
, -
13
) (pi/
6
) (0
, 0
, 1
) (-
1
, 1
, 0
) (1
, 1
, 1
)]
Pour calculer un "beau cône", on commence par récupérer l'angle entre la direction du spot et la position de l'objet. Si l'angle est inférieur à l'angle maximal (comme on regarde le long de la direction du spot, on doit diviser l'angle d'ouverture par deux) alors on calcule un "dégradé". Pour ce faire, on utilise simplement le cosinus de l'angle "si jamais le cône avait une ouverture de 180 degrés". D'où le pi/2 * spotAngle / halfConeAngle. On fait donc le cosinus d'une interpolation linéaire.
III-3. Lumière spéculaire▲
Il ne nous reste plus grand-chose à faire pour gérer aussi la lumière spéculaire.
La lumière spéculaire est un petit halo qui se dessine sur les objets et leur donne un aspect « poli ». Pour produire cet effet, on va calculer la réflexion du rayon de lumière incident (normalisé) sur l'objet. Si kitxmlcodeinlinelatexdvp\vec{R}, \vec{L}, \vec{N}finkitxmlcodeinlinelatexdvp sont respectivement le rayon réfléchi que l'on cherche, moins le rayon de lumière incident, et la normale, alors on obtient kitxmlcodeinlinelatexdvp\vec{R}finkitxmlcodeinlinelatexdvp par la formule kitxmlcodeinlinelatexdvp\vec{R} = (2\vec{L}.\vec{N})\vec{N} - \vec{L}finkitxmlcodeinlinelatexdvp. Ensuite, on prend le cosinus de l'angle entre le rayon réfléchi et le vecteur Objet -> Caméra normalisé. On obtient alors un terme représentant la lumière spéculaire. Il nous faut ensuite l'adapter légèrement selon les caractéristiques de la matière. On utilise donc les deux coefficients specularFactor et specularExponent représentant respectivement l'intensité lumineuse spéculaire et la « rugosité » de la matière. Pour plus d'informations vous avez la page wikipedia.
type
UnaryCameraVector =
Vector
computeSpecular ::
SpecularFactor ->
SpecularExponent ->
UnaryCameraVector ->
UnaryLightVector ->
UnaryNormalVector ->
RealRep
computeSpecular specularFactor specularExponent unaryCameraVector unaryLightVector unaryNormalVector =
specularValue
where
specularValue =
specularFactor *
(phongTerm **
specularExponent)
reflect =
2
*
(unaryLightVector .!
unaryNormalVector)
phongVector =
(reflect *!
unaryNormalVector) -!
unaryLightVector
phongTerm =
max (phongVector .!
unaryCameraVector) 0
On implémente alors les lumières manquantes :
type
LightDirection =
Vector
buildDirectionalLight ::
LightDirection ->
Color ->
Light
buildDirectionalLight lightDirection lightColor =
Light lightFunction
where
unaryLightVector =
normalise $
(-
1
) *!
lightDirection
lightFunction intersection scene =
(diffuseLightFactor +
specularLightFactor) *!
color
where
color =
lightColor `vectorProduct` objectColor
diffuseLightFactor =
computeDiffuse objectDiffuseFactor unaryLightVector normal
specularLightFactor =
computeSpecular specularFactor specularExponent unaryCameraVector unaryLightVector normal
unaryCameraVector =
normalise $
(cameraLocation -!
intersectionLocation)
Scene camera _
_
=
scene
Camera cameraLocation _
_
_
=
camera
Intersection intersectionLocation _
_
object normal =
intersection
Object _
material _
normalFunction =
object
Material objectColor objectDiffuseFactor specularFactor specularExponent =
material
buildOmnidirectionalLight ::
Attenuation ->
Location ->
Color ->
Light
buildOmnidirectionalLight attenuation lightLocation lightColor =
Light lightFunction
where
lightFunction intersection scene =
factor *!
color
where
factor =
attenuationFactor *
(diffuseLightFactor +
specularLightFactor)
unaryLightVector =
normalise (lightLocation -!
(intersectionLocation))
attenuationFactor =
computeAttenuation attenuation distance
color =
lightColor `vectorProduct` objectColor
diffuseLightFactor =
computeDiffuse objectDiffuseFactor unaryLightVector normal
specularLightFactor =
computeSpecular specularFactor specularExponent unaryCameraVector unaryLightVector normal
unaryCameraVector =
normalise $
(cameraLocation -!
intersectionLocation)
Scene camera _
_
=
scene
Camera cameraLocation _
_
_
=
camera
Intersection intersectionLocation _
distance object normal =
intersection
Object _
material _
normalFunction =
object
Material objectColor objectDiffuseFactor specularFactor specularExponent =
material
buildSpotLight ::
LightDirection ->
RadianAngle ->
Attenuation ->
Location ->
Color ->
Light
buildSpotLight lightDirection coneAngle attenuation lightLocation lightColor =
Light lightFunction
where
unaryLightDirection =
normalise $
(-
1
) *!
lightDirection
lightFunction intersection scene =
factor *!
color
where
unaryLightVector =
normalise (lightLocation -!
intersectionLocation)
spotAngle =
acos(unaryLightVector .!
unaryLightDirection)
halfConeAngle =
coneAngle /
2
spotFactor =
if
spotAngle <
halfConeAngle
then
cos(pi *
spotAngle /
coneAngle) -- = cos(2 * pi * spotAngle / halfConeAngle)
else
0
factor =
spotFactor *
attenuationFactor *
(diffuseLightFactor +
specularLightFactor)
attenuationFactor =
computeAttenuation attenuation distance
color =
lightColor `vectorProduct` objectColor
diffuseLightFactor =
computeDiffuse objectDiffuseFactor unaryLightVector normal
specularLightFactor =
computeSpecular specularFactor specularExponent unaryCameraVector unaryLightVector normal
unaryCameraVector =
normalise $
(cameraLocation -!
intersectionLocation)
Scene camera _
_
=
scene
Camera cameraLocation _
_
_
=
camera
Intersection intersectionLocation _
distance object normal =
intersection
Object _
material _
normalFunction =
object
Material objectColor objectDiffuseFactor specularFactor specularExponent =
material
Pour illustrer le résultat, je vous propose de revoir les réglages de la scène :
tableData ::
Int
->
Int
->
UArray (Int
,Int
,Int
) Word8
tableData width height =
listArray ((0
,0
,0
), (height-
1
, width-
1
, 3
)) $
pixels
where
pixels =
[(x, y) |
y <-
[0
..
height -
1
], x <-
[0
..
width -
1
]]
>>=
getPixel scene
camera =
buildDefaultCamera 600
width height
lights =
[buildAmbiantLight (0
, 0
, 15
) (1
, 1
, 1
), buildSpotLight (1
, -
1
, -
13
) (pi/
6
) (0
, 0
, 1
) (-
1
, 1
, 0
) (1
, 1
, 1
)]
objects =
[buildSphere 1
.0
(0
, 1
.2
, -
7
) (Material (1
.0
, 1
.0
, 1
.0
) 1
2
6
),
buildSphere 1
.0
(0
, -
1
.2
, -
7
) (Material (1
.0
, 0
.0
, 1
.0
) 1
2
50
),
buildSphere 1
.0
(1
.2
, 0
, -
7
) (Material (0
.0
, 1
.0
, 1
.0
) 1
2
12
),
buildSphere 1
.0
(-
1
.5
, 0
, -
7
) (Material (0
.0
, 0
.0
, 1
.0
) 1
1
12
),
buildSphere 0
.5
(0
, 2
, -
6
.6
) (Material (1
.0
, 1
.0
, 0
.0
) 1
1
12
)]
scene =
Scene camera objects lights
Et voici le résultat :