HRay: A Haskell ray tracer
by KennethHoste for The Monad.Reader IssueFive
2005/06/11
Abstract. As a thesis subject for Ghent University, I chose to write a ray tracer (HRay) in Haskell. The goal was to show how elegant, short and maintainable a ray tracing implementation would be in a functional language, as opposed to an imperative or procedural language. To achieve that goal, I first created a formal model for the application, using the functional and declarative formalism Funmath [1]. In a next phase, the model was implemented in Haskell to create a working version of the ray tracer. Additional features were added to the model, including a command line and graphical user interface, a parser for input specifications and neat texture support using the Perlin Noise technique.
This article describes the Haskell implementation of HRay. For more information about the formal model and my thesis, check the HRay website at
http://www.elis.ugent.be/~kehoste/Haskell/HRay.
Contents
- Overview
- Ray tracing
- Math functions
- Ray tracing engine
- Types and datatypes
- Intersection functions
- Intersecting
- Determining color
- Recursive functions
- Core functions
- Perlin noise textures
- A scene description parser
- User interfaces
- Performance issues
- Conlusions
- References
1. Overview
Before discussing the actual implementation, a brief description of the ray tracing algorithm is given. The implementation is split up into several modules, which are discussed one by one. First of all, the necessary mathematical functions to support the ray tracing algorithm are discussed. Based on that module, functions are presented which implement the ray tracer. To support some extra textures, a Perlin noise module is implemented, which will be discussed briefly. The two user interfaces are presented, one using command line arguments and one using the Haskell GUI library
Gtk2Hs. Finally, some conclusions are drawn.
2. Ray tracing
The powerful ray tracing algorithm is well known for the amazing results it produces [2]. Because the rest of the article is based on the algorithm, we'll give a brief description of it. If you want more details, a good place to start is [3].
As the name states, the ray tracing algorithm is built upon the notion of rays. A ray is constructed through the point of view and some point of the view window. Given a desired resolution and collection of 3D objects, we calculate the intersection of the ray with the objects, for each pixel of the image. Again using rays, which are constructed as needed, the color for the intersection point (if any) is determined, depending on the color (or texture) of the object and the surrounding objects and light sources. Iterating over all pixels of the image results in a matrix of colors, representing the image.
In pseudo-code:
for each pixel
construct ray from view point to pixel location
(*) determine closest intersection of ray with objects
if no intersection
pixel color = background color
else
for each light source
construct ray from intersection point to light source location
if intersection found between intersection point and light source
shadow, so color = black
else
determine color contribution for light source, remember it
if reflective surface
construct reflected ray, determine contribution (*)
if transparent surface
construct refracted ray, determine contribution (*)
determine color for pixel by summing all contributions
This is all quite imperative. The next step we need is to construct functions which can help with implementing the algorithm using a functional style. This is where the formal model came into play, but because we want to concentrate on the implementation of it all in Haskell, I'll just use the model informally: we'll first construct some basic math functions before moving on to the actual ray tracing engine. The needed types and datatypes are presented as needed.
3. Math functions
3.1. Types and datatypes
First of all, we mention the several (data)types needed to support the needed basic math functions. Because we are making a transition from a 2D to a 3D world, we need to be able to represent points in both worlds. Also, we need vectors for direction purposes, which are defined the same way as points in the 3D world are. This was a little point of critique in the formal model, but because in Haskell giving objects two different types is sufficient to distinguish between them, there is no problem. On the contrary, this way of defining the types supports overloading of functions such as multiplying a point/vector with some factor, and calculating the sum of two points/vectors.
type Point2D = (Int,Int) type Point3D = (Double,Double,Double) type Vector = (Double,Double,Double)
