Skip to content

F# implementation of the ray tracer found in The Ray Tracer Challenge by Jamis Buck

License

Notifications You must be signed in to change notification settings

bmitc/the-ray-tracer-challenge-fsharp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

build and test

The Ray Tracer Challenge in F#

This repository is an F# implementation of the ray tracer found in The Ray Tracer Challenge: A Test-Driven Guide to Your First 3D Renderer by Jamis Buck. The tests described in the book and more are fully implemented, and due to F#'s clean syntax and domain modeling, the tests are nearly identical at moments to the psuedocode used to specify the tests in the book. It has been a pleasure and a lot of fun to write this code in F#.

The book is written from an agnostic point of view regarding programming languages, although the suggested tests and implementations do lean towards mutability and OOP type of languages. This implementation has been made to be as idiomatic to F# as possible, making full use of F#'s functional-first but multiparadigm nature. Functional programming is mixed with OOP naturally, although immutability is still highly preferred.

Latest example image based upon current implementation (see the Example Renders section for more examples and scripts): scene

A note on implementation

Domain-driven design is heavily used in this implementation, where F# types of discriminated unions, records, and sometimes classes are used to model all of the various components of the ray tracer. Probably the largest departure from the book, and an excellent example of functional programming, is defining 3D transformations using a discriminated union:

/// Represents a 3D transform
type Transform =
    | Translation of x: float * y: float * z: float
    | Scaling     of x: float * y: float * z: float
    | Reflection  of Axis
    | Rotation    of Axis * angle: float<radians>
    | Shearing    of ShearComponent * proportion: float
    | Combination of Transform list // transforms will be listed left to right but applied right to left

The book does this differently and thus requires the computation of matrix inverses to compute inverses of these transformations. However, this is actually an example where the functional approach improves performance because creating inverses is as simple as pattern matching on the transformations:

/// Invert the transform
let rec inverse transform =
    match transform with
    | Translation (x, y, z)  -> Translation (-x, -y, -z)
    | Scaling (x, y, z)      -> Scaling (reciprocal x, reciprocal y, reciprocal z)
    | Reflection axis        -> Reflection axis
    | Rotation (axis, r)     -> Rotation (axis, -r)
    | Shearing (c, p)        -> Shearing (c, -p)
    | Combination transforms -> Combination (transforms |> List.rev |> List.map (fun t -> inverse t))

Note that the definition of the inverse of each transform essentially follows and highlights the intuitive notion of the inverse of a transformation.

The matrix representing any given transform can be directly returned by again pattern matching on the transformations. An abbreviated implementation is:

/// Get the matrix that represents the transform
let rec getTransformMatrix transform =
    match transform with
    | Translation (x,y,z) -> Matrix(4, 4, array2D [[1.0; 0.0; 0.0;  x ];
                                                   [0.0; 1.0; 0.0;  y ];
                                                   [0.0; 0.0; 1.0;  z ];
                                                   [0.0; 0.0; 0.0; 1.0]])
    | Scaling (x,y,z)     -> Matrix(4, 4, array2D [[ x ; 0.0; 0.0; 0.0];
                                                   [0.0;  y ; 0.0; 0.0];
                                                   [0.0; 0.0;  z ; 0.0];
                                                   [0.0; 0.0; 0.0; 1.0]])
    // and so on ...

Lastly, one of the tests exhibits the nicety of this approach, in that transforms can simply be strung together in pipe operations:

[<Fact>]
let ``Individual transformations are applied in sequence`` () =
    let p = point(1.0, 0.0, 1.0)
    
    p
    |> rotate (X, pi/2.0)
    |> scale (5.0, 5.0, 5.0)
    |> translate (10.0, 5.0, 7.0)
    |> should equal (point(15.0, 0.0, 7.0))

Each one of the transformation functions simply creates a transformation of the same name and applies it to the point by computing the matrix product of the transform's matrix and the point.

Example renders

Below you'll find code exerpts that generate the displayed image, and these are the examples found in the various chapters of the book. The ray tracer implementation is not yet complete but is fully working through Chapter 10: Patterns.

F# script to generate below scene
type Projectile = { Position: Point<1>; Velocity: Vector }

type Environment = { Gravity: Vector; Wind: Vector }

let tick environment projectile =
    { Position = projectile.Position + projectile.Velocity;
      Velocity = projectile.Velocity + environment.Gravity + environment.Wind }

let initialPosition =
    { Position = point(0.0, 1.0, 0.0);
      Velocity = 11.25 * (normalize (vector(1.0, 1.8, 0.0))) }

let initialEnvironment =
    { Gravity = vector(0.0, -0.1, 0.0);
      Wind    = vector(-0.01, 0.0, 0.0) }

let run environment initialPosition (canvas: Canvas<pixels>) filePath =
    let mutable position = initialPosition
    while (tick environment position).Position.Y >= 0.0 do
        position <- (tick environment position)
        canvas.[roundToInt position.Position.X, canvas.Height - (roundToInt position.Position.Y)] <- green
    writeToPPM canvas filePath

run initialEnvironment
    initialPosition
    (Canvas(900<pixels>, 550<pixels>))
    (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../images/projectile.ppm"))

projectile

F# script to generate below scene
let twelveOClock = point(0.0, 1.0, 0.0)
let angle = pi/6.0
let canvasSize = 200<pixels>
let center = point((float canvasSize)/2.0, (float canvasSize)/2.0, 0.0)

let writeHour h =
    let scaleFactor = (float canvasSize) * 3.0 / 8.0
    twelveOClock
    |> rotate (Z, float(h) * angle)             // move the 12 o'clock position to the hour position
    |> scale (scaleFactor, scaleFactor, 0.0)    // scale the clock radius to 3/8 of canvas size
    |> translate (center.X, center.Y, center.Z) // translate the clock to the middle of the canvas
    |> (fun p -> (roundToInt p.X, roundToInt p.Y))

let canvas = Canvas(canvasSize)

List.iter (fun hour -> let (x,y) = writeHour hour
                       canvas.[x,y] <- white)
          [1..12]

writeToPPM canvas (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../Images/clock.ppm"))

clock

F# script to generate below scene
let rayOrigin = pointu<world>(0.0, 0.0, -5.0)
let wallZ = 10.0<world>
let wallSize = 7.0<world>
let canvasSize = 200.0<pixels>
let pixelSize = wallSize / canvasSize
let halfSize = wallSize / 2.0
let canvas = Canvas(canvasSize)

let compute x y =
    let worldX = -halfSize + pixelSize * x
    let worldY =  halfSize - pixelSize * y
    let position = point(worldX, worldY, wallZ)
    let r = ray rayOrigin (normalize (position - rayOrigin))
    let xs = intersect r sphere
    match hit xs with
    | Some _ -> color(0.0, 0.5, 1.0)
    | None   -> color(0.0, 0.0, 0.0)

#time
canvas.UpdatePixels(fun x y _ -> compute (floatUnits<pixels> x) (floatUnits<pixels> y))
#time

writeToPPM canvas (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../images/circle.ppm"))

circle

F# script to generate below scene
let rayOrigin = pointu<world>(0.0, 0.0, -5.0)
let wallZ = 10.0<world>
let wallSize = 7.0<world>
let canvasSize = 1000.0<pixels>
let pixelSize = wallSize / canvasSize
let halfSize = wallSize / 2.0
let canvas = Canvas(canvasSize)

let m = {Material.Default with Color = color(0.0, 0.5, 1.0)}
let light = {Position = pointu<world>(-10.0, 10.0, -10.0); Intensity = color(1.0, 1.0, 1.0)}

let compute x y =
    let worldX = -halfSize + pixelSize * x
    let worldY =  halfSize - pixelSize * y
    let pos = point(worldX, worldY, wallZ)
    let r = ray rayOrigin (normalize (pos - rayOrigin))
    let xs = intersect r sphere
    match hit xs with
    | Some i -> let point = position r i.Time
                let normal = normalAt i.Object point
                let eye = -r.Direction
                lighting m light point eye normal false
    | None   -> black

#time
canvas.UpdatePixels(fun x y _ -> compute (floatUnits<pixels> x) (floatUnits<pixels> y))
#time

writeToPPM canvas (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../images/lightAndShading.ppm"))

lightAndShading

F# script to generate below scene
//******************************************
// Scene objects
//******************************************

let floor     = {sphere with Transform = Some (Scaling(10.0, 0.01, 10.0));
                         Material = Some {Material.Default with Color = color(1.0, 0.9, 0.9);
                                                                Specular = 0.0}}

let leftWall  = {sphere with Transform = Some (Combination [Translation(0.0,0.0,5.0);
                                                           Rotation(Y,-pi/4.0);
                                                           Rotation(X,pi/2.0);
                                                           Scaling(10.0, 0.01, 10.0)]);
                            Material = floor.Material}

let rightWall = {sphere with Transform = Some (Combination [Translation(0.0,0.0,5.0);
                                                            Rotation(Y,pi/4.0);
                                                            Rotation(X,pi/2.0);
                                                            Scaling(10.0, 0.01, 10.0)]);
                             Material = floor.Material}

let middle    = {sphere with Transform = Some (Translation(-0.5, 1.0, 0.5));
                             Material  = Some {Material.Default with Color = color(0.1, 1.0, 0.5);
                                                                     Diffuse = 0.7;
                                                                     Specular = 0.3}}

let right     = {sphere with Transform = Some (Combination [Translation(1.5, 0.5, -0.5); Scaling(0.5, 0.5, 0.5)]);
                             Material  = Some {Material.Default with Color = color(0.5, 1.0, 0.1);
                                                                     Diffuse = 0.7;
                                                                     Specular = 0.3}}

let left      = {sphere with Transform = Some (Combination [Translation(-1.5, 0.33, -0.75); Scaling(0.33, 0.33, 0.33)]);
                             Material  = Some {Material.Default with Color = color(0.0, 0.5, 0.8);
                                                                     Diffuse = 0.7;
                                                                     Specular = 0.3}}

//******************************************
// World
//******************************************

let light = {Position = pointu<world>(-10.0, 10.0, -10.0); Intensity = color(1.0, 1.0, 1.0)}
let world = {Objects = [floor; leftWall; rightWall; middle; left; right]; LightSource = light}

let camera = {camera(2000<pixels>, 1000<pixels>, pi/3.0)
              with Transform = viewTransform (point(0.0, 1.5, -5.0)) (point(0.0, 1.0, 0.0)) (vector(0.0, 1.0, 0.0)) }

#time
let image = render camera world
#time

writeToPPM image (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../images/scene.ppm"))

scene

F# script to generate below scene
//******************************************
// Scene objects
//******************************************

let floor     = {sphere with Transform = Some (Scaling(10.0, 0.01, 10.0));
                             Material  = Some {Material.Default with Color = color(1.0, 0.9, 0.9);
                                                                     Specular = 0.0}}

let leftWall  = {sphere with Transform = Some (Combination [Translation(0.0,0.0,5.0);
                                                            Rotation(Y,-pi/4.0);
                                                            Rotation(X,pi/2.0);
                                                            Scaling(10.0, 0.01, 10.0)]);
                             Material  = floor.Material}

let rightWall = {sphere with Transform = Some (Combination [Translation(0.0,0.0,5.0);
                                                            Rotation(Y,pi/4.0);
                                                            Rotation(X,pi/2.0);
                                                            Scaling(10.0, 0.01, 10.0)]);
                             Material  = floor.Material}

let middle    = {sphere with Transform = Some (Translation(-0.5, 1.0, 0.5));
                             Material  = Some {Material.Default with Color = color(0.1, 1.0, 0.5);
                                                                     Diffuse = 0.7;
                                                                     Specular = 0.3}}

let right     = {sphere with Transform = Some (Combination [Translation(1.5, 0.5, -0.5); Scaling(0.5, 0.5, 0.5)]);
                             Material  = Some {Material.Default with Color = color(0.5, 1.0, 0.1);
                                                                     Diffuse = 0.7;
                                                                     Specular = 0.3}}

let left      = {sphere with Transform = Some (Combination [Translation(-1.5, 0.33, -0.75); Scaling(0.33, 0.33, 0.33)]);
                             Material  = Some {Material.Default with Color = color(0.0, 0.5, 0.8);
                                                                     Diffuse = 0.7;
                                                                     Specular = 0.3}}

//******************************************
// World
//******************************************

let light  = {Position = pointu<world>(-10.0, 10.0, -10.0); Intensity = color(1.0, 1.0, 1.0)}
let world  = {Objects = [floor; leftWall; rightWall; middle; left; right]; LightSource = light}

let camera = {camera(2000<pixels>, 1000<pixels>, pi/3.0)
              with Transform = viewTransform (point(0.0, 1.5, -5.0)) (point(0.0, 1.0, 0.0)) (vector(0.0, 1.0, 0.0)) }

#time
let image = render camera world
#time

writeToPPM image (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../images/shadows.ppm"))

scene

F# script to generate below scene
//******************************************
// Scene objects
//******************************************

let floor  = {plane with Transform = Some (Rotation (Z, 0.0<radians>));
                         Material  = Some {Material.Default with Color = color(0.5, 0.9, 0.9);
                                                                 Specular = 0.0}}

let wall   = {plane with Transform = Some (Combination [Translation (0.0, 0.0, 5.0); Rotation (Y, -pi/4.0); Rotation (Z, pi/2.0)]);
                         Material  = Some {Material.Default with Color = color(1.0, 1.0, 1.0);
                                                                 Specular = 0.0}}

let middle = {sphere with Transform = Some (Translation(-0.5, 1.0, 0.5));
                          Material  = Some {Material.Default with Color = color(0.1, 1.0, 0.5);
                                                                 Diffuse = 0.7;
                                                                 Specular = 0.3}}

let right  = {sphere with Transform = Some (Combination [Translation(1.5, 0.5, -0.5); Scaling(0.5, 0.5, 0.5)]);
                          Material  = Some {Material.Default with Color = color(0.5, 1.0, 0.1);
                                                                  Diffuse = 0.7;
                                                                  Specular = 0.3}}

let left   = {sphere with Transform = Some (Combination [Translation(-1.5, 0.33, -0.75); Scaling(0.33, 0.33, 0.33)]);
                          Material  = Some {Material.Default with Color = color(0.0, 0.5, 0.8);
                                                                  Diffuse = 0.7;
                                                                  Specular = 0.3}}

//******************************************
// World
//******************************************

let light  = {Position = pointu<world>(-10.0, 10.0, -10.0); Intensity = color(1.0, 1.0, 1.0)}
let world  = {Objects = [floor; wall; middle; left; right]; LightSource = light}

let camera = {camera(2000<pixels>, 1000<pixels>, pi/1.5)
              with Transform = viewTransform (point(0.0, 1.5, -5.0)) (point(0.0, 1.0, 0.0)) (vector(0.0, 1.0, 0.0)) }

#time
let image = render camera world
#time

writeToPPM image (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../images/planes.ppm"))

scene

F# script to generate below scene
//******************************************
// Scene objects
//******************************************

let ground = {plane  with Transform = Some (Rotation (Z, 0.0<radians>));
                          Material  = Some {Material.Default with Specular  = Material.Default.Specular
                                                                  Pattern   = Some (checker skyBlue gray (Some (ScalingEqual 0.9)))}}

let wall   = {plane  with Transform = Some (Combination [Translation (0.0, 0.0, 5.0); Rotation (Y, -pi/4.0); Rotation (Z, pi/2.0)])
                          Material  = Some {Material.Default with Specular  = Material.Default.Specular
                                                                  Pattern   = Some (Perturb (Blend (stripe white hotPink (Some (Combination [ScalingEqual 0.7;
                                                                                                                                             Rotation (Y, pi/4.0)])),
                                                                                                    stripe white hotPink (Some (Combination [ScalingEqual 0.7;
                                                                                                                                             Rotation (Y, 3.0*pi/4.0)])))))}}

let middle = {sphere with Transform = Some (Translation(-0.5, 1.0, 0.5));
                          Material  = Some {Material.Default with Diffuse   = 0.7
                                                                  Specular  = 0.3
                                                                  Shininess = 30
                                                                  Pattern   = Some (Perturb (ring paleGreen purple (Some (Combination [Rotation (Z, pi/4.0);
                                                                                                                                       Rotation (X, pi/4.0);
                                                                                                                                       ScalingEqual 0.09]))))}}

let right  = {sphere with Transform = Some (Combination [Translation(1.5, 0.5, -0.5); ScalingEqual 0.5])
                          Material  = Some {Material.Default with Diffuse   = 0.7
                                                                  Specular  = 0.3
                                                                  Shininess = 40
                                                                  Pattern   = Some (gradient deepPink blue (Some (Combination [Rotation (Y, pi/6.0);
                                                                                                                               Scaling (2, 0, 0);
                                                                                                                               Translation (1.5, 0, 0)])))}}

let left   = {sphere with Transform = Some (Combination [Translation(-1.5, 0.33, -0.75); ScalingEqual 0.33])
                          Material  = Some {Material.Default with Diffuse   = 1.0
                                                                  Specular  = 0.3
                                                                  Shininess = 10
                                                                  Pattern   = Some (checker powderBlue yellow (Some (ScalingEqual 0.4)))}}

//******************************************
// World
//******************************************

let light  = {Position = pointu<world>(-10.0, 10.0, -10.0); Intensity = color(1.0, 1.0, 1.0)}
let world  = {Objects = [ground; wall; middle; left; right]; LightSource = light}

let camera = { camera(2000<pixels>, 1000<pixels>, pi/3.0)
               with Transform = viewTransform (point(0.0, 1.5, -5.0)) (point(0.0, 1.0, 0.0)) (vector(0.0, 1.0, 0.0)) }

#time
let image = render camera world
#time

writeToPPM image (System.IO.Path.Combine(__SOURCE_DIRECTORY__, "../../../images/patterns.ppm"))

scene

About

F# implementation of the ray tracer found in The Ray Tracer Challenge by Jamis Buck

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages