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
This is the dot-product way of saying "the squared distance from
Substituting the ray
Substituting the ray
Expanding:
This is a quadratic equation
| Variable | Expression |
|---|---|
Discriminant
The discriminant
| Discriminant | Meaning | Intersection |
|---|---|---|
| Two real roots | Ray passes through sphere | |
| One real root | Ray is tangent to sphere | |
| No real roots | Ray misses the sphere |
In this chapter we only need to know whether
The hit_sphere Function
The C++ implementation:
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:
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.
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.
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.
return (discriminant > 0); // double > int: implicit conversion is fineIn Rust, comparing values of different types is a compile error. Because discriminant is f64, the comparison must also use f64.
discriminant > 0.0 // f64 > f64: OK
// discriminant > 0 // f64 > i32: compile errorValue vs. reference parameters
Comparing the C++ and Rust signatures:
bool hit_sphere(const point3& center, double radius, const ray& r)fn hit_sphere(center: Point3, radius: f64, r: &Ray) -> boolcenter: Point3:Point3(=Vec3) implementsCopy, so a value parameter is copied cheaply — effectively equivalent to C++const point3&.radius: f64:f64is alsoCopy, so a value parameter is fine.r: &Ray:Raydoes not implementCopy, so we borrow it as&Rayinstead 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.
// C++: braces can be omitted
if (hit_sphere(point3(0,0,-1), 0.5, r))
return color(1, 0, 0);// 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:
[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.
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:
// Chapter 1.5: Rendering a Sphere
#[wasm_bindgen]
pub fn render_sphere() -> String {
r105_sphere::render_image()
}