Now that I’ve got the business of being able to write image files from Haskell sorted, I need to
move on to the next most simple thing: projecting three-dimensional shapes onto the screen. Here
I’m not going to worry about lighting - everything will be flat shaded.
The main source on Github is split into
separate folders for each ‘experiment’. Each folder starts as a copy-paste of the previous,
so you can see what’s changed to get from the previous stage to the next just by diffing
the source trees.
I’ve collected the core maths routines, into a single file:
It’s all pretty standard stuff so I won’t show the code, but suffice to say I defined the following types:
Ray (a combination of a Point and a Vector)
I have a typeclass Transform that allows me to translate (move) Points and Vectors.
Finally I have utility functions:
to: Get a Vector between two Points;
normalize: Convert a Vector to have unit length;
magnitude and magnitudeSquared, to get the length of a vector;
neg, to reverse a vector;
|*|, for vector scaling;
|+|, to add two vectors;
cross, to find the cross-product between two vectors;
|.|, to take the dot product between two vectors.
I represent three-dimensional objects in the scene as surfaces. Each surface only needs two pieces
of information: a function that determines whether a ray intersects the surface, and a color to
shade the surface.
In this first example, I only support two kinds of surfaces: planes and spheres.
Spheres are always created at the origin for simplicity:
That’s clearly very limiting, so I allow a Surface to be translated using the Transform typeclass,
in exactly the same way that I allow Points and Vectors to be translated:
A Scene is simply a collection of Surfaces. In keeping with much of the rest of the code, the actual
data constructur is private to the module, and only a constructor function is exposed:
The primary function of a Scene is to manage testing rays against the surfaces within it. If a Ray
intersects a Surface, we want to know details about that intersection:
When a Ray is cast into a Scene, we need to know the closest Surface that intersected. Here
I do this via a very simple brute-force linear scan of all Surfaces, ordered by distance from
the ray’s origin. (Later I expect I’ll have to change this to a more optimal algorithm, but it will
do for now).
Everything here is in terms of Maybes - a Ray might or might not intersect a Surface.
The rendering function for this experiment is very simple: if we intersect a Surface, we simply
use its defined flat color:
Generating the Rays from pixel positions is slightly more involved, but is a basic perspective
(This function matches the x,y,width,height format used previously, so it slots straight into the
bitmap render function we used before).
For my example scene, I’m using a modified Cornell box:
Finally, my main function is modified slightly to tie everything together, viewed from a suitable
camera angle (supplied as a Ray):
The final image is as follows:
It’s still nowhere near being photorealistic, but at least we have basic intersection tests in place,
plus perspective transforms.