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

5. Adding a Sphere

Ray Tracing in One Weekend (v3.2.3): 5 Adding a Sphere

This chapter places the first object in the scene: the simplest 3D shape, a sphere. We derive the ray–sphere intersection test mathematically and color the hit region red.

Ray–Sphere Intersection

The sphere equation

A point P on a sphere with center C=(Cx,Cy,Cz) and radius r satisfies:

(PC)(PC)=r2

This is the dot-product way of saying "the squared distance from P to C equals r2."

Substituting the ray

Substituting the ray P(t)=A+tb into the sphere equation and letting oc=AC:

(oc+tb)(oc+tb)=r2

Expanding:

t2(bb)+2t(boc)+(ococ)r2=0

This is a quadratic equation at2+bt+c=0 in t, with coefficients:

VariableExpression
abb
b2(boc)
cococr2

Discriminant

The discriminant Δ=b24ac determines the number of real roots:

DiscriminantMeaningIntersection
Δ>0Two real rootsRay passes through sphere
Δ=0One real rootRay is tangent to sphere
Δ<0No real rootsRay misses the sphere

In this chapter we only need to know whether Δ>0.

The hit_sphere Function

The C++ implementation:

cpp
bool hit_sphere(const point3& center, double radius, const ray& r) {
    vec3 oc = r.origin() - center;
    auto a = dot(r.direction(), r.direction());
    auto b = 2.0 * dot(oc, r.direction());
    auto c = dot(oc, oc) - radius*radius;
    auto discriminant = b*b - 4*a*c;
    return (discriminant > 0);
}

The Rust implementation:

rust
fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> bool {
    let oc = r.origin() - center;
    let a = dot(r.direction(), r.direction());
    let b = 2.0 * dot(oc, r.direction());
    let c = dot(oc, oc) - radius * radius;
    let discriminant = b * b - 4.0 * a * c;
    discriminant > 0.0
}

Differences Between C++ and Rust

Trailing expression as return value

In Rust, the last expression without a semicolon at the end of a function body is the return value.

rust
fn hit_sphere(...) -> bool {
    // ...
    discriminant > 0.0   // No semicolon → this bool value is the return value
}

Writing return discriminant > 0.0; is equivalent, but Rust convention omits the trailing return. Whether a semicolon is present or absent is how Rust distinguishes an expression (produces a value) from a statement (discards a value).

For early returns in the middle of a function — as in ray_color — an explicit return is still needed.

rust
fn ray_color(r: &Ray) -> Color {
    if hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
        return Color::new(1.0, 0.0, 0.0);  // Early return
    }
    // ...
    (1.0 - t) * Color::new(1.0, 1.0, 1.0) + t * Color::new(0.5, 0.7, 1.0)
    // The trailing expression is the return value
}

Float comparisons

In C++, discriminant > 0 compares a double to an int, which compiles fine thanks to implicit conversion.

cpp
return (discriminant > 0);  // double > int: implicit conversion is fine

In Rust, comparing values of different types is a compile error. Because discriminant is f64, the comparison must also use f64.

rust
discriminant > 0.0  // f64 > f64: OK
// discriminant > 0  // f64 > i32: compile error

Value vs. reference parameters

Comparing the C++ and Rust signatures:

cpp
bool hit_sphere(const point3& center, double radius, const ray& r)
rust
fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> bool
  • center: Point3: Point3 (= Vec3) implements Copy, so a value parameter is copied cheaply — effectively equivalent to C++ const point3&.
  • radius: f64: f64 is also Copy, so a value parameter is fine.
  • r: &Ray: Ray does not implement Copy, so we borrow it as &Ray instead of moving ownership.

Mandatory braces for if

In C++, braces can be omitted when the if body is a single statement. In Rust, braces are always required.

cpp
// C++: braces can be omitted
if (hit_sphere(point3(0,0,-1), 0.5, r))
    return color(1, 0, 0);
rust
// Rust: braces are required
if hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
    return Color::new(1.0, 0.0, 0.0);
}

Implementing the r105-sphere Crate

Set up r105-sphere/Cargo.toml:

r105-sphere/Cargo.toml
toml
[package]
name = "r105-sphere"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

The complete implementation in r105-sphere/src/lib.rs. The camera setup and render loop are identical to the previous chapter's r104-ray-camera-background; the changes are the addition of hit_sphere and the update to ray_color.

r105-sphere/src/lib.rs
rust
use common::{Color, Point3, Vec3, dot, unit_vector, write_color, Ray};

fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> bool {
    let oc = r.origin() - center;
    let a = dot(r.direction(), r.direction());
    let b = 2.0 * dot(oc, r.direction());
    let c = dot(oc, oc) - radius * radius;
    let discriminant = b * b - 4.0 * a * c;
    discriminant > 0.0
}

fn ray_color(r: &Ray) -> Color {
    if hit_sphere(Point3::new(0.0, 0.0, -1.0), 0.5, r) {
        return Color::new(1.0, 0.0, 0.0);
    }
    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 {
    let aspect_ratio = 16.0_f64 / 9.0;
    let image_width = 384_i32;
    let image_height = (image_width as f64 / aspect_ratio) as i32;

    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);

    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
}

WASM Export

Add to raytracing-demos/src/lib.rs:

raytracing-demos/src/lib.rs
rust
// Chapter 1.5: Rendering a Sphere
#[wasm_bindgen]
pub fn render_sphere() -> String {
    r105_sphere::render_image()
}