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

Cochoy Jérémy

Premier article d'une série visant la réalisation d'un raytracer minimaliste en haskell. Quelques connaissances de base sur les monades et la manipulation des listes seront nécessaires.

Votre avis et vos suggestions nous intéressent !
Alors après votre lecture, n'hésitez pas  : Commentez

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

- Auteur : Cochoy Jérémy

- Parution : 11 mai 2013

- Mise à jour : 29 mai 2013

- Public visé :

- Licence : 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.

Sommaire

- I. Introduction

- II. Génération d'une image

- II-0. Avant-propos

- II-1. Production d'un fichier et initialisation de DevIl

- II-2. Production d'une image

- III. Un plan et une caméra

- III-1. Un module Raytracer

- III-2. La caméra

- III-3. Lancers de rayons

- IV. Objets et intersection

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

- IV-2. Calcul des intersections

- IV-3. Premier rendu

- V. Remerciements

I - Introduction

Cet article est le premier d'une série consacré à la réalisation d'un raytracer en haskell. Il s'agit d'un tutoriel pas à pas suivant la construction d'une application, où chacune des fonctionnalités est d'abord commentée puis implémentée. Certains points relatifs au langage seront commentés pour plus de clarté, notamment les fonctions les plus complexes. Une archive contenant l'ensemble du code produit au long de l'article, prêt à compiler, vous est fournie ici.

Ce code compile avec GHC 7.6.3. Si vous avez des difficultés pour compiler le code avec une version antérieur, il est possible que votre version d'Haskell requière le module Control.Monad.Instances.

II - Génération d'une image

II-0 - Avant-propos

Nous travaillerons dans un répertoire /src. Pour compiler, vous pourrez utiliser ghc avec la ligne de commande :

ghc -O -prof -rtsopts -fprof-auto --make Main

GHC compilera tous les fichiers .hs et cherchera l'entry-point dans le fichier Main.hs.

Les options -prof, -rtsopts et -fprof-auto servent au profiling, c'est-à-dire quand nous souhaiterons mesurer les performances de notre programme. L'option -O active les optimisations du compilateur, ce qui peut réduire le temps d'exécution de façon importante.

II-1 - Production d'un fichier et initialisation de DevIl

On va d'abord mettre en place le nécessaire pour produire une image, puis l'on se concentrera sur le principe du raytracing, et son implémentation en haskell.

Commençons par le nécessaire en matière d'IO. Notre programme enregistrera l'image raytracée dans le fichier "image.png". Puisque ce fichier peut déjà exister, on va prendre la peine de le déplacer.

Main.hs

module Main (
main,
) where

import System.Directory

main = do
ilInit
exists <- doesFileExists "image.png"
if exists
then renameFile "image.png" "image_back.png"
else return ()
return () -- Ici viendra le code de l'enregistrement de l'image

La ligne ilInit permet d'initialiser la bibliothèque. La seconde ligne « décapsule » un booléen, nous informe de l'existence ou non du fichier. Par la suite, s'il existe, on le déplace. Dans le cas contraire, on retourne une valeur monadique qui sera ignorée.

II-2 - Production d'une image

Comment enregistrer une image ? Cela se fait en deux temps. D'abord, produire un tableau de données, ensuite, l'écrire au format d'image voulue. On doit produire un tableau tridimensionnel, où les deux premières dimensions sont largeur et hauteur, puis la troisième les couleurs. Dans notre cas, il y a trois couleurs : Rouge, Vert et Bleu suivies de la Transparence (0 signifie transparent et 255 opaque).

On veut donc un tableau de type UArray (Int, Int, Int) Word8, où un Word8 correspond à la plage de valeurs 0-255, avec 0 signifiant l'absence de couleur et 255 la saturation maximale.

On rajoute donc les lignes suivantes dans les imports :

Main.hs

import Codec.Image.DevIL
import Data.Array.Base

Puis on construit notre tableau, accompagné de quelques constantes :

Main.hs

tableData :: Int -> Int -> UArray (Int,Int,Int) Word8
tableData width height = listArray ((0,0,0), (height-1, width-1, 3)) $ repeat (111 :: Word8)

La troisième ligne construit un tableau tridimensionnel, avec des coordonnées comprises entre (0,0,0) (couleur rouge du pixel en bas à gauche) et (height-1, width-1, 2) (couleur bleue du pixel en haut à droite). La dernière instruction produit la liste infinie [111, 111, 111, …] dans laquelle les valeurs seront piochées.

Enregistrons maintenant cette image :

Main.hs

--Remplaçons la dernière ligne
return ()
--Par
writeImage "image.png" (tableData 800 600)

Nous avons alors une magnifique image grise, de 800 par 600.

III - Un plan et une caméra

III-1 - Un module Raytracer

Bien, allons-y. Il nous faut une fonction capable de nous donner les couleurs à placer dans l'image. On veut donc une fonction getPixel :: Scene -> (Int, Int) -> [Word8] où le premier paramètre est « la scène », le second la position du pixel que l'on cherche à connaitre, et la valeur de retour est une liste contenant nos trois couleurs.

On ne sait pas trop pour le moment ce qu'est une scène, ni comment écrire la fonction. Mais ça ne nous posera pas de problème pour compiler le module. Ah, et tant qu'à faire, nous aurons besoin d'une caméra pour représenter la position depuis laquelle on regarde, et connaitre la taille de « l'écran » sur lequel viendront se dessiner les images.

Raytracer.hs

module Raytracer (
Scene,
Camera,
getPixel,
) where

import Data.Word

data Camera = Camera
data Scene = Scene
type PixelLocation = (Int, Int)

getPixel :: Scene -> PixelLocation -> [Word8]
getPixel = undefined

On doit inclure le module Data.Word pour utiliser le type Word8.

Remarquez que l'on a construit des types « vide » pour Camera et Scene. De même, appeler getPixel provoquerait une exception (si l'appel est vraiment exécuté). Mais on peut tout de même vérifier que nos types sont cohérents.

Pour que notre module soit compilé, il faut l'importer dans le module Main. Ajoutez donc la ligne suivante à la suite de toutes les importations.

Main.hs

import Raytracer

Appelons maintenant la fonction getPixel en modifiant la définition de tableData.

Main.hs

tableData width height = listArray ((0,0,0), (height, width, 3)) $ pixels
where
pixels = concat . map (getPixel emptyScene) $ [(x, y) | y <- [0..height - 1], x <- [0..width - 1]]

Nous avons besoin d'une scène vide, emptyScene, que l'on rajoutera dans le module Raytracer. Mais regardons déjà ces quelques lignes.

D'abord, pour plus de lisibilité, on va construire la liste de couleurs sous le nom pixelsList. On applique la fonction (getPixel emptyScene) qui est de type PixelLocation -> [Word8] sur chacune des coordonnées (x,y). L'ordre d'énumération est préservé, et c'est très important, car la première dimension du tableau est la hauteur, et ensuite la largeur. On veut donc que la liste contienne les pixels ligne par ligne. C'est pourquoi on énumère d'abord le y puis le x. Si quelques doutes persistent, reportez-vous aux listes en compréhension et aux listes comme monades.

En parlant de monades, cette histoire de "map-concat", ça ne vous rappelle rien ? C'est bien sûr l'opérateur >>= que l'on utilise sans le dire. Eh bien, disons-le !

Main.hs

pixelsList = [(x, y) | y <- [0..height - 1], x <- [0..width - 1]] >>= getPixel emptyScene

Voilà qui est plus lisible. Comme promis, rajoutons une scène vide :

Raytracer.hs

emptyScene :: Scene
emptyScene = undefined

III-2 - La caméra

Pour raytracer des objets, il nous faut un point à partir duquel lancer des rayons. Ces rayons viendront intersecter les objets, et l'on saura alors à quelle distance ils se trouvent. On pourra aussi récupérer la couleur de l'objet, et l'angle entre l'objet et l'observateur, pour calculer la lumière. De plus, on ne projette pas des rayons n'importe où. On va construite un rectangle de la taille de l'image, découper ce rectangle en pixels, comme ceux de l'image que l'on veut générer, et projeter des rayons provenant de la position de l'observateur en direction de chacun de ces pixels.

De nombreux facteurs entrent en jeu. Où se trouve le plan par rapport à l'observateur ? À quelle distance ? Comment est-il orienté ? Comment le placer, gérer les changements de taille de l'image ?

Dans les jeux vidéo, ou plus généralement en 3D, on entend parler « d'angle de vision ». L'angle de vision est directement affecté par la distance entre le « plan de projection » (le plan dont on parle) et l'observateur (la position de la caméra).

On va décider que la caméra regardera toujours vers l'axe des Z négatif, et que le plan sera face à la caméra, parallèle au plan Z = 0. De cette façon, les x croissants seront sur la droite de l'observateur, et les y croissants vers le bas (comme notre image).

Et si l'on veut regarder dans une autre direction pensez-vous ? Eh bien, il suffira de faire tourner la caméra, chose qui sera identique à la rotation des objets raytracés. Mais n'allons pas trop vite en besogne.

De plus, une scène doit contenir des objets. Disons donc que nous avons une caméra, et une liste d'objets. Ne me demandez pas ce qu'est un objet :)

Raytracer.hs

--Représentation des nombres réels
type RealRep = Double
--Représentation d'une position dans l'espace
type Location = (RealRep, RealRep, RealRep)
--Représentation d'une distance
type Distance = RealRep
--Dimensions du plan
type PlanWidth = Int
type PlanHeight = Int
type PlanSize = (PlanWidth, PlanHeight)

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

--On object raytracable
data Object = Object

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

--Une coordonnée dans l'image
type PixelLocation = (Int, Int)

--Une fonction qui fournit une camera centrée en 0,0,0
buildDefaultCamera :: Distance -> PlanWidth -> PlanHeight -> Camera
buildDefaultCamera distance planWidth planHeight = Camera (0,0,0) distance (planWidth, planHeight)

On en profite pour ajouter une méthode permettant de construire une caméra par défaut. N'oublions pas de revoir les exports :

Raytracer.hs

module Raytracer (
Scene,
Camera,
Object,
getPixel,
emptyScene,
buildDefaultCamera,
) where

III-3 - Lancers de rayons

Il nous faut maintenant construire un vecteur provenant de la caméra, à partir d'une scène et des coordonnées d'un pixel. De plus, il faut que le vecteur soit de norme 1, de façon à ce que la distance soit simplement le nombre de fois où il faut placer des copies de ce vecteur bout à bout pour atteindre l'objet.

En fait, il suffit que tous les vecteurs aient la même norme, car on ne cherchera qu'à comparer les distances entre elles. Si tous les vecteurs n'ont pas la même norme, c'est dû au fait que la distance du point d'observation aux angles du plan est plus grande que la distance d'observation au centre du plan (la distance indiquée dans la caméra).

Commençons par le nécessaire pour construire un vecteur à partir des coordonnées d'un pixel, et une fonction pour rendre un vecteur unitaire.

Au passage, on crée un nouveau module Tools, et l'on déplace quelques types « élémentaires ». N'oubliez pas d'importer notre nouveau module dans le module Raytracer.

Tools.hs

module Tools (
Location,
Distance,
RealRep,
Vector,
Ray,
normalise,
int2RealRep,
) where

--Représentation des nombres réels
type RealRep = Double

--Représentation d'une position dans l'espace
type Location = (RealRep, RealRep, RealRep)
--Représentation d'une distance
type Distance = RealRep

--Un vecteur
type Vector = (RealRep, RealRep, RealRep)
type Ray = Vector

normalise :: Vector -> Vector
normalise vector@(a, b, c) = (a / n, b / n, c / n)
where
n = norm vector

norm :: Vector -> RealRep
norm (a, b, c) = sqrt(a^2 + b^2 + c^2)

int2RealRep :: Int -> RealRep
int2RealRep = fromIntegral

On aura aussi besoin de convertir des entiers en RealRep dans quelques instants, alors tant qu'à faire, on définit un alias pour fromIntegral, qui s'occupera des conversions de types numériques.

Implémentons une fonction computeRay :: PixelLocation -> Scene -> Vector. Pourquoi la scène en dernier argument ? Parce que tout ce que l'on va produire va dépendre de la scène, et l'on peut donc considérer tous les résultats comme « valeur dans un contexte ». Humm, vous avez dit monade ?

Raytracer.hs

--Construit un rayon à partir d'un pixel
computeRay :: PixelLocation -> Scene -> Ray
computeRay (x, y) (Scene camera objects) = normalise (vx, vy, vz)
where Camera _ distance (planWidth, planHeight) = camera
halfPlanWidth = planWidth `div` 2
halfPlanHeight = planHeight `div` 2
vx = int2RealRep (x - halfPlanWidth)
vy = int2RealRep (y - halfPlanHeight)
vz = -distance

On commence par diviser la largeur et la hauteur par deux, pour « centrer » le plan sur l'axe des Z. On calcule alors un vecteur de coordonnées (vx, vy, vz), qui relie le point (0, 0, distance) au plan Z = 0. On regarde donc la différence entre les coordonnées (0, 0, distance) et le point (x - halfPlanWidth, y - halfPlanHeight, 0) du plan Z = 0. La position de la caméra n'a aucune importance dans ce calcul, car quelle que soit la translation que l'on applique a la caméra, les vecteurs (les directions dans lesquelles on regarde) ne sont pas changés. La position de la caméra entrera en jeu plus tard, comme « point de départ » du rayon.

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

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

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 où 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 , on aura les coordonnées du point 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 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

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

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

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

emptyScene :: Camera -> Scene
emptyScene camera = Scene camera []

On modifie donc l'appel dans le main.

Raytracer.hs

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

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

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

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

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

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

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

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 :

V - Remerciements

Un grand merci à ClaudeLELOUP pour sa relecture orthographique.