Writing a Ray Tracer in Go - Part 4
Friday, June 3, 2016
I'm roughly following the e-book Ray Tracing in One Weekend, but translating all of the code into Go.
All of the code for this post can be found on my Github.
Last time we added the ability to shade our sphere and added anti-aliasing to make everything look better.
This resulted in the following image:
This time we are going to take a major leap forward and introduce different materials to our path tracer. This will allow us to make more interesting images and will more closely simulate real world objects.
The two material types we are going to introduce are matte materials and metal materials. Let's get to work.
Matte Is Where It's At
Before we begin adding a matte material type, it's worth going over what actually gives different materials their ‘look’. Meaning why do some materials look matte while others look mirrored or even translucent? It all comes down to how light is reflected and/or absorbed from these different material types.
Matte, or diffuse surfaces may absorb some light, but also reflect light rays in random directions. Any surface that reflects light in this manner will look matte.
In order to model this in code, I first want to introduce the concept of a Material. This will allow us to cleanly model any type of material we want.
This interface will have two methods Bounce and Color. Bounce is responsible for implementing how each material type will reflect or ‘bounce’ light off of it, while Color is responsible for returning that material's intrinsic color as a Vector.
As you can see, Bounce takes in a input Ray and a Hit record and returns two types, a boolean that represents if that material reflected light and a Ray that represents that resulting reflection (if it occurred).
For our matte materials, we are going to use a method of calculating reflection called Lambertian reflectance. For our purposes, this method is implemented by taking a random point on the unit sphere that is tangent to our object's hit point and then sending a ray from that hit point to this random point.
This can be better show in code:
These changes require that we also update our color method in our main file to work with the Material interface like so:
Putting all of this together results in the following image:
Reflective surfaces such as many metals have a much different way of reflecting light than matte surfaces. Instead of reflecting light in random directions such as with diffuse reflectance, metals reflect light in a way that is described as specular reflection.
The algorithm for determining the direction of the reflected ray can be described in pseudo code as
incident_vector - (2 * dot(incident_vector, surface_normal) * surface_normal).
Putting this in code yields the following implementation for our Metal type:
Adding two metal spheres adjacent to our initial sphere, each with two different intrinsic colors results in the following image:
Note that both of these spheres are perfectly reflective such as a mirror would be. In order to add more realistic looking metals we can introduce a fuzz coefficient to randomize the reflected direction a little. We can use a similar method to the one we used with diffuse materials by adding the following code to our Metal type:
Playing around with different fuzz coefficients between 0.0 and 1.0 for the right-most sphere can give us an image similar to this:
One small feature that I wanted to add that's not covered in the book is the ability to show progress as the image is being rendered as well as the time spent rendering. I came across the time package from the Go standard library which has everything required to implement a simple progress indicator.
It also provides a nice API to get the duration of time passed by using the Since function like so:
After the operation completes, this will output:
Done. Elapsed: 8.264296922s
Finally, to add the progress indicator while the rendering is taking place we need to get a little more clever. Luckily, the Go time package again provides a simple solution with the Ticker type. The Ticker type contains a channel that is sent a tick after a specified interval expires. We can then consume from this channel and output
. each time the tick occurs.
Putting all this together results in more informative output:
$ go run *.go .................................................................................. Done. Elapsed: 8.264296922s
That's it for this post, we're one huge step closer to completing our path tracer. Hopefully you found this interesting. Please let me know if so, or ask any questions on Twitter.
Update: Part 5 is available here: https://markphelps.me/2016/07/24/writing-a-ray-tracer-in-go-5/