Rendering in Haskell, Part 4: Specular Lighting

Specular lighting This is a really tiny diff compared to the last experiment - I’m just going to add a new material type of ‘specular’.

I factored out a bit of common code (lightDistanceAndRay), and added specularMaterial:

diffuseMaterial :: Color -> Double -> [PointLightSource] -> Ray -> Point -> UnitVector -> Light
diffuseMaterial !col !factor !lights _ intersectionPosition surfaceNormal =
    sumLights $ map diffuseLight lights
  where
    diffuseLight (PointLightSource !lightPosition !lightColor)
        | diffuseFactor > 0 = lightColor `colored` col `scaled` diffuseFactor
        | otherwise         = black
      where
        (lightDistance, lightRay) = lightDistanceAndRay intersectionPosition lightPosition
        lightAttenuation          = 1.0 / lightDistance
        diffuseFactor             = factor * (surfaceNormal |.| lightRay) * lightAttenuation

specularMaterial :: Double -> Double -> [PointLightSource] -> Ray -> Point -> UnitVector -> Light
specularMaterial !factor !shininess !lights (Ray _ !rd) intersectionPosition surfaceNormal =
    sumLights $ map specularLight lights
  where
    specularLight (PointLightSource !lightPosition !lightColor)
        | cosFactor > 0 = lightColor `scaled` factor `scaled` (cosFactor ** shininess) `scaled` lightAttenuation
        | otherwise     = black
      where
        (lightDistance, lightRay) = lightDistanceAndRay intersectionPosition lightPosition
        lightReflect              = surfaceNormal |*| ((surfaceNormal |*| 2) |.| lightRay) |-| lightRay
        cosFactor                 = lightReflect |.| neg rd
        lightAttenuation          = 1.0 / lightDistance

lightDistanceAndRay :: Point -> Point -> (Double, UnitVector)
lightDistanceAndRay intersectionPosition lightPosition
    = (lightDistance, lightRay)
  where
    lightVector   = intersectionPosition `to` lightPosition
    lightDistance = magnitude lightVector
    lightRay      = normalize lightVector

Notice that in diffuseMaterial, we ignore the Ray parameter (the ray from camera to surface), but in specularMaterial, it’s part of the lighting calculation. Hopefully this makes sense - a matt material looks roughly the same no matter where you view it from, but a shiny surface will have different brightness depending on the viewing angle. (If you happen to catch a reflection of a light, it will appear bright at that point, but if not, the surface at that point will be dim).

Rather than have material variants for only diffuse, only specular, and part-diffuse-part-specular surfaces, I’ve added a method that lets me combine different surface material kinds:

addMaterials :: [Material] -> [PointLightSource] -> Ray -> Point -> UnitVector -> Light
addMaterials materials lights ray ixp surfaceNormal =
    sumLights $ map (\m -> m lights ray ixp surfaceNormal) materials

So with all that in place, the final image looks as follows:

Specular lighting

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

Written on July 20, 2015. Category: Rendering in Haskell