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:
pointLightSourcesVisibleFrom
function, pass it all the way down through all intersection functions, and ignore it, or: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.
Published: Sunday, November 29, 2015
Hackification.io is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to amazon.com. I may earn a small commission for my endorsement, recommendation, testimonial, and/or link to any products or services from this website.