Sunday, September 7, 2014

Inheritance for a Swift ray tracer

I just did a test to see how class hierarchies are set up in Swift.  Standard design in a ray tracer is that all geometric models in the scene have a "hit" function and the program is blind to the black box that can answer "do I hit you?".   This design was first popularized (and as far as I know it was the first to be implimented) by David Kirk and Jim Arvo back during the Cold War days (The Ray Tracing Kernel," by David Kirk and James Arvo. In Proceedings of Ausgraph '88, Melbourne, Australia, July 1988.).  I found the Swift syntex pretty yummy overall for my "abstract class" (in Swift lingo a "protocol") "hitable".  Note that picking a name for this is often painful; graphics people often say "object" (too broad) and I used to say "surface" (too narrow as volumes are sometimes subclasses), so I now say what it can be asked.  The sphere class is the only hitable so far:

import Foundation

protocol hitable {
    func hit(r : ray, tmin : Double) -> (Bool, Double)
    func normal(p: vec3) -> vec3

class sphere : hitable  {
    var center : vec3 = vec3(x: 0.0, y: 0.0, z: 0.0)
    var radius : Double  = 0.0
    func hit(r : ray, tmin : Double) -> (Bool, Double) {
        var A : Double = dot(r.direction, r.direction)
        var B : Double = 2.0*dot(r.direction,r.origin - center)
        var C : Double = dot(r.origin - center,r.origin - center) - radius*radius
        let discriminant = B*B - 4.0*A*C
        if discriminant > 0 {
            var t : Double = (-B - sqrt(discriminant) ) / (2.0*A)
            if t > tmin {
                t  = (-B + sqrt(discriminant)) / (2.0*A)
            return (t > tmin, t)
        } else {
            return (false, 0.0)
    func normal(p: vec3) -> vec3 {
        return p - center

Note that the hit function returns a tuple so we don't need to define some struct to store stuff.  I've gone with not computing normal at time of hit as I'm a fan of compute it when you need it, but that is just taste.  Foundation we need to get "sqrt()".

Now in main we have (and my initialization is probably clumsy):

let my_sphere = sphere() = vec3(x: 0.0, y: 0.0, z: 0.0)
my_sphere.radius = 2.0
let the_world : hitable = my_sphere

let along_z = ray(origin: vec3(x: 0.0, y: 0.0, z: -10.0), direction: vec3(x: 0.0, y: 0.0, z: 1.0))
let hit_info  = the_world.hit(along_z, tmin: 100.0)
println("hit =\(hit_info.0), t = \(hit_info.1)")

Which prints out:
hit =false, t = 8.0

All in all, I really like how clean Swift makes this; so far it is my favorite language for writing a ray tracer (main complaint: not yet portable and I don't know what Apple's plan for that is).  I have no idea if it will actually be "swift", nor whether I need to make changes to make it swifter, but that is for the future.


Jono said...

Surprised to see your "normal" function which just takes in position. Easy for a sphere, but for a polygon mesh this would be a pretty expensive function, no? Don't you want some information from "hit" to make its way into "normal" besides position?

Peter Shirley said...

I think you are right! I ended up folding normal into the tuple that hit returns. I'll post that now.