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: