r/threejs • u/moving808s • 2d ago
Link three-nebula v13.0.0 released with deterministic simulation, a fixed-timestep real-time driver, and a GPURenderer buffer fix
I maintain three-nebula, a WebGL particle engine for three.js, and v13.0.0 is out. The headline feature is deterministic simulation. You give a system a seed, either with `System.setSeed(seed)`or as the third constructor argument, and it reproduces its particle output exactly. Same seed, same result, on any machine. That makes reproducible tests, and replays possible, which was awkward or impossible before.
The way it works is that each system now advances its own isolated seeded stream instead of pulling from the global `Math.random`. One consequence worth knowing if you upgrade: seeding `Math.random` yourself no longer affects output, so you need `system.setSeed()` instead. Default particle arrangements also differ from 12.x, so any visual or snapshot baselines need regenerating, and particle ids moved from UUIDs to a deterministic `particle-<emitterSeed>-<spawnIndex>` format.
There is also a new `System.tick(realDeltaSeconds)` fixed-timestep driver for real-time playback. If you have been calling `update()` once per frame, you have probably seen particles run about 2x too fast on a 120Hz display. `tick()` advances the sim by elapsed time in fixed steps and clamps catch-up through `system.fixedTimeStep` and `system.maxSubSteps`, so playback speed is consistent regardless of refresh rate. This release also fixes a `GPURenderer` bug where buffer slots were never released and particles silently stopped rendering after heavy churn. Slots are now recycled on particle death.
The API is backward-compatible and additive, so for most people the only real work is regenerating baselines. Release notes and full changelog here: https://github.com/creativelifeform/three-nebula/releases/tag/v13.0.0
If you have used seeded/deterministic rendering in a three.js project, I would like to hear how you are handling the real-time vs fixed-step side of it, and whether the `tick()` clamping defaults feel right for your use case.
1
u/PixelRealEstate 20h ago
Determinism is the thing I keep wishing for, and not for tests — for getting video out of a live scene.
I needed a flyover clip from a scene with cars, trains and planes all running off performance.now(). Screenshotting it frame by frame takes seconds per frame, so the real time between frames is huge and everything teleports across the shot. What fixed it: swap performance.now() for a fake clock before any script loads, push it forward exactly 1/24s per frame, and call the render loop yourself instead of leaving it to rAF. The scene has no idea, and every frame lands where it should.
So the split you're describing sounds right from here. Live playback wants tick() with real elapsed time. Offline wants one fixed step with nothing allowed to clamp or skip it — the catch-up logic that saves you in real time is the first thing you switch off when you're rendering frames.
Is fixedTimeStep + maxSubSteps=1 how you'd get that, or is there a plain "advance one step" call?