Haskell Raytracer Project - Part 2

articles ✒ haskell-raytracer-2

Parts of this tutorial

  1. Bitmap output
  2. Vector handling
  3. Scene drawing

This is the second article of a series that show how to make a basic raytracer (3D renderer) with Haskell.

In the first article we made a function to output bitmap (PPM) image files from Haskell. In this article we'll introduce some functions to help manipulate vectors.

Vectors

Our raytracer will involve a lot of calculation with vectors, so for clarity, rather than using [Double], we'll introduce a new type.

This code should go in Vector.hs.

module Vector where
 
data Vector = Vector [Scalar]
-- a vector is a list of scalars
 
instance Num Vector where
    (+) (Vector xs) (Vector ys) = Vector (zipWith (+) xs ys)
    (-) (Vector xs) (Vector ys) = Vector (zipWith (-) xs ys)
    (*) (Vector xs) (Vector ys) = Vector (zipWith (*) xs ys)
    negate (Vector xs) = Vector (map negate xs)
    fromInteger x = error "you probably didn't mean to do that"
 
-- you can add, subtract, check equality of and multiply vectors term-by-term
 
instance Eq Vector where
    (==) (Vector xs) (Vector ys) = all id (zipWith (==) xs ys)
 
instance Show Vector where
    show (Vector xs) = show xs
 
-- to convert a vector to a string, just convert its value list to string
 
mult :: Scalar -> Vector -> Vector
mult l (Vector xs) = Vector (map (*l) xs)
 
normalise :: Vector -> Vector
normalise v@(Vector vs) = Vector (map (/mag) vs)
    where mag = (sqrt . vectorsum) (v * v)
 
type Scalar = Double
-- store scalars as floating-point doubles
 
vectorsum :: Vector -> Scalar
vectorsum (Vector xs) = sum xs
-- get the sum of the scalar elements of a vector

Surfaces

The code from here onwards should go in Intersect.hs.

Let's think about what 3D objects we could have in our scene - we'll call them surfaces.

For now, we'll just consider spheres, since the maths is simple.

A sphere has:

data Surface = Sphere Vector Scalar Colour

Intersections

We're going to use raytracing to render our scene. In short, we take a point in our scene, then fire lots of rays outwards from it (one per pixel in the image) then see what they hit.

Clearly a ray is simply a source and a direction vector:

data Ray = Ray Vector Vector

Now, let's introduce a new function that gives the distance at which a ray (half-line) first intersects a surface (or returns Nothing if no such intersection exists).

intersect :: Ray -> Surface -> Maybe Scalar
 
-- Intersect returns Nothing if the ray does not intersect the surface
-- or if it does intersect, Just d where rx+(d*rv) is where intersects the surface
 
intersect (Ray rx rv) (Sphere sx radius col)
    | insersect' == [] = Nothing
    | otherwise  = Just (minimum insersect')        
    where insersect' | dscr < 0 = []
                     | otherwise = filter (>0.0000001) [((-b) + sqrtdscr) / (2.0 * a), ((-b) - sqrtdscr) / (2.0 * a)]
                     -- we don't to see out of the back of our head!
          se = (sx - rx)
          b = (-2.0) * vectorsum (rv * se) 
          dscr = (b * b) - (4.0 * a * c)
          a = vectorsum (rv * rv)
          c = vectorsum (se * se) - (radius * radius)
          sqrtdscr = sqrt dscr

The maths is quite simple - we want to find points on the half-line that are at a distance radius from the point centre, so we solve the resulting quadratic equation, and then pick the first intersection if any exists. We also make sure we only consider intersections that occur forward in time - i.e. in front of the camera, not behind it!

Next part: Scene drawing.

comment on this page