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+t⋅b
  • A: the ray origin (camera position)
  • b: the ray direction vector
  • t: a real-valued parameter (considering only t>0 gives 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, I follow the original book's definition here. In Rust, I 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 the methods 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=origin−horizontal2−vertical2−(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+u⋅horizontal+v⋅vertical−origin

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)=(1−t)⋅white+t⋅blue(0≤t≤1)

After normalizing the ray direction, the y component falls in [−1.0, 1.0]. Mapping it to [0, 1] gives 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 makes the ray's contents available 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()
}