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.rsThe three-layer dependency chain is as follows:
common (rlib) ← r1XX-* (rlib) ← raytracing-demos (cdylib) → WASMVitePress only directly calls the functions exported by raytracing-demos.
Cargo Workspace Configuration
The root Cargo.toml centrally manages the versions of shared dependencies.
[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.
# common/Cargo.toml
[package]
name = "common"
version = "0.1.0"
edition = "2024"
[dependencies]
# No wasm-bindgenlib.rs re-exports all modules to outside the crate.
// 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
rprefix exists because Cargo prohibits crate names that start with a digit;rfor "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 withr3XX-*.
Chapter crates depend only on common and have no wasm-bindgen.
# 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.
// 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.
# 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 = truelib.rs simply wraps each chapter crate's render_image() with #[wasm_bindgen].
// 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
# Run inside master/wasm/raytracing/
cargo new --lib r114-new-chapterAdd 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
[workspace]
members = [
# ...
"r114-new-chapter", # ← add
"raytracing-demos",
]3. Add the Crate to [dependencies] in raytracing-demos/Cargo.toml
r114-new-chapter = { version = "*", path = "../r114-new-chapter" }4. Add the Export to raytracing-demos/src/lib.rs
#[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
# Run inside master/wasm/raytracing/
bash build.shInternally, this runs the following command:
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:
- Loading
raytracing_demos.jsvia thefetch + Blob URLpattern and initializing the WASM module. - Calling the target exported function (e.g.,
render_gradient). - 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.