My Tour of Rust – Day 3 – Ray Tracing Part 1

My blog updates have slowed down over the last week. I have been dealing with a cold. I still hope to have two posts a week, but at a bare minimum one post a week.

In planning the next steps for this series, I wanted to provide more structure to my learning, so I decided to work on an actual project. I follow Peter Shirley on twitter @Peter_shirley and I have been meaning to go through his book Ray Tracing in a Weekend. So I decided to kill two birds with one stone and use that as a structure to learning some Rust.

I will just be covering the first two chapters of the book in this post, since just these two chapters required me to learn many new concepts in Rust. I provide the first code example from the book, but will not include any of the rest of the code. Please get Peter’s excellent book if you are interested.

Chapter 1

The first chapter is very basic. Write simple program that prints data for a PPM formatted file to stdout.

On OSX, the file is viewable by default, but on Windows, you will need
a program or you can use this online viewer. Below is the starting code in C++ that I am implementing in Rust.

#include <iostream>

int main()
{
	int nx = 200;
	int ny = 100;
	std::cout << "P3\n" << nx << " " << ny << "\n255\n";
	for (int j = ny - 1; j >= 0; j--) {
		for (int i = 0; i < nx; i++) {
			float r = float(i) / float(nx);
			float g = float(j) / float(ny);
			float b = 0.2;
			int ir = int(255.99*r);
			int ig = int(255.99*g);
			int ib = int(255.99*b);
			std::cout << ir << " " << ig << " " << ib << "\n";
		}
	}
}

Working with ranges in for loops

Like I discussed in a previous post, for loops in Rust can work with either and iterator or a ranges. One nice thing about ranges is that also have “rev” function so you can reverse through the loop without the, in my opinion, awkwardness that is required to do the same in C++ or Java.

for j in (0..ny).rev() {
    for i in 0..nx {

Type Casting

Another thing that this block of code required me to learn, was how type casting works in Rust. It is handled with the “as” notation. I find this a bit clunky compared with cast in other languages, especially with requiring extra parentheses in a complex expression.

fn main() {
    let nx = 200;
    let ny = 100;

    print!("P3\n{} {}\n255\n", nx, ny);
    for j in (0..ny).rev() {
        for i in 0..nx {
            let r = i as f32 / nx as f32;
            let g = j as f32 / ny as f32;
            let b :f32 = 0.2;
            let ir = (255.00*r) as i32;
            let ig = (255.00*g) as i32;
            let ib = (255.00*b) as i32;
            print!("{} {} {}\n", ir, ig, ib);
        }
    }
}

Now let’s run the example. I am using the release option, to compile and run the optimized version of the code instead of the debug version. This was pointed out to me as feedback from my previous post.

cargo run --release > image.ppm

Success, here is the image produced.

Chapter 2

This chapter is focused on creating a 3D vector class that will be used throughout the book. This chapter presented a lot of learning opportunities in porting the code to Rust.

Structs and Methods

One of the first things that I learned here is that there are not classes in Rust in the same sense as Java and C++. Instead there are structs, just like C and more recently Go, and they just contain the data. Then functions are added on passing a reference to “self” to provide the equivalent of methods. Another implication of this, is there is no inheritance from traditional OOP. I will talk about Traits later. The resulting struct needed for this chapter is trivial.

pub struct Vec3 {
    e : [f32; 3],
}

Traits

The power of Rust comes when adding traits. Traits can be thought of as Interfaces from Java, but they can also provide a default implementation so they can do more. There are a couple of ways to use traits. The one below adds default behavior of Copy and Clone that just duplicates the memory of the struct and then adds the Debug attribute that prints in the contents of the struct.

#[derive(Copy, Clone, Debug)]
pub struct Vec3 {
    e : [f32; 3],
}

Custom Constructors

In Rust there are only two constructors, you either create the struct with or without initialization. Instead of using these constructors, there is a convention in rust to create a “class level” function. There is some guidance provided in the style documentation.

impl Vec3 {
    pub fn new(e0: f32, e1: f32, e2: f32) -> Vec3 {
        Vec3 { e: [e0, e1, e2] }
    }
}

Operator Overloading

In the vec3 implementation in the book, he makes use of the C++ operator overloading functionality. I started by looking at operator overloading in Rust. Then I had to find the documentation for the Traits here.

The one thing that tripped me up for a bit was function overloading that was seemingly need based on the following operators.

inline vec3& operator*=(const vec3 &v2);
inline vec3& operator*=(const float t);

However after additional research, I found that you can do it by explicitly specifying the type of the Right Hand expression as seen below. This provides the needed information to the compiler to handle the type coercion.

impl std::ops::MulAssign<Vec3> for Vec3 {
    fn mul_assign(&mut self, other: Vec3) {
        self.e[0] *= other.e[0];
        self.e[1] *= other.e[1];
        self.e[2] *= other.e[2];
    }
}

impl std::ops::MulAssign<f32> for Vec3 {
    fn mul_assign(&mut self, other: f32) {
        self.e[0] *= other;
        self.e[1] *= other;
        self.e[2] *= other;
    }
}

Ownership and Borrowing

During the process of working of this I got the following error. This lead me to do additional reading about ownership and borrowing. I am very comfortable with memory management in C++ and differences between references and copy constructors, along with const correctness, but I am still not 100% comfortable with these concepts in Rust yet. I may write a followup post digging into this topic more in depth.

cargo test --no-run --package rtinw_ch2 --bin rtinw_ch2 tests
   Compiling rtinw_ch2 v0.1.0 (/my-tour-of-rust/day3/rtinw_ch2)
error[E0382]: borrow of moved value: `v`
  --> src\main.rs:43:13
   |
43 |         v / v.length()
   |         -   ^ value borrowed here after move
   |         |
   |         value moved here
   |
   = note: move occurs because `v` has type `Vec3`, which does not implement the `Copy` trait

Final Code

The full source code for my solution can be found here.

Summary

This post took much more time that I was originally planning. I did find it very rewarding and challenging, but I am not sure if I should focus on smaller more focused posts or continue to be more free flowing. Please let me know what you think and consider giving me a follow over on twitter @rushtonality.