Some people are remarkably picky. They actually want to see reflections in a ray tracer. Can you believe that? Here you go then!

Some people are remarkably picky. They actually want to see reflections in a ray tracer. Can you believe that? Here you go then!

I’m giving a talk at a user group meeting next week on how to program in Haskell. Rather than just listing off language constructs, I wanted to motivate the whole thing with an actual application. So that’s how I ended up spending yesterday afternoon writing a ray tracer in Haskell. I’m certainly not the first to do so, nor the best, but I am writing this blog post to explain the process I went through. I’ll try making this a literate Haskell post, so you can copy and paste it into a source file (extension .lhs) and run it. The actual code is split into three (very short) modules, but this should work.
Some imports to start us off:
> import Data.Maybe (mapMaybe) > import Data.Function (on) > import Data.List (minimumBy) > import Data.Ix (range) > import Control.Monad (forM_) > import Graphics.UI.Gtk hiding (Color, Point, Object) > import qualified Graphics.UI.Gtk as GTK
First, I built a couple basics of vector math. Out of a desire to demonstrate some list manipulation functions in the presentation, my Vector type is simply [Double].
> type Vector = [Double] > > (<+>) = zipWith (+) > (<->) = zipWith (-) > (<*>) = zipWith (*) > vlength u = sqrt (sum (map (^2) u)) > normalize u = map (/ vlength u) u > u .*. v = sum (u <*> v) > k #* v = map (*k) v
Since vectors don’t really even approximate a ring, it seems quite inappropriate to call them Num. Therefore, I’ve invented my own operators. The operators in angle brackets are component-wise, while the normal dot and scalar products are given their own operators, .*. and #*, respectively. I found this to be quite usable, though it would be equally reasonable to eschew operators, and define these as named functions to be used in infix notation.
I then define a few precedences, to make these easier to use.
> infixl 6 <+> > infixl 6 <-> > infixl 7 <*> > infixl 7 .*. > infixl 7 #*
I’ll treat points as displacement vectors from an origin, and colors as vectors in a color space, but it helps to use separate names for them when writing type signatures:
> type Point = Vector > type Color = Vector
Now for a few real data types. The idea is that an object has some general-purpose properties, plus a shape. The shape could be arbitrary, but is currently limited to only spheres and infinite planes. (Hey, this talk is only an hour and a half long!) So I’ve separated it out. The other properties I’ve got are material properties: how shiny (that is, reflective) is the surface, and what is its color. These are defined as functions from a point in space to the desired values, but in practice I’m just going to use const.
> data Object = Obj { shape :: Shape,
> objectColor :: Point -> Color,
> objectShine :: Point -> Double }
>
> data Shape = Sphere { center :: Point, radius :: Double }
> | Plane { point :: Point, normal :: Vector }
And lights are important to. Right now, I’ve got point lights and ambient lights. Adding directed lights (e.g. spot lights) wouldn’t be too difficult, but it is neither of critical importance nor necessary to demonstrate a Haskell language idea, so I’ve left it out.
> data Light = PointLight { lightPt :: Point, lightColor :: Color }
> | AmbientLight { lightColor :: Color }
The two things we need to know about any shape are how to compute the distance to it along a given ray (specified by origin and a unit direction vector), and how to find the normal (another unit vector) at a given point on the surface of the object. These next two functions do this. They are defined for spheres and planes, but can of course be extended to new variants of Shape via additional pattern matching.
> shapeDistance :: Shape -> Point -> Vector -> Maybe Double > > shapeDistance (Sphere c r) orig dir = > let p = orig <+> (c <-> orig) .*. dir #* dir > d_cp = vlength (p <-> c) > d = vlength (p <-> orig) - sqrt (r^2 - d_cp^2) > ans | d_cp >= r = Nothing > | (p <-> orig) .*. dir <= 0 = Nothing > | otherwise = Just d > in ans > > shapeDistance (Plane p n) orig dir > | dir .*. n >= 0 = Nothing > | otherwise = Just (((p <-> orig) .*. n) / (dir .*. n)) > > shapeNormal :: Shape -> Point -> Vector > shapeNormal (Sphere c r) pt = normalize (pt <-> c) > shapeNormal (Plane p n) pt = n
The math there can be derived without a whole lot of trouble, provided you’re relatively familiar with the basic ideas of vector operations. It surprises me, though, how many people only understand dot products in terms of their implementation (the sum of the component-wise products, or a b cos(theta)), and not in terms of what they mean. In particular, when one of the vectors in a dot product is a unit vector, the dot product is the component of the other vector along the direction of the unit vector. That’s a pretty powerful tool to have.
Next come the lights. For the lights, the general question is how much (in each color component) the given light illuminates a particular point with a particular normal. The object list is needed here as well, since lights can be occluded. This is all captured in a function applyLight, which is implemented for each type of light. It’s trivial for ambient lights, since by definition they ignore all the specifics of the situation; but it’s non-trivial for point lights. I also fence all light components to eliminate “negative” lights.
> fence a b x | x < a = a > | x > b = b > | otherwise = x > > applyLight :: [Object] -> Point -> Vector -> Light -> Vector > applyLight objs pt n (PointLight lpt color) = > let dir = normalize (lpt <-> pt) > dist = vlength (lpt <-> pt) > lval = (n .*. dir) / dist^2 #* color > final = map (fence 0 9999) lval > in case findNearest objs pt dir of > Just (_,opt) > | vlength (opt <-> pt) < dist -> [0,0,0] > | otherwise -> final > Nothing -> final > > applyLight objs pt n (AmbientLight color) = color
Now for the common stuff. A very common thing we want is to take our list of objects and find the nearest point of intersection for a given ray. This function does that, by calling objectDistance for each object in turn, and then choosing the minimum one. It gives back Nothing if there is no intersection, or Just (obj,point) if there is.
> findNearest :: [Object] -> Point -> Vector -> Maybe (Object, Point) > findNearest objs orig dir = > let consider obj = case shapeDistance (shape obj) orig dir of > Nothing -> Nothing > Just d -> Just (obj, d) > result = mapMaybe consider objs > in case result of > [] -> Nothing > _ -> let (obj,d) = minimumBy (compare `on` snd) result > in Just (obj, orig <+> d #* dir)
And finally, the ray tracing itself. I’ve also thrown in defaultColor, which is the color assigned to rays that have either reflected back and forth beyond the recursion limit, or that proceed into infinity. For now, this is always black.
> defaultColor :: Vector -> Color > defaultColor _ = [0, 0, 0] > > trace :: [Object] -> [Light] -> Point -> Vector -> Int -> Color > trace objs lights orig dir 0 = defaultColor dir > trace objs lights orig dir limit = > case findNearest objs orig dir of > Nothing -> defaultColor dir > Just (obj, pt) -> > let n = shapeNormal (shape obj) pt > ndir = dir <-> 2 * (n .*. dir) #* n > refl = trace objs lights pt ndir (limit - 1) > color = objectColor obj pt > shine = objectShine obj pt > lvals = map (applyLight objs pt n) lights > lighting = (foldl (<+>) [0,0,0] lvals) > in_light = shine #* refl <+> (1 - shine) #* lighting > in map (fence 0 1) (color <*> in_light)
Finally, there’s the GUI stuff. I won’t discuss this much, except to say that it’s horridly bad. I’m doing the actual ray tracing in an onExpose event handler. If I had even a modicum of common sense, I’d be rendering to an image or something, so that I wouldn’t have to redo all the ray tracing just because you switched over to a different window. Oh well. I’ll improve this before the talk on Tuesday.
> objs = [
> Obj (Sphere [-50, -50, 100] 100)
> (const [1.0, 0.3, 1.0])
> (const 0.1),
> Obj (Sphere [ 50, 50, 100] 100)
> (const [0.2, 1.0, 0.2])
> (const 0.1),
> Obj (Plane [-100,100,0] (normalize [1,-0.2,-0.2]))
> (const [1.0,1.0,1.0])
> (const 0.0)
> ]
>
> lights = [ PointLight [175, -200, 10] [40000.0, 20000, 20000],
> AmbientLight [0.0, 0.05, 0.1]
> ]
>
> drawScene d ev = do
> dw <- widgetGetDrawWindow d
> (w,h) <- widgetGetSize d
> gc <- gcNew dw
>
> let eye = [0, 0, -500]
>
> forM_ (range ((0,0), (w-1, h-1))) $ \(x,y) -> do
> let (x', y') = (fromIntegral x - fromIntegral w / 2,
> fromIntegral y - fromIntegral h / 2)
> let [r,g,b] = trace objs lights eye
> (normalize ([x',y',0] <-> eye)) 5
> let fg = GTK.Color (round (65535 * r))
> (round (65535 * g))
> (round (65535 * b))
> gcSetValues gc $ newGCValues { foreground = fg }
> drawPoint dw gc (x,y)
> return True
>
> main = do
> initGUI
> w <- windowNew
> d <- drawingAreaNew
> windowSetTitle w "Ray Tracer"
> containerAdd w d
> onExpose d (drawScene d)
> onDestroy w mainQuit
> widgetShowAll w
> mainGUI
And there you go: a ray tracer! Of course, there are many things that ought to be improved. They range from the trivial (render to an off-screen image, perhaps save to an image file, and read from a file instead of coding the scene as an immediate data structure) to the very complex (implement diffuse effects, more complex interpolated curves, etc.). But as a quick demo goes, this one didn’t turn out so bad.

Last Thursday, I attended a meeting of the Colorado Springs Open Source Meetup Group. I ended up offering to record and post videos of the presentations. They are now available. Matthew McCullough’s presentation on JavaFX is here, and Scott Ryan’s presentation on jQuery is here. I should mention that I had absolutely nothing to do with either of these presentations, except that I showed up with the video camera and microphone. This was a test run for recording my presentations at C-SPLAT (the Colorado Springs Programming Languages and Tools interest group) next week. So as far as content goes, I take no responsibility for inaccuracies or unusual views expressed in the talks.
As test runs go, all was pretty smooth. The biggest problem was that Google Video has been having some problems, and took most of a week to actually make the videos available for viewing after I uploaded them. Also, Google’s processing reduced the quality to the point that you can’t easily read the presentation slides. Next time around, I might see about seeding them in BitTorrent as well as uploading to Google Video; and of course I’ll make my slides available in PDF alongside the video.
Well, after the catastrophic failure of my web site earlier this week, I’ve put everything back at http://www.pphsg.org/cdsmith.
In case you’re wondering, the PPHSG is a group I started in 2001 to organize science and math education activities for home-schooled children. At the time, I had a neighbor who was home-schooling. The PPHSG still continues to this day, but I’ve passed on leadership to some actual home-schooling parents who have decided not to use the web site. So voila, instant replacement personal web site!