WebGPU notes / 39
Ray-Traced Shadows in WebGPU with Rust and WGSL
Trace a primary ray against ten analytic objects, launch a bounded visibility ray toward an animated point light, and turn occlusion into crisp contact shadows on spheres, boxes, and a procedural checkerboard.
- Scene objects
- 10
- Shadow visibility
- 30%
- Trace cap
- 1024 px
Add a visibility ray between each visible hit and the light
The previous compute ray tracing example mixed hard shadows with stylized reflection steps. This page isolates direct-light visibility in a cleaner scene. One primary ray finds the visible surface. A second ray starts just above that surface and asks whether any other object lies before the point light. The answer is binary, so the edge remains hard.
The introducing ray tracing shadows commit added the Rust source, compute shader, present shader, and both screenshots. A later camera-basis fix chooses Z-up when a view points almost vertically, and the current WebAssembly entry update modernized startup. Rendering uses sib::render.
Build a ten-object room from typed storage records
Each 64-byte scene record holds four vec4 values: primary geometry, extra box extents, RGB plus specular exponent, and integer IDs. Rust writes three spheres, three axis-aligned boxes, a floor, a back plane, and two side planes into a single read-only storage buffer.
Scroll sideways to see all table columns.
| Type | Count | Shape data | Surface treatment | Role |
|---|---|---|---|---|
| Sphere | 3 | Center + radius | Blue, red, and green; exponents 32–48 | Curved receivers and occluders |
| Box | 3 | Center + half extents | Cyan, yellow, and orange; exponents 20–28 | Flat-faced receivers and occluders |
| Plane | 4 | Normal + signed distance | Checker floor, gray back, red and green sides | Room enclosure |
The fixed camera sits at (0, 1.55, 8.8), looks toward (0, 0.42, -1.15), and uses a 40-degree vertical field of view. The point light orbits mostly above the objects: X sweeps around −1.4 with radius 2.4, Y oscillates around 5.8, and Z moves around 3.2. Animation time advances at 0.25 times elapsed wall time and wraps every cycle.
Intersect spheres, slab-tested boxes, and infinite planes
Spheres use the two quadratic roots and accept the far root if the near root is behind the origin. Planes solve their signed equation. Boxes use a slab test: a guarded reciprocal prevents zero direction components from producing invalid bounds, then the shader compares the largest entry distance with the smallest exit distance. The winning object also supplies a normalized surface normal, procedural color, and specular exponent.
let t0 = (min_bounds - ray_o) * safe_inverse(ray_d);
let t1 = (max_bounds - ray_o) * safe_inverse(ray_d);
let t_min = max(max(min(t0, t1).x, min(t0, t1).y), min(t0, t1).z);
let t_max = min(min(max(t0, t1).x, max(t0, t1).y), max(t0, t1).z);
if (t_max < max(t_min, EPSILON)) { return -1.0; }
The checkerboard is not a texture. On the floor, the shader adds floor(x * 0.75) and floor(z * 0.75), then multiplies the base gray by either 0.52 or 0.82. That keeps this example free of runtime image assets.
Trace a bounded ray and dim blocked light to 30 percent
After the primary hit, the shader computes the exact point-to-light vector and distance. It offsets the origin by normal * 0.004, skips the surface's own ID, and stops at the first positive intersection nearer than the light:
for (var i = 0u; i < object_count(); i = i + 1u) {
if (i32(scene_objects[i].ids.x) == object_id) { continue; }
let t = object_intersect(ray_o, ray_d, scene_objects[i]);
if ((t > EPSILON) && (t < max_t)) { return true; }
}
An unblocked hit receives base color times diffuse plus a Blinn-style highlight. Diffuse has a 0.2 floor. When blocked, the complete lit color is multiplied by 0.3, retaining visible ambient detail rather than becoming black. Distance fog blends toward a dark blue background over 24 world units.
Trace at most 19 analytic intersections per active pixel
The trace target caps its largest dimension at 1,024 while preserving aspect ratio. At the screenshot's 1024×576 resolution, one dispatch contains 64×36 workgroups and exactly 589,824 active 16×16 invocations. A primary ray always tests ten objects. A shadowed surface can test up to nine more after excluding itself, although early exit often stops sooner. Misses do no shadow work.
Scroll sideways to see all table columns.
| Resource | Logical layout | Bytes | Lifetime | Use |
|---|---|---|---|---|
| Storage target | 1024×576 Rgba8Unorm | 2,359,296 | Recreated when capped size changes | Compute output, then sampled input |
| Scene buffer | 10 × 64 bytes | 640 | Uploaded once | All analytic geometry and materials |
| Uniform buffer | 4 × vec4 | 64 | Written each frame | Light, camera, dimensions, time |
| Embedded font | Vazirmatn TrueType; SIL OFL 1.1 | 122,752 stored | Overlay atlas | Runtime diagnostics |
The three passes are compute, a three-vertex full-screen presentation draw, and a color-load text overlay. The target has one mip and one sample. Linear filtering scales a capped ray image to the presentation surface; no depth attachment, blending, hardware ray-tracing API, or multisampling participates.
Controls and limitations
There are no interactive controls. The camera is fixed, the light animates automatically, and the overlay reports frame timing, device, ten objects, shadow-ray status, and trace dimensions. The camera-basis helper contains a Z-up fallback for vertical views even though the current fixed view does not approach that singularity.
Every shadow sample is binary and uses one point light, so edges cannot soften with blocker distance. Infinite planes and axis-aligned boxes have no object transforms. The linear object list scales poorly, specular highlights are not physically based, output is clamped directly into linear Rgba8Unorm, and there is no anti-aliasing, transparency, refraction, global illumination, temporal accumulation, or denoising.
Run and modify the example
Run the native target:
cargo run --example raytracingshadows
Build and serve the browser version:
scripts/build-wasm.sh --release raytracingshadows
cargo run --bin serve
Open http://127.0.0.1:8080/raytracingshadows/ with WebGPU enabled. All shader and font inputs are embedded. Try sampling several points across an area light for soft shadows, returning a continuous visibility estimate, or adding a BVH before increasing the object count.