GPUParticleSystem keeps particles in GPU buffers and updates them with transform feedback on WebGL 2 (Transform Feedback) or a compute shader on WebGPUEngine (GPU Particle Updates). The API is nearly the same, but it lacks sub-emitters, mesh emitters, manualEmitCount and disposeOnStop; test GPUParticleSystem.IsSupported first. Timing both:
scene.useConstantAnimationDeltaTime = true;
for (const make of [() => new BABYLON.ParticleSystem('cpu', 100000, scene),
() => new BABYLON.GPUParticleSystem('gpu', { capacity: 100000 }, scene)]) {
const system = make(); // created one at a time
Object.assign(system, { particleTexture: BookNestParticles.createDotTexture(scene),
emitRate: 20000, minLifeTime: 5, maxLifeTime: 5, updateSpeed: 1 / 60 });
system.start();
await scene.whenReadyAsync();
for (let frame = 0; frame < 60; frame++) scene.render(); // fill: 20,000 per second
const start = performance.now();
for (let frame = 0; frame < 120; frame++) scene.render();
view.getContext('webgl2').finish(); // Babylon's context: wait for the GPU
const ms = (performance.now() - start) / 120;
console.log(system.name, system.getActiveCount(), `${ms.toFixed(2)} ms a frame`);
system.dispose();
}Output
cpu 57600 8.53 ms a frame gpu 57600 0.27 ms a frame
Over 14 runs on a busy shared test machine (320 x 180 canvas) the GPU system needed 1/17 to 1/37 of the CPU system's frame time; trust the ratio, not the milliseconds. Use it for tens of thousands of particles.
<!doctype html>
<style>
body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
canvas { width: 100%; max-width: 600px; height: 320px; display: block; touch-action: none; outline: none; }
#info { max-width: 600px; padding: 4px 8px; font-family: monospace; }
</style>
<canvas id="view"></canvas>
<div id="info"></div>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.js"></script>
<script src="https://cdn.jsdelivr.net/npm/babylonjs-gui@9.28.0/babylon.gui.min.js"></script>
<script>
function createDotTexture(scene) { // a soft white dot painted on a canvas
const canvas = document.createElement('canvas');
canvas.width = canvas.height = 64;
const g = canvas.getContext('2d');
const glow = g.createRadialGradient(32, 32, 0, 32, 32, 32);
glow.addColorStop(0, 'rgba(255,255,255,1)');
glow.addColorStop(0.4, 'rgba(255,255,255,0.6)');
glow.addColorStop(1, 'rgba(255,255,255,0)');
g.fillStyle = glow;
g.fillRect(0, 0, 64, 64);
return new BABYLON.Texture(canvas.toDataURL(), scene); // loaded like an image file
}
const canvas = document.getElementById('view');
const engine = new BABYLON.Engine(canvas, true);
const scene = new BABYLON.Scene(engine);
scene.clearColor = BABYLON.Color4.FromHexString('#2f3440ff');
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.3, 7, new BABYLON.Vector3(0, 1.2, 0), scene);
camera.attachControl(canvas, true);
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
const texture = createDotTexture(scene);
// The same fountain twice; only the class and the numbers differ
function fountain(system, x, hex, rate) {
Object.assign(system, { particleTexture: texture, emitter: new BABYLON.Vector3(x, 0, 0),
emitRate: rate, minLifeTime: 2.5, maxLifeTime: 2.5, updateSpeed: 1 / 60,
minSize: 0.015, maxSize: 0.035, minEmitPower: 3, maxEmitPower: 4.5,
gravity: new BABYLON.Vector3(0, -3.5, 0) });
system.createConeEmitter(0.1, Math.PI / 8);
system.color1 = system.color2 = BABYLON.Color4.FromHexString(hex);
system.colorDead = new BABYLON.Color4(0, 0, 0, 0);
system.start();
const base = new BABYLON.TransformNode('b', scene);
base.position.x = x;
const label = new BABYLON.GUI.TextBlock('', '');
Object.assign(label, { color: '#eee', fontSize: 12, resizeToFit: true });
ui.addControl(label);
label.linkWithMesh(base);
label.linkOffsetY = 20;
return label;
}
const cpu = new BABYLON.ParticleSystem('cpu', 1000, scene); // updated in JavaScript
const cpuLabel = fountain(cpu, -1.6, '#ffab91cc', 400);
let gpu = null, gpuLabel = null;
if (BABYLON.GPUParticleSystem.IsSupported) { // transform feedback (WebGL 2) or compute (WebGPU)
gpu = new BABYLON.GPUParticleSystem('gpu', { capacity: 10000 }, scene);
gpuLabel = fountain(gpu, 1.6, '#80cbc4cc', 4000);
}
const info = document.getElementById('info');
scene.onAfterRenderObservable.add(() => {
cpuLabel.text = `ParticleSystem\n${cpu.getActiveCount()} alive`;
if (gpu) gpuLabel.text = `GPUParticleSystem\n${gpu.getActiveCount()} alive`;
info.textContent = gpu ? `GPUParticleSystem.IsSupported: true ${engine.getFps().toFixed(0)} fps`
: 'GPUParticleSystem is not supported here';
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>