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

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 y=1000. Because it is so large, its top surface appears flat — like the ground.

rust
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 xz plane and place one small sphere per cell.

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 (b also running 11..11), but we reduced the grid size for the WASM demo.

We add a small random offset to each cell center to determine the sphere position.

rust
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. y=0.2 equals the sphere radius, keeping spheres resting on the ground.

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.

rust
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 [0,1) selects the material for each sphere.

ConditionProbabilityMaterial
choose_mat < 0.880%Lambertian (diffuse)
choose_mat < 0.9515%Metal
Otherwise5%Dielectric (glass)

The Lambertian albedo is generated as the product of two random_double() values. Squaring a value in [0,1) biases the distribution toward 0, producing darker, more muted colors rather than bright whites.

rust
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 [0.5,1.0), giving metals their characteristic gleam. The fuzz coefficient is random in [0,0.5).

Three Large Focal Spheres

Three radius-1.0 spheres with different materials are placed near the center of the scene.

PositionMaterialColor
(4,1,0) (left)LambertianBrown (0.4, 0.2, 0.1)
(0,1,0) (center)Dielectric (glass)Transparent, ref_idx 1.5
(4,1,0) (right)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.

ParameterValueDescription
lookfrom(13,2,3)Elevated viewpoint from upper-right rear
lookat(0,0,0)Looking at the scene center
vfov20°Narrow field of view (telephoto feel)
aperture0.1Subtle depth of field
focus_dist10.0Focus 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

ParameterWASM demoOriginal (C++)
Resolution300 × 1691200 × 800
Samples/px30500
Max depth5050

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 [a,b).

Probabilistic if–else

C++:

cpp
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 pointershared_ptr<material>Arc<dyn Material>
Reference countingYesYes
Thread safetyshared_ptr is not thread-safeArc 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:

cpp
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:

rust
let albedo = Color::new(
    random_double() * random_double(),
    random_double() * random_double(),
    random_double() * random_double(),
);

Complete Implementation

r113-final-scene/Cargo.toml
toml
[package]
name = "r113-final-scene"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }
r113-final-scene/src/lib.rs
rust
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:

rust
// 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:

  1. Lights — Treat emissive objects as light sources
  2. Triangles — The fundamental primitive for mesh models
  3. Surface textures — Mapping images onto surfaces
  4. Procedural textures — Perlin noise and friends
  5. Volumes — Fog, smoke, and other participating media
  6. 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) →