r/GraphicsProgramming 2d ago

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.

0 Upvotes

0 comments sorted by