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.
// C++
class vec3 {
public:
double e[3];
double x() const { return e[0]; } // Member function defined inside the class
};// 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.
// C++
vec3 operator+(const vec3& u, const vec3& v) { ... } // Defined as a free function// 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.
// C++
vec3() : e{0,0,0} {}
vec3(double e0, double e1, double e2) : e{e0, e1, e2} {}// 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
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
#[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 viav.clone().Clonemust be present forCopyto work.Copy: Activates implicit copy on assignment (let v2 = v1;) and function argument passing. Because[f64; 3]isCopy,Vec3can also be madeCopy.PartialEq: Enables the==operator, which is required for assertions such asassert_eq!(v1, v2)in tests.
Value-passing self in accessor methods
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
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
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
impl AddAssign for Vec3 { ... } // Vec3 += Vec3
impl MulAssign<f64> for Vec3 { ... } // Vec3 *= f64
impl DivAssign<f64> for Vec3 { ... } // Vec3 /= f64AddAssign 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
impl Mul for Vec3 { ... } // Component-wise multiplicationVec3 * 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
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
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:
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:
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
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
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:
[package]
name = "r103-vec3"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }Then implement render_image() in r103-vec3/src/lib.rs:
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."