Emitters

Emitter Shapes and Particle Textures

The emitter is a position or a mesh; its shape sets where particles are born and which way they fly. createPointEmitter(dir1, dir2) and createBoxEmitter(dir1, dir2, minBox, maxBox) pick a direction between two vectors; createSphereEmitter, createHemisphericEmitter, createCylinderEmitter and createConeEmitter emit outward from those volumes (createDirected... variants fix the directions); createMeshEmitter(mesh) uses a mesh's triangles, on the CPU system only.

All particles share one texture, tinted by their color and blended by blendMode: ONEONE (additive, the default), ADD (additive, weighted by alpha), STANDARD (alpha), MULTIPLY, MULTIPLYADD or SUBTRACT. BookNest's motes use createDotTexture(scene) from l2-particles.js, a white radial gradient painted on a canvas (Canvas 2D) and loaded from canvas.toDataURL(). A DynamicTexture version failed once a sub-emitter (Sub-Emitters) cloned the system: the clone's texture was empty and never ready.

Five emitter shapes side by side: point, box, sphere, cone and cylinder, sharing one canvas-painted dot textureHTMLLive
<!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; }
</style>
<canvas id="view"></canvas>
<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');   // additive motes need a dark backdrop
const camera = new BABYLON.ArcRotateCamera('cam', -Math.PI / 2, 1.25, 9, new BABYLON.Vector3(0, 0.9, 0), scene);
camera.attachControl(canvas, true);
new BABYLON.HemisphericLight('sky', new BABYLON.Vector3(0, 1, 0), scene).intensity = 0.4;
const ui = BABYLON.GUI.AdvancedDynamicTexture.CreateFullscreenUI('ui');
const texture = createDotTexture(scene);            // one texture for every system
const V = (x, y, z) => new BABYLON.Vector3(x, y, z);
const COLORS = ['#ffe082', '#80cbc4', '#b39ddb', '#ffab91', '#a5d6a7'];

const SHAPES = [
  ['Point', (ps) => ps.createPointEmitter(V(-0.3, 1, -0.3), V(0.3, 1, 0.3))],
  ['Box', (ps) => ps.createBoxEmitter(V(0, 1, 0), V(0, 1, 0), V(-0.4, 0, -0.4), V(0.4, 0.1, 0.4))],
  ['Sphere', (ps) => ps.createSphereEmitter(0.5)],
  ['Cone', (ps) => ps.createConeEmitter(0.4, Math.PI / 5)],
  ['Cylinder', (ps) => ps.createCylinderEmitter(0.35, 0.8, 0)],
];
SHAPES.forEach(([name, shape], i) => {
  const x = (i - 2) * 1.7;
  const ps = new BABYLON.ParticleSystem(name, 800, scene);
  ps.particleTexture = texture;
  ps.emitter = V(x, 0.4, 0);
  shape(ps);                                         // where particles are born, which way they fly
  const color = BABYLON.Color3.FromHexString(COLORS[i]);
  Object.assign(ps, { emitRate: 160, minLifeTime: 1.2, maxLifeTime: 1.8, updateSpeed: 1 / 60,
    minSize: 0.04, maxSize: 0.09, minEmitPower: 0.6, maxEmitPower: 1,
    color1: color.toColor4(1), color2: color.toColor4(0.7), colorDead: color.toColor4(0),
    blendMode: BABYLON.ParticleSystem.BLENDMODE_ONEONE,   // additive, the default
    preWarmCycles: 100 });
  ps.start();
  const base = new BABYLON.TransformNode('base' + i, scene);
  base.position.set(x, 0, 0);
  const label = new BABYLON.GUI.TextBlock('', `create${name}\nEmitter()`);
  Object.assign(label, { color: '#eee', fontSize: 11, resizeToFit: true });
  ui.addControl(label);
  label.linkWithMesh(base);
  label.linkOffsetY = 30;
});
engine.runRenderLoop(() => scene.render());
addEventListener('resize', () => engine.resize());
</script>