hz-particles
v1.4.2
Published
High-performance WebGPU particle engine with compute shaders, GPU instancing, GLB model support, and skeletal animations
Maintainers
Readme
hz-particles
Prompt-generated, real-time WebGPU FX for Three.js and React Three Fiber.
Design particle effects in the HZ editor — by hand or from a prompt — export them as
JSON / .hzfx presets, and render them faithfully in your app with the same WebGPU
engine the editor uses. What you design is exactly what renders.
import { HZFaithfulFX } from 'hz-particles/r3f';
import firePreset from './fire.json';
export function Scene() {
return <HZFaithfulFX preset={firePreset} position={[0, 1, 0]} />;
}- Prompt FX generator / editor: https://particles.hole.zone/editor
- Gallery (live WebGPU previews): https://particles.hole.zone/discover
- Docs: https://particles.hole.zone/docs
- NPM: https://www.npmjs.com/package/hz-particles
Why hz-particles?
Most AI/VFX tools generate videos or spritesheets. hz-particles generates real-time, editable FX presets that run inside your Three.js / R3F scene.
- Not a video — effects are simulated in real time, on the GPU, every frame.
- Not a screenshot — presets are editable
JSON/.hzfxfiles you can re-tune or re-prompt. - Not a separate renderer — the R3F component renders the same engine as the editor, so there's no "looked great in the editor, renders wrong in-game" gap.
- Game-ready — moving emitters, trails, depth occlusion, bloom, GLB-mesh particles.
Text-to-realtime-FX, not text-to-video. The HZ editor turns a prompt into an editable preset; this package is the runtime that renders it in your app.
The full pipeline:
HZ editor (manual or prompt) → preset (JSON / .hzfx) → faithful WebGPU runtime → Three.js / R3FFeatures
- WebGPU Compute Shaders — GPU-accelerated particle physics simulation
- GPU Instancing — efficient rendering of thousands of particles
- Engine-faithful R3F component —
HZFaithfulFXruns the real overlay inline on three'sWebGPURenderer: shader shapes, noise, per-system blending, multi-pass bloom, faithful trails, depth occlusion. Editor parity by construction. - Faithful trails & moving emitters — comet trails that follow an arbitrary path (ball trails)
- GLB model support — use 3D models as particle shapes, with automatic texture extraction
- Skeletal animations — animated GLB models with full animation control
- 20+ emitter shapes — volumes & surfaces: sphere, cube, cylinder, cone, torus, capsule,
frustum, hemisphere, disc, annulus, arc, spiral, polygon,
cubeSurface,sphereSurface,boxFrame, and more - Real-time physics — gravity, attractors, drag, velocity, lifetime
- Particle orientation modes —
billboard(fixed/random/velocity-aligned),oriented(a fixed world angle via EulerorientX/Y/Z), andcylindrical(upright, faces the camera horizontally) - Keyframe animation — animate per-system parameters over time with curves + easing
(
keyframes+keyframesEnabled), driven by the unified clock; per-emitterstartAtsequencing - Serialized presets — self-contained
.hzfxbinary package (textures + GLB inlined) or plain JSON - 3D object support — static 3D objects alongside particle systems
Requirements
A WebGPU-capable browser is required:
- Chrome / Edge 113+ (stable)
- Firefox Nightly (experimental)
- Safari Technology Preview / Safari 18+ (experimental)
Check browser support: caniuse.com/webgpu
Installation
npm install hz-particles// React Three Fiber (recommended)
import { HZFaithfulFX } from 'hz-particles/r3f';
// Standalone WebGPU / overlay
import { initHzFxOverlay, ParticleSystemManager } from 'hz-particles';For R3F usage you also need the peers: react, three (with its WebGPU backend), and
@react-three/fiber.
Usage modes
Pick the entry point that matches how much control you need:
1. Drop-in R3F FX — <HZFaithfulFX />
The fastest path. Give it a preset, drop it in your scene, done. Engine-faithful by construction.
2. Overlay / inline renderer — initHzFxOverlay()
For non-React apps, custom render loops, post-processing pipelines, or when you want to host many coexisting effects and drive the camera yourself. Same faithful engine, full control.
3. Low-level particle engine — ParticleSystemManager / ParticleSystem
Build your own engine on top of the raw compute + render pipelines.
Quick Start (React Three Fiber)
<HZFaithfulFX> runs the real engine inline on three's WebGPURenderer: shader-drawn shapes,
noise, per-system blending, multi-pass bloom, faithful trails, and depth occlusion. Pass a
position for a static FX, or a positionRef for a moving one (e.g. a ball trail) — the overlay
pulls the ref each frame.
Your R3F Canvas must use a WebGPU renderer. With @react-three/fiber v9 and three/webgpu:
import { Canvas } from '@react-three/fiber';
import * as THREE from 'three/webgpu';
import { useRef } from 'react';
import { HZFaithfulFX } from 'hz-particles/r3f';
import firePreset from './fire.json';
import trailPreset from './trail.json';
export function App() {
const ballRef = useRef<THREE.Vector3>(null); // updated by your app each frame
return (
<Canvas
// Provide a WebGPURenderer instead of the default WebGLRenderer:
gl={async (props) => {
const renderer = new THREE.WebGPURenderer(props as any);
await renderer.init();
return renderer;
}}
camera={{ position: [0, 2, 6], fov: 50 }}
>
<HZFaithfulFX preset={firePreset} position={[0, 1, 0]} /> {/* static FX */}
<HZFaithfulFX preset={trailPreset} positionRef={ballRef} scale={1.5} /> {/* ball trail */}
</Canvas>
);
}Important integration note —
<HZFaithfulFX>drives rendering: itsuseFramecallsgl.renderitself, so R3F stops auto-rendering. This is what guarantees the FX composites on top of your scene with correct depth occlusion.
- Use
<HZFaithfulFX>when it can own the frame (the common case).- If your host owns its own render loop (custom RAF, a post-processing / EffectComposer pipeline), use
initHzFxOverlay()directly instead so you stay in control of when the scene and the FX render.Re-trigger an effect by remounting it (change its
key).
Loading .hzfx presets
The preset prop is a plain SceneData object. JSON presets can be imported directly (above).
For the binary .hzfx package, fetch it with fetchPreset (auto-detects .hzfx vs JSON):
import { useEffect, useState } from 'react';
import { fetchPreset } from 'hz-particles/r3f';
import { HZFaithfulFX } from 'hz-particles/r3f';
function Fire() {
const [preset, setPreset] = useState(null);
useEffect(() => { fetchPreset('/fx/fire.hzfx').then(setPreset); }, []);
return preset ? <HZFaithfulFX preset={preset} position={[0, 1, 0]} /> : null;
}Props reference
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| preset | SceneData | — | Particle preset (JSON object exported from the editor) |
| position | [number, number, number] | [0, 0, 0] | Static emitter world position (when positionRef is omitted) |
| positionRef | RefObject<Vector3 \| null> | — | Moving emitter (e.g. a ball trail): the overlay reads this ref every frame |
| scale | number | 1 | Uniform scale applied to the preset (sizes, speeds, emission, trail) |
| active | boolean | true | When false, a moving emitter pauses (no new trail) |
| renderPriority | number | 1 | useFrame priority. This component drives rendering, so keep it > 0 |
| noOcclusion | boolean | false | Skip scene-depth occlusion (particles never hidden behind geometry) |
The hz-particles/r3f entry also exports HZTrailRibbon (a lightweight ribbon-trail mesh driven
by a positionRef) and the helpers scalePreset, computeParticleSize, computeParticleOpacity.
Engine-faithful overlay
initHzFxOverlay() is the same render loop the HZ previews and editor use — shader-drawn
shapes, per-system blending, noise distortion, GLB-mesh particles + animation, multi-pass bloom,
and faithful trails. It's the recommended way to drop HZ effects into any app (vanilla JS,
TypeScript, or a React app with its own render loop — one core, no per-framework reimplementation).
import { initHzFxOverlay, makeThreeSceneDepth } from 'hz-particles';
// OVERLAY mode: pass a canvas — the overlay owns its WebGPU context and renders on a
// transparent background. Stack it above your scene with pointer-events: none.
const fx = await initHzFxOverlay(canvas);
await fx.loadPreset(preset); // or fx.setEmitters([{ preset, position }])
function frame(dt) {
fx.setCamera(proj, view, [cx, cy, cz]); // column-major matrices + camera world pos
fx.render(dt);
}
// INLINE mode (share your own WebGPU renderer, e.g. three's WebGPURenderer):
const fx2 = await initHzFxOverlay(
{ device, context, canvas },
{ getSceneDepth: makeThreeSceneDepth(renderer) } // one-line occlusion behind your geometry
);
// each frame, AFTER renderer.render(scene, camera): fx2.setCamera(...); fx2.render(dt);Coexisting groups & moving emitters — one overlay hosts many FX at once. addEmitter(preset, pos)
adds a static group; addMovingEmitter(preset, { getPosition }) adds a moving one (e.g. a ball trail)
whose comet trail follows the path you supply (the overlay pulls getPosition() each frame; return
null to pause). Both return { remove() }.
| Method | Description |
| --- | --- |
| setCamera(proj, view, pos) / setCameraMVP(mvp, pos) | Drive the camera (column-major). |
| loadPreset(preset, pos?) / setEmitters([...]) | (Re)build emitters. |
| addEmitter(preset, pos?) → { remove } | Add a STATIC group that coexists with others. |
| addMovingEmitter(preset, { getPosition }) → { setPosition, remove } | Add a MOVING group (ball trail). |
| render(dt) | Simulate + render one frame (inline: call after your scene render). |
| resize() | Recreate render textures (also auto-detected from the canvas). |
| clearCaches() | Drop cached bind groups after a replaceSystems/addSystems that reuses ids. |
| trackHistoryGroup(ids, getPosition) → { remove } | Give existing manager systems a moving trail (history only). |
| destroy() | Release GPU resources. |
Options: { getSceneDepth, autoRespawn, manager }. Pass manager to share an existing
ParticleSystemManager so your app keeps owning it while the overlay is the single render path.
Advanced: standalone WebGPU engine
If you want to build your own renderer on the raw engine (no overlay, no R3F), drive the
ParticleSystemManager / ParticleSystem directly:
import { initWebGPU, ParticleSystemManager } from 'hz-particles';
// 1. Setup WebGPU context
const canvas = document.getElementById('webgpu-canvas');
canvas.width = 800;
canvas.height = 600;
const { device, context, format } = await initWebGPU(canvas);
// 2. Create manager + a particle system
const manager = new ParticleSystemManager(device);
manager.createParticleSystem({ maxParticles: 10000, particleCount: 1000 });
// 3. Initialize the compute pipeline
const system = manager.getActiveSystem();
await system.initComputePipeline(device);
// 4. Render loop
let lastTime = performance.now();
function render() {
const now = performance.now();
const dt = (now - lastTime) / 1000;
lastTime = now;
manager.updateAllSystems(dt);
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}],
});
system.render(pass);
pass.end();
device.queue.submit([encoder.finish()]);
requestAnimationFrame(render);
}
render();Preset configuration
Presets are plain SceneData. A few key per-system fields:
| Field | Type | Description |
|-------|------|-------------|
| particleCount | number | Number of active particles |
| lifetime | number | Particle lifetime in seconds |
| emissionRate | number | Particles emitted per second |
| emissionShape | string | Emitter shape (default cube). Volumes: cube/box, sphere, cylinder, cone, torus, capsule, frustum, hemisphere, spiral. Flat: circle, square, rectangle, disc, annulus, arc, polygon, plain, line. Surfaces: cubeSurface, sphereSurface, boxFrame |
| particleSize | number | Base size of each particle |
| colors | string[] | Hex colors interpolated over lifetime |
| fadeIn | number | Opacity fade-in duration (0–1, fraction of lifetime) |
| fadeOut | number | Opacity fade-out start (0–1, fraction of lifetime) |
| bloom | boolean | Enable bloom glow on particles |
| gravity | number | Gravity strength applied to particles |
| damping | number | Velocity damping factor (0 = no drag, 1 = full stop) |
| rotationMode | string | Particle orientation: fixed / random / velocity (billboard), oriented (fixed world angle), cylindrical (upright, camera-facing) |
| orientX / orientY / orientZ | number | Euler angles (radians) for rotationMode: 'oriented' |
| keyframesEnabled | boolean | Enable keyframe animation of system parameters over time |
| keyframes | object | Per-parameter keyframe tracks (value + curve/easing) applied via the clock |
| startAt | number | Delay (seconds) before this emitter starts, for relative sequencing |
| emissionTrailEnabled | boolean | Enable particle trail rendering |
| emissionTrailDuration | number | How long trail segments persist (seconds) |
| emissionTrailWidth | number | Width of the emission trail |
serializeSystemConfig(config) is the single source of truth for the full set of serializable
fields (used by saveScene and the editor's code export).
API reference
initWebGPU(canvas)
Convenience helper to initialize a WebGPU context and device.
async function initWebGPU(canvas: HTMLCanvasElement): Promise<{
device: GPUDevice,
context: GPUCanvasContext,
format: GPUTextureFormat,
canvas: HTMLCanvasElement
}>Set canvas.width / canvas.height before calling. Throws if WebGPU is unsupported.
ParticleSystem
Core particle system class managing simulation and rendering.
class ParticleSystem {
constructor(device: GPUDevice, config?: object)
}Config: maxParticles (default 10000), particleCount (default 100).
| Method | Description |
|--------|-------------|
| initComputePipeline(device) | Initialize GPU compute and render pipelines. Call before rendering. |
| setTexture(imageBitmap) | Set particle texture from an ImageBitmap. |
| setGLBModel(arrayBuffer) | Use GLB model geometry as particle shape (auto-extracts textures). |
| updateParticles(deltaTime) | Execute a physics simulation step on the GPU. |
| spawnParticles() | Emit new particles according to emitter configuration. |
| setGravity(value) | Set gravity strength (default 9.8). |
| setAttractor(strength, position) | Set an attractor point [x, y, z]. |
| render(renderPass) | Render particles to the current render pass. |
ParticleSystemManager
Manages multiple particle systems within a single scene.
class ParticleSystemManager {
constructor(device: GPUDevice)
}| Method | Description |
|--------|-------------|
| createParticleSystem(config?) | Create a new particle system. Returns system ID. |
| getActiveSystem() | Get the currently active ParticleSystem. |
| getActiveConfig() | Get the active system configuration object. |
| setActiveSystem(index) | Switch active system by index. |
| removeSystem(index) | Remove a system by index. |
| updateAllSystems(deltaTime) | Update physics for all systems. |
| getSystemsList() | List all systems (name, id, index, isActive). |
| duplicateActiveSystem() | Clone the active system. Returns new system ID. |
| replaceSystems(sceneData) | Load a scene from serialized data. |
parseGLB(arrayBuffer)
Parse GLB binary and extract geometry.
async function parseGLB(arrayBuffer: ArrayBuffer): Promise<{
positions: Float32Array,
normals: Float32Array,
indices: Uint16Array | Uint32Array,
texCoords: Float32Array | null,
vertexCount: number,
indexCount: number,
animationData: object | null,
hasBaseColorTexture: boolean
}>GLBAnimator
Skeletal animation playback for animated GLB models.
class GLBAnimator {
constructor(animationData: object)
currentTime: number
playing: boolean
speed: number // default 1.0
loop: boolean // default true
}| Method | Description |
|--------|-------------|
| setRestPose(positions, normals) | Set the T-pose/bind pose. |
| update(deltaTime) | Advance animation. Returns { positions, normals, changed }. |
| setAnimation(index) | Switch to animation clip by index. |
| getAnimationNames() | List available animation clip names. |
GLB models & animation
import { parseGLB, GLBAnimator } from 'hz-particles';
const arrayBuffer = await (await fetch('model.glb')).arrayBuffer();
const glbData = await parseGLB(arrayBuffer);
await system.setGLBModel(arrayBuffer); // use as particle shape
if (glbData.animationData) {
const animator = new GLBAnimator(glbData.animationData);
animator.setRestPose(glbData.positions, glbData.normals);
animator.playing = true;
// in the render loop:
const { positions, normals, changed } = animator.update(deltaTime);
if (changed) { /* push updated geometry to the system */ }
}Scene save / load
import { saveScene, loadScene } from 'hz-particles';
// Save (downloads a self-contained .hzfx package; Alt-click → JSON):
saveButton.addEventListener('click', () => saveScene(manager));
// Load from a file input (.hzfx or JSON, auto-detected):
fileInput.addEventListener('change', async (e) => {
if (await loadScene(e, manager)) console.log('Scene loaded');
});TypeScript
Type declarations are shipped — the package's types entry points at
dist-lib/hz-particles.d.ts (and dist-lib/hz-particles-r3f.d.ts for the hz-particles/r3f
subpath), so imports are fully typed out of the box:
import { initHzFxOverlay, ParticleSystemManager, serializeSystemConfig } from 'hz-particles';
import { HZFaithfulFX } from 'hz-particles/r3f';Secondary exports
Also exported for advanced usage:
fetchPreset(url)— load a preset from a URL (.hzfxor JSON, auto-detected)packHZFX/unpackHZFX/isHZFX— build/read the.hzfxbinary package formatserializeSystemConfig(config)— single source of truth for a system's serializable fieldssaveScene(manager)/loadScene(event)— export/import a scene (.hzfxor JSON)ParticleEmitter— emission-shape configurationParticlePhysics— physics parameter managementParticleTextureManager— texture loading utilitiesObjects3DManager— 3D object scene managementextractGLBTexture(arrayBuffer)— extract base color texture from a GLB- Shader / geometry / pipeline helpers — low-level WGSL and pipeline construction
Online editor
Generate effects from a prompt — or build them by hand — in the HZ editor, then export a preset and render it with this package.
- Hosted editor & prompt FX generator: https://particles.hole.zone/editor
- Browse the community gallery (live previews): https://particles.hole.zone/discover
The editor provides real-time configuration, emitter-shape tuning, GLB import + animation control, a
preset library, and JSON / .hzfx export.
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes
- Push and open a Pull Request
License
MIT License — see LICENSE.
Copyright (c) 2025-2026 HZ
