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:
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.
allPointLightSources simply returns all light sources from the scene:
pointLightSourcesVisibleFrom filters those light sources according to whether they’re
visible from that point:
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:
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
pointLightSourcesVisibleFromfunction, 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:
Code is in Github, if you want to take a look.