Skip to content
Copied!
published on 2026-04-05

3. The vec3 Class

Ray Tracing in One Weekend (v3.2.3): 3 The vec3 Class

The original book defines a vec3 class in C++ that is used for colors, positions, and directions alike. In this document, we implement it as a Rust struct. Because Vec3 is used in every subsequent chapter, we place it in the common crate of the workspace.

Differences Between C++ and Rust

Separation of struct definition and impl block

In C++, member functions are defined inside the class body. In Rust, the struct (the data type) and the impl block (the method implementations) are separated.

cpp
// C++
class vec3 {
public:
    double e[3];
    double x() const { return e[0]; }  // Member function defined inside the class
};
rust
// Rust
struct Vec3 {
    e: [f64; 3],  // Data definition
}

impl Vec3 {
    pub fn x(self) -> f64 { self.e[0] }  // Methods go in the impl block
}

Explicit copy behavior via the Copy trait

In C++, a class holding double members gets an implicit copy constructor. In Rust, copy behavior must be declared explicitly with #[derive(Clone, Copy)]. A type implementing Copy is automatically duplicated on assignment and function argument passing. Because Copy is a subtrait of Clone, both are derived together.

Operator overloading and traits

In C++, operator+ is defined as a member function or a free function. In Rust, operators become available by implementing standard-library traits such as Add and Sub.

cpp
// C++
vec3 operator+(const vec3& u, const vec3& v) { ... }  // Defined as a free function
rust
// Rust: implement the std::ops::Add trait
impl std::ops::Add for Vec3 {
    type Output = Vec3;
    fn add(self, v: Vec3) -> Vec3 { ... }
}

Which types support which operators is declared explicitly as trait implementations.

Constructors are just associated functions

In C++, constructors are special syntax recognized by the language. Rust has no language-level constructors. Instead, an associated function such as Vec3::new(...) is defined by convention. Zero-initialization without arguments is handled by implementing the Default trait.

cpp
// C++
vec3() : e{0,0,0} {}
vec3(double e0, double e1, double e2) : e{e0, e1, e2} {}
rust
// Rust
impl Default for Vec3 {
    fn default() -> Self {
        Vec3 { e: [0.0, 0.0, 0.0] }
    }
}

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

Implementing Vec3

Create common/src/vec3.rs. Below is the complete implementation.

common/src/vec3.rs
rust
use std::ops::{
    Add, AddAssign, Div, DivAssign, Index, IndexMut, Mul, MulAssign, Neg, Sub,
};

/// A 3D vector with `f64` components.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Vec3 {
    /// Components stored as `[x, y, z]`.
    pub e: [f64; 3],
}

/// Type alias for a 3D point.
pub type Point3 = Vec3;
/// Type alias for an RGB color (components in [0, 1]).
pub type Color = Vec3;

// --- Constructors and Accessors ---

impl Vec3 {
    /// Creates a new `Vec3` from three `f64` components.
    pub fn new(e0: f64, e1: f64, e2: f64) -> Self {
        Vec3 { e: [e0, e1, e2] }
    }

    /// Returns the x component.
    pub fn x(self) -> f64 { self.e[0] }
    /// Returns the y component.
    pub fn y(self) -> f64 { self.e[1] }
    /// Returns the z component.
    pub fn z(self) -> f64 { self.e[2] }

    /// Returns the squared length (dot product with itself).
    pub fn length_squared(self) -> f64 {
        self.e[0] * self.e[0] + self.e[1] * self.e[1] + self.e[2] * self.e[2]
    }

    /// Returns the Euclidean length.
    pub fn length(self) -> f64 {
        self.length_squared().sqrt()
    }
}

impl Default for Vec3 {
    fn default() -> Self {
        Vec3 { e: [0.0, 0.0, 0.0] }
    }
}

// --- Output ---

impl std::fmt::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])
    }
}

// --- Operator Overloads ---

impl Neg for Vec3 {
    type Output = Vec3;
    fn neg(self) -> Vec3 {
        Vec3::new(-self.e[0], -self.e[1], -self.e[2])
    }
}

impl Index<usize> for Vec3 {
    type Output = f64;
    fn index(&self, i: usize) -> &f64 { &self.e[i] }
}

impl IndexMut<usize> for Vec3 {
    fn index_mut(&mut self, i: usize) -> &mut f64 { &mut self.e[i] }
}

impl AddAssign for Vec3 {
    fn add_assign(&mut self, v: Vec3) {
        self.e[0] += v.e[0];
        self.e[1] += v.e[1];
        self.e[2] += v.e[2];
    }
}

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

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

impl Add for Vec3 {
    type Output = Vec3;
    fn add(self, v: Vec3) -> Vec3 {
        Vec3::new(self.e[0] + v.e[0], self.e[1] + v.e[1], self.e[2] + v.e[2])
    }
}

impl Sub for Vec3 {
    type Output = Vec3;
    fn sub(self, v: Vec3) -> Vec3 {
        Vec3::new(self.e[0] - v.e[0], self.e[1] - v.e[1], self.e[2] - v.e[2])
    }
}

impl Mul for Vec3 {
    type Output = Vec3;
    fn mul(self, v: Vec3) -> Vec3 {
        Vec3::new(self.e[0] * v.e[0], self.e[1] * v.e[1], self.e[2] * v.e[2])
    }
}

impl Mul<f64> for Vec3 {
    type Output = Vec3;
    fn mul(self, t: f64) -> Vec3 {
        Vec3::new(self.e[0] * t, self.e[1] * t, self.e[2] * t)
    }
}

impl Mul<Vec3> for f64 {
    type Output = Vec3;
    fn mul(self, v: Vec3) -> Vec3 { v * self }
}

impl Div<f64> for Vec3 {
    type Output = Vec3;
    fn div(self, t: f64) -> Vec3 { self * (1.0 / t) }
}

// --- Utility Functions ---

/// Computes the dot product of two vectors.
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]
}

/// Computes the cross product of two vectors.
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],
    )
}

/// Returns the unit vector in the same direction as `v`.
pub fn unit_vector(v: Vec3) -> Vec3 {
    v / v.length()
}

Implementation Details

The #[derive(...)] macro

rust
#[derive(Debug, Clone, Copy, PartialEq)]

The #[derive] macro auto-generates boilerplate trait implementations.

  • Debug: Enables debug output via the {:?} format specifier — e.g., println!("{:?}", v).
  • Clone: Enables explicit duplication via v.clone(). Clone must be present for Copy to work.
  • Copy: Activates implicit copy on assignment (let v2 = v1;) and function argument passing. Because [f64; 3] is Copy, Vec3 can also be made Copy.
  • PartialEq: Enables the == operator, which is required for assertions such as assert_eq!(v1, v2) in tests.

Value-passing self in accessor methods

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

Unlike C++'s const member functions, Rust methods take self, &self, or &mut self. Here we use value-passing self. Normally, this would move ownership, but because Vec3 implements Copy, calling self.x() does not consume self; it can still be used after the call.

The Default trait

rust
impl Default for Vec3 {
    fn default() -> Self {
        Vec3 { e: [0.0, 0.0, 0.0] }
    }
}

This is the Rust equivalent of C++'s default constructor vec3() : e{0,0,0} {}. It can be called as Vec3::default() and is also used in struct update syntax such as Vec3 { ..Default::default() }.

Output via the Display trait

rust
impl std::fmt::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])
    }
}

This is the Rust equivalent of C++'s operator<<. Implementing Display makes the {} format specifier available. Debug ({:?}) can be auto-generated with derive, but Display ({}) must be implemented manually as it represents the user-facing format.

Type parameters for compound assignment operators

rust
impl AddAssign for Vec3 { ... }      // Vec3 += Vec3
impl MulAssign<f64> for Vec3 { ... } // Vec3 *= f64
impl DivAssign<f64> for Vec3 { ... } // Vec3 /= f64

AddAssign needs no type parameter because both sides are Vec3. MulAssign<f64> specifies the right-hand operand type via a type parameter.

DivAssign delegates to MulAssign: *self *= 1.0 / t dereferences self and invokes the MulAssign implementation.

Vec3 * Vec3 is the Hadamard product

rust
impl Mul for Vec3 { ... }  // Component-wise multiplication

Vec3 * Vec3 is component-wise multiplication (the Hadamard product), not the dot product. The dot product is computed by the dot() function described below. The Hadamard product appears in later chapters for operations such as blending texture colors.

Reversed implementation for f64 * Vec3

rust
impl Mul<Vec3> for f64 {
    type Output = Vec3;
    fn mul(self, v: Vec3) -> Vec3 { v * self }
}

Rust trait implementations are directional: they are looked up from the type on the left-hand side of impl ... for Type. Therefore, to enable 2.0 * v, a separate implementation of f64 * Vec3 is needed in addition to Vec3 * f64.

The Index and IndexMut traits

rust
impl Index<usize> for Vec3 { ... }
impl IndexMut<usize> for Vec3 { ... }

Implementing Index enables read access via v[0]; implementing IndexMut additionally enables write access such as v[0] = 1.0.

Utility functions

dot, cross, and unit_vector are free functions defined outside impl Vec3 (analogous to free functions in C++). In Rust, there is no functional difference between placing them inside or outside the impl block, but binary operations like dot(u, v) read more naturally as free functions.

cross is implemented via the determinant expansion:

u×v=(uyvzuzvyuzvxuxvzuxvyuyvx)

The write_color Function

The original book defines write_color() in color.h. The C++ version writes directly to a std::ostream, but here, as in the previous chapter, we return a String. Since Color is a type alias for Vec3, the function accepts a Vec3 value directly.

Append to common/src/vec3.rs:

common/src/vec3.rs
rust
pub fn write_color(pixel_color: Color) -> String {
    let ir = (255.999 * pixel_color.x()) as i32;
    let ig = (255.999 * pixel_color.y()) as i32;
    let ib = (255.999 * pixel_color.z()) as i32;
    format!("{} {} {}", ir, ig, ib)
}

Meaning of type Color = Vec3

rust
pub type Point3 = Vec3;
pub type Color = Vec3;

Just as in C++, Rust's type aliases are not checked by the compiler. Color and Point3 are treated as exactly the same type, and mixing them will not produce a compile error. If you want to prevent such mixing, the newtype pattern can be used, but we follow the original design and keep simple type aliases here.

Integrating into the common Crate

To expose Vec3 from the common crate, create common/src/lib.rs:

common/src/lib.rs
rust
// common/src/lib.rs
pub mod vec3;
pub use vec3::{Color, Point3, Vec3, cross, dot, unit_vector, write_color};

Rewriting the Previous Chapter's Code

The original book rewrites main.cc using the color type and write_color(). Here we create a new crate r103-vec3.

First, set up r103-vec3/Cargo.toml to add common as a dependency:

r103-vec3/Cargo.toml
toml
[package]
name = "r103-vec3"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

Then implement render_image() in r103-vec3/src/lib.rs:

r103-vec3/src/lib.rs
rust
use common::{Color, write_color};

pub fn render_image() -> String {
    let mut output = String::new();

    let image_width = 256;
    let image_height = 256;

    output.push_str("P3\n");
    output.push_str(&format!("{} {}\n", image_width, image_height));
    output.push_str("255\n");

    for j in 0..image_height {
        for i in 0..image_width {
            let pixel_color = Color::new(
                i as f64 / (image_width - 1) as f64,
                j as f64 / (image_height - 1) as f64,
                0.0,
            );
            output.push_str(&format!("{}\n", write_color(pixel_color)));
        }
    }

    output
}

The individual r, g, and b variables are gone; the pixel color is constructed directly with Color::new(r, g, b). By delegating the color conversion to write_color, render_image can focus purely on "what color to produce."