IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Raytracer minimaliste en Haskell : Affichage d'une sphère


précédentsommaire

IV. Objets et intersection

Nous y sommes presque. Encore quelques lignes, et l'on verra se dessiner notre première image.

IV-1. Une sphère, et de la matière

Pour commencer, nos objets auront des couleurs. Il nous faut donc quelque chose pour les représenter. Allons-y pour un type représentant la matière de nos objets.

Materiel.hs
Sélectionnez
module Materiel (
  Red,
  Green,
  Blue,
  Color,
  Material (Material),
  ) where

--Couleur, où 0.0 signifit "pas de couleur" et 1.0 "toute la couleur".
type Red = RealRep
type Green = RealRep
type Blue = RealRep
type Color = (Red, Green, Blue)

data Material = Material Color

Nous allons placer nos objets dans un module « Objects ». Nous allons définir un objet comme constitué de trois éléments : des informations sur sa position dans l'espace, des informations sur la matière dont il est constitué, et enfin une façon de calculer si un rayon intersecte l'objet, ou non.

Objects.hs
Sélectionnez
odule Objects (
  Object (Object),
  Camera (Camera),
  Scene (Scene),
  Intersection,
  ) where

import Tools
import Material

--Représentation de l'observateur
data Camera = Camera Location Distance PlanSize

--Une scène
data Scene = Scene Camera [Object]

--Une intersection
data Intersection = Intersection

--On object raytracable
data Object = Object Location Material (Vector -> Scene -> Intersection)

On en profite pour déplacer le type Object dans le module Objects, et donc ses dépendances, et on pensera bien à ajouter l'import dans le module Raytracing.

Les types PlanSize et PixelLocation sont déplacés dans le module Tools.

Une sphère est définie par l'équation kitxmlcodeinlinelatexdvpx^2 + y^2 + z^2 = r^2finkitxmlcodeinlinelatexdvp où kitxmlcodeinlinelatexdvprfinkitxmlcodeinlinelatexdvp est le rayon de la sphère. Si l'on se place sur la position de la caméra, et que l'on se déplace le long de notre vecteur, construit dans la section précédente, il est possible qu'en avançant assez on « tombe » sur la sphère, c'est-à-dire que pour une bonne valeur de kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp, on aura les coordonnées du point kitxmlcodeinlinelatexdvp(o_x + k . r_x, o_y + k . r_y, o_z + k r_z) := \text{camera} + k . \text{rayon}finkitxmlcodeinlinelatexdvp solution de l'équation. Si l'on injecte les valeurs de la dernière équation dans celle de la sphère, on trouve un polynôme en kitxmlcodeinlinelatexdvpkfinkitxmlcodeinlinelatexdvp de degré 2.

Cette méthode a quelque chose de général. On définit un objet par une équation implicite, comme l'équation de la sphère, puis l'on injecte le point d'arrivée du rayon (qui nous est pour le moment inconnu). De cette façon, on se retrouve avec une équation à une variable. Si l'on sait la résoudre, les solutions, si elles existent, sont les points d'intersection du rayon et de l'objet. S'il n'y a pas de solution, c'est tout simplement que l'on ne peut pas voir l'objet.

Par bonheur, la sphère a la chance de nous donner une équation polynomiale de degré 2. On sait tous, depuis le lycée, que l'on peut calculer les solutions grâce au coefficient du polynôme. Ajoutons donc le nécessaire pour calculer les racines d'un polynôme de degré 2. Comme il peut y en avoir aucune, une (dite double), ou encore deux, nous stockerons les solutions dans une liste.

On en profite pour définir une intersection comme un point, la distance, et l'objet. On construit l'intersection à partir des distances. N'oublions pas que grâce à la paresse, elles ne seront calculées que si l'on a vraiment besoin de leur valeur.

Objects.hs
Sélectionnez
--Une intersection
type Intersection = (Location, Distance, Object)

--On object raytracable
data Object = Object Location Material (Vector -> Scene -> [Distance])

type Radius = RealRep

--Cherche les intersections avec un sphère de rayon "radius" centré en (0, 0, 0)
sphereIntersect :: Radius -> Ray -> Scene -> [Distance]
sphereIntersect radius ray@(rx, ry, rz) scene = solvePoly2 (a, b, c)
  where
    Scene camera _ = scene
    Camera cameraLocation _ _ = camera
    (cx, cy, cz) = cameraLocation
    a = rx^2 + ry^2 + rz^2
    b = 2 * (rx * cx + ry * cy + rz * cz)
    c = cx^2 + cy^2 + cz^2 - radius^2

buildSphere :: Radius -> Location -> Material -> Object
buildSphere radius loc mat = Object loc mat (sphereIntersect radius)
Tools.hs
Sélectionnez
module Tools (
  Location,
  Distance,
  RealRep,
  Vector,
  Ray,
  PlanSize,
  PlanWidth,
  PlanHeight,
  PixelLocation,
  normalise,
  int2RealRep,
  (+!),
  (-!),
  (*!),
  ) where

-- ...

(+!) :: Vector -> Vector -> Vector
(+!) (a, b, c) (a', b', c') = (a + a', b + b', c + c')
(-!) :: Vector -> Vector -> Vector
(-!) (a, b, c) (a', b', c') = (a - a', b - b', c - c')
(*!) :: RealRep -> Vector -> Vector
(*!) l (a, b, c) = (l * a, l* b, l * c)

--Les coefficients (a, b, c) d'un polynôme de la forme ax^2 + bx + c
type Polynom2 = (RealRep, RealRep, RealRep)
solvePoly2 :: Polynom2 -> [RealRep]
solvePoly2 (a, b, c)
  | discriminant < 0 = []
  | discriminant > 0 = [(-b + sqrtDiscriminant) / den, (-b - sqrtDiscriminant) / den]
  | otherwise = [-b / den]
  where
    discriminant = b^2 - 4 * a * c
    sqrtDiscriminant = sqrt discriminant
    den = 2 * a

On construit maintenant une fonction pour transformer la liste de distances en liste d'intersections.

Tools.hs
Sélectionnez
distanceToIntersection :: Object -> Ray -> Distance -> Scene -> Intersection
distanceToIntersection obj ray objectDistance scene = (cameraLocation +! (objectDistance *! ray), objectDistance, obj)
  where
    Scene camera _ = scene
    Camera cameraLocation _ _ = camera

IV-2. Calcul des intersections

Construisons une vraie scène vide

Raytracer.hs
Sélectionnez
emptyScene :: Camera -> Scene
emptyScene camera = Scene camera []

On modifie donc l'appel dans le main.

Raytracer.hs
Sélectionnez
    pixels = [(x, y) | y <- [0..height - 1], x <- [0..width - 1]] >>= (getPixel . emptyScene $ buildDefaultCamera 300 width height)

Il ne nous reste plus qu'à récupérer la liste d'objets, calculer les intersections pour chacun, et garder la plus proche. On doit translater toute la scène, car les objets sont supposés se trouver en (0, 0, 0). Il est plus simple de raisonner ainsi, que ce soit pour calculer les intersections (les équations implicites sont plus simples pour les objets centrés à l'origine) ou encore l'orientation (plus tard on voudra pouvoir faire tourner des objets). On déplace donc la caméra.

Raytracer.hs
Sélectionnez
--Calcule les intersections du rayon avec un objet.
computeIntersection :: Ray -> Object -> Scene -> [Intersection]
computeIntersection ray object scene = intersect ray >>= mapM (distanceToIntersection object ray) $ scene'
  where
    Object objectLocation _ intersect = object
    Scene camera objects = scene
    Camera cameraLocation distance planSize = camera
    camera' = Camera (cameraLocation -! objectLocation) distance planSize
    scene' = Scene camera' objects

--Applique le calcul d'intersections sur chacun des objets
computeIntersections :: Ray -> Scene -> [Intersection]
computeIntersections ray scene = concat (contextualIntersections scene)
  where Scene _ objects = scene
  contextualIntersections = sequence $ map (computeIntersection ray) objects

La fonction computeIntersection prend le rayon que nous avons lancé, et l'intersecte avec l'objet qu'elle reçoit. Elle réalise le calcul de l'intersection grâce à la fonction gentiment fournie par l'objet. Cette fonction fournit une liste de distances. On produit donc des intersections (contenant : la distance, la position de l'intersection, et l'objet, car nous aurons besoin de tout ça dans la suite) en mappant la fonction de conversion sur la liste.

Comme le typage est un peu compliqué, voici le détail. L'expression intersect ray est de type Scene -> [Distance], que l'on pensera comme du m [a]. L'expression distanceToIntersection object ray est elle de type Distance -> Scene -> Intersection que l'on pensera a -> m b. On voudrait appliquer map, mais alors on aurait quelque chose de type [a] -> [m b]. En appliquant mapM à cette expression, on la transforme en du [a] -> m [b]. On n'a plus qu'à utiliser l'opérateur >>= pour appliquer cette expression à l'intérieur de intersect ray. On convertit donc bien une liste de distances dépendant de la scène en une liste d'objets, dépendant de la scène.

Dans la seconde fonction, on doit convertir une liste d'objets en une liste d'intersections. Mais comme il y a quelques monades dans l'histoire, on utilise sequence pour convertir du [m [b]] en m [[b]].

Maintenant, on élimine toutes les intersections situées derrière la caméra, et on récupère l'intersection la plus proche de nous, si c'est possible.

Raytracer.hs
Sélectionnez
--Ne conserve que les intersections devant la caméra, et récupère la plus proche
getClosestIntersection :: [Intersection] -> Scene -> Maybe Intersection
getClosestIntersection intersections = return intersection
  where
    allowedIntersections = filter isAhead intersections
    isAhead intersection@(_, distance, _) = distance > 0
    compareZ a b = compare ad bd
      where
        (_, ad, _) = a
        (_, bd, _) = b
    closestIntersection = minimumBy compareZ allowedIntersections
    intersection = if null allowedIntersections
                   then Nothing
                   else Just closestIntersection

On filtre et compare les intersections d'après leur distance à la caméra. On déconstruit le type Intersection pour récupérer les distances ad, bd et distance.

IV-3. Premier rendu

Il nous faut récupérer la couleur de l'objet depuis l'intersection, afin d'afficher un pixel de la bonne couleur. Comme nous n'avons pas encore de gestion de la lumière, on dira que plus l'objet est loin, moins il est lumineux. Ce n'est pas tout à fait vrai, mais on pourra obtenir une première image de cette façon.

Raytracer.hs
Sélectionnez
import Material

-- ...

--Construit la couleur à partir d'une intersection
computeColor :: Intersection -> Color
computeColor (_, distance, Object _ material _) = (r / distance, g / distance, b / distance)
  where
    Material (r, g, b) = material

Et pour finir, convertissons cette couleur en pixel, sous la forme de trois Word8. On ajoute donc une fonction utilitaire dans le module Tools que l'on pensera bien à exporter (il faudra importer Data.Word). Il faudra aussi exporter la fonction.

Tools.hs
Sélectionnez
realRep2Word8 :: RealRep -> Word8
realRep2Word8 = truncate

On peut alors ajouter la fonction de conversion dans le module Material. Il faudra importer les modules Tools et Data.Word.

Material.hs
Sélectionnez
colorToPixel :: Color -> [Word8]
colorToPixel (r, g, b) = map realRep2Word8 [r * 255, g * 255, b * 255, 255]

Nous pouvons maintenant écrire getPixels à partir de tout ce que nous avons produit.

Raytracer.hs
Sélectionnez
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 (colorToPixel . computeColor) (intersection scene)

On obtient à nouveau une image, mais elle est vide. Il faut rajouter des objets. Testons une simple sphère :

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
    scene = Scene camera objects
    camera = buildDefaultCamera 300 width height
    objects = [buildSphere 1.0 (0, 0, -2) (Material (0.0, 0.0, 1.0))]

Voici donc le premier rendu : Premier rendu

V. Remerciements

Un grand merci à ClaudeLELOUP pour sa relecture orthographique.


précédentsommaire

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