Ray Tracing in One Weekend: Part 1 A Basic Render

Matthew MacFarquhar
4 min readNov 1, 2024

--

Introduction

I have been reading through this book series on Raytracing which walks the reader through the creation of a Raytracer using C++. In this series of articles, I will be going through this book and implementing the lessons in Rust instead and diving deep into the pieces.

In this first article, we will set up our renderer system with some necessary structs and a main to run through all our image pixels and generate a basic image.

The following link is the commit in my Github repo that matches the code we will go over.

Creating an Image

Our first step is to just set up code to generate an image — we will not be doing any raytracing at all in this first article. First, we will set up some useful structs for our journey (Vec3 and Color). Then we will make a simple main app which will iterate through pixels in an image and set them to a color.

Vec3.rs

Our Vec3 struct simply has an array of length three to hold our x, y and z values.

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

Our implementation has some getters and some functions to compute values like length of the vector.

impl Vec3 {
pub fn new(x: f64, y: f64, z: f64) -> Vec3 {
Vec3 {
e:[x,y,z]
}
}

pub fn x(&self) -> f64 {
self.e[0]
}

pub fn y(&self) -> f64 {
self.e[1]
}

pub fn z(&self) -> f64 {
self.e[2]
}

pub fn length(&self) -> f64 {
f64::sqrt(self.length_squared())
}

pub fn length_squared(&self) -> f64 {
self.e[0] * self.e[0] + self.e[1] * self.e[1] + self.e[2] * self.e[2]
}
}

We also implement a bunch of traits for Vec3 to support operations on Vec3s and printing.

impl Display for Vec3 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {} {}", self.e[0], self.e[1], self.e[2])
}
}

impl Neg for Vec3 {
type Output = Vec3;

fn neg(self) -> Self::Output {
Vec3::new(-self.x(), -self.y(), -self.z())
}
}

impl Add for Vec3 {
type Output = Vec3;

fn add(self, v: Vec3) -> Vec3 {
Vec3::new(self.x() + v.x(), self.y() + v.y(), self.z() + v.z())
}
}

impl AddAssign for Vec3 {
fn add_assign(&mut self, rhs: Self) {
*self = *self + rhs;
}
}

impl Sub for Vec3 {
type Output = Vec3;

fn sub(self, v: Vec3) -> Vec3 {
Vec3::new(self.x() - v.x(), self.y() - v.y(), self.z() - v.z())
}
}

impl SubAssign for Vec3 {
fn sub_assign(&mut self, rhs: Self) {
*self = *self - rhs;
}
}

impl Mul for Vec3 {
type Output = Vec3;

fn mul(self, v: Vec3) -> Vec3 {
Vec3::new(self.x() * v.x(), self.y() * v.y(), self.z() * v.z())
}
}

impl MulAssign for Vec3 {
fn mul_assign(&mut self, rhs: Self) {
*self = *self * rhs;
}
}

impl Div<f64> for Vec3 {
type Output = Vec3;

fn div(self, t: f64) -> Vec3 {
Vec3::new(self.x() / t, self.y() / t, self.z() / t)
}
}

impl DivAssign<f64> for Vec3 {
fn div_assign(&mut self, t: f64) {
*self = *self / t;
}
}

impl Mul<f64> for Vec3 {
type Output = Vec3;

fn mul(self, rhs: f64) -> Self::Output {
Vec3::new(self.x() * rhs, self.y() * rhs, self.z() * rhs)
}
}

impl Mul<Vec3> for f64 {
type Output = Vec3;

fn mul(self, rhs: Vec3) -> Self::Output {
Vec3::new(self * rhs.x(), self * rhs.y(), self * rhs.z())
}
}

and we export some public functions and a type alias for Point3

pub fn dot(u: Vec3, v: Vec3) -> f64 {
u.e[0] * v.e[0] + u.e[1] * v.e[1] + u.e[2] * v.e[2]
}

pub fn cross(u: Vec3, v: Vec3) -> Vec3 {
Vec3::new(
u.e[1] * v.e[2] - u.e[2] * v.e[1],
u.e[2] * v.e[0] - u.e[0] * v.e[2],
u.e[0] * v.e[1] - u.e[1] * v.e[0],
)
}

pub fn unit_vector(v: Vec3) -> Vec3 {
v / v.length()
}

pub type Point3 = Vec3;

Vec3 is going to be our workhorse struct in our raytracing journey, so it’s a good thing to get all the necessary functionality implemented early.

Color.rs

Color is another alias for Vec3 — with the addition of a new function to write the rgb value to an outstream.

pub type Color = Vec3;

pub fn write_color(out: &mut impl Write, pixel_color: Color) {
let r = (255.99 * pixel_color.x()) as i32;
let g = (255.99 * pixel_color.y()) as i32;
let b = (255.99 * pixel_color.z()) as i32;
writeln!(out, "{} {} {}", r, g, b).expect("writing color");
}

Main.rs

Our main function will now iterate over the rows and cols of our image and output the pixel data for the ppm format, we will render our images as if the (0,0) is the bottom left corner. For our pixels, blue is static but red and green vary by the current row and col of the pixel we are coloring.

mod vec3;
mod color;

fn main() {
const IMAGE_WIDTH: i32 = 256;
const IMAGE_HEIGHT: i32 = 256;

print!("P3\n{} {}\n255\n", IMAGE_WIDTH, IMAGE_HEIGHT);

for j in (0..IMAGE_HEIGHT).rev() {
eprint!("\rScanlines remaining: {}", j);
for i in 0..IMAGE_WIDTH {
let r: f64 = (i as f64) / (IMAGE_WIDTH - 1) as f64;
let g: f64 = (j as f64) / (IMAGE_HEIGHT - 1) as f64;
let b: f64 = 0.25;

let pixel_color = Color::new(r, g, b);
color::write_color(&mut io::stdout(), pixel_color);
}
}

eprint!("\nDone.\n");
}

After running the application, we will get an image like this.

Conclusion

In this article we set up the ground work for our renderer, creating the application logic and the structs needed for us to build on in the coming articles.

--

--

Matthew MacFarquhar
Matthew MacFarquhar

Written by Matthew MacFarquhar

I am a software engineer working for Amazon living in SF/NYC.

No responses yet