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
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:
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