Haskell Raytracer Project

Parts of this tutorial

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

In this series of articles I'll show you how to make a basic raytracer (3D renderer) with Haskell. You'll need to have ghc and ghci installed. On Ubuntu you can do sudo apt-get update && sudo apt-get install ghc to install them. At the end of the series the raytracer will be capable of producing images like this one:

Raytraced Image

If you're primarily interested in the raytracing algorithm, feel free to skip to part 2.

In this part we will make functions to write bitmap images from Haskell. I've chosen to use the PPM image format because it's really simple. The only downside to the PPM format is that the files it makes are very large - but we can always use another program to convert them to PNGs afterwards.

PPM Format

A PPM file is a text-based format for storing images. Example:

P3
50
50
255
100 200 50
84 21 4
...
  • P3 identifies the file as being a PPM file with 3 colour channels per pixel - red, green and blue.
  • 50 is the width of the image (in pixels).
  • 50 is the height of the image (in pixels).
  • 255 means that colour values will be integers between 0 and 255.

Each line after those three contains the red, green and blue values for a single pixel, scanning the image in rows left to right, downwards.

So e.g. a 2x3 image would have its pixels stored in this order:

1 2
3 4
5 6

We'll represent images interally as a list of colour values held in the same order - i.e. in rows left to right, downwards.

Formatting with spaces

We'll want a function to join strings together to use in our function to output PPM files.

-- Join a list of strings using a separator string.
join :: String -> [String] -> String
 
join sep [x]    = x
join sep (x:xs) = x ++ sep ++ join sep xs

It's important to test code as its written, so write a test for join and put it in the same file.

This is my test:

-- Test for "join"
test_join :: Bool
test_join =  join "t" ["12", "3", "", "xy"] == "12t3ttxy"

Writing a test that returns a boolean makes it easy to automate testing (i.e. to run lots of tests at once from one command).

Use ghci to run your test.

ghci Image.hs

*Image> test_join
True

Colours and Images

We'll represent a colour by a list of integers, and an image by width, height and a list of colour samples.

type Colour = [Integer]
type Image = (Integer, Integer, [Colour])

Using what we know about PPM files, and with our join function, we can construct a function to input an image (see 'Image' above) and output the contents of the PPM file representing that image.

Let's start with the type. Simple stuff: it takes an image and produces a string.

-- Create a string suitable for saving as a PPM file to represent an image.
create_ppm :: Image -> String

Implementing the function is just string manipulation now. Have a look at the definition below and work out what it's doing - compare it to the description of PPM files from earlier.

create_ppm (width, height, im) = join "\n" line_list ++ "\n"    
    where line_list = ["P3", show width ++ " " ++ show height, "255", 
                       map (join " " . map show) im]

Let's add a regression test. We'll make a 2x3 image with some arbitrary colour values. All values passed in are distinct so this should catch any bugs where values are output in the wrong order.

test_create_ppm :: Bool
test_create_ppm =  create_ppm (2, 3, [[11, 12, 13], [22, 23, 24],
                                      [33, 34, 35], [44, 45, 46],
                                      [55, 56, 57], [66, 67, 68]]) ==
                   "P3\n2 3\n255\n11 12 13\n22 23 24\n33 34 35\n44 45 46\n" ++
                   "55 56 57\n66 67 68\n"

Run the test:
ghci Image.hs

*Image> test_create_ppm
True

We ought to check that our understanding of the PPM format matches everyone else's. Let's write a simple PPM image file and check we can open it with another program (e.g. The GIMP).

test0 = writeFile "test0.ppm" (createppm 16 16 (map mkcol [0..255]))
        where mkcol x = [x,x,x]

The image produced (scaled up 16x and converted to PNG):

PPM file converted to PNG

In part 2 we write some code to make manipulating vectors easier, before moving on to working out intersections of lines and spheres in part 3.