My Tour of Rust – Day 4 – Ray Tracing Part 2

It has been three weeks since the last installment of my 2-3 post a week blog. As I noted in an earlier post. I am committing to myself to spend at least an hour per day learning new things and writing them up in this blog. I decided that I wanted to complete the “Ray Tracing in One Weekend” book for this post. The previous post can be found here. I underestimated the new Rust concepts and conventions that would be required to do this and how long it would take to write up my experience. So now these weeks later, I am finally finished.

I have also made a decision to move on from Rust for a while and pursue some other interests in up coming posting. I have found myself really loving Rust. I really appreciate all of the compile time checks that can prevent problems later. There were only a couple of times that I got a good compile, that I did not get the result I was expecting when I ran the program.

This exercise really forced me to understand Ownership and borrowing more thoroughly then.

Chapter 3 & 4 – Splitting code into modules

My first objective in starting the rest of the chapters of the book was to figure out how to structure a project with multiple files and modules.

The first thing that I did was start creating a separate file to split different functionality. That turned out being fair straight forward. It was just forward declaring a module with the same name as the file from the the main.rs file.

pub mod raytrace;

Then I wanted to split things even more and create a tree structure of directories. So now I created a raytrace sub-directory, but I not sure what to do next, because if I tried to do the mod statement above, then all of my modules were not found. Below is the file structure that I had.

src/main.rs
src/raytrace
src/raytrace/ray.rs
src/raytrace/material.rs
src/raytrace/vec.rs
src/raytrace/camera.rs

Then I searched and found this page about splitting. So I saw that I needed a file mod.rs that would act like raytrace.rs did in my first step. Then in that file I could bring in the other nested modules as below.

pub mod camera;
pub mod material;
pub mod ray;
pub mod vec;

Chapter 5 – Traits, Collections and Polymorphism

Traits

This chapter introduced the very common object orient concept of inheritance and polymorphism.

class hitable {
    public:
        virtual bool hit(const ray& r, float t_min, float t_max, hit_record& rec) const = 0;
};

class sphere: public hitable  {
    public:
        sphere() {}
        sphere(vec3 cen, float r, material *m) : center(cen), radius(r), mat_ptr(m)  {};
        virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
        vec3 center;
        float radius;
};

class hitable_list: public hitable  {
    public:
        hitable_list() {}
        hitable_list(hitable **l, int n) {list = l; list_size = n; }
        virtual bool hit(const ray& r, float tmin, float tmax, hit_record& rec) const;
        hitable **list;
        int list_size;
};

So now I needed to create my first trait. Here I decided to do a direct port of the C++ code, and used an out parameter (&mut HitRecord) to return the results. I decision causes me many headaches later, as discussed below.

pub trait Hitable {
    fn hit(&self, r : ray::Ray, t_min : f32, t_max : f32, rec : &mut HitRecord) -> bool;
}

Polymorphism

The following code, does not work and that would seem obvious, but how do I have a polymorphic pointer or reference.

    let a : Hitable = Sphere::new(...);
    let b : Hitable = HitableList::new(...); 

So I thought about this, but how is ownership going to work here. This still did not feel right.

    let a : &Hitable = &Sphere::new(...);
    let b : &Hitable = &HitableList::new(...);

Then I found out about Box. This seems to mostly parallel the idea of a pure pointer in C++ and the memory is allocated in the heap, so that seemed to be what I needed.

Collections

With this knowledge, I can now create the my list. I decided to use Vec as my underlying collection. This left me with the following for HitableList.

pub struct HitableList {
    list : Vec<Box<Hitable>>,
}

Chapter 6 – drand48() – Crates

In this chapter, the author started using a random number generator. I need to figure out how to do this in Rust. Searching the Internet I found the solution, but the common solution was to use a 3rd party crate to provide this functionality, so I got the experience the process of adding a new dependency.

Adding dependency to the Cargo.toml file

[dependencies]
rand = "0.6"

Using the external library in the code.

extern crate rand;
use rand::Rng;

pub fn drand48() -> f32 {
    let rand_float : f32 = rand::thread_rng().gen();
    rand_float
}

Pretty straight forward and similar to maven and gradle in the Java world, but not quite as trivial as being able to search add using the npm command in the NodeJS/Javascript world.

Chapter 7 – Nothing new related to Rust

This chapter was fairly uneventful regarding learning new Rust concepts.

Chapter 8 – HitRecord and Material

This chapter is where I hit a wall, and learning Rust got real! This book uses the common C/C++ idea of passing an out parameter of a reference and then using that to get the output of the function. Starting back in chapter 5, I was doing a direct port of this in Rust and it was working, until I now. When a pointer to material was added to the hit record.

Until now, I was adding the copy functionality to all of my structs, included Vec3, Ray, and HitRecord. Like below.

#[derive(Copy, Clone, Debug)]
pub struct HitRecord {
    pub t : f32,
    pub p : vec::Vec3,
    pub normal : vec::Vec3,
}

However with the following change in the C++ code, this became a problem, because now that the polymorphic Box was added I could no longer have this since Box forbids you from deriving Copy.

struct hit_record
{
    float t;
    vec3 p;
    vec3 normal;
    material *mat_ptr
};

Now I have the following struct, but I no long get the relative ease/laziness of not having to worry about ownership and moving out of contexts.

pub struct HitRecord {
    pub t : f32,
    pub p : vec::Vec3,
    pub normal : vec::Vec3,
    pub material : Box<Material>,
}

Ownership and Borrowing

One of the key concepts of Rust is the idea of ownership of memory. Rust tries to solve some of the core issues with languages like C++ where memory leaks and corruption can happen fairly easily with out using a Garbage collection mechanism. There are way to do reference counting in Rust, but by default you should be able to determine at compile time if ownership is being honored correctly.

Moving vs Copying

Previously with the Vec3, Ray, and HitRecord classes, I did not have to worry about this much since they could be copied. Now if I did not pass by reference, I was doing a move.

Out References

Previously I mentioned that I decided to do a direct port of the C++ code in regards to an out parameter. Here I decided that a better approach would be to just return the struct that I created in the function and let Rust’s move logic deal with the passing of the ownership of the struct out of the method.

Option returns to handle nulls

So now that my hit function returns/moves the struct out of the function, what happens when I need to return a null pointer. This is where the Option enum comes into play.

Final implementations using Option

impl Hitable for Sphere {
    fn hit(&self, r : ray::Ray, t_min : f32, t_max : f32) -> Option<HitRecord> {
        let oc = r.origin() - self.center;
        let a = vec::dot(r.direction(), r.direction());
        let b = vec::dot(oc, r.direction());
        let c = vec::dot(oc, oc) - self.radius*self.radius;
        let discriminant = b*b - a*c;
        if discriminant > 0.0 {
            let temp = (-b - f32::sqrt(b*b-a*c))/a;
            if temp < t_max && temp > t_min {
                let t = temp;
                let p = r.point_at_parameter(t);
                let normal = (p - self.center) / self.radius;
                return Some(HitRecord{t, p, normal, material: self.material.clone()});
            }
            let temp = (-b + f32::sqrt(b*b-a*c))/a;
            if temp < t_max && temp > t_min {
                let t = temp;
                let p = r.point_at_parameter(t);
                let normal = (p - self.center) / self.radius;
                return Some(HitRecord{t, p, normal, material: self.material.clone()});
            }
        }
        None
    }
}
impl Hitable for HitableList {
    fn hit(&self, r: ray::Ray, t_min: f32, t_max: f32) -> Option<HitRecord> {
        let mut result : Option<HitRecord> = None;
        let mut closest_so_far = t_max;

        for a in &self.list {
            let temp_result = a.hit(r, t_min, closest_so_far);
            match temp_result {
                Some(rec) => {
                    closest_so_far = rec.t;
                    result = Some(rec);
                }
                None => {}
            }
        }
        result
    }
}

Chapter 9-12 – Again, not much new related to Rust

Once I got through chapter 8 and all of the hurdles there, the rest of the book was pretty straight forward as far as the coding was concerned. The last chapter had an exercise to generate the images from the cover of the book. Here is my image.

Chapter 12 Image

Concluding

The full code for this can be found here. Like I noted in the start of this post. This will likely be my last post on Rust for a while. I plan to branch off into some other technologies. I hope someone has found this useful, because it has been very valuable for me in forcing me to document what I have been learning. Thank you for taking time to read and follow me on twitter @rushtonality for future updates and anything else I my find interesting.

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.