13. Where Next?
Ray Tracing in One Weekend (v3.2.3): 13 Where Next?
In this chapter we implement the final scene of Ray Tracing in One Weekend — the cover image of the book, bringing together every technique developed in Week 1.
- A large gray ground sphere
- A field of small spheres with random materials
- Three large focal spheres at the center (Lambertian, glass, metal)
No new techniques are introduced. We simply combine everything we have built so far.
The random_scene() Function
This function generates the entire scene.
Ground
A large Lambertian sphere of radius 1000 is placed at
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -1000.0, 0.0),
1000.0,
Arc::new(Lambertian::new(Color::new(0.5, 0.5, 0.5))),
)));Grid Layout
We lay out a 22×10 grid in the
a in -11..11 (X direction: 22 cells)
b in -5..5 (Z direction: 10 cells)The original book (C++) uses a 22×22 grid (
We add a small random offset to each cell center to determine the sphere position.
let center = Point3::new(
a as f64 + 0.9 * random_double(),
0.2,
b as f64 + 0.9 * random_double(),
);0.9 * random_double() produces a random offset within the cell.
Avoiding Overlaps
To prevent small spheres from overlapping the three large focal spheres (radius 1.0), we check the distance from each small sphere's center to each focal sphere position and skip it if the distance is less than 0.9.
let sphere_center_1 = Point3::new(4.0, 0.2, 0.0);
let sphere_center_2 = Point3::new(0.0, 1.0, 0.0);
let sphere_center_3 = Point3::new(-4.0, 0.2, 0.0);
if (center - sphere_center_1).length() > 0.9
&& (center - sphere_center_2).length() > 0.9
&& (center - sphere_center_3).length() > 0.9
{
// Place sphere
}Material Probabilities
A random value choose_mat in
| Condition | Probability | Material |
|---|---|---|
choose_mat < 0.8 | 80% | Lambertian (diffuse) |
choose_mat < 0.95 | 15% | Metal |
| Otherwise | 5% | Dielectric (glass) |
The Lambertian albedo is generated as the product of two random_double() values. Squaring a value in
if choose_mat < 0.8 {
let albedo = Color::new(
random_double() * random_double(),
random_double() * random_double(),
random_double() * random_double(),
);
world.add(Box::new(Sphere::with_material(
center, 0.2, Arc::new(Lambertian::new(albedo)),
)));
} else if choose_mat < 0.95 {
let albedo = Color::new(
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
);
let fuzz = random_double_range(0.0, 0.5);
world.add(Box::new(Sphere::with_material(
center, 0.2, Arc::new(Metal::new(albedo, fuzz)),
)));
} else {
world.add(Box::new(Sphere::with_material(
center, 0.2, Arc::new(Dielectric::new(1.5)),
)));
}Metal albedo uses random_double_range(0.5, 1.0) to stay in the bright range
Three Large Focal Spheres
Three radius-1.0 spheres with different materials are placed near the center of the scene.
| Position | Material | Color |
|---|---|---|
| Lambertian | Brown (0.4, 0.2, 0.1) | |
| Dielectric (glass) | Transparent, ref_idx 1.5 | |
| Metal (fuzz=0) | Silver (0.7, 0.6, 0.5) |
Camera Settings
The camera is placed high and far back to overlook the entire scene.
| Parameter | Value | Description |
|---|---|---|
lookfrom | Elevated viewpoint from upper-right rear | |
lookat | Looking at the scene center | |
vfov | 20° | Narrow field of view (telephoto feel) |
aperture | 0.1 | Subtle depth of field |
focus_dist | 10.0 | Focus distance |
With aperture = 0.1 (much smaller than the 2.0 in Chapter 12), the blur is very subtle — the three focal spheres are in sharp focus while the background small spheres are ever so slightly blurred, adding realism.
Rendering Settings
| Parameter | WASM demo | Original (C++) |
|---|---|---|
| Resolution | 300 × 169 | 1200 × 800 |
| Samples/px | 30 | 500 |
| Max depth | 50 | 50 |
We cut the resolution and sample count significantly for the WASM demo because WASM runs single-threaded and synchronously. The native optimized build takes about 2.4 seconds; WASM takes roughly 5–10 seconds.
Differences from C++
Grid Iteration
C++'s for (int a = -11; a < 11; a++) becomes for a in -11..11 in Rust. The range a..b is the half-open interval
Probabilistic if–else
C++:
double choose_mat = random_double();
if (choose_mat < 0.8) {
// Diffuse
} else if (choose_mat < 0.95) {
// Metal
} else {
// Glass
}Rust: The structure is identical. Because Rust's if is an expression, each branch can directly call world.add(...).
Arc<dyn Material> vs shared_ptr<material>
| C++ | Rust | |
|---|---|---|
| Smart pointer | shared_ptr<material> | Arc<dyn Material> |
| Reference counting | Yes | Yes |
| Thread safety | shared_ptr is not thread-safe | Arc is thread-safe |
We don't use threads in WASM, but Rust's type system requires Arc for shared ownership across potential thread boundaries.
Random Color Generation
The original C++ writes:
auto albedo = color::random() * color::random();color::random() is shorthand for (random_double(), random_double(), random_double()). We haven't defined this shortcut for Color in Rust, so we write each component explicitly:
let albedo = Color::new(
random_double() * random_double(),
random_double() * random_double(),
random_double() * random_double(),
);Complete Implementation
[package]
name = "r113-final-scene"
version = "0.1.0"
edition = "2024"
[dependencies]
common = { workspace = true }r113-final-scene/src/lib.rs
use std::sync::Arc;
use common::{
Camera, Color, Dielectric, Hittable, HittableList, Lambertian, Metal, Point3,
Sphere, unit_vector, write_color_gamma, Ray, random_double, random_double_range,
};
fn ray_color(r: &Ray, world: &dyn Hittable, depth: i32) -> Color {
if depth <= 0 {
return Color::new(0.0, 0.0, 0.0);
}
if let Some(rec) = world.hit(r, 0.001, f64::INFINITY) {
if let Some(mat) = &rec.mat {
if let Some((attenuation, scattered)) = mat.scatter(r, &rec) {
return attenuation * ray_color(&scattered, world, depth - 1);
}
}
return Color::new(0.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)
}
fn random_scene() -> HittableList {
let mut world = HittableList::new();
// Ground: large Lambertian sphere (gray)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, -1000.0, 0.0),
1000.0,
Arc::new(Lambertian::new(Color::new(0.5, 0.5, 0.5))),
)));
// Place spheres randomly on a 22×10 grid (a: -11..11, b: -5..5, up to 220 cells).
for a in -11..11 {
for b in -5..5 {
let choose_mat = random_double();
let center = Point3::new(
a as f64 + 0.9 * random_double(),
0.2,
b as f64 + 0.9 * random_double(),
);
// Avoid overlapping the three large focal spheres.
let sphere_center_1 = Point3::new(4.0, 0.2, 0.0);
let sphere_center_2 = Point3::new(0.0, 1.0, 0.0);
let sphere_center_3 = Point3::new(-4.0, 0.2, 0.0);
if (center - sphere_center_1).length() > 0.9
&& (center - sphere_center_2).length() > 0.9
&& (center - sphere_center_3).length() > 0.9
{
if choose_mat < 0.8 {
// Lambertian (diffuse): 80%
let albedo = Color::new(
random_double() * random_double(),
random_double() * random_double(),
random_double() * random_double(),
);
world.add(Box::new(Sphere::with_material(
center,
0.2,
Arc::new(Lambertian::new(albedo)),
)));
} else if choose_mat < 0.95 {
// Metal: 15%
let albedo = Color::new(
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
random_double_range(0.5, 1.0),
);
let fuzz = random_double_range(0.0, 0.5);
world.add(Box::new(Sphere::with_material(
center,
0.2,
Arc::new(Metal::new(albedo, fuzz)),
)));
} else {
// Dielectric (glass): 5%
world.add(Box::new(Sphere::with_material(
center,
0.2,
Arc::new(Dielectric::new(1.5)),
)));
}
}
}
}
// Three prominent focal spheres
// Left: brown Lambertian
world.add(Box::new(Sphere::with_material(
Point3::new(-4.0, 1.0, 0.0),
1.0,
Arc::new(Lambertian::new(Color::new(0.4, 0.2, 0.1))),
)));
// Center: Dielectric (glass sphere)
world.add(Box::new(Sphere::with_material(
Point3::new(0.0, 1.0, 0.0),
1.0,
Arc::new(Dielectric::new(1.5)),
)));
// Right: silver Metal sphere (fuzz=0)
world.add(Box::new(Sphere::with_material(
Point3::new(4.0, 1.0, 0.0),
1.0,
Arc::new(Metal::new(Color::new(0.7, 0.6, 0.5), 0.0)),
)));
world
}
pub fn render_image() -> String {
let aspect_ratio = 16.0_f64 / 9.0;
let image_width = 300_i32;
let image_height = (image_width as f64 / aspect_ratio) as i32;
let samples_per_pixel = 30_i32;
let max_depth = 50_i32;
let world = random_scene();
// Camera: positioned high and far to overlook the entire scene.
let lookfrom = Point3::new(13.0, 2.0, 3.0);
let lookat = Point3::new(0.0, 0.0, 0.0);
let vup = Point3::new(0.0, 1.0, 0.0);
let dist_to_focus = 10.0_f64;
let aperture = 0.1_f64;
let camera = Camera::new(
lookfrom,
lookat,
vup,
20.0,
aspect_ratio,
aperture,
dist_to_focus,
);
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 mut pixel_color = Color::new(0.0, 0.0, 0.0);
for _ in 0..samples_per_pixel {
let u = (i as f64 + random_double()) / (image_width - 1) as f64;
let v = (j as f64 + random_double()) / (image_height - 1) as f64;
let r = camera.get_ray(u, v);
pixel_color += ray_color(&r, &world, max_depth);
}
output.push_str(&format!(
"{}\n",
write_color_gamma(pixel_color, samples_per_pixel)
));
}
}
output
}WASM Export
This has already been added to raytracing-demos/src/lib.rs:
// Chapter 1.13: Final Scene
#[wasm_bindgen]
pub fn render_final_scene() -> String {
r113_final_scene::render_image()
}
#[wasm_bindgen]
pub fn render_final_scene_hq() -> String {
r113_final_scene_hq::render_image()
}Constraints in the Web Environment
This scene is rendered at 300×169 with 30 samples/px in the WASM demo. The native optimized build takes about 2.4 seconds; WASM takes roughly 5–10 seconds.
Progress display is not implemented — this is due to the event-loop blocking constraint discussed in Chapter 2. Just press the render button and wait.
What's Next?
The book suggests these directions for further exploration:
- Lights — Treat emissive objects as light sources
- Triangles — The fundamental primitive for mesh models
- Surface textures — Mapping images onto surfaces
- Procedural textures — Perlin noise and friends
- Volumes — Fog, smoke, and other participating media
- Parallelism — Multi-threading and GPU acceleration
These are the topics of Ray Tracing: The Next Week and Ray Tracing: The Rest of Your Life.
Week 1 is now complete.
If you have time to spare, try rendering the high-quality version: High-quality version (600×400, 100 samples/px, WASM 5–10 min) →