DEV Community

Almast
Almast

Posted on

A reproducible Three.js particle burst with NixieFX

If you are evaluating a three-nebula alternative, start with a small integration test: author an effect, export it, load it in your Three.js scene, and trigger it from a user action. This tutorial does that with a short orange burst. It is not a performance or feature comparison with three-nebula.

The example was run with Node 22.13.1, NixieFX 0.1.17, Three.js 0.185.1, and Vite 8.3.1. The CLI reported 0 errors and 0 warnings for the Three.js target, the export completed, and the browser rendered the effect. The screenshot below is from that running browser demo.

A running Three.js page showing the orange NixieFX particle burst

1. Create the app and the effect project

Create a vanilla JavaScript Vite app and install the pinned packages:

npm create vite@latest nixie-burst-demo -- --template vanilla
cd nixie-burst-demo
npm install
npm install nixie-fx@0.1.17 three@0.185.1
Enter fullscreen mode Exit fullscreen mode

Create a folder named vfx-project. Inside it, create vfx-editor.prj:

{
  "app": "vfx-editor",
  "kind": "project",
  "version": 1,
  "id": "nixie-burst-demo",
  "name": "Nixie Burst Demo",
  "settings": {
    "effectDataPath": "particle-data/effects",
    "assetRootPath": "assets",
    "outputPath": "out/vfx",
    "materialsFolder": "materials",
    "allowExternalOutput": false
  },
  "createdAt": "2026-10-01T00:00:00.000Z",
  "updatedAt": "2026-10-01T00:00:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

Now scaffold a valid procedural effect. The CLI starter is a white stationary emitter; the next step turns it into the burst shown above.

npx nixie-fx effect create --project ./vfx-project --name "Impact Burst" --profile three-world-3d
Enter fullscreen mode Exit fullscreen mode

Create configure-effect.mjs in the app root:

import { readFile, writeFile } from "node:fs/promises";

const path = "vfx-project/particle-data/effects/impact-burst.json";
const effect = JSON.parse(await readFile(path, "utf8"));
const emitter = effect.emitters[0];

function constant(scalar, value) {
  scalar.value = value;
  scalar.min = value;
  scalar.max = value;
  for (const point of [...scalar.curve, ...scalar.curveB]) point.y = value;
}

emitter.loop = false;
emitter.duration = 0.8;
emitter.maxParticles = 48;
constant(emitter.spawn.rateValue, 0);
emitter.spawn.rate = 0;
emitter.spawn.bursts = [{
  time: 0, count: 24, cycles: 1, interval: 0.1, probability: 1
}];
emitter.spawn.shape = "sphere";
emitter.spawn.radius = 0.1;
constant(emitter.spawn.radiusValue, 0.1);
emitter.modules.velocity = true;
emitter.initializeParticle.velocity.mode = "shapeDirection";
constant(emitter.initializeParticle.velocity.speed, 4);
constant(emitter.initializeParticle.lifetime, 0.7);
emitter.initializeParticle.color.color = [1, 0.55, 0.08, 1];
emitter.initializeParticle.color.colorB = [1, 0.55, 0.08, 1];
emitter.billboard.sizeStart = 0.05;
emitter.billboard.sizeEnd = 0.05;
constant(emitter.billboard.sizeValue, 0.05);

await writeFile(path, JSON.stringify(effect, null, 2) + "\n");
Enter fullscreen mode Exit fullscreen mode

Validate, export, and copy the game bundle into Vite's public folder:

node configure-effect.mjs
npx nixie-fx validate ./vfx-project
npx nixie-fx export ./vfx-project
mkdir -p public/vfx
cp -R vfx-project/out/vfx/. public/vfx/
Enter fullscreen mode Exit fullscreen mode

The validator also printed two informational PixiJS notes about depth behavior. This effect targets Three.js; those notes were not Three.js warnings. Do not publish an effect if validation or export reports an error or backend blocker.

2. Load it in Three.js

Use this minimal index.html:

<!doctype html>
<html lang="en">
  <meta charset="UTF-8">
  <title>Three.js effect burst</title>
  <style>
    body { background: #101827; color: white; font: 16px system-ui; }
    main { width: min(760px, 95vw); margin: 2rem auto; }
  </style>
  <main>
    <h1>Three.js effect burst</h1>
    <button id="replay">Replay burst</button>
    <p id="status">Loading...</p>
    <div id="stage"></div>
  </main>
  <script type="module" src="/src/main.js"></script>
</html>
Enter fullscreen mode Exit fullscreen mode

Replace src/main.js with:

import * as THREE from "three";
import { loadVfxExportBundle } from "nixie-fx/export";
import { ThreeVfxRenderer } from "nixie-fx/three";

const stage = document.querySelector("#stage");
const status = document.querySelector("#status");
const replay = document.querySelector("#replay");
replay.disabled = true;

async function getJSON(url) {
  const response = await fetch(url);
  if (!response.ok) throw new Error("Could not load " + url);
  return response.json();
}

async function start() {
  const scene = new THREE.Scene();
  scene.background = new THREE.Color(0x101827);
  const camera = new THREE.PerspectiveCamera(55, 760 / 400, 0.1, 100);
  camera.position.set(0, 0, 5);
  camera.lookAt(0, 0, 0);

  const renderer = new THREE.WebGLRenderer({ antialias: true });
  renderer.setSize(760, 400);
  stage.appendChild(renderer.domElement);

  const root = "/vfx";
  const manifest = await getJSON(root + "/manifest.json");
  const effectsByPath = Object.fromEntries(
    await Promise.all(manifest.effects.map(async ({ path }) => [
      path, await getJSON(root + "/" + path)
    ]))
  );
  const bundle = loadVfxExportBundle(
    { manifest, effectsByPath }, { requiredBackend: "three3d" }
  );
  const effect = bundle.effectsById.get("impact-burst");
  if (!effect) throw new Error("impact-burst was not exported");

  const vfx = new ThreeVfxRenderer({ scene, camera });
  const burst = vfx.createEffect(effect, {
    position: [0, 0, 0], seed: 42
  });
  replay.disabled = false;
  replay.addEventListener("click", () => burst.restart());

  const clock = new THREE.Clock();
  renderer.setAnimationLoop(() => {
    vfx.update(clock.getDelta()); // seconds, exactly once per frame
    renderer.render(scene, camera);
  });
  status.textContent = "Ready. Select Replay burst.";
}

start().catch((error) => {
  status.textContent = error.message;
  console.error(error);
});
Enter fullscreen mode Exit fullscreen mode

Run npm run dev, open Vite's local URL, and select Replay burst. You should see a short orange burst in the dark canvas. If the button stays disabled, read the status text and browser console, and confirm that public/vfx/manifest.json exists.

What this test does and does not establish

This checks the authoring-to-export-to-runtime path for one procedural, asset-free Three.js effect. The export loader checks backend support, and the renderer advances the effect once per frame using seconds. An asset-backed effect needs the appropriate texture or mesh provider. A production choice also needs visual review in your actual scene and frame-time measurements on the devices you support. The NixieFX Three.js runtime guide covers those additional integration points.

Disclosure: This article was generated with AI for NixieFX marketing. The code was run and the screenshot came from the working browser demo; readers should verify the result in their own environment.

Top comments (0)