Chapter 3 of Peter Shirley's book starts us of with Rays, a simple camera and background. We have to make some additions to our vec3 class, to implement operator overloading. Rust allows for limited operator overloading, where only certain operators can be overloaded.

Moving and Copying

In chapter 3 we also implement the unit_vector function, which takes a Vec3 vector as input and finds it's unit length. Unfortunately this moves the original Vec3 vector out of scope, and it can not be used anymore. There are two ways to avoid this problem, copying or borrowing. Copying allows to perform the operator overloading and returning of values without the original pointer going out of scope. Implementing copying for Vec3 is super simple and is done by adding the line #[derive(Copy,Clone)] where Vec3 is defined.

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

Borrowing

Another way to solve the moving problem is to not move at all, but rather use references. Thus saving on memory but making the code harder to read. Borrowing is performed by sending the address of a variable using the & symbol. See slide 31 from the 2015 Boston Rust Meetup

Background

Now that we understand copying, moving, and borrowing we can implement the raytracer for generating our background.

 
    extern crate ray;
    extern crate vec3;

    fn color(r: &ray::Ray) -> vec3::Vec3 {
      let unit_direction = vec3::unit_vector(r.direction());
      let t:f64 = 0.5*(unit_direction.y()+1.0);
      vec3::Vec3::new(1.0,1.0,1.0)*(1.0-t) + vec3::Vec3::new(0.5, 0.7, 1.0)*t
    }

    fn main() {
      let nx: i32 = 400;
      let ny: i32 = 200;
      print!("P3\n{} {}\n255\n",nx,ny);
      let lower_left_corner = vec3::Vec3::new(-2.0,-1.0,-1.0);
      let horizontal = vec3::Vec3::new(4.0,0.0,0.0);
      let vertical = vec3::Vec3::new(0.0,2.0,0.0);
      let origin = vec3::Vec3::new(0.0,0.0,0.0);
      for j in (0..ny-1).rev() {
        for i in 0..nx {
          let u: f64 = (i as f64)/(nx as f64);
          let v: f64 = (j as f64)/(ny as f64);
          let r = ray::Ray::new(&origin,&(lower_left_corner+horizontal*u+vertical*v));
          let col: vec3::Vec3 = color(&r);
          let ir = (255.99*col[0]) as i32;
          let ig = (255.99*col[1]) as i32;
          let ib = (255.99*col[2]) as i32;
          println!("{} {} {}",ir,ig,ib);
        }
      }
    }

The above code uses a ray defined by the function p(t) = A + t*B where p is the 3D ray, A is its origin and B is the ray direction. The t parameter moves the point along the ray. Positive t will take you to parts in from of A, and negative t will take you to points behind A. The task of the ray tracer is to send rays through pixels and compute the color seen in the direction of those rays. The code above will traverse the screen from the lower left hand corner and use two offset vectors along the screen sides to move the ray endpoint across the screen. The color(r: &ray::Ray) function linearly blends white and blue depending on the y coordinate and gives us the background you see below.

Ray Background

Figure 1: Background gradient generated by ray!.

That's it for chapter 3! If you want to see the source code discussed in this post, go to github.