Categories
Uncategorized

Ray tracing in a Swift playground

Today we are porting a ray tracer from Peter Shirley’s mini book, into a Swift playground. Since I am not going to describe what Ray Tracing is and how it works, I am inviting you to go ahead and read this book as it’s free for Kindle subscribers. If you are not a subscriber, just buy the book like I did. Paying $2.99 is absolutely worth your every penny if you are interested in this topic.

The first thing we would want to do is create a data structure to hold pixel information. In the playground, create a new file named pixel.swift under the Sources folder. Next, let’s write the Pixelstruct. It’s just a simple struct with one variable for each of the RGBA channels. We initialize the alpha channel to 255 which means completely opaque on a [0 - 255] scale:

public struct Pixel {
    var r: UInt8
    var g: UInt8
    var b: UInt8
    var a: UInt8
    init(red: UInt8, green: UInt8, blue: UInt8) {
        r = red
        g = green
        b = blue
        a = 255
    }
}

Next, we need to create an array to hold all the pixels on the screen. To compute the color for each pixel we just set Red to be 0 for all pixels, while Green goes from 0 (no Green at all) in the left corner of the screen, to 255 (full Green) in the right corner of the screen. Similarly, the Blue color goes from 0 at the top of the screen, to 255 at the bottom of the screen.

public func makePixelSet(width: Int, _ height: Int) -> ([Pixel], Int, Int) {
    var pixel = Pixel(red: 0, green: 0, blue: 0)
    var pixels = [Pixel](count: width * height, repeatedValue: pixel)
    for i in 0..<width {
        for j in 0..<height {
            pixel = Pixel(red: 0, green: UInt8(Double(i * 255 / width)), blue: UInt8(Double(j * 255 / height)))
            pixels[i + j * width] = pixel
        }
    }
    return (pixels, width, height)
}

Finally, we need a way to create a drawable image from pixels. The Core Image framework provides the CGImageCreate() method, which takes in several parameters necessary for image rendering:

public func imageFromPixels(pixels: ([Pixel], width: Int, height: Int)) -> CIImage {
    let bitsPerComponent = 8
    let bitsPerPixel = 32
    let rgbColorSpace = CGColorSpaceCreateDeviceRGB()
    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.PremultipliedLast.rawValue) // alpha is last

    let providerRef = CGDataProviderCreateWithCFData(NSData(bytes: pixels.0, length: pixels.0.count * sizeof(Pixel)))
    let image = CGImageCreate(pixels.1, pixels.2, bitsPerComponent, bitsPerPixel, pixels.1 * sizeof(Pixel), rgbColorSpace, bitmapInfo, providerRef, nil, true, CGColorRenderingIntent.RenderingIntentDefault)
    return CIImage(CGImage: image!)
}

Next, in the main playground page create the set of pixels for a window with given width and height, and then create the rendered image using this set:

let width = 800
let height = 400
var pixelSet = makePixelSet(width, height)
var image = imageFromPixels(pixelSet)
image

You should see the following image:

alt text

Ok, you might be wondering, but where does ray tracing come into play? Let’s deal with that next. Under the Sources folder again, let’s create a convenience class named vec3.swift where we will have a few math helper methods:

struct vec3 {
    var x = 0.0
    var y = 0.0
    var z = 0.0
}

func * (left: Double, right: vec3) -> vec3 {
    return vec3(x: left * right.x, y: left * right.y, z: left * right.z)
}

func + (left: vec3, right: vec3) -> vec3 {
    return vec3(x: left.x + right.x, y: left.y + right.y, z: left.z + right.z)
}

func - (left: vec3, right: vec3) -> vec3 {
    return vec3(x: left.x - right.x, y: left.y - right.y, z: left.z - right.z)
}

func dot (left: vec3, _ right: vec3) -> Double {
    return left.x * right.x + left.y * right.y + left.z * right.z
}

func unit_vector(v: vec3) -> vec3 {
    let length : Double = sqrt(dot(v, v))
    return vec3(x: v.x/length, y: v.y/length, z: v.z/length)
}

Next, we need to create a ray struct. It has as members an origin, a direction and a method that computes the ray tracing equation for any given parameter:

struct ray {
    var origin: vec3
    var direction: vec3
    func point_at_parameter(t: Double) -> vec3 {
        return origin + t * direction
    }
}

Then we need to compute the color based on whether the ray hits a sphere which we create in the center of the screen:

func color(r: ray) -> vec3 {
    let minusZ = vec3(x: 0, y: 0, z: -1.0)
    var t = hit_sphere(minusZ, 0.5, r)
    if t > 0.0 {
        let norm = unit_vector(r.point_at_parameter(t) - minusZ)
        return 0.5 * vec3(x: norm.x + 1.0, y: norm.y + 1.0, z: norm.z + 1.0)
    }
    let unit_direction = unit_vector(r.direction)
    t = 0.5 * (unit_direction.y + 1.0)
    return (1.0 - t) * vec3(x: 1.0, y: 1.0, z: 1.0) + t * vec3(x: 0.5, y: 0.7, z: 1.0)
}

You noticed that we used another method, called hit_sphere() to determine whether we hit the sphere with the ray, or whether we miss it:

func hit_sphere(center: vec3, _ radius: Double, _ r: ray) -> Double {
    let oc = r.origin - center
    let a = dot(r.direction, r.direction)
    let b = 2.0 * dot(oc, r.direction)
    let c = dot(oc, oc) - radius * radius
    let discriminant = b * b - 4 * a * c
    if discriminant < 0 {
        return -1.0
    } else {
        return (-b - sqrt(discriminant)) / (2.0 * a)
    }
}

Back in the pixel.swift file, change the makePixelSet(:) to include creating a ray for each pixel and compute its color before adding the pixel to the set:

public func makePixelSet(width: Int, _ height: Int) -> ([Pixel], Int, Int) {
    var pixel = Pixel(red: 0, green: 0, blue: 0)
    var pixels = [Pixel](count: width * height, repeatedValue: pixel)
    let lower_left_corner = vec3(x: -2.0, y: 1.0, z: -1.0)
    let horizontal = vec3(x: 4.0, y: 0, z: 0)
    let vertical = vec3(x: 0, y: -2.0, z: 0)
    let origin = vec3()
    for i in 0..<width {
        for j in 0..<height {
            let u = Double(i) / Double(width)
            let v = Double(j) / Double(height)
            let r = ray(origin: origin, direction: lower_left_corner + u * horizontal + v * vertical)
            let col = color(r)
            pixel = Pixel(red: UInt8(col.x * 255), green: UInt8(col.y * 255), blue: UInt8(col.z * 255))
            pixels[i + j * width] = pixel
        }
    }
    return (pixels, width, height)
}

In the main playground page, see the generated new image:

alt text

Stay tuned for a part 2 of this article, where we will calculate lights and shades for a more realistic image rendering. The source code is posted on Github as usual.

Until next time!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s