Raytracer minimaliste en Haskell : Gestion des lumières


précédentsommairesuivant

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).

Types.hs
Sélectionnez
--Représentation d'une position dans un repère local
type LocalLoc = Location
Types.hs
Sélectionnez
--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.

Object.hs
Sélectionnez
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.

Raytracer.hs
Sélectionnez
--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) :

Type.hs
Sélectionnez
--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 :

Objects.hs
Sélectionnez
--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 :

Objects.hs
Sélectionnez
--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.

Tools.hs
Sélectionnez
--Produit scalaire
(.!) :: Vector -> Vector -> RealRep
(.!) (a, b, c) (a', b', c') = a * a' + b * b' + c * c'
Lights.hs
Sélectionnez
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.

Types.hs
Sélectionnez
-- Une matière
type DiffuseFactor = RealRep
type SpecularFactor = RealRep
data Material = Material Color DiffuseFactor SpecularFactor SpecularExponent

Ajoutons maintenant la source de lumière directionnelle.

Lights.hs
Sélectionnez
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.

Raytracer.hs
Sélectionnez
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
Teste de la lumière diffuse

On passe maintenant à la lumière omnidirectionnelle.

Main.hs
Sélectionnez
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 :

Main.hs
Sélectionnez
    lights = [buildAmbiantLight (0, 0, 2) (1, 1, 1), buildOmnidirectionalLight (0, 0, 0.8) (0, 0, -5) (1, 1, 1)]
Lumière omnidirectionelle

Ajoutons maintenant notre spot lumineux :

Types.hs
Sélectionnez
--Un angle
type RadianAngle = RealRep
Lights.hs
Sélectionnez
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 :

Main.hs
Sélectionnez
    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.

Un spot

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.

Lights.hs
Sélectionnez

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 :

Lights.hs
Sélectionnez

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
Lights.hs
Sélectionnez

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
Lights.hs
Sélectionnez

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 :

Main.hs
Sélectionnez
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 :

Spot avec lumière spéculaire

précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Zenol. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts. Droits de diffusion permanents accordés à Developpez LLC.