Brandon Eleuterio

Articles

Learning Rust – Part 5: Sphere Surfaces

Brandon Eleuterio

In Part 4 we drew a sphere. This time we’re giving the sphere surface normals. Normals are things like shading and lighting that make the object look more three-dimensional.

Bug in a Hay Stack

This one was a doozy! After I whizzed through coding everything in this chapter, I realized in my haste I had made a mistake. I lost my sphere and I had absolutely no idea where to find the bug.

Blue and White Rectangle
Where’s the sphere?

In coding, when something doesn’t work as expected, there are three options:

  1. Step through the code
  2. Write tests
  3. Refactor

Step Through

Stepping through the code involves running the program step-by-step (or debugging) and inspecting the results at each step. Hopefully, at some point, the result doesn’t match our expectations. This can be painful, especially if we’re not entirely sure what results to expect at each step. This was my issue, so I decided to start writing tests for simple pieces of code.

Write Tests

I wrote tests for many of the Vec3 math functions such as negation, addition, subtraction, multiplication, and division.

Rust has some nice testing options. All it takes to write a test is to write a function with an attribute macro. This test function can live in the same file as the rest of your code. No need to create a separate file just for testing.

#[test]
fn can_negate_vec3() {
    let src = Vec3::new(1.0, 2.0, 3.0);
    let res = Vec3::new(-1.0, -2.0, -3.0);
    assert_eq!(-src, res);
    assert_eq!(src[0], 1.0);
}

My favorite way to write tests is to add them as part of the documentation. Because Rust supports markdown in their comments, we can add actual executable test code that doubles as documentation. So nice!

/// Multiplies parts of two Vec3 objects
///
/// # Examples
/// ```
/// use ray_tracer::{Vec3};
/// let res = Vec3::new(1.0, 2.0, 3.0).dot(Vec3::new(4.0, 5.0, 6.0));
/// assert_eq!(res, 32.0);
/// ```
pub fn dot(&self, v: Vec3) -> f64 {
    self.x() * v.x() + self.y() * v.y() + self.z() * v.z()
}

Refactor

After I wrote a few tests and I realized I wasn’t finding the bug, I decided to move to the refactor phase. Refactoring code involves making code simpler and easier to read. In my case, I was dealing with lots of code in one file – lib.rs. Generally, code is easier to read if it’s split up into multiple files rather than crammed into one or two giant files, so I created two additional files – ray.rs and vec3.rs – and moved code from lib.rs into these new files.

In my next refactor step, I removed the extraneous ampersands. After some research into how pointers and memory management work in Rust, I realized I went way overboard with references. So, code like this:

let lower_left_corner = &(&origin - &(horizontal / 2_f64))- &(&(vertical / 2_f64) - &Vec3::new(0.0, 0.0, focal_length));

Turned into this:

let lower_left_corner = (origin - (horizontal / 2_f64)) - ((vertical / 2_f64) - Vec3::new(0.0, 0.0, focal_length));

Much easier to read when we remove six ampersands! And now I can see the bug! Here’s the code that works:

let lower_left_corner = origin - (horizontal / 2_f64) - (vertical / 2_f64) - Vec3::new(0.0, 0.0, focal_length);

If you look carefully, the bug is an order of operations problem from a middle school math class. I simply removed the extra parenthesis.

The Sphere is Back!

Now that I fixed our bug, running the program gives a sphere again:

3D sphere

Notice the sphere has multiple colors starting to indicate some dimension. Exactly what we wanted to see at this stage of the project.

Next

In the next chapter, we’ll add support for drawing multiple spheres. Until then, feel free to follow along on GitHub and the online book.

Tags:

Leave a Reply

Your email address will not be published.

Back to top