hz-particles 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +286 -235
- package/dist-lib/hz-particles-r3f.cjs +2 -2
- package/dist-lib/hz-particles-r3f.d.ts +2 -0
- package/dist-lib/hz-particles-r3f.mjs +136 -125
- package/dist-lib/hz-particles.cjs +72 -30
- package/dist-lib/hz-particles.d.ts +16 -1
- package/dist-lib/hz-particles.mjs +3715 -3596
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,59 +1,242 @@
|
|
|
1
1
|
# hz-particles
|
|
2
2
|
|
|
3
|
+
**Prompt-generated, real-time WebGPU FX for Three.js and React Three Fiber.**
|
|
4
|
+
|
|
5
|
+
Design particle effects in the HZ editor — by hand or **from a prompt** — export them as
|
|
6
|
+
`JSON` / `.hzfx` presets, and render them **faithfully** in your app with the *same* WebGPU
|
|
7
|
+
engine the editor uses. What you design is exactly what renders.
|
|
8
|
+
|
|
3
9
|
[](https://www.npmjs.com/package/hz-particles)
|
|
4
10
|
[](https://github.com/jguyet/particle-system/blob/main/LICENSE)
|
|
5
11
|
[](https://caniuse.com/webgpu)
|
|
6
12
|
|
|
7
|
-
|
|
13
|
+
```tsx
|
|
14
|
+
import { HZFaithfulFX } from 'hz-particles/r3f';
|
|
15
|
+
import firePreset from './fire.json';
|
|
16
|
+
|
|
17
|
+
export function Scene() {
|
|
18
|
+
return <HZFaithfulFX preset={firePreset} position={[0, 1, 0]} />;
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
- **Prompt FX generator / editor:** https://particles.hole.zone/editor
|
|
23
|
+
- **Gallery (live WebGPU previews):** https://particles.hole.zone/discover
|
|
24
|
+
- **Docs:** https://particles.hole.zone/docs
|
|
25
|
+
- **NPM:** https://www.npmjs.com/package/hz-particles
|
|
26
|
+
|
|
27
|
+
## Why hz-particles?
|
|
28
|
+
|
|
29
|
+
Most AI/VFX tools generate **videos** or **spritesheets**. hz-particles generates **real-time,
|
|
30
|
+
editable FX presets** that run inside your Three.js / R3F scene.
|
|
31
|
+
|
|
32
|
+
- **Not a video** — effects are simulated in real time, on the GPU, every frame.
|
|
33
|
+
- **Not a screenshot** — presets are editable `JSON` / `.hzfx` files you can re-tune or re-prompt.
|
|
34
|
+
- **Not a separate renderer** — the R3F component renders the *same engine* as the editor, so
|
|
35
|
+
there's no "looked great in the editor, renders wrong in-game" gap.
|
|
36
|
+
- **Game-ready** — moving emitters, trails, depth occlusion, bloom, GLB-mesh particles.
|
|
37
|
+
|
|
38
|
+
> **Text-to-realtime-FX, not text-to-video.** The HZ editor turns a prompt into an editable
|
|
39
|
+
> preset; this package is the runtime that renders it in your app.
|
|
40
|
+
|
|
41
|
+
The full pipeline:
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
HZ editor (manual or prompt) → preset (JSON / .hzfx) → faithful WebGPU runtime → Three.js / R3F
|
|
45
|
+
```
|
|
8
46
|
|
|
9
47
|
## Features
|
|
10
48
|
|
|
11
49
|
- **WebGPU Compute Shaders** — GPU-accelerated particle physics simulation
|
|
12
|
-
- **GPU Instancing** —
|
|
13
|
-
- **
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
50
|
+
- **GPU Instancing** — efficient rendering of thousands of particles
|
|
51
|
+
- **Engine-faithful R3F component** — `HZFaithfulFX` runs the real overlay inline on three's
|
|
52
|
+
`WebGPURenderer`: shader shapes, noise, per-system blending, multi-pass bloom, faithful trails,
|
|
53
|
+
depth occlusion. Editor parity by construction.
|
|
54
|
+
- **Faithful trails & moving emitters** — comet trails that follow an arbitrary path (ball trails)
|
|
55
|
+
- **GLB model support** — use 3D models as particle shapes, with automatic texture extraction
|
|
56
|
+
- **Skeletal animations** — animated GLB models with full animation control
|
|
57
|
+
- **20+ emitter shapes** — volumes & surfaces: sphere, cube, cylinder, cone, torus, capsule,
|
|
58
|
+
frustum, hemisphere, disc, annulus, arc, spiral, polygon, `cubeSurface`, `sphereSurface`,
|
|
59
|
+
`boxFrame`, and more
|
|
60
|
+
- **Real-time physics** — gravity, attractors, drag, velocity, lifetime
|
|
61
|
+
- **Particle orientation modes** — `billboard` (fixed/random/velocity-aligned), `oriented` (a fixed
|
|
62
|
+
world angle via Euler `orientX/Y/Z`), and `cylindrical` (upright, faces the camera horizontally)
|
|
63
|
+
- **Keyframe animation** — animate per-system parameters over time with curves + easing
|
|
64
|
+
(`keyframes` + `keyframesEnabled`), driven by the unified clock; per-emitter `startAt` sequencing
|
|
65
|
+
- **Serialized presets** — self-contained `.hzfx` binary package (textures + GLB inlined) or plain JSON
|
|
66
|
+
- **3D object support** — static 3D objects alongside particle systems
|
|
20
67
|
|
|
21
68
|
## Requirements
|
|
22
69
|
|
|
23
|
-
**WebGPU-
|
|
24
|
-
- Chrome/Edge 113+ (stable)
|
|
70
|
+
**A WebGPU-capable browser is required:**
|
|
71
|
+
- Chrome / Edge 113+ (stable)
|
|
25
72
|
- Firefox Nightly (experimental)
|
|
26
|
-
- Safari Technology Preview (experimental)
|
|
73
|
+
- Safari Technology Preview / Safari 18+ (experimental)
|
|
27
74
|
|
|
28
75
|
Check browser support: [caniuse.com/webgpu](https://caniuse.com/webgpu)
|
|
29
76
|
|
|
30
77
|
## Installation
|
|
31
78
|
|
|
32
|
-
### WebGPU
|
|
33
|
-
|
|
34
79
|
```bash
|
|
35
80
|
npm install hz-particles
|
|
36
81
|
```
|
|
37
82
|
|
|
38
83
|
```javascript
|
|
39
|
-
|
|
84
|
+
// React Three Fiber (recommended)
|
|
85
|
+
import { HZFaithfulFX } from 'hz-particles/r3f';
|
|
86
|
+
|
|
87
|
+
// Standalone WebGPU / overlay
|
|
88
|
+
import { initHzFxOverlay, ParticleSystemManager } from 'hz-particles';
|
|
40
89
|
```
|
|
41
90
|
|
|
42
|
-
|
|
91
|
+
For R3F usage you also need the peers: `react`, `three` (with its WebGPU backend), and
|
|
92
|
+
`@react-three/fiber`.
|
|
43
93
|
|
|
44
|
-
|
|
45
|
-
|
|
94
|
+
## Usage modes
|
|
95
|
+
|
|
96
|
+
Pick the entry point that matches how much control you need:
|
|
97
|
+
|
|
98
|
+
### 1. Drop-in R3F FX — `<HZFaithfulFX />`
|
|
99
|
+
The fastest path. Give it a preset, drop it in your scene, done. Engine-faithful by construction.
|
|
100
|
+
|
|
101
|
+
### 2. Overlay / inline renderer — `initHzFxOverlay()`
|
|
102
|
+
For non-React apps, custom render loops, post-processing pipelines, or when you want to host many
|
|
103
|
+
coexisting effects and drive the camera yourself. Same faithful engine, full control.
|
|
104
|
+
|
|
105
|
+
### 3. Low-level particle engine — `ParticleSystemManager` / `ParticleSystem`
|
|
106
|
+
Build your own engine on top of the raw compute + render pipelines.
|
|
107
|
+
|
|
108
|
+
## Quick Start (React Three Fiber)
|
|
109
|
+
|
|
110
|
+
`<HZFaithfulFX>` runs the **real engine** inline on three's `WebGPURenderer`: shader-drawn shapes,
|
|
111
|
+
noise, per-system blending, multi-pass bloom, faithful trails, and depth occlusion. Pass a
|
|
112
|
+
`position` for a static FX, or a `positionRef` for a moving one (e.g. a ball trail) — the overlay
|
|
113
|
+
pulls the ref each frame.
|
|
114
|
+
|
|
115
|
+
Your R3F `Canvas` must use a **WebGPU** renderer. With `@react-three/fiber` v9 and `three/webgpu`:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { Canvas } from '@react-three/fiber';
|
|
119
|
+
import * as THREE from 'three/webgpu';
|
|
120
|
+
import { useRef } from 'react';
|
|
121
|
+
import { HZFaithfulFX } from 'hz-particles/r3f';
|
|
122
|
+
import firePreset from './fire.json';
|
|
123
|
+
import trailPreset from './trail.json';
|
|
124
|
+
|
|
125
|
+
export function App() {
|
|
126
|
+
const ballRef = useRef<THREE.Vector3>(null); // updated by your app each frame
|
|
127
|
+
|
|
128
|
+
return (
|
|
129
|
+
<Canvas
|
|
130
|
+
// Provide a WebGPURenderer instead of the default WebGLRenderer:
|
|
131
|
+
gl={async (props) => {
|
|
132
|
+
const renderer = new THREE.WebGPURenderer(props as any);
|
|
133
|
+
await renderer.init();
|
|
134
|
+
return renderer;
|
|
135
|
+
}}
|
|
136
|
+
camera={{ position: [0, 2, 6], fov: 50 }}
|
|
137
|
+
>
|
|
138
|
+
<HZFaithfulFX preset={firePreset} position={[0, 1, 0]} /> {/* static FX */}
|
|
139
|
+
<HZFaithfulFX preset={trailPreset} positionRef={ballRef} scale={1.5} /> {/* ball trail */}
|
|
140
|
+
</Canvas>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
46
143
|
```
|
|
47
144
|
|
|
48
|
-
|
|
145
|
+
> **Important integration note —** `<HZFaithfulFX>` **drives rendering**: its `useFrame` calls
|
|
146
|
+
> `gl.render` itself, so R3F stops auto-rendering. This is what guarantees the FX composites on
|
|
147
|
+
> top of your scene with correct depth occlusion.
|
|
148
|
+
>
|
|
149
|
+
> - Use `<HZFaithfulFX>` when it can own the frame (the common case).
|
|
150
|
+
> - If your host owns its own render loop (custom RAF, a post-processing / EffectComposer
|
|
151
|
+
> pipeline), use [`initHzFxOverlay()`](#engine-faithful-overlay) directly instead so *you* stay
|
|
152
|
+
> in control of when the scene and the FX render.
|
|
153
|
+
>
|
|
154
|
+
> Re-trigger an effect by remounting it (change its `key`).
|
|
155
|
+
|
|
156
|
+
### Loading `.hzfx` presets
|
|
157
|
+
|
|
158
|
+
The `preset` prop is a plain `SceneData` object. JSON presets can be imported directly (above).
|
|
159
|
+
For the binary `.hzfx` package, fetch it with `fetchPreset` (auto-detects `.hzfx` vs JSON):
|
|
160
|
+
|
|
161
|
+
```tsx
|
|
162
|
+
import { useEffect, useState } from 'react';
|
|
163
|
+
import { fetchPreset } from 'hz-particles/r3f';
|
|
49
164
|
import { HZFaithfulFX } from 'hz-particles/r3f';
|
|
165
|
+
|
|
166
|
+
function Fire() {
|
|
167
|
+
const [preset, setPreset] = useState(null);
|
|
168
|
+
useEffect(() => { fetchPreset('/fx/fire.hzfx').then(setPreset); }, []);
|
|
169
|
+
return preset ? <HZFaithfulFX preset={preset} position={[0, 1, 0]} /> : null;
|
|
170
|
+
}
|
|
50
171
|
```
|
|
51
172
|
|
|
52
|
-
|
|
173
|
+
### Props reference
|
|
53
174
|
|
|
54
|
-
|
|
175
|
+
| Prop | Type | Default | Description |
|
|
176
|
+
|------|------|---------|-------------|
|
|
177
|
+
| `preset` | `SceneData` | — | Particle preset (JSON object exported from the editor) |
|
|
178
|
+
| `position` | `[number, number, number]` | `[0, 0, 0]` | Static emitter world position (when `positionRef` is omitted) |
|
|
179
|
+
| `positionRef` | `RefObject<Vector3 \| null>` | — | Moving emitter (e.g. a ball trail): the overlay reads this ref every frame |
|
|
180
|
+
| `scale` | `number` | `1` | Uniform scale applied to the preset (sizes, speeds, emission, trail) |
|
|
181
|
+
| `active` | `boolean` | `true` | When false, a moving emitter pauses (no new trail) |
|
|
182
|
+
| `renderPriority` | `number` | `1` | `useFrame` priority. This component drives rendering, so keep it > 0 |
|
|
183
|
+
| `noOcclusion` | `boolean` | `false` | Skip scene-depth occlusion (particles never hidden behind geometry) |
|
|
55
184
|
|
|
56
|
-
|
|
185
|
+
The `hz-particles/r3f` entry also exports `HZTrailRibbon` (a lightweight ribbon-trail mesh driven
|
|
186
|
+
by a `positionRef`) and the helpers `scalePreset`, `computeParticleSize`, `computeParticleOpacity`.
|
|
187
|
+
|
|
188
|
+
## Engine-faithful overlay
|
|
189
|
+
|
|
190
|
+
`initHzFxOverlay()` is **the same render loop the HZ previews and editor use** — shader-drawn
|
|
191
|
+
shapes, per-system blending, noise distortion, GLB-mesh particles + animation, multi-pass bloom,
|
|
192
|
+
and faithful trails. It's the recommended way to drop HZ effects into any app (vanilla JS,
|
|
193
|
+
TypeScript, or a React app with its own render loop — one core, no per-framework reimplementation).
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
import { initHzFxOverlay, makeThreeSceneDepth } from 'hz-particles';
|
|
197
|
+
|
|
198
|
+
// OVERLAY mode: pass a canvas — the overlay owns its WebGPU context and renders on a
|
|
199
|
+
// transparent background. Stack it above your scene with pointer-events: none.
|
|
200
|
+
const fx = await initHzFxOverlay(canvas);
|
|
201
|
+
await fx.loadPreset(preset); // or fx.setEmitters([{ preset, position }])
|
|
202
|
+
|
|
203
|
+
function frame(dt) {
|
|
204
|
+
fx.setCamera(proj, view, [cx, cy, cz]); // column-major matrices + camera world pos
|
|
205
|
+
fx.render(dt);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// INLINE mode (share your own WebGPU renderer, e.g. three's WebGPURenderer):
|
|
209
|
+
const fx2 = await initHzFxOverlay(
|
|
210
|
+
{ device, context, canvas },
|
|
211
|
+
{ getSceneDepth: makeThreeSceneDepth(renderer) } // one-line occlusion behind your geometry
|
|
212
|
+
);
|
|
213
|
+
// each frame, AFTER renderer.render(scene, camera): fx2.setCamera(...); fx2.render(dt);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
**Coexisting groups & moving emitters** — one overlay hosts many FX at once. `addEmitter(preset, pos)`
|
|
217
|
+
adds a static group; `addMovingEmitter(preset, { getPosition })` adds a moving one (e.g. a ball trail)
|
|
218
|
+
whose comet trail follows the path you supply (the overlay *pulls* `getPosition()` each frame; return
|
|
219
|
+
`null` to pause). Both return `{ remove() }`.
|
|
220
|
+
|
|
221
|
+
| Method | Description |
|
|
222
|
+
| --- | --- |
|
|
223
|
+
| `setCamera(proj, view, pos)` / `setCameraMVP(mvp, pos)` | Drive the camera (column-major). |
|
|
224
|
+
| `loadPreset(preset, pos?)` / `setEmitters([...])` | (Re)build emitters. |
|
|
225
|
+
| `addEmitter(preset, pos?)` → `{ remove }` | Add a STATIC group that coexists with others. |
|
|
226
|
+
| `addMovingEmitter(preset, { getPosition })` → `{ setPosition, remove }` | Add a MOVING group (ball trail). |
|
|
227
|
+
| `render(dt)` | Simulate + render one frame (inline: call after your scene render). |
|
|
228
|
+
| `resize()` | Recreate render textures (also auto-detected from the canvas). |
|
|
229
|
+
| `clearCaches()` | Drop cached bind groups after a `replaceSystems`/`addSystems` that reuses ids. |
|
|
230
|
+
| `trackHistoryGroup(ids, getPosition)` → `{ remove }` | Give existing manager systems a moving trail (history only). |
|
|
231
|
+
| `destroy()` | Release GPU resources. |
|
|
232
|
+
|
|
233
|
+
Options: `{ getSceneDepth, autoRespawn, manager }`. Pass `manager` to **share an existing
|
|
234
|
+
`ParticleSystemManager`** so your app keeps owning it while the overlay is the single render path.
|
|
235
|
+
|
|
236
|
+
## Advanced: standalone WebGPU engine
|
|
237
|
+
|
|
238
|
+
If you want to build your own renderer on the raw engine (no overlay, no R3F), drive the
|
|
239
|
+
`ParticleSystemManager` / `ParticleSystem` directly:
|
|
57
240
|
|
|
58
241
|
```javascript
|
|
59
242
|
import { initWebGPU, ParticleSystemManager } from 'hz-particles';
|
|
@@ -64,31 +247,25 @@ canvas.width = 800;
|
|
|
64
247
|
canvas.height = 600;
|
|
65
248
|
const { device, context, format } = await initWebGPU(canvas);
|
|
66
249
|
|
|
67
|
-
// 2. Create particle system
|
|
250
|
+
// 2. Create manager + a particle system
|
|
68
251
|
const manager = new ParticleSystemManager(device);
|
|
252
|
+
manager.createParticleSystem({ maxParticles: 10000, particleCount: 1000 });
|
|
69
253
|
|
|
70
|
-
// 3.
|
|
71
|
-
const systemId = manager.createParticleSystem({
|
|
72
|
-
maxParticles: 10000,
|
|
73
|
-
particleCount: 1000,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
// 4. Initialize compute pipeline
|
|
254
|
+
// 3. Initialize the compute pipeline
|
|
77
255
|
const system = manager.getActiveSystem();
|
|
78
256
|
await system.initComputePipeline(device);
|
|
79
257
|
|
|
80
|
-
//
|
|
258
|
+
// 4. Render loop
|
|
81
259
|
let lastTime = performance.now();
|
|
82
|
-
|
|
83
260
|
function render() {
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
lastTime =
|
|
261
|
+
const now = performance.now();
|
|
262
|
+
const dt = (now - lastTime) / 1000;
|
|
263
|
+
lastTime = now;
|
|
87
264
|
|
|
88
|
-
manager.updateAllSystems(
|
|
265
|
+
manager.updateAllSystems(dt);
|
|
89
266
|
|
|
90
|
-
const
|
|
91
|
-
const
|
|
267
|
+
const encoder = device.createCommandEncoder();
|
|
268
|
+
const pass = encoder.beginRenderPass({
|
|
92
269
|
colorAttachments: [{
|
|
93
270
|
view: context.getCurrentTexture().createView(),
|
|
94
271
|
clearValue: { r: 0, g: 0, b: 0, a: 1 },
|
|
@@ -96,81 +273,45 @@ function render() {
|
|
|
96
273
|
storeOp: 'store',
|
|
97
274
|
}],
|
|
98
275
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
device.queue.submit([commandEncoder.finish()]);
|
|
276
|
+
system.render(pass);
|
|
277
|
+
pass.end();
|
|
278
|
+
device.queue.submit([encoder.finish()]);
|
|
103
279
|
|
|
104
280
|
requestAnimationFrame(render);
|
|
105
281
|
}
|
|
106
|
-
|
|
107
282
|
render();
|
|
108
283
|
```
|
|
109
284
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
`<HZFaithfulFX>` runs the **real engine** (the overlay) inline on three's `WebGPURenderer`: shader
|
|
113
|
-
shapes, noise, per-system blending, multi-pass bloom, faithful trails + depth occlusion. What you
|
|
114
|
-
design in the editor is exactly what renders. Pass a `position` for a static FX, or a `positionRef`
|
|
115
|
-
for a moving one (a ball trail) — the overlay pulls the ref each frame.
|
|
285
|
+
## Preset configuration
|
|
116
286
|
|
|
117
|
-
|
|
118
|
-
import { useRef } from 'react';
|
|
119
|
-
import { HZFaithfulFX } from 'hz-particles/r3f';
|
|
120
|
-
import firePreset from './presets/fire.json';
|
|
121
|
-
|
|
122
|
-
function Scene() {
|
|
123
|
-
const ballRef = useRef(null); // THREE.Vector3, updated by your app each frame
|
|
124
|
-
return (
|
|
125
|
-
<>
|
|
126
|
-
<HZFaithfulFX preset={firePreset} position={[0, 1, 0]} /> {/* static FX */}
|
|
127
|
-
<HZFaithfulFX preset={trailPreset} positionRef={ballRef} scale={1.5} /> {/* ball trail */}
|
|
128
|
-
</>
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
`<HZFaithfulFX>` **drives rendering** (its `useFrame` calls `gl.render` itself, so R3F stops
|
|
134
|
-
auto-rendering). If your host owns its own render loop (custom RAF, post-processing pipeline), use
|
|
135
|
-
`initHzFxOverlay()` + `makeThreeSceneDepth()` directly (see above). Re-trigger an effect by remounting
|
|
136
|
-
it (change its `key`).
|
|
137
|
-
|
|
138
|
-
## Props Reference
|
|
139
|
-
|
|
140
|
-
The `HZFaithfulFX` component accepts the following props:
|
|
141
|
-
|
|
142
|
-
| Prop | Type | Default | Description |
|
|
143
|
-
|------|------|---------|-------------|
|
|
144
|
-
| `preset` | `SceneData` | — | Particle preset (JSON exported from the editor) |
|
|
145
|
-
| `position` | `[number, number, number]` | `[0, 0, 0]` | Static emitter world position (used when `positionRef` is omitted) |
|
|
146
|
-
| `positionRef` | `React.RefObject<Vector3 \| null>` | — | Moving emitter (e.g. a ball trail): the overlay reads this ref every frame |
|
|
147
|
-
| `scale` | `number` | `1` | Uniform scale applied to the preset (sizes, speeds, emission, trail) |
|
|
148
|
-
| `active` | `boolean` | `true` | When false, a moving emitter pauses (no new trail) |
|
|
149
|
-
| `renderPriority` | `number` | `1` | `useFrame` priority. This component drives rendering, so keep it > 0 |
|
|
150
|
-
| `noOcclusion` | `boolean` | `false` | Skip scene-depth occlusion (particles never hidden behind geometry) |
|
|
151
|
-
|
|
152
|
-
## Preset Configuration
|
|
153
|
-
|
|
154
|
-
Presets are plain JSON files that describe a particle scene. Key fields:
|
|
287
|
+
Presets are plain `SceneData`. A few key per-system fields:
|
|
155
288
|
|
|
156
289
|
| Field | Type | Description |
|
|
157
290
|
|-------|------|-------------|
|
|
158
291
|
| `particleCount` | `number` | Number of active particles |
|
|
159
292
|
| `lifetime` | `number` | Particle lifetime in seconds |
|
|
160
293
|
| `emissionRate` | `number` | Particles emitted per second |
|
|
161
|
-
| `emissionShape` | `string` | Emitter shape: `
|
|
294
|
+
| `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` |
|
|
162
295
|
| `particleSize` | `number` | Base size of each particle |
|
|
163
|
-
| `colors` | `string[]` |
|
|
296
|
+
| `colors` | `string[]` | Hex colors interpolated over lifetime |
|
|
164
297
|
| `fadeIn` | `number` | Opacity fade-in duration (0–1, fraction of lifetime) |
|
|
165
298
|
| `fadeOut` | `number` | Opacity fade-out start (0–1, fraction of lifetime) |
|
|
166
299
|
| `bloom` | `boolean` | Enable bloom glow on particles |
|
|
167
300
|
| `gravity` | `number` | Gravity strength applied to particles |
|
|
168
301
|
| `damping` | `number` | Velocity damping factor (0 = no drag, 1 = full stop) |
|
|
302
|
+
| `rotationMode` | `string` | Particle orientation: `fixed` / `random` / `velocity` (billboard), `oriented` (fixed world angle), `cylindrical` (upright, camera-facing) |
|
|
303
|
+
| `orientX` / `orientY` / `orientZ` | `number` | Euler angles (radians) for `rotationMode: 'oriented'` |
|
|
304
|
+
| `keyframesEnabled` | `boolean` | Enable keyframe animation of system parameters over time |
|
|
305
|
+
| `keyframes` | `object` | Per-parameter keyframe tracks (value + curve/easing) applied via the clock |
|
|
306
|
+
| `startAt` | `number` | Delay (seconds) before this emitter starts, for relative sequencing |
|
|
169
307
|
| `emissionTrailEnabled` | `boolean` | Enable particle trail rendering |
|
|
170
|
-
| `emissionTrailDuration` | `number` | How long trail segments persist
|
|
308
|
+
| `emissionTrailDuration` | `number` | How long trail segments persist (seconds) |
|
|
171
309
|
| `emissionTrailWidth` | `number` | Width of the emission trail |
|
|
172
310
|
|
|
173
|
-
|
|
311
|
+
`serializeSystemConfig(config)` is the single source of truth for the full set of serializable
|
|
312
|
+
fields (used by `saveScene` and the editor's code export).
|
|
313
|
+
|
|
314
|
+
## API reference
|
|
174
315
|
|
|
175
316
|
### `initWebGPU(canvas)`
|
|
176
317
|
|
|
@@ -185,14 +326,7 @@ async function initWebGPU(canvas: HTMLCanvasElement): Promise<{
|
|
|
185
326
|
}>
|
|
186
327
|
```
|
|
187
328
|
|
|
188
|
-
|
|
189
|
-
- `canvas` (required) — Canvas element. Set `canvas.width` and `canvas.height` before calling.
|
|
190
|
-
|
|
191
|
-
**Returns:** Object with WebGPU device, canvas context, texture format, and canvas element.
|
|
192
|
-
|
|
193
|
-
**Throws:** Error if `canvas` is missing or WebGPU is not supported.
|
|
194
|
-
|
|
195
|
-
---
|
|
329
|
+
Set `canvas.width` / `canvas.height` before calling. Throws if WebGPU is unsupported.
|
|
196
330
|
|
|
197
331
|
### `ParticleSystem`
|
|
198
332
|
|
|
@@ -204,25 +338,19 @@ class ParticleSystem {
|
|
|
204
338
|
}
|
|
205
339
|
```
|
|
206
340
|
|
|
207
|
-
**Config
|
|
208
|
-
- `maxParticles` (number, default `10000`) — Maximum particle buffer size
|
|
209
|
-
- `particleCount` (number, default `100`) — Initial active particle count
|
|
210
|
-
|
|
211
|
-
**Key Methods:**
|
|
341
|
+
**Config:** `maxParticles` (default `10000`), `particleCount` (default `100`).
|
|
212
342
|
|
|
213
343
|
| Method | Description |
|
|
214
344
|
|--------|-------------|
|
|
215
|
-
| `initComputePipeline(device)` | Initialize GPU compute and render pipelines.
|
|
345
|
+
| `initComputePipeline(device)` | Initialize GPU compute and render pipelines. Call before rendering. |
|
|
216
346
|
| `setTexture(imageBitmap)` | Set particle texture from an `ImageBitmap`. |
|
|
217
|
-
| `setGLBModel(arrayBuffer)` | Use GLB model geometry as particle shape
|
|
347
|
+
| `setGLBModel(arrayBuffer)` | Use GLB model geometry as particle shape (auto-extracts textures). |
|
|
218
348
|
| `updateParticles(deltaTime)` | Execute a physics simulation step on the GPU. |
|
|
219
349
|
| `spawnParticles()` | Emit new particles according to emitter configuration. |
|
|
220
350
|
| `setGravity(value)` | Set gravity strength (default `9.8`). |
|
|
221
|
-
| `setAttractor(strength, position)` | Set an attractor point
|
|
351
|
+
| `setAttractor(strength, position)` | Set an attractor point `[x, y, z]`. |
|
|
222
352
|
| `render(renderPass)` | Render particles to the current render pass. |
|
|
223
353
|
|
|
224
|
-
---
|
|
225
|
-
|
|
226
354
|
### `ParticleSystemManager`
|
|
227
355
|
|
|
228
356
|
Manages multiple particle systems within a single scene.
|
|
@@ -233,25 +361,21 @@ class ParticleSystemManager {
|
|
|
233
361
|
}
|
|
234
362
|
```
|
|
235
363
|
|
|
236
|
-
**Key Methods:**
|
|
237
|
-
|
|
238
364
|
| Method | Description |
|
|
239
365
|
|--------|-------------|
|
|
240
366
|
| `createParticleSystem(config?)` | Create a new particle system. Returns system ID. |
|
|
241
|
-
| `getActiveSystem()` | Get the currently active `ParticleSystem
|
|
367
|
+
| `getActiveSystem()` | Get the currently active `ParticleSystem`. |
|
|
242
368
|
| `getActiveConfig()` | Get the active system configuration object. |
|
|
243
|
-
| `setActiveSystem(index)` | Switch active system by index.
|
|
244
|
-
| `removeSystem(index)` | Remove a system by index.
|
|
369
|
+
| `setActiveSystem(index)` | Switch active system by index. |
|
|
370
|
+
| `removeSystem(index)` | Remove a system by index. |
|
|
245
371
|
| `updateAllSystems(deltaTime)` | Update physics for all systems. |
|
|
246
|
-
| `getSystemsList()` |
|
|
372
|
+
| `getSystemsList()` | List all systems (`name`, `id`, `index`, `isActive`). |
|
|
247
373
|
| `duplicateActiveSystem()` | Clone the active system. Returns new system ID. |
|
|
248
|
-
| `replaceSystems(sceneData)` | Load a scene from serialized data.
|
|
249
|
-
|
|
250
|
-
---
|
|
374
|
+
| `replaceSystems(sceneData)` | Load a scene from serialized data. |
|
|
251
375
|
|
|
252
376
|
### `parseGLB(arrayBuffer)`
|
|
253
377
|
|
|
254
|
-
Parse GLB binary
|
|
378
|
+
Parse GLB binary and extract geometry.
|
|
255
379
|
|
|
256
380
|
```typescript
|
|
257
381
|
async function parseGLB(arrayBuffer: ArrayBuffer): Promise<{
|
|
@@ -266,16 +390,13 @@ async function parseGLB(arrayBuffer: ArrayBuffer): Promise<{
|
|
|
266
390
|
}>
|
|
267
391
|
```
|
|
268
392
|
|
|
269
|
-
---
|
|
270
|
-
|
|
271
393
|
### `GLBAnimator`
|
|
272
394
|
|
|
273
|
-
|
|
395
|
+
Skeletal animation playback for animated GLB models.
|
|
274
396
|
|
|
275
397
|
```typescript
|
|
276
398
|
class GLBAnimator {
|
|
277
399
|
constructor(animationData: object)
|
|
278
|
-
|
|
279
400
|
currentTime: number
|
|
280
401
|
playing: boolean
|
|
281
402
|
speed: number // default 1.0
|
|
@@ -283,164 +404,94 @@ class GLBAnimator {
|
|
|
283
404
|
}
|
|
284
405
|
```
|
|
285
406
|
|
|
286
|
-
**Key Methods:**
|
|
287
|
-
|
|
288
407
|
| Method | Description |
|
|
289
408
|
|--------|-------------|
|
|
290
|
-
| `setRestPose(positions, normals)` | Set the T-pose/bind pose
|
|
409
|
+
| `setRestPose(positions, normals)` | Set the T-pose/bind pose. |
|
|
291
410
|
| `update(deltaTime)` | Advance animation. Returns `{ positions, normals, changed }`. |
|
|
292
411
|
| `setAnimation(index)` | Switch to animation clip by index. |
|
|
293
|
-
| `getAnimationNames()` |
|
|
412
|
+
| `getAnimationNames()` | List available animation clip names. |
|
|
294
413
|
|
|
295
|
-
## GLB
|
|
414
|
+
## GLB models & animation
|
|
296
415
|
|
|
297
416
|
```javascript
|
|
298
417
|
import { parseGLB, GLBAnimator } from 'hz-particles';
|
|
299
418
|
|
|
300
|
-
|
|
301
|
-
const response = await fetch('model.glb');
|
|
302
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
419
|
+
const arrayBuffer = await (await fetch('model.glb')).arrayBuffer();
|
|
303
420
|
const glbData = await parseGLB(arrayBuffer);
|
|
304
421
|
|
|
305
|
-
//
|
|
306
|
-
await system.setGLBModel(arrayBuffer);
|
|
422
|
+
await system.setGLBModel(arrayBuffer); // use as particle shape
|
|
307
423
|
|
|
308
|
-
// Setup animation (if model has animations)
|
|
309
424
|
if (glbData.animationData) {
|
|
310
425
|
const animator = new GLBAnimator(glbData.animationData);
|
|
311
426
|
animator.setRestPose(glbData.positions, glbData.normals);
|
|
312
427
|
animator.playing = true;
|
|
313
|
-
animator.loop = true;
|
|
314
|
-
animator.speed = 1.0;
|
|
315
428
|
|
|
316
|
-
//
|
|
429
|
+
// in the render loop:
|
|
317
430
|
const { positions, normals, changed } = animator.update(deltaTime);
|
|
318
|
-
if (changed) {
|
|
319
|
-
// Update particle system with new geometry
|
|
320
|
-
// (See full API documentation for geometry update methods)
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
## Engine-faithful Integration (overlay renderer)
|
|
326
|
-
|
|
327
|
-
`initHzFxOverlay()` is **the same render loop the HZ previews and editor use** — shader-drawn
|
|
328
|
-
shapes, per-system blending, noise distortion, GLB mesh particles + animation, multi-pass bloom,
|
|
329
|
-
and faithful trails. It's the recommended way to drop HZ effects into any app (vanilla JS,
|
|
330
|
-
TypeScript, or React — one core, no per-framework reimplementation).
|
|
331
|
-
|
|
332
|
-
```javascript
|
|
333
|
-
import { initHzFxOverlay, makeThreeSceneDepth } from 'hz-particles';
|
|
334
|
-
|
|
335
|
-
// OVERLAY mode: pass a canvas — the overlay owns its WebGPU context and renders on a
|
|
336
|
-
// transparent background. Stack it above your scene with pointer-events: none.
|
|
337
|
-
const fx = await initHzFxOverlay(canvas);
|
|
338
|
-
await fx.loadPreset(preset); // or fx.setEmitters([{ preset, position }])
|
|
339
|
-
|
|
340
|
-
function frame(dt) {
|
|
341
|
-
fx.setCamera(proj, view, [cx, cy, cz]); // column-major matrices + camera world pos
|
|
342
|
-
fx.render(dt);
|
|
431
|
+
if (changed) { /* push updated geometry to the system */ }
|
|
343
432
|
}
|
|
344
|
-
|
|
345
|
-
// INLINE mode (share your own WebGPU renderer, e.g. three's WebGPURenderer):
|
|
346
|
-
const fx2 = await initHzFxOverlay(
|
|
347
|
-
{ device, context, canvas },
|
|
348
|
-
{ getSceneDepth: makeThreeSceneDepth(renderer) } // one-line occlusion behind your geometry
|
|
349
|
-
);
|
|
350
|
-
// each frame, AFTER renderer.render(scene, camera): fx2.setCamera(...); fx2.render(dt);
|
|
351
433
|
```
|
|
352
434
|
|
|
353
|
-
|
|
354
|
-
adds a static group; `addMovingEmitter(preset, { getPosition })` adds a moving one (e.g. a ball trail)
|
|
355
|
-
whose comet trail follows the path you supply (the overlay *pulls* `getPosition()` each frame; return
|
|
356
|
-
`null` to pause). Both return `{ remove() }`.
|
|
357
|
-
|
|
358
|
-
| Method | Description |
|
|
359
|
-
| --- | --- |
|
|
360
|
-
| `setCamera(proj, view, pos)` / `setCameraMVP(mvp, pos)` | Drive the camera (column-major). |
|
|
361
|
-
| `loadPreset(preset, pos?)` / `setEmitters([...])` | (Re)build emitters. |
|
|
362
|
-
| `addEmitter(preset, pos?)` → `{ remove }` | Add a STATIC group that coexists with others. |
|
|
363
|
-
| `addMovingEmitter(preset, { getPosition })` → `{ setPosition, remove }` | Add a MOVING group (ball trail). |
|
|
364
|
-
| `render(dt)` | Simulate + render one frame (inline: call after your scene render). |
|
|
365
|
-
| `resize()` | Recreate render textures (also auto-detected from the canvas). |
|
|
366
|
-
| `clearCaches()` | Drop cached bind groups after a `replaceSystems`/`addSystems` that reuses ids. |
|
|
367
|
-
| `trackHistoryGroup(ids, getPosition)` → `{ remove }` | Give existing manager systems a moving trail (history only). |
|
|
368
|
-
| `destroy()` | Release GPU resources. |
|
|
369
|
-
|
|
370
|
-
Options: `{ getSceneDepth, autoRespawn, manager }`. Pass `manager` to **share an existing
|
|
371
|
-
`ParticleSystemManager`** so your app keeps owning it while the overlay is the single render path.
|
|
372
|
-
|
|
373
|
-
## Scene Save/Load
|
|
435
|
+
## Scene save / load
|
|
374
436
|
|
|
375
437
|
```javascript
|
|
376
438
|
import { saveScene, loadScene } from 'hz-particles';
|
|
377
439
|
|
|
378
|
-
// Save
|
|
379
|
-
|
|
380
|
-
saveScene(manager); // Downloads a self-contained .hzfx package (or .hzfx → JSON on Alt-click)
|
|
381
|
-
});
|
|
440
|
+
// Save (downloads a self-contained .hzfx package; Alt-click → JSON):
|
|
441
|
+
saveButton.addEventListener('click', () => saveScene(manager));
|
|
382
442
|
|
|
383
|
-
// Load
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
if (success) {
|
|
387
|
-
console.log('Scene loaded successfully');
|
|
388
|
-
}
|
|
443
|
+
// Load from a file input (.hzfx or JSON, auto-detected):
|
|
444
|
+
fileInput.addEventListener('change', async (e) => {
|
|
445
|
+
if (await loadScene(e, manager)) console.log('Scene loaded');
|
|
389
446
|
});
|
|
390
447
|
```
|
|
391
448
|
|
|
392
449
|
## TypeScript
|
|
393
450
|
|
|
394
|
-
Type declarations **are shipped** — the package's `types` entry points at
|
|
395
|
-
(and `dist-lib/hz-particles-r3f.d.ts` for the `hz-particles/r3f`
|
|
396
|
-
out of the box:
|
|
451
|
+
Type declarations **are shipped** — the package's `types` entry points at
|
|
452
|
+
`dist-lib/hz-particles.d.ts` (and `dist-lib/hz-particles-r3f.d.ts` for the `hz-particles/r3f`
|
|
453
|
+
subpath), so imports are fully typed out of the box:
|
|
397
454
|
|
|
398
455
|
```typescript
|
|
399
456
|
import { initHzFxOverlay, ParticleSystemManager, serializeSystemConfig } from 'hz-particles';
|
|
400
457
|
import { HZFaithfulFX } from 'hz-particles/r3f';
|
|
401
458
|
```
|
|
402
459
|
|
|
403
|
-
## Secondary
|
|
460
|
+
## Secondary exports
|
|
404
461
|
|
|
405
|
-
|
|
462
|
+
Also exported for advanced usage:
|
|
406
463
|
|
|
407
|
-
- **`
|
|
408
|
-
- **`
|
|
409
|
-
- **`
|
|
464
|
+
- **`fetchPreset(url)`** — load a preset from a URL (`.hzfx` or JSON, auto-detected)
|
|
465
|
+
- **`packHZFX` / `unpackHZFX` / `isHZFX`** — build/read the `.hzfx` binary package format
|
|
466
|
+
- **`serializeSystemConfig(config)`** — single source of truth for a system's serializable fields
|
|
467
|
+
- **`saveScene(manager)` / `loadScene(event)`** — export/import a scene (`.hzfx` or JSON)
|
|
468
|
+
- **`ParticleEmitter`** — emission-shape configuration
|
|
469
|
+
- **`ParticlePhysics`** — physics parameter management
|
|
470
|
+
- **`ParticleTextureManager`** — texture loading utilities
|
|
410
471
|
- **`Objects3DManager`** — 3D object scene management
|
|
411
|
-
- **`
|
|
412
|
-
-
|
|
413
|
-
- **`serializeSystemConfig(config)`** — Single source of truth for a system's serializable config fields (shared by `saveScene` + the editor's code export; add binary assets yourself)
|
|
414
|
-
- **`initHzFxOverlay(target, options?)`** — Engine-faithful overlay/inline renderer (see above)
|
|
415
|
-
- **`packHZFX` / `unpackHZFX` / `isHZFX`** — Build/read the `.hzfx` binary package format
|
|
416
|
-
- **`extractGLBTexture(arrayBuffer)`** — Extract base color texture from GLB file
|
|
417
|
-
- **Shader exports** — WGSL shader code for compute and rendering pipelines
|
|
418
|
-
- **Geometry helpers** — Primitive shape generators (cube, sphere, etc.)
|
|
419
|
-
- **Render pipeline creators** — Low-level WebGPU pipeline construction
|
|
420
|
-
|
|
421
|
-
## Online Editor
|
|
422
|
-
|
|
423
|
-
Try the interactive particle editor at `http://localhost:8110/editor` when running the Docker container (`docker-compose up` from the repository root).
|
|
424
|
-
|
|
425
|
-
The editor provides a visual interface for:
|
|
426
|
-
- Real-time particle system configuration
|
|
427
|
-
- Emitter shape selection and tuning
|
|
428
|
-
- GLB model import and animation control
|
|
429
|
-
- Scene preset library
|
|
430
|
-
- Export/import scene JSON
|
|
472
|
+
- **`extractGLBTexture(arrayBuffer)`** — extract base color texture from a GLB
|
|
473
|
+
- **Shader / geometry / pipeline helpers** — low-level WGSL and pipeline construction
|
|
431
474
|
|
|
432
|
-
##
|
|
475
|
+
## Online editor
|
|
476
|
+
|
|
477
|
+
Generate effects from a prompt — or build them by hand — in the HZ editor, then export a preset and
|
|
478
|
+
render it with this package.
|
|
479
|
+
|
|
480
|
+
- Hosted editor & prompt FX generator: https://particles.hole.zone/editor
|
|
481
|
+
- Browse the community gallery (live previews): https://particles.hole.zone/discover
|
|
433
482
|
|
|
434
|
-
|
|
483
|
+
The editor provides real-time configuration, emitter-shape tuning, GLB import + animation control, a
|
|
484
|
+
preset library, and `JSON` / `.hzfx` export.
|
|
485
|
+
|
|
486
|
+
## Contributing
|
|
435
487
|
|
|
436
488
|
1. Fork the repository
|
|
437
489
|
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
438
|
-
3. Commit your changes
|
|
439
|
-
4. Push
|
|
440
|
-
5. Open a Pull Request
|
|
490
|
+
3. Commit your changes
|
|
491
|
+
4. Push and open a Pull Request
|
|
441
492
|
|
|
442
493
|
## License
|
|
443
494
|
|
|
444
|
-
MIT License
|
|
495
|
+
MIT License — see [LICENSE](LICENSE).
|
|
445
496
|
|
|
446
|
-
Copyright (c) 2025 HZ
|
|
497
|
+
Copyright (c) 2025-2026 HZ
|