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

1. Structure of raytracing_demos

This page covers the WASM setup specific to the Ray Tracing project. For general VitePress WASM loading constraints (the fetch + Blob URL pattern and related restrictions), see Daydream Notes.

The workspace cargo-doc is available here.

Workspace Overview

master/wasm/raytracing is a Cargo workspace with the following layout:

master/wasm/raytracing/
├── Cargo.toml                    # Workspace definition
├── build.sh                      # wasm-pack build script
├── common/                       # Shared library (no wasm-bindgen dependency)
│   └── src/
│       ├── lib.rs
│       ├── vec3.rs               # Vec3, Color, Point3, utility functions
│       ├── ray.rs                # Ray
│       ├── hittable.rs           # HitRecord, Hittable trait
│       ├── hittable_list.rs      # HittableList
│       ├── sphere.rs             # Sphere
│       ├── material.rs           # Material trait, Lambertian, Metal, Dielectric
│       ├── camera.rs             # Camera
│       └── utils.rs              # Random numbers, math constants
├── r102-gradient/                # Chapter 1.2
├── r103-vec3/                    # Chapter 1.3
│   ⋮  (per-chapter crates)
├── r113-final-scene-hq/          # Chapter 1.13 (high-quality version)
└── raytracing-demos/             # WASM export umbrella crate (cdylib)
    └── src/
        └── lib.rs

The three-layer dependency chain is as follows:

common (rlib)  ←  r1XX-* (rlib)  ←  raytracing-demos (cdylib)  →  WASM

VitePress only directly calls the functions exported by raytracing-demos.

Cargo Workspace Configuration

The root Cargo.toml centrally manages the versions of shared dependencies.

toml
[workspace]
members = [
    "common",
    "r102-gradient",
    # ... per-chapter crates ...
    "raytracing-demos",
]
resolver = "2"

[workspace.dependencies]
common       = { path = "common" }
wasm-bindgen = "0.2"

Dependencies declared under [workspace.dependencies] can be referenced from each crate's Cargo.toml with { workspace = true }. This is why chapter crates do not need to manage their common version individually.

Three-Layer Structure

common: Shared Library

common is a pure Rust library with no dependency on wasm-bindgen.

toml
# common/Cargo.toml
[package]
name    = "common"
version = "0.1.0"
edition = "2024"

[dependencies]
# No wasm-bindgen

lib.rs re-exports all modules to outside the crate.

rust
// common/src/lib.rs (excerpt)
pub mod vec3;
pub use vec3::{
    Color, Point3, Vec3, dot, cross, reflect, refract, unit_vector,
    write_color, write_color_gamma, random_in_unit_sphere, /* ... */
};
pub mod ray;          pub use ray::Ray;
pub mod hittable;     pub use hittable::{HitRecord, Hittable};
pub mod sphere;       pub use sphere::Sphere;
pub mod material;     pub use material::{Material, Lambertian, Metal, Dielectric};
pub mod hittable_list; pub use hittable_list::HittableList;
pub mod camera;       pub use camera::Camera;
pub mod utils;        pub use utils::{INFINITY, PI, degrees_to_radians, random_double, /* ... */};

Chapter Crates (r1XX-*)

These are the crates that implement each chapter's demo. The naming convention is:

r + week number + chapter number (2 digits) + - + description

Examples: r102-gradient, r106-sphere-normals, r109-metal-fuzz
  • The leading r prefix exists because Cargo prohibits crate names that start with a digit; r for "raytracing" was chosen as a minimal workaround.
  • When a chapter has multiple demos, the description portion differs (e.g., r108-diffuse, r108-lambertian, r108-hemisphere).
  • Week 2 (Ray Tracing: The Next Week) follows the same convention with r2XX-*, and week 3 with r3XX-*.

Chapter crates depend only on common and have no wasm-bindgen.

toml
# r109-metal/Cargo.toml (example)
[package]
name    = "r109-metal"
version = "0.1.0"
edition = "2024"

[dependencies]
common = { workspace = true }

Each crate exposes exactly one public API function: pub fn render_image() -> String.

rust
// r102-gradient/src/lib.rs (example)
pub fn render_image() -> String {
    let mut output = String::new();
    output.push_str("P3\n256 256\n255\n");
    for j in 0..256 {
        for i in 0..256 {
            // Compute and write R, G, B values
            output.push_str(&format!("{} {} {}\n", ir, ig, ib));
        }
    }
    output
}

Because wasm-bindgen is not a dependency, you can verify correctness with ordinary cargo test.

raytracing-demos: Umbrella Crate

This is the crate that bundles all chapter crates and outputs a WASM binary.

toml
# raytracing-demos/Cargo.toml
[lib]
crate-type = ["cdylib"]       # Required to output as WASM (.wasm)

[dependencies]
r102-gradient = { version = "*", path = "../r102-gradient" }
# ... all chapter crates ...
r113-final-scene-hq = { version = "*", path = "../r113-final-scene-hq" }
wasm-bindgen.workspace = true

lib.rs simply wraps each chapter crate's render_image() with #[wasm_bindgen].

rust
// raytracing-demos/src/lib.rs (excerpt)
use wasm_bindgen::prelude::*;

// 1.2: Gradient
#[wasm_bindgen] pub fn render_gradient()  -> String { r102_gradient::render_image() }

// 1.4: Ray, Camera, Background
#[wasm_bindgen] pub fn render_sky()       -> String { r104_ray_camera_background::render_image() }

// 1.5: Rendering a Sphere
#[wasm_bindgen] pub fn render_sphere()    -> String { r105_sphere::render_image() }

// ...

// 1.13: Final Scene (high quality)
#[wasm_bindgen] pub fn render_final_scene_hq() -> String { r113_final_scene_hq::render_image() }

This design means each chapter's rendering logic can be tested without wasm-bindgen; only the export declarations are consolidated in raytracing-demos.

Adding a New Demo

Whenever a new chapter is added (for example, an additional chapter from the v4 book or a week-2 chapter), the procedure is always the same.

1. Create the Chapter Crate

bash
# Run inside master/wasm/raytracing/
cargo new --lib r114-new-chapter

Add edition = "2024" to r114-new-chapter/Cargo.toml, and common = { workspace = true } if needed. Implement pub fn render_image() -> String { … } in src/lib.rs.

2. Add the Crate to members in the Workspace Cargo.toml

toml
[workspace]
members = [
    # ...
    "r114-new-chapter",      # ← add
    "raytracing-demos",
]

3. Add the Crate to [dependencies] in raytracing-demos/Cargo.toml

toml
r114-new-chapter = { version = "*", path = "../r114-new-chapter" }

4. Add the Export to raytracing-demos/src/lib.rs

rust
#[wasm_bindgen]
pub fn render_new_chapter() -> String {
    r114_new_chapter::render_image()
}

That is all. Running build.sh will produce a WASM binary that includes the new function. The procedure is the same for week 2 (r2XX-*) and beyond.

Building

bash
# Run inside master/wasm/raytracing/
bash build.sh

Internally, this runs the following command:

bash
wasm-pack build --target web raytracing-demos \
  --out-dir <repo>/docs/public/wasm/raytracing_demos --release

--target web outputs in ES Modules format (raytracing_demos.js + raytracing_demos_bg.wasm). Omitting --release produces a debug build, but for computationally intensive workloads like ray tracing, performance will be significantly reduced.

Why Return a PPM String

Each demo function returns a PPM (P3) string because:

  • It follows the learning progression of the original book (C++ outputs a PPM string to stdout; the Rust version mirrors that step by step).
  • Data transfer across the WebAssembly boundary is kept simple.
  • The header (P3 / width height / 255) is easy to visually inspect during debugging.

How VitePress Receives the Output

On the VitePress side, the PPMRenderer component is responsible for:

  1. Loading raytracing_demos.js via the fetch + Blob URL pattern and initializing the WASM module.
  2. Calling the target exported function (e.g., render_gradient).
  3. Parsing the returned PPM string and drawing it to a <canvas>.

The canvas dimensions are read from the PPM header first; drawing happens only after Vue's reactive update is complete. Reversing this order causes the canvas to be reset after putImageData (see PPMRenderer Implementation for details).

Output Location

build.sh directs wasm-pack output to docs/public/wasm/raytracing_demos/. The generated raytracing_demos.js and raytracing_demos_bg.wasm are placed there automatically.