Rendering in Haskell, Part 1: Writing an Image

Image drawn in Haskell As part of my ongoing aim of trying to learn Haskell, I decided to take my ray-tracing experiments a step further and this time write a global illumination renderer.

The previous ray-tracing code implemented local illumination: each pixel rendered, and hence each surface drawn, has its illumination calculated completely independently of any other surface. This is fast, easy to implement, but has the downside of producing images that look pretty artificial. This series aims to implement a global illumination renderer: the light is calculated across the scene as a whole.

The first thing I needed to check was that I could actually successfully write out an image, from Haskell code.

Simple stuff first: I need a representation of an RGB value:

data Color = Color
  { red    :: !Double
  , green  :: !Double
  , blue   :: !Double
  }

I need something that will generate some colors on the final image, and let me check I have vertical and horizontal orientation correct:

testRenderFunction :: Int -> Int -> Int -> Int -> Color
testRenderFunction !x !y !w !h =
     Color (fromIntegral x / fromIntegral w)
           (fromIntegral y / fromIntegral h)
           0.0

(So I have red increasing left-ro-right, and green increasing top-to-bottom. There will be no blue component).

For the actual writing of the image, I found Codec.BMP. (Unfortunately there doesn’t seem to be an easy way to write PNGs… I might look into it a little more at a later date, but for now BMPs will do).

The following function will write a BMP, with pixel colors determined by a rendering function that takes (x, y, w, h) values and gives a color:

saveRender :: FilePath -> Int -> Int -> (Int -> Int -> Int -> Int -> Color) -> IO ()
saveRender path width height render = do
    let bytes = concat $ do
            !y <- [height - 1, height - 2 .. 0]
            !x <- [0 .. width - 1]
            let (Color r g b) = render x y width height
            return [toByte r, toByte g, toByte b, 255]
    let rgba = pack bytes
    let bmp  = packRGBA32ToBMP width height rgba
    writeBMP path bmp

toByte :: Double -> Word8
toByte !d =
    truncate (d * 255)

Finally, I can launch the whole thing from a simple main function:

main :: IO ()
main = do
    putStrLn "Starting render..."
    createDirectoryIfMissing True "output"
    saveRender "output/experiment00.bmp" 640 480 testRenderFunction
    putStrLn "Written output to output/experiment00.bmp"

All together, that gives me the following image:

A simple RGB image written from Haskell

It’s not a three-dimensional rendered scene, but as a first step I’ve shown how to write an image from Haskell.

Code is in Github, if you want to take a look.

Written on July 5, 2015. Category: Rendering in Haskell