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:
Code is in Github, if you want to take a look.
Published: Saturday, November 28, 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.