Quantcast
Channel: Martin Doms » graphics
Viewing all articles
Browse latest Browse all 3

Ray Tracer in F# – Part III

0
0

In part 2 we created a working lighting model for our ray tracer and got a step closer to creating a realistic-looking sphere. Soon we will increase the realism further by applying reflections and creating shapes other than spheres. However, this article is going to be about multiplicity. We are going to extend our program to handle multiple spheres and multiple light sources. This will help teach you about working with important concepts in functional programming as well as learn about some of the F# collections libraries.

But before we get started I just want to tidy up the code a little, because it is just a bit badly formed.

At the moment a sphere has a diffuse colour and we are assuming that specular highlights are always white. While this generally gives good results, it’s not perfect. Because we will want to do more with a shape’s material than just lighting at some point (textures, translucency, etc) let’s break out material properties into another type. Also, the intersection type doesn’t really need a reference to the shape it intersects – it only needs to know the material (as well as the other information it has). So let’s change that too. After you make these simple changes to your types, go through the code and make the appropriate changes, including fixing the code that breaks and removing magic constants from the colorAt function.

type Material = { diffuseColor: Color; specularColor: Color; shininess:float }
type Sphere   = { center:Point3D; radius:float; material:Material }
type Intersection = { normal:Vector3D; point:Point3D; ray:Ray; material:Material; t:float }

//.... and constructing the sphere will look like
let material = { diffuseColor=Color(0.8, 0.1, 0.1); specularColor=Color(0.7,0.7,0.7); shininess=40.0 }
let sphere = { center=Point3D(1.0,1.0,1.0); radius=0.4; material=material }

For completeness, here is the colorAt function.

let colorAt intersections scene =
    let closest = List.minBy(fun i -> i.t) intersections
    let kd = closest.material.diffuseColor
    let ks = closest.material.specularColor
    let Ia = scene.ambientLight
    let L = norm (scene.light.position - closest.point)
    let Id = Math.Max(0.0, Vector3D.DotProduct(L, closest.normal))
    let V = scene.camera.position - closest.point
    let H = norm (L + V)
    let Is = Math.Pow(Math.Max(0.0, Vector3D.DotProduct(H,closest.normal)), closest.material.shininess)
    let ambient = kd * Ia
    let diffuse = kd * Id
    let specular = ks * Is
    ambient + diffuse + specular

I’m still not quite happy with the layout of this code. It’s weird that colorAt does the calculation to find the closest intersection point to the camera (or ray origin, more precisely). I’d prefer to move that into the calling function, so in the main nested loop

match intersects with
            | [] -> bmp.SetPixel(x,y,Color.Gray)
            | _ -> let color = colorAt intersects scene
                   bmp.SetPixel(x,y, Color.FromArgb(255, (int)(color.r*255.0), (int)(color.g*255.0), (int)(color.b*255.0)))

becomes

match intersects with
            | [] -> bmp.SetPixel(x,y,Color.Gray)
            | _ -> let color = colorAt (List.minBy(fun i -> i.t) intersects) scene
                   bmp.SetPixel(x,y, Color.FromArgb(255, (int)(color.r*255.0), (int)(color.g*255.0), (int)(color.b*255.0)))

And making the corresponding change in colorAt is simple:

let colorAt intersection scene =
    let kd = intersection.material.diffuseColor
    let ks = intersection.material.specularColor
    // etc...

Now we want to allow for multiple spheres. Change the definition for Scene so it looks like

type Scene = { camera:Camera; spheres:Sphere list; ambientLight:Color; light:Light }

Note the word ‘list’. This means that the ‘spheres’ field will carry a typed list of Sphere values.

This of course breaks our castRay function, which depends heavily on the Scene having just a single sphere. So currently the castRay function performs the task of calculating intersection points. Really what castRay should be doing is delegating this task to another function and simply collecting and returning the results. So let’s make some changes to castRay and also define a separate intersect function to handle the sphere intersect code.

let intersect ray sphere =
    let s = ray.origin - sphere.center
    let rayDir = norm ray.direction
    let sv = Vector3D.DotProduct(s,rayDir)
    let ss = Vector3D.DotProduct(s,s)
    let discr = sv*sv - ss + sphere.radius*sphere.radius
    if discr < 0.0 then []
    else 
        let normalAtTime t = norm (pointAtTime ray t - sphere.center)
        let (t1,t2) = (-sv + sqrt(discr), -sv - sqrt(discr))
        [ { normal = normalAtTime t1; point = pointAtTime ray t1; ray = ray; material=sphere.material; t=t1 };
          { normal = normalAtTime t2; point = pointAtTime ray t2; ray = ray; material=sphere.material; t=t2 } ]

let castRay ray (scene:Scene) =
    scene.spheres |> List.collect (fun x -> intersect ray x)

The intersect function merely returns a list of intersections with a sphere. We will be making this more generic to handle other shapes later, but for now this works fine. Incidentally, this is the reason we’re using a list instead of a tuple or something where handling only two results would have been more appropriate.

Notice the |> operator in castRay. This is called the pipeline operator and Dom Syme, the man behind the F# language, it is “perhaps the most important operator in F# programming.” (This quote is from Expert F# 2.0). Basically it allows us to “pipe” the value of some expression into another expression, which results in some pretty cool looking code. We’ll be using the pipelining feature more later on.

So this is pretty much all we need to render multiple shapes. Let’s add another shape to the scene and take a look.

    let material1 = { diffuseColor=Color(0.8, 0.1, 0.1); specularColor=Color(0.7,0.7,0.7); shininess=40.0 }
    let sphere1 = { center=Point3D(-0.1, -0.1, 1.8); radius=0.39; material=material1 }
    let material2 = { diffuseColor=Color(0.1, 0.1, 0.8); specularColor=Color(0.7,0.7,0.7); shininess=20.0 }
    let sphere2 = { center=Point3D(0.4, 0.3, 2.0); radius=0.3; material=material2 }

    let scene = { camera=camera; spheres=[sphere1;sphere2]; ambientLight=Color(0.2,0.2,0.2); light=light }

Of course you are free to use as many spheres as you like – experiment with procedurally creating arrays of spheres, it can be pretty fun.

We’re going to take a similar approach for using multiple light sources. Here we’re going to use nested inner functions (closures) to break our code into functions which we can hide from the calling code. colorAt now looks like

let colorAt intersection scene =
    // nested function for ambient color
    let ambientColorAt intersection scene =
        scene.ambientLight * intersection.material.diffuseColor

    // nested function for specular color
    let specularColorAt intersection scene =
        let ks = intersection.material.specularColor
        let V = scene.camera.position - intersection.point
        
        let specularAtLight light =
            let L = norm (light.position - intersection.point)
            let H = norm (L + V)
            let Is = light.color * Math.Pow(Math.Max(0.0, Vector3D.DotProduct(H,intersection.normal)), intersection.material.shininess)
            ks * Is

        List.sumBy(fun x -> specularAtLight x) scene.lights

    //nested function for diffuse color
    let diffuseColorAt intersection scene =
        let kd = intersection.material.diffuseColor

        let diffuseAtLight light =
            let L = norm (light.position - intersection.point)
            let Id = light.color * Math.Max(0.0, Vector3D.DotProduct(L, intersection.normal))
            kd * Id

        List.sumBy(fun x -> diffuseAtLight x) scene.lights

    let ambient = ambientColorAt intersection scene
    let specular = specularColorAt intersection scene
    let diffuse = diffuseColorAt intersection scene

    ambient + diffuse + specular

On a side note, I just noticed that throughout this series I have made an embarrassing mistake – I forgot to multiply the light intensity values with the surface reflectance, resulting in all lights behaving as though they were coloured 1.0,1.0,1.0. This is fixed in the preceding code.

You can see from this snippet that we are summing the values of all colours collected from all light to create specular and diffuse values. Setting up a second light source allows us to view the results.

    let light = { position=Point3D(1.0,4.0,-3.0); color=Color(0.5,0.5,0.5) }
    let light2 = { position=Point3D(-3.0,-2.0,-1.0); color=Color(0.8,0.8,0.8) }
    let scene = { camera=camera; spheres=[sphere1;sphere2]; ambientLight=Color(0.2,0.2,0.2); lights=[light;light2] }

Now there’s one more thing I want to do before I close off this entry. Right now other objects are ignored when we are summing up the lights. If we just check for intersections between each intersection point and the light source we are inspecting we can see if that intersection point should be in shadow. Seems simple enough, so let’s implement some simple shadowing.

So all we need to do is check whether a ray between the intersection point and the light source intersects ANY other shapes. If not, we know there’s a clear path to the light source. If so, that light source doesn’t contribute to the lighting at that intersection point. We can ignore ambient lighting, because it applies whether we are in shadow or not. Make the following modifications to the colorAt function.

let colorAt intersection scene =
    // check if we're in shadow
    let inShadow point light =
        let ray = { origin = intersection.point; direction = light.position - intersection.point}
        let intersections = castRay ray scene
        if intersections.Length = 0 then false
        else true
// ...then where diffuse light is calculated...
    let diffuseColorAt intersection scene =
        let kd = intersection.material.diffuseColor

        let diffuseAtLight light =
            let L = norm (light.position - intersection.point)
            let Id = light.color * Math.Max(0.0, Vector3D.DotProduct(L, intersection.normal))
            if inShadow intersection.point light then Color(0.0,0.0,0.0)
            else kd * Id            

        List.sumBy(fun x -> diffuseAtLight x) scene.lights

Although I’m sure technically we should do the same for specular light, in my experience it looks like ass with these sharp shadows, so I keep specular light around. I’ll let you judge for yourself which is better. At some stage we’ll implement more advanced lighting and shadowing and we won’t need to decide. So here’s the scene rendered earlier with our shadow model. On the left if with specular lighting shadowed, the right not shadowed. I prefer the right.

The banding effect on the shadows occurs because I included a third light to demonstrate that shadows can have multiple levels of darkness, depending on how many lights are blocked.

In the next article we will finally start actually ray tracing, which means reflections! We’ll get to play with recursion and we’ll also start using shapes other than spheres. The source code so far is available here.


Viewing all articles
Browse latest Browse all 3

Latest Images

Trending Articles



Latest Images