path-traced-3d example when a 3D shot needs real light: soft shadows, light bouncing between objects and reflections of the scene itself. It previews with ordinary Three.js raster rendering and exports with a path tracer, from the same scene file.
Terminal
What is in the project
Everything loads from the project folder, so a render never fetches a library from the network.
Preview in raster, export path traced
Work in raster, then export once with the tracer:
Terminal
Without a hardware GPU, Chrome draws WebGL in software and the tracer is much slower: on SwiftShader its first shader run alone took about a minute at 320×180, longer than the default 45-second readiness wait. Check path mode at a small size with 1 or 2 samples, and raise the wait with
--timeout 170000 for check and snapshot or --player-ready-timeout 180000 for render.How it stays deterministic
HyperFrames seeks frames one by one, in any order and across several workers, sorender.js follows these rules. Keep them when you change it.
- Setup is one
window.__hf.buildReadypromise. It builds the scene, compiles the shaders, sets up the tracer and draws one throwaway frame, because the first frame a renderer draws differs from later ones. It then reads one pixel back, so the GPU has really finished that first, slowest run before setup resolves. - Frames are drawn only in the
hf-seeklistener, registered withevent.detail.waitUntil(promise). Nothing runs a render loop. - Each new time starts from zero. The handler poses the scene for that time, rebuilds the tracer’s copy of it and traces exactly
samplespasses. A seek back to the time already on screen does no work. - The tracer never waits on time.
renderDelayandfadeDurationare 0, because the tracer times them with a clock that does not move during a render. - Randomness is seeded. Three’s object uuids, which set the order the tracer sorts lights, meshes and textures in, and the tracer’s noise textures come from
Math.random, so the scene and the tracer are built under a fixed seed. Without it, every render worker draws different grain. Anything you add later, such as a model loaded during setup, must also be created under the seed.
The frozen clock
During a render,Date.now() and performance.now() return the current seek time and do not move between seeks (they read 0 until the first seek), and requestAnimationFrame callbacks wait for a seek. The first seek waits for setup. Setup that waits for an animation frame or for the clock to advance never finishes. Composition code uses no timers of its own either. Await the library’s readiness promise instead:
assets/render.js
isCompiling flag. The promise behind that flag is three’s compileAsync, so render.js awaits it in one helper, tracerCompiled(), whenever a renderSample() call leaves isCompiling set. It decides only when drawing may start, never what is drawn.
Do not wait with scheduler.yield(). Its continuations run ahead of timers, so a loop that yields until a compile finishes starves the timer compileAsync polls with, and the page never becomes ready. Do not swap compileAsync for the synchronous compile to hide that; fix the wait.
Check a change
Seek to a time, an earlier time, then the first time again, and compare the two stills:Terminal
registry/examples/path-traced-3d/source/verify-render.mjs runs these checks in both modes at 320×180 and checks the exported pixels.
Related
- Frame adapters for how the runtime seeks Three.js
- Variables for
--variablesand batch renders - Determinism for the general rules
- Performance for render speed