Finally we will produce some output. And after the work we did so far it will be rather easy. The last “hard” part will be the shading:
First (very) simple shading
The idea is very simple. A object will reflect light equal to the amount that hits is surface. To simulate this we look at the angle between the normal at a point of it’s surface and a imagined line from the light to this point. For 0° degree the light is most direct and should reflect most “color” for 90° and greater it “misses” the point all together and shouldn’t reflect “color” at all.
Angle between vectors? Well we have seen a lot of this stuff already here and indeed we will just use the cosine of this angle (incidentally this is just the dot-product ) to simulate this behavior.
We will take only one small further step: we will include some ambient light. This is just a constant light that hits everywhere – you can think of it as the light you see on a very cloudy day – there is no sun, there might be no shadows but you can see – ultimately this is due to very many refractions of light (on fog, other object, etc.) so that there is always some light around even if you cannot see the sun behind the clouds.
Don’t think to hard about this – it’s not indented to be physical simulation but a mean to produce nice and real looking pictures, so let’s look at some code:
let shade (hit : HitResult) (light : Light) : Color = // lighting factors let ambient = 0.4 let diffuse = 0.6 // helper - we are only interessted in positive values let suppresNeg x = if x <= 0.0 then 0.0 else x // get the direction of the light for this point let lightDir = hit.Pos |> light.GetDirection // get the fraction of diffuse Light (cap at 0) let diffF = (hit.Normal <*> lightDir) |> suppresNeg // calculate the total color by ambient + diffuse light let s = diffuse * diffF + ambient // return the shaded color s * hit.Color
As you can see there isn’t much behind this just some vector-arithmetic (you can think of color as just a stupid 3D-vector) and the dot-product.
Let’s trace a ray
The algorithm for our first try is very simple: given a sequence of lights and possible objects in the path of a ray (as you will see below for now we give ALL scene-objects as possible object) it looks for the first object the ray will hit and use the shading we just saw to calculate the resulting color:
let traceRay (objs : 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 (ray, objs) // get the shaded color if a point was hit let shadeCol = hit |> Option.map (fun hit -> lgs |> Array.sumBy (strMod << shade hit)) // return the shaded color shadeCol
This uses this simple helper:
let findHitObj (ray : Ray, objs : SceneObj seq) = objs // 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 // 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)
To create the complete scene we just create a raster of rays with the viewport and trace each of this ray with the method above.
To help us keep track of the lights and objects in the scene we rap those into generic .net-List as well:
type Scene(viewPort : ViewPort) = let objects = new System.Collections.Generic.List<SceneObj>() let lights = new System.Collections.Generic.List<Light>() member s.AddObject(obj : SceneObj) = objects.Add(obj) member s.AddLight(lg : Light) = lights.Add(lg) member s.Trace(resX, resY) = let rays = viewPort.CreateRaster(resX, resY) let tracer = RayTrace.traceRay (objects, lights) rays |> Array2D.map tracer
As a first sample we will render a red sphere sitting just in front of the virtual eye with two light-sources:
module SampleScene = 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) let sphere = Sphere.Create (Vector.Create(0.0, 0.0, 10.0), 10.0, Colors.Red) s.AddObject(sphere) let sun = PointLight.CreateWhite(Vector.Create(1.0, 10.0, -10.0)) s.AddLight(sun) let dir = DirectionalLight.CreateWhite(Vector.Create (0.0, -1.0, 0.1)) s.AddLight(dir) s let CreateImage (resX, resY) = scene.Trace(resX, resY)
and use a simple console-application to put this into a bitmap-file:
let toSysColor (c : Color) = let f x = 255.0 * min 1.0 (max 0.0 x) |> int System.Drawing.Color.FromArgb(255, f c.R, f c.G, f c.B) let drawImage (resX : int, resY : int) = let pixels = SampleScene.CreateImage(resX, resY) use bitmap = new System.Drawing.Bitmap(resX, resY) let setPixel x y c = match c with | None -> () | Some color -> bitmap.SetPixel(x, resY - 1 - y, color |> toSysColor) use dc = System.Drawing.Graphics.FromImage(bitmap) dc.Clear(System.Drawing.Color.DarkSlateGray) pixels |> Array2D.iteri setPixel bitmap.Save(@"c:\temp\FunTracer.bmp") // Main: drawImage (400, 400)
This will finally create this image:
You can find the program-code so far here.