From the Archive

Writing A Ray Tracer in Go - Part 2

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 backward. 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 whether 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:

  • positive - there are 2 real solutions
  • negative - there are 0 real solutions
  • zero - there is 1 real solution

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 Sphere

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.

Update: Part 3 is available here: Writing A Ray Tracer in Go - Part 3