FunTracer: reflection

It’s time for one of flagship-features of ray tracing: reflection. Indeed this will be rather easy (again) – we basically did have every thing we need already – all that is left is to introduce some recursion in the tracing to follow reflected rays.

As this one will add another property to the HitResult-type (the reflection strength) I choose to separate this a bit more. So let’s extract Color, Specular and the new Reflection into a new type – material:

type Material = { Color : Color; Specular : float; Reflection : float }
type HitResult = { Ray : Ray; Distance : float; Pos : Point; Normal : Direction; Material : Material }

Of course this means refactoring – see the end of this article for the new core-code. But first take a look at the sample-scene for today:

module SampleScene =

    let redMaterial = { Color = Colors.Red; Specular = 1.0; Reflection = 0.7 }
    let greenMaterial = { Color = Colors.Green; Specular = 1.5; Reflection = 0.3 }
    let blueMaterial = { Color = Colors.Blue; Specular = 1.0; Reflection = 0.9 }
    let mirrorMaterial = { Color = Colors.White; Specular = 2.0; Reflection = 1.0 }

    let private 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(-5.0, -2.0, 15.0), 4.0, blueMaterial)
        |> s.AddObject

        Sphere.Create (Vector.Create(5.0, -2.0, 15.0), 4.0, redMaterial)
        |> s.AddObject

        Sphere.Create (Vector.Create(0.0, -8.0, 7.0), 4.0, greenMaterial)
        |> s.AddObject

        Sphere.Create (Vector.Create(0.0, 9.0, 16.0), 7.0, mirrorMaterial)
        |> 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

FunTracer without reflection

The code for the reflection is really simple – we just reflect the incoming ray to a hit-point – creating a new ray:

let reflDir = hitPoint.Ray.Direction - 2.0 * (hitPoint.Normal <*> hitPoint.Ray.Direction) * hitPoint.Normal;
let reflRay = Ray.Create(hitPoint.Pos, reflDir)

and use this to recursively compute a additional color-value (of the reflected ray) and add this (scaled by the reflection-strength) to our outcome we had so far (from the shading) – all this only if the reflection-strength was greater than zero:

// calculate reflected color
let reflectHit (hitPoint : HitResult) =
   if hitPoint.Material.Reflection |> IsPositive then
      let reflDir = hitPoint.Ray.Direction - 2.0 * (hitPoint.Normal <*> hitPoint.Ray.Direction) * hitPoint.Normal;
      let reflRay = Ray.Create(hitPoint.Pos, reflDir)
      let reflRes = traceRay (iter - 1) getSigObjs lgs reflRay
      if reflRes.IsNone then Colors.Black else hitPoint.Material.Reflection * reflRes.Value
   else
      Colors.Black

// return the shaded color + reflection color
let color = hit |> Option.map (fun hit -> shadeHit hit + reflectHit hit)

This will produce this image:

FunTracer with reflection

Here is the complete code – note the recursion in traceRay I choose to limit to 10 for this image:

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

        // reflected direction
        let reflDir = hit.Ray.Direction - 2.0 * (hit.Normal <*> hit.Ray.Direction) * hit.Normal;

        // lighting factors
        let ambient = 0.4
        let diffuse = 0.6
        let diffF, specF =
            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)
                let diffF = (hit.Normal <*> lightDir) |> suppresNeg
                // get the specular factor
                let specF = (reflDir <*> lightDir) |> suppresNeg
                diffF, specF
            else
                0.0, 0.0

        // calculate the total color by ambient + diffuse + specular light
        let diffuseColor = diffuse * diffF * hit.Material.Color * light.Color
        let specColor = System.Math.Pow(specF, 10.0) * hit.Material.Specular * light.Color
        let ambientColor = ambient * hit.Material.Color * light.Color
        // return the shaded color
        ambientColor + diffuseColor + specColor

    let rec traceRay (iter : int) (getSigObjs : Ray -> SceneObj seq) (lgs : Light seq) (ray : Ray) : Color option =
        if iter <= 0 then None else

        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

        // calculate shaded color
        let shadeHit hitPoint = lgs |> Array.sumBy (strMod << shade getSigObjs hitPoint)

        // calculate reflected color
        let reflectHit (hitPoint : HitResult) =
            if hitPoint.Material.Reflection |> IsPositive then
                let reflDir = hitPoint.Ray.Direction - 2.0 * (hitPoint.Normal <*> hitPoint.Ray.Direction) * hitPoint.Normal;
                let reflRay = Ray.Create(hitPoint.Pos, reflDir)
                let reflRes = traceRay (iter - 1) getSigObjs lgs reflRay
                if reflRes.IsNone then Colors.Black else hitPoint.Material.Reflection * reflRes.Value
            else
                Colors.Black

        // return the shaded color + reflection color
        let color = hit |> Option.map (fun hit -> shadeHit hit + reflectHit hit)
        color