Arrowised Material Shaders in Purely Functional Ray Tracing
This article assumes at least a basic understanding of arrows as they are implemented in Haskell. It you haven’t come across them before, I recommend this excellent tutorial by Ertugrul Söylemez which should tell you everything you need to know.
For my final year project at university, I developed a purely functional ray tracer in Haskell which I have since open sourced. While working on it as a project I had extremely limited flexibility with regards to adding additional features or substantially altering the design. However, now that the project is done and dusted, I have been working on a few changes I’ve been wanting to make for a while; one of which is a fully programmable material shader system to replace the original pre-defined set of choices for materials.
1 2 3 4 5 6
The different constructors for
Material were pattern-matched in a giant monolithic function which handled each material type in its own way. While I wasn’t happy with this system, it served its purpose for the project, which didn’t require much in the way of flexibility or extensibility. Even though HaskRay isn’t intended for any sort of serious use, I felt a a more programmable system would not only be interesting but could teach me a lot at the same time. This post describes and attempts to explain the system I devised using John Hughes’ arrow abstraction to create programmable materials.
What is a material really?
If you’ve ever used a 3D program such as Blender or 3DS Max you’ve probably seen some kind of material properties panel (left). If not, it’s a very simple method of defining the way a surface is rendered by choosing colours and numerical values for different elements such as a base colour, specular highlights, transparency, reflection, etc. This provides a certain intuition of what a material is to an artist, but how materials are really represented inside a ray tracer is quite different. The mathematical core of a ray traced material is the Bidirectional Reflectance Distribution Function (BRDF) – a function which defines the ratio of light reflected in the eye direction to the negative incident light direction, with respect to the surface normal (right).
What does a BRDF look like as a Haskell function? Well in order to calculate raw intensity we only require three values: view direction, light direction and the surface normal. HaskRay’s
Intersection type encapsulates the view direction and surface normal, the light direction can be provided as a separate parameter. The type
Intersection -> Vec3 -> Scattering seems reasonable, and such a function can be partially applied to get a function
Vec3 -> Intensity which can then be mapped over multiple light sources. This type could also be made into a monad by replacing
Scattering with a type parameter. This would enable us to compose materials together to create more complicated ones – just like functions in the
State monad can be composed.
This works well enough for simple diffuse materials, but in order to create mirror or glass-like materials, or cast shadows, the BRDF must be able to trace additional rays. The trace function in HaskRay is in the
Render monad – a simple state monad used elsewhere in the renderer for random number generation and tracking recursive depth. Adding the HaskRay trace function into our BRDF type gives us
(Ray -> Render (Maybe (Scalar, Intersection, Scattering Colour, Bool))) -> Intersection -> Vec3 -> Render a which is still a monad – even though it’s using another monad internally.
However, there is one more piece missing from the puzzle, and that is the ability to track which materials emit light and need to be treated as a light source. This important for the renderer to know so that it can calculate light direction vectors to pass to the BRDFs (it could assume that every material emits light but that would be computationally impractical). This extra piece of information must be kept along side the BRDF and preserved when composed with other materials. However, the material is no longer a monad at this point, but an arrow instead. This is what it looks like:
1 2 3 4 5 6 7 8 9 10 11
Material a monad? Looking at the type of
>>= makes it quite clear:
(>>=) :: Monad m => m a -> (a -> m b) -> m b. If the left hand side of
>>= is emissive then the combination with another material must also be emissive. However, if the left hand side is not emissive, then the new value of
isEmissive depends on the computation generated by the right hand side – which can’t be known within the definition of
>>=. Not convinced? Try and implement
>>= for yourself!
Even though it’s not a monad,
Material is an arrow! Here are the instance definitions for
1 2 3 4 5 6 7 8 9 10 11
The definitions are a little messy, but such is life when dealing with a type such as
Material. It could also be beneficial in some cased to define materials with conditional statements. To enable these, we also require an instance for
1 2 3 4 5
Lets define some basic materials. My own understanding of how these functions should be implemented is limited at best; it’s something I intend to improve later down the line. If you’re not familiar with shading algorithms, feel free to gloss over these definitions:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
It would also be nice to include some functions which can combine the effects of different materials. Here we define
mixMaterial which operate on
Scattering values to blend them together.
1 2 3 4 5 6 7 8 9 10 11
By composing primitive materials like
transmissive with functions like
mixMaterial, more complex materials can be created. For example, a glass-like material, which adds a small amount of reflection to a refraction, can be created from these simple building blocks like so:
1 2 3 4 5 6
All this provides an extensible framework for building arbitrarily complex materials, while enabling the renderer to recognise materials which act as light sources. I intend to continue working on this when I have time – both creating more exotic materials with this system and improving the rest of the renderer. To finish, here is a scene rendered with the materials and system just discussed.