Let’s continue working on our `ray tracer`

and pick up where we left off last week. First, as you are already used to, we’ll do more code cleaning. I went ahead and replaced all classes with structs, and also used proper naming conventions (such as capitalization of types) this time. You can see the modified code in this week’s repository. To keep this article short, I am not going over the cleaning procedure this time, but you will notice the transformations were rather minimal.

Last time we looked at how `lambertian`

and `metal`

materials are rendered. The last type of material we need to look into is called **dielectric** and you can recognize it when looking at water or at objects made of glass. When `dielectrics`

are hit by a ray, the ray splits in two: a `reflected`

(bounced) ray and a `refracted`

(propagated) ray. `Refraction`

is described by Snell’s law. Let’s see how this law translates into code. In `material.swift`

create a **refract()** function:

func refract(v: float3, n: float3, ni_over_nt: Float) -> float3? { let uv = normalize(v) let dt = dot(uv, n) let discriminant = 1.0 - ni_over_nt * ni_over_nt * (1.0 - dt * dt) if discriminant > 0 { return ni_over_nt * (uv - n * dt) - n * sqrt(discriminant) } return nil }

Next, let’s create the **Dielectric** struct. Notice that `attenuation`

is always **1** because `dielectrics`

never absorb anything from an incident ray:

struct Dielectric: Material { var ref_index: Float = 1 func scatter(ray_in: Ray, _ rec: Hit_record, inout _ attenuation: float3, inout _ scattered: Ray) -> Bool { var ni_over_nt: Float = 1 var outward_normal = float3() let reflected = reflect(ray_in.direction, n: rec.normal) attenuation = float3(1, 1, 1) if dot(ray_in.direction, rec.normal) > 0 { outward_normal = -rec.normal ni_over_nt = ref_index } else { outward_normal = rec.normal ni_over_nt = 1 / ref_index } let refracted = refract(ray_in.direction, n: outward_normal, ni_over_nt: ni_over_nt) if refracted != nil { scattered = Ray(origin: rec.p, direction: refracted!) } else { scattered = Ray(origin: rec.p, direction: reflected) return false } return true } }

We first compute an outward normal depending on whether the dot product between the ray and the hit point is positive or not, and then we use that to compute the refracted ray. In case it comes out `nil`

, we reflect the ray, otherwise we refract it. In `pixel.swift`

replace the second `metal`

sphere with a `dielectric`

one:

object = sphere(c: float3(x: -1, y: 0, z: -1), r: 0.5, m: Dielectric())

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

Glass surfaces have reflectivity that varies with the angle. When you look at it perpendicularly, the reflectivity is the lowest possible, if any. The smaller the viewing angle gets, the higher the reflectivity is, and other objects from the world are mirrored more clearly by the glass surface. This effect can be computed with the Schlick polynomial approximation:

func schlick(cosine: Float, _ index: Float) -> Float { var r0 = (1 - index) / (1 + index) r0 = r0 * r0 return r0 + (1 - r0) * powf(1 - cosine, 5) }

The **scatter()** function needs to be adapted to use this approximation:

func scatter(ray_in: Ray, _ rec: Hit_record, inout _ attenuation: float3, inout _ scattered: Ray) -> Bool { var reflect_prob: Float = 1 var cosine: Float = 1 var ni_over_nt: Float = 1 var outward_normal = float3() let reflected = reflect(ray_in.direction, n: rec.normal) attenuation = float3(1, 1, 1) if dot(ray_in.direction, rec.normal) > 0 { outward_normal = -rec.normal ni_over_nt = ref_index cosine = ref_index * dot(ray_in.direction, rec.normal) / length(ray_in.direction) } else { outward_normal = rec.normal ni_over_nt = 1 / ref_index cosine = -dot(ray_in.direction, rec.normal) / length(ray_in.direction) } let refracted = refract(ray_in.direction, n: outward_normal, ni_over_nt: ni_over_nt) if refracted != nil { reflect_prob = schlick(cosine, ref_index) } else { scattered = Ray(origin: rec.p, direction: reflected) reflect_prob = 1.0 } if Float(drand48()) < reflect_prob { scattered = Ray(origin: rec.p, direction: reflected) } else { scattered = Ray(origin: rec.p, direction: refracted!) } return true }

Note that we are now refracting based on a reflectivity threshold we set to **1**. There is an easy way (trick) to get a hollow glass surface. If the radius is negative, even though the geometry remains unaffected, the normal will point inward and the result will be a nice looking hollow glass sphere. Let’s add one more `dielectric`

sphere with a negative radius:

object = sphere(c: float3(x: -1, y: 0, z: -1), r: -0.49, m: Dielectric())

You should be able to see the hollow glass sphere now. Before wrapping up, we need to do one more thing – fix the camera, so we can look at objects from different angles and distances. First, we need a `field of view`

for our camera. Then, we need a `lookFrom`

point and a `lookAt`

point to set the direction our camera will look. Finally, we need an `up`

vector so we can rotate the camera around its direction, and always know where `up`

is. In `ray.swift`

let’s replace our old camera with this one:

struct Camera { let lower_left_corner, horizontal, vertical, origin, u, v, w: float3 var lens_radius: Float = 0.0 init(lookFrom: float3, lookAt: float3, vup: float3, vfov: Float, aspect: Float) { let theta = vfov * Float(M_PI) / 180 let half_height = tan(theta / 2) let half_width = aspect * half_height origin = lookFrom w = normalize(lookFrom - lookAt) u = normalize(cross(vup, w)) v = cross(w, u) lower_left_corner = origin - half_width * u - half_height * v - w horizontal = 2 * half_width * u vertical = 2 * half_height * v } func get_ray(s: Float, _ t: Float) -> Ray { return Ray(origin: origin, direction: lower_left_corner + s * horizontal + t * vertical - origin) } }

In `pixel.swift`

replace the line where we make a call to the camera, with this code:

let lookFrom = float3(0, 1, -4) let lookAt = float3() let vup = float3(0, -1, 0) let cam = Camera(lookFrom: lookFrom, lookAt: lookAt, vup: vup, vfov: 50, aspect: Float(width) / Float(height))

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

The source code is posted on Github as usual.

Until next time!