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.
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
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"
}
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
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");
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/
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>
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);
});
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)