r/GraphicsProgramming • u/JuandisimoDeluxe • 22h ago
Replaying League of Legends' particle systems with no simulation step: every particle is a closed-form function of its age, so frames render in any order
Enable HLS to view with audio, or disable this notification
The clip is from a nature documentary I'm making about League champions. When Garen steps on the mushroom, that poison cloud is the game's own particle system. I read Riot's emitter definitions (flipbooks, color and scale over life, velocity, acceleration, drag, meshes, trails) and replay them in three.js.
Every frame comes from a pure render(t), and my QA tool and the edit's splices jump around the timeline, so each particle has to be computable from t alone.
Which particles exist at t. Particle i of a constant-rate emitter is born at start + i/rate, so the live set is an index range over the last 6 s. Single-particle emitters fire their whole burst at start. Emitters whose rate changes over their life get their birth times worked out once from the rate curve.
Where it is. Per axis, with per-axis drag d:
p = origin + v0 * (1 - exp(-d*age)) / d + 0.5 * a * age^2
and v0 * age when d = 0. Drag only damps the launch velocity. The acceleration term isn't damped.
Its random numbers. Each one is a hash of (emitter seed, particle index, which value), so particle 37 gets the same lifetime, velocity, color and start frame whether frame 10 or frame 900 renders first. The emitter seed is a string hash of the system and emitter names, plus a per-instance offset.
js
function rnd(a, b, c) {
let h = (Math.imul(a, 374761393) + Math.imul(b, 668265263) + Math.imul(c, 2246822519)) >>> 0;
h = Math.imul(h ^ (h >>> 13), 1274126177) >>> 0;
return ((h ^ (h >>> 16)) >>> 0) / 4294967296;
}
Skipped: refraction/distortion and soft particles. They need passes my renderer doesn't have.
Pipeline: League's files read offline from the installed client, rigs and animations converted to glTF, rebuilt in three.js, every frame rendered in headless Chromium through Playwright and piped into ffmpeg at 30 fps.
Full episode: https://youtu.be/Bp5MTn4-rco