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

13. Where Next? ​

Ray Tracing in One Weekend (v3.2.3): 13 Where Next?

This chapter implements 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. The scene simply combines everything implemented 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 ​

A 22×10 grid is laid out in the xz plane, with one small sphere in each 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 I reduced the grid size for the WASM demo.

A small random offset is added 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), the implementation checks the distance from each small sphere's center to each focal sphere position and skips 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

I significantly reduced the resolution and sample count 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

The WASM implementation does not use threads, 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()). This shortcut is not defined for Color in the Rust version, so each component is written 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) →