Let’s continue working on our ray tracer
and pick up where we left off last week. Now that we know how to generate spheres of different materials and know how to look at them from different angles, let’s see how we can generate way more than just 3 spheres.
In pixel.swift
let’s create a new random_scene() method:
func random_scene() -> Hitable_list {
var objects = [Hitable]()
objects.append(Sphere(c: float3(0, -1000, 0), r: 1000, m: Lambertian(albedo: float3(0.5, 0.5, 0.5))))
for a in -2..<3 {
for b in -2..<3 {
let materialChoice = drand48()
let center = float3(Float(a) + 0.9 * Float(drand48()), 0.2, Float(b) + 0.9 * Float(drand48()))
if length(center - float3(4, 0.2, 0)) > 0.9 {
if materialChoice < 0.8 { // diffuse
let albedo = float3(Float(drand48()) * Float(drand48()), Float(drand48()) * Float(drand48()), Float(drand48()) * Float(drand48()))
objects.append(Sphere(c: center, r: 0.2, m: Lambertian(albedo: albedo)))
} else if materialChoice < 0.95 { // metal
let albedo = float3(0.5 * (1 + Float(drand48())), 0.5 * (1 + Float(drand48())), 0.5 * (1 + Float(drand48())))
objects.append(Sphere(c: center, r: 0.2, m: Metal(albedo: albedo, fuzz: Float(0.5 * drand48()))))
} else { // glass
objects.append(Sphere(c: center, r: 0.2, m: Dielectric()))
}
}
}
}
objects.append(Sphere(c: float3(0, 0.7, 0), r: 0.7, m: Dielectric()))
objects.append(Sphere(c: float3(-3, 0.7, 0), r: 0.7, m: Lambertian(albedo: float3(0.4, 0.2, 0.1))))
objects.append(Sphere(c: float3(3, 0.7, 0), r: 0.7, m: Metal(albedo: float3(0.7, 0.6, 0.5), fuzz: 0.0)))
return Hitable_list(list: objects)
}
This method generates roughly 25 smaller spheres and assigns a material to each of them based on a random number that determines whether the sphere material will be lambertian
, metal
or glass
. Then we add each sphere to a list that the function needs to return. We also add the big sphere and the 3 initial smaller spheres we used to have.
Then inside the imageFromPixels() method we replace the code block where we used to add the spheres:
var objects = [Hitable]()
var object = Sphere(c: float3(0, -100.5, -1), r: 100, m: Lambertian(albedo: float3(0.7, 0.23, 0.12)))
objects.append(object)
object = Sphere(c: float3(1, 0, -1), r: 0.5, m: Metal(albedo: float3(0.8, 0.6, 0.2), fuzz: 0.1))
objects.append(object)
object = Sphere(c: float3(-1, 0, -1), r: 0.5, m: Dielectric())
objects.append(object)
object = Sphere(c: float3(-1, 0, -1), r: -0.49, m: Dielectric())
objects.append(object)
object = Sphere(c: float3(0, 0, -1), r: 0.5, m: Lambertian(albedo: float3(0.24, 0.5, 0.15)))
objects.append(object)
let world = Hitable_list(list: objects)
with a single line that creates the random world instead:
let world = random_scene()
I would normally tell you to try rendering the scene now, but there is a great speed up hint I learned from hyperjeff and which allows us to get images of greater quality, much faster. Still inside the imageFromPixels() method, replace the outer loop first line:
for i in 0..<width {
with this block of code:
let queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)
dispatch_apply(width, queue) { i in
By using Grand Central Dispatch
threading, the rendering finishes 3 times faster! In the main playground page, see the generated new image:

This image was generated using a value of ns = 50, a sphere generator range from -7..<7 and an image resolution of 800 x 400. The rendering took 752 seconds to run so if you want a quick, 5-second rendering I suggest using a value of ns = 10, a sphere generator range from -2..<3 and an image resolution of 400 x 200. The source code is posted on Github as usual.
Until next time!