This time we will see how to add shadows to our scene. Imagine this scene:
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: