Rendering in Haskell, Part 6: Shadows

Shadows This is another simple diff compared to the last experiment. This change adds hard shadows to the existing ray-tracing algorithm.

This is also the last in the series about ray-tracing and local illumination: the next article will be about global illumination via photon mapping techniques. (Photon mapping produces much more realistic images, at the cost of performance).

To render hard shadows, I need the ability to determine which light sources are visible at a given point in the scene. (If a light source cannot be ‘seen’ from a point on a surface, then that light source doesn’t contribute any light to the point).

So I’m going to replace the pointLightSources function with two others: allPointLightSources, which does exactly the same as before, but which has a name that indicates that it’s returning all the lights in the scene, and pointLightSourcesVisibleFrom, a version that only returns those lights that arre visible from a particular point.

Deliberately breaking existing code by renaming functions is a very useful technique when splitting functionality like this. It forces you to consider, at each point where the old function was used, which of the new variants should be used in its place.

So allPointLightSources simply returns all light sources from the scene:

allPointLightSources :: Scene -> [PointLightSource]
allPointLightSources (Scene _ !pointLightSources) =
    pointLightSources

Whereas pointLightSourcesVisibleFrom filters those light sources according to whether they’re visible from that point:

pointLightSourcesVisibleFrom :: Scene -> Point -> [PointLightSource]
pointLightSourcesVisibleFrom scene@(Scene _ !lights) !point =
    filter (isLightVisibleFromPoint scene point) lights

isLightVisibleFromPoint :: Scene -> Point -> PointLightSource -> Bool
isLightVisibleFromPoint !scene !point (PointLightSource !lightPosition _) =
    go maybeIntersection
  where
    (!toLight, lightOffset)                  = normalizeWithLength (point `to` lightPosition)
    !rayToLight                              = Ray point toLight
    !maybeIntersection                       = sceneIntersection scene rayToLight
    go Nothing                               = True
    go (Just (Intersection _ _ !ixOffset _)) = ixOffset > lightOffset

With this function in place, I can modify the core render function to only consider those lights that are visible when rendering a point on a surface:

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
        let surfaceNormal    = nrm wp
        let movedFromSurface = translate (surfaceNormal |*| epsilon) wp
        let lights           = pointLightSourcesVisibleFrom scene movedFromSurface
        let recursiveRender  = renderRayRecursive scene (level - 1)
        return $ mat lights rt wp surfaceNormal recursiveRender
    epsilon = 0.0001

The complexity there is the movedFromSurface part. I need to explain that…

When I first implemented the shadows feature, it sort of worked, but with obvious visual artifacts (striping, or some surfaces simply unlit).

After a while it because obvious that the problem was that when trying to work out whether each light was visible from a point on a surface, the surface itself was interfering with the process: depending on the whims of floating point rounding, the surface would sometimes occlude all lights in the scene.

I could think of two potential fixes for this problem:

  • Pass the active surface to the pointLightSourcesVisibleFrom function, pass it all the way down through all intersection functions, and ignore it, or:
  • Move the start position of the intersection test a tiny amount way from the surface (along the surface normal), and work from there.

Although the first solution is the “correct” one, and the second is definitely a fudge, the second is also way simpler to implement. Since I knew I was moving on to global illumination shortly anyway, and the fudge solution produced good results, I went with that one.

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 29, 2015. Category: Rendering in Haskell