Package Exports
- hz-particles
- hz-particles/r3f
Readme
hz-particles
A high-performance WebGPU particle engine for the web. Use it as a standalone WebGPU renderer or as a plug-and-play React Three Fiber component.
Features
- WebGPU Compute Shaders — GPU-accelerated particle physics simulation
- GPU Instancing — Efficient rendering of thousands of particles
- GLB Model Support — Use 3D models as particle shapes with automatic texture extraction
- Skeletal Animations — Animated GLB models with full animation control
- Scene Serialization — Save/load particle configurations as JSON
- Multiple Emitter Shapes — Sphere, cube, cylinder, circle, square emission patterns
- Real-time Physics — Gravity, attractors, drag, velocity, and lifetime management
- React Three Fiber Component — Drop-in
HZParticlesFXcomponent with preset support - 3D Object Support — Static 3D objects alongside particle systems
Requirements
WebGPU-compatible browser required:
- Chrome/Edge 113+ (stable)
- Firefox Nightly (experimental)
- Safari Technology Preview (experimental)
Check browser support: caniuse.com/webgpu
Installation
WebGPU
npm install hz-particlesimport { initWebGPU, ParticleSystemManager } from 'hz-particles';React Three Fiber
npm install hz-particlesimport { HZParticlesFX } from 'hz-particles/r3f';Important: Your R3F
Canvasmust useWebGPURenderer. Passgl={{ powerPreference: 'high-performance' }}and configure Three.js to use its WebGPU backend, or use@react-three/fiberwith a WebGPU-capable renderer setup.
Quick Start
WebGPU
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 particle system manager
const manager = new ParticleSystemManager(device);
// 3. Create a particle system
const systemId = manager.createParticleSystem({
maxParticles: 10000,
particleCount: 1000,
});
// 4. Initialize compute pipeline
const system = manager.getActiveSystem();
await system.initComputePipeline(device);
// 5. Render loop
let lastTime = performance.now();
function render() {
const currentTime = performance.now();
const deltaTime = (currentTime - lastTime) / 1000;
lastTime = currentTime;
manager.updateAllSystems(deltaTime);
const commandEncoder = device.createCommandEncoder();
const renderPass = commandEncoder.beginRenderPass({
colorAttachments: [{
view: context.getCurrentTexture().createView(),
clearValue: { r: 0, g: 0, b: 0, a: 1 },
loadOp: 'clear',
storeOp: 'store',
}],
});
system.render(renderPass);
renderPass.end();
device.queue.submit([commandEncoder.finish()]);
requestAnimationFrame(render);
}
render();React Three Fiber
Minimal example with a preset file:
import { HZParticlesFX } from 'hz-particles/r3f';
import explosionPreset from './presets/explosion.json';
function Scene() {
return (
<HZParticlesFX
preset={explosionPreset}
position={[0, 1, 0]}
/>
);
}Complete example with all key props:
import { useRef, useState } from 'react';
import { HZParticlesFX } from 'hz-particles/r3f';
import explosionPreset from './presets/explosion.json';
function Scene() {
const targetRef = useRef();
const [resetKey, setResetKey] = useState(0);
return (
<>
<mesh ref={targetRef} position={[0, 1, 0]}>
<boxGeometry />
<meshStandardMaterial />
</mesh>
<HZParticlesFX
preset={explosionPreset}
positionRef={targetRef}
scale={1.5}
resetKey={resetKey}
onComplete={() => console.log('effect finished')}
/>
<button onClick={() => setResetKey(k => k + 1)}>
Replay
</button>
</>
);
}Props Reference
The HZParticlesFX component accepts the following props:
| Prop | Type | Default | Description |
|---|---|---|---|
preset |
SceneData | string |
— | Particle preset — pass a JSON object or a URL/path to a JSON file |
position |
[number, number, number] |
[0, 0, 0] |
Static world-space position of the effect |
positionRef |
React.RefObject<Object3D> |
— | Attach the effect to a scene object; position tracks the ref each frame |
autoPlay |
boolean |
true |
Start playing immediately when mounted |
visible |
boolean |
true |
Show or hide the effect without unmounting |
scale |
number |
1 |
Uniform scale applied to the entire effect |
resetKey |
number |
0 |
Increment to reset and respawn the particle system |
onComplete |
() => void |
— | Callback fired when the effect finishes playing |
Preset Configuration
Presets are plain JSON files that describe a particle scene. Key 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: sphere, cube, cylinder, circle, square |
particleSize |
number |
Base size of each particle |
colors |
string[] |
Array of hex color values 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) |
emissionTrailEnabled |
boolean |
Enable particle trail rendering |
emissionTrailDuration |
number |
How long trail segments persist in seconds |
emissionTrailWidth |
number |
Width of the emission trail |
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
}>Parameters:
canvas(required) — Canvas element. Setcanvas.widthandcanvas.heightbefore calling.
Returns: Object with WebGPU device, canvas context, texture format, and canvas element.
Throws: Error if canvas is missing or WebGPU is not supported.
ParticleSystem
Core particle system class managing simulation and rendering.
class ParticleSystem {
constructor(device: GPUDevice, config?: object)
}Config options:
maxParticles(number, default10000) — Maximum particle buffer sizeparticleCount(number, default100) — Initial active particle count
Key Methods:
| Method | Description |
|---|---|
initComputePipeline(device) |
Initialize GPU compute and render pipelines. Must be called before rendering. |
setTexture(imageBitmap) |
Set particle texture from an ImageBitmap. |
setGLBModel(arrayBuffer) |
Use GLB model geometry as particle shape. Automatically 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 with strength and 3D position [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)
}Key Methods:
| Method | Description |
|---|---|
createParticleSystem(config?) |
Create a new particle system. Returns system ID. |
getActiveSystem() |
Get the currently active ParticleSystem instance. |
getActiveConfig() |
Get the active system configuration object. |
setActiveSystem(index) |
Switch active system by index. Returns success status. |
removeSystem(index) |
Remove a system by index. Returns success status. |
updateAllSystems(deltaTime) |
Update physics for all systems. |
getSystemsList() |
Get list of all systems with name, id, index, isActive. |
duplicateActiveSystem() |
Clone the active system. Returns new system ID. |
replaceSystems(sceneData) |
Load a scene from serialized data. Returns success status. |
parseGLB(arrayBuffer)
Parse GLB binary format and extract geometry data.
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
Handles 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
}Key Methods:
| Method | Description |
|---|---|
setRestPose(positions, normals) |
Set the T-pose/bind pose for animation. |
update(deltaTime) |
Advance animation. Returns { positions, normals, changed }. |
setAnimation(index) |
Switch to animation clip by index. |
getAnimationNames() |
Get list of available animation clip names. |
GLB Models & Animation
import { parseGLB, GLBAnimator } from 'hz-particles';
// Load GLB file
const response = await fetch('model.glb');
const arrayBuffer = await response.arrayBuffer();
const glbData = await parseGLB(arrayBuffer);
// Use as particle shape
await system.setGLBModel(arrayBuffer);
// Setup animation (if model has animations)
if (glbData.animationData) {
const animator = new GLBAnimator(glbData.animationData);
animator.setRestPose(glbData.positions, glbData.normals);
animator.playing = true;
animator.loop = true;
animator.speed = 1.0;
// In render loop:
const { positions, normals, changed } = animator.update(deltaTime);
if (changed) {
// Update particle system with new geometry
// (See full API documentation for geometry update methods)
}
}Scene Save/Load
import { saveScene, loadScene } from 'hz-particles';
// Save current scene
document.getElementById('save-button').addEventListener('click', () => {
saveScene(manager); // Downloads scene.json
});
// Load scene from file input
document.getElementById('file-input').addEventListener('change', async (event) => {
const success = await loadScene(event, manager);
if (success) {
console.log('Scene loaded successfully');
}
});TypeScript
Type declarations are not yet included in this package. For TypeScript projects, suppress the import error with:
// @ts-ignore
import { ParticleSystem, initWebGPU } from 'hz-particles';
// @ts-ignore
import { HZParticlesFX } from 'hz-particles/r3f';Community-contributed type definitions are welcome via PR.
Secondary Exports
The following modules are also exported for advanced usage:
ParticleEmitter— Emission shape configuration (sphere, cube, cylinder, circle, square)ParticlePhysics— Physics simulation parameter managementParticleTextureManager— Texture loading utilitiesObjects3DManager— 3D object scene managementsaveScene(manager)— Export scene as JSON file downloadloadScene(event)— Load scene from file input eventextractGLBTexture(arrayBuffer)— Extract base color texture from GLB file- Shader exports — WGSL shader code for compute and rendering pipelines
- Geometry helpers — Primitive shape generators (cube, sphere, etc.)
- Render pipeline creators — Low-level WebGPU pipeline construction
Online Editor
Try the interactive particle editor at http://localhost:8110/editor when running the Docker container (docker-compose up from the repository root).
The editor provides a visual interface for:
- Real-time particle system configuration
- Emitter shape selection and tuning
- GLB model import and animation control
- Scene preset library
- Export/import scene JSON
Contributing
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
License
MIT License - see LICENSE file for details.
Copyright (c) 2025 HZ