Rendering in Haskell, Part 5: Reflections

Reflections This is another simple diff compared to the last experiment. All I’m adding is the ability to create mirrored surfaces.

Mirrored surfaces require the ability to recursively render the scene from different viewpoints. To capture this ability, I add a new type that represents a function that can render a scene, given a Ray, and produce a Light value:

type RenderRay = Ray -> Light

The Material type also changes to take this function as a parameter:

type Material = [PointLightSource] -> Ray -> Point -> UnitVector -> RenderRay -> Light

The various functions that already exist within the Material module change accordingly to pass this parameter around, or to ignore it.

Since many of the reflective surfaces I want to represent won’t be fully reflective, I need a way of scaling different materials. For this, I added the scaleMaterial function:

scaleMaterial :: Material -> Double -> [PointLightSource] -> Ray -> Point -> UnitVector -> RenderRay -> Light
scaleMaterial material factor lights ray ixp surfaceNormal renderRay =
    material lights ray ixp surfaceNormal renderRay `scaled` factor

(Although thinking about it, I haven’t been very consistent here. All the core material functions take a scaling factor as a parameter, but the addMaterials function doesn’t. I should probably have removed the scaling factor from all of them, and left the scaling to the above function.)

With that refactoring in place, I can now write the actual reflectiveMaterial function. It calculates the reflected ray from the surface, and then uses the recursive function supplied to calculate the light being reflected:

reflectiveMaterial :: Double -> [PointLightSource] -> Ray -> Point -> UnitVector -> RenderRay -> Light
reflectiveMaterial !factor _ (Ray _ !rd) intersectionPosition surfaceNormal renderRay =
    reflectLight `scaled` factor
  where
    c1 = - (surfaceNormal |.| rd)
    r1 = rd |+| (surfaceNormal |*| (2 * c1))
    reflectRay = Ray intersectionPosition (normalize r1)
    reflectLight = renderRay reflectRay

Of course, with any recursive algorithm, I have to make sure that the recursion terminates after a set number of iterations. To support this, I modify the renderRay function:

renderRay :: Ray -> Scene -> Color
renderRay ray scene =
    toColor $ renderRayRecursive scene 4 ray

That value 4 is the maximum number of times light is allowed to bounce within the scene. In normal situations, this is perfectly acceptable – it’s unlikely to show artifacts unless the scene contains mirrors that face each other.

Finally, renderRayRecursive is defined as follows:

renderRayRecursive :: Scene -> Int -> Ray -> Light
renderRayRecursive scene level ray
    | level <= 0 = black
    | otherwise  = fromMaybe black maybeColor
  where
    maybeColor = do
        (Intersection rt (Surface _ nrm mat) _ wp) <- sceneIntersection scene ray
        return $ mat (pointLightSources scene) rt wp (nrm wp) $ renderRayRecursive scene (level - 1)

If the limit is exceeded, simply return no light, otherwise calculate the intersection and render as usual. Notice that the recursive render function is pased to the material, but with a reflection counter one less than before.

The end result is an image that looks as follows:

Reflections

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

Written on November 28, 2015. Category: Rendering in Haskell