Writing A Ray Tracer in Go - Part 2

March 26, 2016

This is part 2 of my journey to try and write a ray/path tracer in Go. Checkout part 1 here.

I’m roughly following the e-book Ray Tracing in One Weekend, but translating all of the code into Go.

In the previous post we covered how a path tracer works and got an image to display on the screen by blending red, green and blue into a cool looking gradient. This time around we’ll draw a sphere instead, but by actually sending rays into the scene and marking the pixels where they hit the object.

This is the first major step into building a fully functional path tracer. Lets get to it.

All of the code for this post can be found on my Github.

Rays

Every ray/path tracer has one thing in common, a way to model a ray. A ray is defined as:

In geometry, a ray is a line with a single endpoint (or point of origin) that extends infinitely in one direction.

So, a ray has two parts: origin and direction.

We can model this in our code by defining a struct like so:

type Ray struct {
	Origin, Direction Vector
}

Next, we need to be able to move along our ray either forward or backword. The function to allow us to do so is defined as: p(t) = A + t * B where A is the ray origin, B is the ray direction, and t is a real number.

In code, this looks like:

func (r Ray) Point(t float64) Vector {
	b := r.Direction.MultiplyScalar(t)
	a := r.Origin
	return a.Add(b)
}

This method takes in a t as a float64 and returns a position vector in 3D space, which is our new position on our ray.

Adding a Sphere

Now that we can define and move along a ray, we need to add the second piece of the puzzle, a sphere.

In geometry a sphere is defined as having a center and radius.

We can model this in Go with another simple struct:

type Sphere struct {
	Center Vector
	Radius float64
}

Now comes the fun part, adding the ability for a ray to determine wheter or not it comes in contact with a sphere.

Intersecting with the Sphere

Note: Math ahead. I had trouble remembering a lot of this from algebra/geometry class, so if like me you need a refresher, this link may come in handy.

The book goes into greater detail, however the basic formula for determining if a point is on a sphere is as follows:

dot((p - C),(p - C)) = R * R

Where p is the point, C is the center of our sphere, and R is the sphere radius.

Now this is great, but we want to know if our ray p(t) = A + t * B ever hits the sphere anywhere. So basically, is there any t for which p(t) satisfies the sphere equation.

This corresponds to:

dot((p(t) - C),(p(t) - C)) = R * R

Which expands to:

dot((A + t * B - C),(A + t * B - C)) = R * R

Expanding and moving all terms to the LHS, we get:

t * t * dot(B, B) + 2 * t * dot(A-C, A-C) + dot(C, C)

Since this is quadratic, we can use the quadratic formula to solve for t. When solving the quadratic, there is the discriminant portion of the equation (b * b - 4ac) which tells us the number of solutions.

If the discriminant is:

All this boils down to the following method:

func (r Ray) HitSphere(s Sphere) bool {
	oc := r.Origin.Subtract(s.Center)
	a := r.Direction.Dot(r.Direction)
	b := 2.0 * oc.Dot(r.Direction)
	c := oc.Dot(oc) - s.Radius*s.Radius
	discriminant := b*b - 4*a*c

	return discriminant > 0
}

Adding Color

Now that we can determine if our rays hit our sphere, lets add some color. We want our image to show that when a ray does hit the sphere the pixel is marked red, and when they don’t, it shows up as a nice blue gradient.

Again the book goes into greater detail on how this is done, but I tried to comment the Color method as best I could:

func (r Ray) Color() Vector {
	sphere := Sphere{Center: Vector{0, 0, -1}, Radius: 0.5}

	if r.HitSphere(sphere) {
		return Vector{1.0, 0.0, 0.0} // red
	}

	// make unit vector so y is between -1.0 and 1.0
	unitDirection := r.Direction.Normalize()

	// scale t to be between 0.0 and 1.0
	t := 0.5 * (unitDirection.Y + 1.0)

	// linear blend
	// blended_value = (1 - t) * white + t * blue
	white := Vector{1.0, 1.0, 1.0}
	blue := Vector{0.5, 0.7, 1.0}

	return white.MultiplyScalar(1.0 - t).Add(blue.MultiplyScalar(t))
}

Putting it All Together

After updating our code to use these new methods, running the program via go run *.go yields the out.ppm file which gives us:

'Red'

Note the jagged edges of the sphere, this is because we haven’t implemented anti-aliasing yet, so the pixel is either red or part of the background (there is no blending happening). We’re going to fix this in a later edition.

Well, that’s enough for now. Next time we’ll work on shading and add another object to the scene.

As always, let me know what you thought of this post in the comments or on Twitter.

Did you find this content helpful?


Let me send you more stuff like this! Unsubscribe at any time. No spam ever. Period.


Subscribe to MarkPhelps.me

* indicates required

Discussion, links, and tweets

Mark Phelps

I'm a Software Engineer in Durham, NC. I mostly write about Go, Ruby, and some Java from time to time.