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