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:
: the ray origin (camera position) : the ray direction vector : a real-valued parameter (by considering only , we get a half-line extending from the camera forward)
Varying at(t) method.
The Ray Struct
In C++, this is defined in ray.h.
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.
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.
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).
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:
| Receiver | Meaning |
|---|---|
self | Value (moves or copies ownership) |
&self | Immutable borrow (read-only, no copy) |
&mut self | Mutable 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:
| Parameter | Value | Description |
|---|---|---|
| Aspect ratio | 16:9 | Image width : height |
| Viewport height | 2.0 units | Height of the virtual screen |
| Focal length | 1.0 units | Distance 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:
For each pixel, horizontal parameter
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):
After normalizing the ray direction, the y component falls in
(straight up) → : blue (horizontal) → : midpoint color (straight down) → : white
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.
// C++
auto u = double(i) / (image_width-1);// 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:
[package]
name = "r104-ray-camera-background"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }Implement r104-ray-camera-background/src/lib.rs:
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
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:
// Chapter 1.4: Ray, Camera, Background
#[wasm_bindgen]
pub fn render_sky() -> String {
r104_ray_camera_background::render_image()
}