- Hardware ray tracing via VK_KHR_ray_tracing_pipeline / VK_KHR_acceleration_structure
- Monte Carlo path tracing with progressive sample accumulation
- Physically-based Cook-Torrance BRDF (GGX NDF, Fresnel-Schlick, VNDF importance sampling)
- Next Event Estimation (NEE) with explicit emissive triangle sampling
- Multiple Importance Sampling (MIS) between BRDF and NEE paths
- ReSTIR DI (Bitterli et al. 2020) - RIS over M light candidates, plus
temporal and spatial reservoir reuse, on top of a G-buffer pass split.
Reuse passes are plain compute using
VK_KHR_ray_query. - Per-triangle emissive radiance baked from emissive textures, so textured emissives (Bistro's 20k string lights, street lights and shop signs) become real sampleable lights
- HDR environment map lighting (equirectangular)
- tonemapping + gamma correction + firefly clamping
- glTF 2.0 / GLB loading (fastgltf) and Wavefront OBJ (tinyobjloader)
- PBR material textures: albedo, metallic-roughness, normal, emissive
- shader hot reloading
- ImGui UI: camera editor, light controls, frame time graph (very basic so far)
- Scene config persistence (scene.ini) with Ctrl+S save
- Headless mode (engine_headless) for offline batch rendering to PNG
# reference path tracer (ground truth)
build/debug/engine_headless --model scene.gltf --samples 512
# ReSTIR DI, all stages
build/debug/engine_headless --model scene.gltf --restir on --samples 512
# single-frame quality, which is what ReSTIR is actually for:
# renders N frames but keeps the progressive average at 1, so you get the
# last frame with a warm temporal history
build/debug/engine_headless --model scene.gltf --restir on \
--accumulate off --samples 32| flag | default | meaning |
|---|---|---|
--restir on|off |
off | enable the ReSTIR pass split |
--nee-candidates N |
32 | RIS candidates M. 1 is plain uniform NEE |
--restir-temporal on|off |
on | reuse last frame's reservoir |
--restir-spatial on|off |
on | reuse neighbouring pixels' reservoirs |
--restir-initial-visibility on|off |
on | shadow-test candidates before reuse |
--spatial-samples k |
5 | neighbours per spatial iteration |
--spatial-radius px |
30 | first-iteration search radius |
--m-clamp N |
20 | temporal M cap, as a multiple of M |
--bias-mode 0|1|2 |
1 | 0 = 1/M (biased), 1 = 1/Z, 2 = MIS |
--emissive-scale s |
1 | scales every material's emissive factor |
--accumulate on|off |
on | off outputs the last frame only |
--bounces N |
4 | 1 isolates direct lighting |
tools/compare_images.py ref.png a.png b.png reports RMSE, relative bias and
MAPE - bias shows up as a consistent mean shift, not as noise, so it will not
survive eyeballing.
Measured on Bistro Exterior (20,638 light triangles), direct lighting only, single frame at 1 shadow ray per pixel, 960x540, against a 2048 spp reference:
| configuration | RMSE | MAPE |
|---|---|---|
| uniform NEE (the old path) | 15.55 | 109% |
| RIS, M = 32 | 13.32 | 93% |
| ReSTIR, initial candidates only | 13.40 | 93% |
| + spatial reuse | 9.90 | 58% |
| + temporal reuse | 9.97 | 56% |
| full ReSTIR DI | 8.57 | 49% |
At 1 spp the uniform-NEE image is essentially black with sparse fireflies; the full ReSTIR image is readable. Note this is direct lighting only - at the default 4 bounces most of the remaining noise in Bistro is sky and indirect, which ReSTIR DI does not address.
Reuse is nearly free - the whole cost is the RIS candidate loop - so it substitutes for candidates. At equal time (RTX 3070, 960x540, 4 bounces):
| ms/frame | RMSE | |
|---|---|---|
| uniform NEE, M = 1 | 22.8 | 15.55 |
| RIS M = 32, no reuse | 26.3 | 13.32 |
| full ReSTIR, M = 4 | 26.4 | 8.89 |
| full ReSTIR, M = 32 | 36.8 | 8.57 |
M = 4..8 is the right operating point once reuse is on.


