The Node Particle Editor

The Node Particle Editor (npe.babylonjs.com, since 8.12.1, June 2025) does for particle systems what The Node Material Editor's editor does for materials: blocks for creation (emitter shape, random values), per-frame updates driven by values such as age, triggers that start other systems, and a System block. The graph, a NodeParticleSystemSet, saves as JSON and builds ordinary CPU particle systems:

Building the editor's default graph in codeJavaScript
const graph = BABYLON.NodeParticleSystemSet.CreateDefault('dustGraph');   // the starter graph
const set = await graph.buildAsync(scene);         // a ParticleSystemSet of ordinary systems
console.log(graph.serialize().blocks.map((block) => block.name).join('/'));   // saved JSON
console.log(set.systems[0].getClassName(), set.systems[0].getCapacity());
set.start();
Output
Particle system/Update position/Box shape/Create particle/Add/Position/Scaled direction/Texture
ParticleSystem 1000

The starter graph emits from a box and adds the scaled direction to each position. Saved graphs load with NodeParticleSystemSet.ParseFromSnippetAsync('#ID'), and graph.editAsync() opens the editor on a live scene.

The Node Particle Editor's starter graph built in code into an ordinary particle system, with its blocks listedHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f3ec; font: 13px system-ui, sans-serif; color: #333; }
  canvas { width: 100%; max-width: 600px; height: 300px; display: block; touch-action: none; outline: none; }
  #info { max-width: 600px; margin: 0; padding: 4px 8px; font-family: monospace; white-space: pre-wrap; }
</style>
<canvas id="view"></canvas>
<pre id="info">Building the graph…</pre>
<script src="https://cdn.jsdelivr.net/npm/babylonjs@9.28.0/babylon.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.2, 5.5, new BABYLON.Vector3(0, 1.3, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0, 1, 0), scene);
const box = BABYLON.MeshBuilder.CreateBox('emitterBox', { size: 1 }, scene);   // the starter's box emitter, drawn
box.material = new BABYLON.StandardMaterial('boxMat', scene);
box.material.wireframe = true;
box.material.emissiveColor = new BABYLON.Color3(0.6, 0.8, 1);

(async () => {
  const graph = BABYLON.NodeParticleSystemSet.CreateDefault('dustGraph');   // the starter graph
  // The starter's texture block points at a file on Babylon's CDN; use a painted dot instead
  const dot = createDotTexture(scene);
  await new Promise((resolve) => dot.onLoadObservable.addOnce(resolve));
  for (const block of graph.attachedBlocks) {
    if ('url' in block) block.url = dot.url;         // a data: URL, no download
  }
  const set = await graph.buildAsync(scene);         // a ParticleSystemSet of ordinary systems
  const system = set.systems[0];
  set.start();
  document.getElementById('info').textContent =
    `blocks: ${graph.serialize().blocks.map((b) => b.name).join(' / ')}\n` +
    `built: ${system.getClassName()}, capacity ${system.getCapacity()}`;
})();
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>