Haskell Raytracer Project
In this series of articles I'll show you how to make a basic raytracer
(3D renderer) with Haskell. You'll need to have
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:
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.
A PPM file is a text-based format for storing images. Example:
P3 50 50 255 100 200 50 84 21 4 ...
P3identifies the file as being a PPM file with 3 colour channels per pixel - red, green and blue.
50is the width of the image (in pixels).
50is the height of the image (in pixels).
255means 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:
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):
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.