FunTracer: adding shadow

This time we will see how to add shadows to our scene. Imagine this scene:

FunTracer without Shadow

that is generated by this snippet:

    let scene =
        let eye = Vector.Create(0.0, 0.0, -10.0)
        let center = Vector.Zero
        let up = Vector.Create(0.0, 1.0, 0.0)
        let right = Vector.Create(1.0, 0.0, 0.0)
        let vp = new ViewPort (eye, center, up, right, 20.0, 20.0)
        let s = new Scene(vp)

        Sphere.Create (Vector.Create(0.0, 0.0, 10.0), 5.0, Colors.Red)
        |> s.AddObject

        Sphere.Create (Vector.Create(-3.5, -9.0, 9.0), 4.0, Colors.Green)
        |> s.AddObject

        Sphere.Create (Vector.Create(0.5, 7.5, 4.0), 3.0, Colors.Blue)
        |> s.AddObject

        let sun = PointLight.CreateWhite(Vector.Create(1.0, 20.0, -10.0))
        s.AddLight(sun)

        let dir = DirectionalLight.CreateWhite(Vector.Create (0.0, -1.0, 0.1))
        s.AddLight(dir)

        s

The basic idea is to only use a directional light (ambient will always be used) if there is no other object between the point (we are shading) and the light-source.

Of course we can easily check this using findHitObj from our last article. But as we need to test maybe a couple of other objects (remember we passed traceRay the possible objects in it’s path). Therefore we have to rethink this approach. Instead of giving the objects I will pass a method around that retrieves the possible objects passed on a given ray – for now this will just result in every object in the scene but we might improve on this. So here is the adapted code:

module RayTrace =

    let findHitObj (getSigObjs : Ray -> SceneObj seq) (ray : Ray) =
        ray |> getSigObjs
        // intersect ray with each possible object
        |> Seq.map (fun o -> o.HitTest ray)
        // filter and map to get all positive hit-results
        |> Seq.filter Option.isSome |> Seq.map Option.get
        // should have some Distance from the start-point
        |> Seq.filter (fun h -> h.Distance |> IsPositive)
        // we are only interested in the neareast, so sort
        |> Seq.sortBy (fun h -> h.Distance)
        // and get the neareast (or none if no hit)
        |> Seq.tryFind (fun _ -> true)

    let shade (getSigObjs : Ray -> SceneObj seq) (hit : HitResult) (light : Light) : Color =
        // get the direction of the light for this point
        let lightDir = hit.Pos |> light.GetDirection
        // is the light visible?
        let rayToLight = Ray.Create(hit.Pos, lightDir)
        let lightVisible = findHitObj getSigObjs rayToLight |> Option.isNone

        // lighting factors
        let ambient = 0.4
        let diffuse = 0.6
        let diffF =
            if lightVisible then
                // helper - we are only interessted in positive values
                let suppresNeg x = if x <= 0.0 then 0.0 else x
                // get the fraction of diffuse Light (cap at 0)
                (hit.Normal <*> lightDir) |> suppresNeg
            else
                0.0
        // calculate the total color by ambient + diffuse light
        let s = diffuse * diffF + ambient
        // return the shaded color
        s * hit.Color

    let traceRay (getSigObjs : Ray -> SceneObj seq) (lgs : Light seq) (ray : Ray) =
        let lgs = lgs |> Array.ofSeq
        // modify the strength of the shading to account for multiple
        // lights (so for two lights each will contribute 1/2 to the color)
        let strMod (color : Color) = (1.0 / (float lgs.Length)) * color
        // search for a hitpoint
        let hit = findHitObj getSigObjs ray
        // get the shaded color if a point was hit
        let shadeCol = hit |> Option.map (fun hit -> lgs |> Array.sumBy (strMod << shade getSigObjs hit))
        // return the shaded color
        shadeCol

As you can see I only check if there is something between the hit-point and the light here:

        // get the direction of the light for this point
        let lightDir = hit.Pos |> light.GetDirection
        // is the light visible?
        let rayToLight = Ray.Create(hit.Pos, lightDir)
        let lightVisible = findHitObj getSigObjs rayToLight |> Option.isNone

And only set diffF at a non-zero value if not.

And here is the result:

FunTracer with Shadow