Raytracer minimaliste en Haskell : Gestion des lumières


précédentsommaire

V. Anti-aliasing : supersampling stochastique

L'aliasing est un phénomène inhérent aux calculs de nature discrète. Il regroupe les deux phénomènes communs suivants :

  • Le phénomène de crénelage, qui se caractérise par l'apparition d'un motif en escalier le long du bord des objets. Il est fortement visible sur toutes les images que nous avons générées. Vous trouverez d'autres exemples sur la page wikipedia.
  • Un second phénomène très courant, que nous n'avons pas encore rencontré (faute de disposer d'assez d'objets), mais qui se produit trop souvent, par exemple dès qu'il est question d'appliquer une texture sur un plan, est l'apparition de motifs d'interférences. Cela est dû au fait que l'on superpose notre grille de pixels (que l'on peut considérer comme un réseau) sur un réseau formé par des objets / un motif qui se répète. Des exemples sont disponibles sur cette page ainsi que sur wikipedia.

Il existe plusieurs façons de réduire l'effet d'aliasing. L'une des plus simples consiste à lancer 4 rayons sur les sommets du pixel (vue comme un carré) plutôt qu'un unique rayon central, et de faire la moyenne. Cela revient à générer une image de Largeur + 1 par Hauteur +1 pixels. On est donc dans la famille du sur-échantillonnage, ou encore supersampling.

La solution que je vous propose est du même genre, mais légèrement plus raffinée (et malheureusement, coûteuse en termes de performances). Nous allons subdiviser chaque pixel en kitxmlcodeinlinelatexdvpn . nfinkitxmlcodeinlinelatexdvp sous-pixels, où n est le taux de sur-échantillonnage. Pour chacun de ces sous-pixels, on lancera un rayon dont la position est aléatoire, mais reste à l'intérieur de ce sous-pixel. De cette façon, on construit une "grille irrégulière", ce qui permet de réduire les interférences. Ensuite, pour obtenir la couleur du pixel, on moyenne tous les sous-pixels qui le composent.

Plutôt que de moyenner tous les sous-pixels de façon aveugle, on pourrait les pondérer selon une courbe gaussienne, ou encore courbe en cloche.

On commence par une fonction utilitaire, qui va nous permettre de subdiviser une liste infinie de nombres aléatoires en sous-listes de taille fixe.

Tools.hs
Sélectionnez
--Coupe une liste en une liste de morceaux de longueur length
spliter :: Int -> [a] -> [[a]]
spliter n [] = []
spliter n list = first : (spliter second)
  where
    (first, second) = splitAt n list

On ajoute aussi la possibilité de produire une liste de valeurs aléatoires qui restent identique à chaque exécution (on ne souhaite pas que deux exécutions de notre programme avec la même scène nous donne deux valeurs différentes).

Tools.hs
Sélectionnez
import System.Random

-- ...

--Produit des valeurs entre -offset et +offset
randomOffsets :: RealRep -> [RealRep]
randomOffsets offset = randomRs (0, offset) g
  where
    g = mkStdGen 42

On va vouloir manipuler des pixels aux coordonnées non entières. Puisqu'au final, nos pixels ne sont que des coordonnées sur une grille, pourquoi ne pas les stocker dans des RealReps ?

Tools.hs
Sélectionnez
--Une coordonnée dans l'image
type PixelLocation = (RealRep, RealRep)

Les conversions de computeRay deviennent alors inutiles et doivent être modifiées.

Raytracer.hs
Sélectionnez
--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 = int2RealRep (planWidth `div` 2)
        halfPlanHeight = int2RealRep (planHeight `div` 2)
        vx = x - halfPlanWidth
        vy = y - halfPlanHeight
        vz = -distance

Nous allons modifier la fonction getPixel pour qu'elle prenne un paramètre entier supplémentaire : le taux de sur-échantillonnage. La valeur 1 correspondra à lancer un seul rayon, alors que n signifiera n*n rayons lancés. Dans cette première version, c'est un sur-échantillonnage où le rayon sera toujours lancé dans l'angle inférieur gauche (x minimal, y minimal) du sous pixel.

Raytracer.hs
Sélectionnez
type SamplingRate = Int
getPixel :: SamplingRate -> Scene -> PixelLocation -> [Word8]
getPixel samplingRate scene pixelLocation@(px, py) = colorToPixel color
  where
    --Conversion en nombre à virgule
    samplingRate' = int2RealRep samplingRate
    --Produit l'intersection
    pixelIntersection pixelLocation = return pixelLocation
                                 >>= computeRay
                                 >>= computeIntersections
                                 >>= getClosestIntersection
    --Construit la couleur du pixel
    Scene _ _ lights = scene
    intersectionToPixel intersection = computeColor intersection $ scene
    maybePixelColor pixelLocation = fmap intersectionToPixel (pixelIntersection pixelLocation scene)
    pixelColor maybeColor = case maybeColor of
      Nothing -> (0, 0, 0)
      Just color -> color
    --Calcule l'écart entre deux sous-pixels
    offset = 1 / samplingRate'
    --Construit une liste de couleurs à partir des sous-pixels
    colorList = map (pixelColor.maybePixelColor) (pixels samplingRate)
    --Construit la liste des sous-pixels
    pixels 1 = [pixelLocation]
    pixels samplingRate = [(px + x, py + y) | x <- subCells, y <- subCells]
      where
        subCells = map (\x -> (int2RealRep x) * offset - 1/2) [0..samplingRate-1]
    --Fait la moyenne de toutes les sources lumineuses
    color = (offset*offset) *! (vectorSum colorList)

Vous pouvez maintenant tester cette anti-aliasing avec le main (main si ça veut le coup).

Pour rajouter un comportement aléatoire, nous allons légèrement modifier la fonction getPixel pour qu'elle puisse prendre en paramètre une liste d'offsets à appliquer à chaque sous-pixel (elle se voit donc renommée).

Raytracer.hs
Sélectionnez

type SamplingRate = Int
getPixel :: SamplingRate -> Scene -> PixelLocation -> [Word8]
getPixel sr sc px = stochasticGetPixel sr sc px []

type RandList = [RealRep]
stochasticGetPixel :: SamplingRate -> Scene -> PixelLocation -> RandList -> [Word8]
stochasticGetPixel samplingRate scene pixelLocation@(px, py) randList = colorToPixel color
  where
    -- ...
    --Construit la liste des sous pixels
    pixels 1 = [pixelLocation]
    pixels samplingRate = [(px + x, py + y) | x <- subCells, y <- subCells]
      where
        subCells = map (\x -> (int2RealRep x) * offset - 1/2) [0..samplingRate-1]
        stocasticCells = if null randList then subCells else newSubCells
          where
            newSubCells = zipWith (+) subCells randList
    -- ...

On ajoute quelques fonctions utilitaires pour appeler plus simplement stochasticGetPixel :

Raytracer.hs
Sélectionnez
getPixelMap :: SamplingRate -> Scene -> [PixelLocation] -> [Word8]
getPixelMap samplingRate scene locations = pixels
  where
    stochasticPixels = map
                       (stochasticGetPixel samplingRate scene)
                       locations
    pixels = concat $ zipWith ($) stochasticPixels (generateRandLists samplingRate)

generateRandLists :: SamplingRate -> [RandList]
generateRandLists samplingRate = spliter (samplingRate*samplingRate) $ randomOffsets (1 / samplingRate')
  where
    samplingRate' = int2RealRep samplingRate

On remplace maintenant l'appel dans le main :

Main.hs
Sélectionnez
tableData width height = listArray ((0,0,0), (height-1, width-1, 3)) $ pixels
  where
    pixels = getPixelMap 2 scene [(int2RealRep x, int2RealRep y)
                                 | y <- [0..height - 1], x <- [0..width - 1]]

Et c'est terminé! On se quitte sur une scène illustrant tout ce que nous avons ajouté au cours de cet article.

Conclusion

VI. Remerciements

Un grand merci à Cédric Duprez pour sa relecture orthographique.


précédentsommaire

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.