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

4. Rays, a Simple Camera, and Background

Ray Tracing in One Weekend (v3.2.3): 4 Rays, a Simple Camera, and Background

This chapter implements the heart of a ray tracer: casting a ray and computing its color based on the direction it travels.

What Is a Ray?

A ray is mathematically described by:

P(t)=A+tb
  • A: the ray origin (camera position)
  • b: the ray direction vector
  • t: a real-valued parameter (by considering only t>0, we get a half-line extending from the camera forward)

Varying t yields different points P(t) along the ray. This is implemented as the at(t) method.

The Ray Struct

In C++, this is defined in ray.h.

cpp
class ray {
public:
  ray() {}
  ray(const point3& origin, const vec3& direction)
    : orig(origin), dir(direction) {}
  point3 origin() const  { return orig; }
  vec3 direction() const { return dir; }
  point3 at(double t) const { return orig + t*dir; }
public:
  point3 orig;
  vec3 dir;
};

Personally, since the getter member functions origin() and direction() are public, ideally the member variables orig and dir would be private so they cannot be accessed from outside the class. However, we follow the original book's definition here. In Rust, we implement this in common/src/ray.rs.

common/src/ray.rs
rust
use crate::vec3::{Point3, Vec3};

/// A ray defined by an origin and a direction.
pub struct Ray {
    /// Ray origin.
    pub orig: Point3,
    /// Ray direction.
    pub dir: Vec3,
}

impl Ray {
    /// Creates a new ray from `origin` and `direction`.
    pub fn new(origin: Point3, direction: Vec3) -> Self {
        Ray { orig: origin, dir: direction }
    }

    /// Returns the ray origin.
    pub fn origin(&self) -> Point3 { self.orig }
    /// Returns the ray direction.
    pub fn direction(&self) -> Vec3 { self.dir }

    /// Returns the point at parameter `t` along the ray: **P**(*t*) = **orig** + *t* **dir**.
    pub fn at(&self, t: f64) -> Point3 {
        self.orig + t * self.dir
    }
}

Differences Between C++ and Rust

const member functions and &self

In C++, a const member function takes the this pointer as const, preventing modification of the object.

cpp
point3 origin() const { return orig; }
vec3 direction() const { return dir; }
point3 at(double t) const { return orig + t*dir; }

In Rust, the same intent is expressed with &self (immutable borrow).

rust
pub fn origin(&self) -> Point3 { self.orig }
pub fn direction(&self) -> Vec3 { self.dir }
pub fn at(&self, t: f64) -> Point3 { self.orig + t * self.dir }

There are three kinds of self receiver in Rust methods:

ReceiverMeaning
selfValue (moves or copies ownership)
&selfImmutable borrow (read-only, no copy)
&mut selfMutable borrow (allows modification)

The return types of origin() and direction() are Point3 = Vec3 (which derives Copy), so we can borrow the fields via &self and return their values as copies.

No default constructor

C++'s ray() is a default constructor taking no arguments. Rust has no language-level default constructors; when needed, the Default trait is implemented. Since every call site always provides an origin and direction, Default is omitted here.

Camera Setup

The camera is defined by the following parameters:

ParameterValueDescription
Aspect ratio16:9Image width : height
Viewport height2.0 unitsHeight of the virtual screen
Focal length1.0 unitsDistance from camera origin to viewport
Camera position(0, 0, 0)Right-hand coordinate system, −z is forward

The lower_left_corner — the bottom-left corner of the viewport — is the reference point for computing ray directions toward each pixel:

lower_left_corner=originhorizontal2vertical2(0, 0, focal_length)

For each pixel, horizontal parameter u and vertical parameter v are normalized to [0,1], and the ray direction is:

direction=lower_left_corner+uhorizontal+vverticalorigin

Background Color: Linear Interpolation

The ray_color function computes the "sky color" returned when a ray hits nothing. It blends white and blue based on the ray direction's y component. This is called linear interpolation (lerp):

blended(t)=(1t)white+tblue(0t1)

After normalizing the ray direction, the y component falls in [1.0, 1.0]. We map it to [0, 1] to get the blend factor t:

t=0.5×(y+1.0)
  • y=1.0 (straight up) → t=1.0: blue (0.5, 0.7, 1.0)
  • y=0.0 (horizontal) → t=0.5: midpoint color
  • y=1.0 (straight down) → t=0.0: white (1.0, 1.0, 1.0)

The gradient appears not only vertically but also diagonally, because after normalization the y component varies with the x component as well.

Differences Between C++ and Rust

Integer-to-float conversion

In C++, integers are implicitly converted to double. In Rust, an explicit as cast is required.

cpp
// C++
auto u = double(i) / (image_width-1);
rust
// Rust
let u = i as f64 / (image_width - 1) as f64;

image_width has type i32, so image_width - 1 is computed as i32, then converted to f64 with as f64.

Implementing the r104-ray-camera-background Crate

Set up r104-ray-camera-background/Cargo.toml:

r104-ray-camera-background/Cargo.toml
toml
[package]
name = "r104-ray-camera-background"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

Implement r104-ray-camera-background/src/lib.rs:

r104-ray-camera-background/src/lib.rs
rust
use common::{Color, Point3, Vec3, unit_vector, write_color, Ray};

fn ray_color(r: &Ray) -> Color {
    let unit_direction = unit_vector(r.direction());
    let t = 0.5 * (unit_direction.y() + 1.0);
    (1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
}

pub fn render_image() -> String {
    // Image dimensions
    let aspect_ratio = 16.0_f64 / 9.0;
    let image_width = 384_i32;
    let image_height = (image_width as f64 / aspect_ratio) as i32;

    // Camera setup
    let viewport_height = 2.0_f64;
    let viewport_width = aspect_ratio * viewport_height;
    let focal_length = 1.0_f64;

    let origin = Point3::new(0.0, 0.0, 0.0);
    let horizontal = Vec3::new(viewport_width, 0.0, 0.0);
    let vertical = Vec3::new(0.0, viewport_height, 0.0);
    let lower_left_corner =
        origin - horizontal / 2.0 - vertical / 2.0 - Vec3::new(0.0, 0.0, focal_length);

    // Render
    let mut output = String::new();
    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).rev() {
        for i in 0..image_width {
            let u = i as f64 / (image_width - 1) as f64;
            let v = j as f64 / (image_height - 1) as f64;
            let r = Ray::new(
                origin,
                lower_left_corner + u * horizontal + v * vertical - origin,
            );
            let pixel_color = ray_color(&r);
            output.push_str(&format!("{}\n", write_color(pixel_color)));
        }
    }

    output
}

Borrowing in ray_color

ray_color takes &Ray. Because Ray does not implement Copy, passing by value (r: Ray) would move ownership. Borrowing as &Ray lets us read the ray's contents while keeping ownership at the call site.

r.direction() returns a Vec3. Since Vec3 implements Copy, the field inside the &self reference can be copied out and returned as a value.

Updating the common Crate and Exporting to WASM

Create common/src/ray.rs and update 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};

pub mod ray;
pub use ray::Ray;

Add the WASM export function to raytracing-demos/src/lib.rs:

raytracing-demos/src/lib.rs
rust
// Chapter 1.4: Ray, Camera, Background
#[wasm_bindgen]
pub fn render_sky() -> String {
    r104_ray_camera_background::render_image()
}