@interverse/three-particles
v1.7.0
Published
AAA-quality GPU particle system for Three.js with Niagara/Unity VFX Graph-like features
Maintainers
Readme
@interverse/three-particles
AAA-quality GPU particle system for Three.js with Niagara/Unity VFX Graph-like features, powered by TSL (Three Shader Language).
Features
- ✅ GPU Compute Shaders - Physics simulation via TSL compute
- ✅ High Performance - 100K+ particles at 60fps
- ✅ Indirect Rendering - Efficient instanced mesh rendering
- ✅ Ribbon Trails - GPU-accelerated smooth trails with position history
- ✅ Advanced Curves - Non-linear control over size, opacity, and color
- ✅ Texture Sheet Animation - Sprite sheet support with configurable FPS
- ✅ Soft Particles - Depth-aware edge fading
- ✅ Depth Buffer Collisions - Particles bounce off scene geometry
- ✅ Provider System - Extensible behaviors (physics, vortex, boids, etc.)
- ✅ VFX Graph Foundation - Node-based effect composition
- ✅ TypeScript Support - Full type definitions included
Requirements
- Three.js ≥ 0.182.0
- WebGPU-enabled browser (Chrome 113+, Edge 113+, or Firefox Nightly)
Installation
yarn add @interverse/three-particles
# or
npm install @interverse/three-particlesQuick Start
import * as THREE from 'three';
import { WebGPURenderer } from 'three/webgpu';
import { GPUParticleSystem } from '@interverse/three-particles';
// Create WebGPU renderer
const renderer = new WebGPURenderer();
await renderer.init();
document.body.appendChild(renderer.domElement);
// Create scene and camera
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight);
camera.position.z = 5;
// Create particle system
const particles = new GPUParticleSystem({
maxParticles: 10000,
emissionRate: 500,
lifetime: 2.0,
// Visual properties
sizeStart: 0.1,
sizeEnd: 0.02,
colorStart: new THREE.Color(1, 0.5, 0),
colorEnd: new THREE.Color(1, 0, 0),
opacityStart: 1.0,
opacityEnd: 0.0,
// Physics
velocity: new THREE.Vector3(0, 2, 0),
velocityVariation: new THREE.Vector3(0.5, 0.5, 0.5),
gravity: new THREE.Vector3(0, -9.8, 0),
drag: 0.1,
});
scene.add(particles);
// Emit particles
particles.burst(1000);
// Animation loop
function animate() {
requestAnimationFrame(animate);
particles.update(renderer, 0.016, camera);
renderer.render(scene, camera);
}
animate();Runtime Property Updates (v1.5.0+)
Particle appearance properties can be updated at runtime without rebuilding the particle system. These properties use GPU uniforms for real-time updates:
// Color
particles.setColor(new THREE.Color(0x00ff00)); // Set both start and end
particles.setColorStart(new THREE.Color(1, 0.5, 0)); // Just start color
particles.setColorEnd(new THREE.Color(1, 0, 0)); // Just end color
// Size
particles.setSize(0.2, 0.05); // Start and end size
particles.setSizeStart(0.3); // Just start size
particles.setSizeEnd(0.01); // Just end size
// Opacity
particles.setOpacity(1.0, 0.0); // Start and end opacity
particles.setOpacityStart(0.8); // Just start opacity
particles.setOpacityEnd(0.2); // Just end opacity
// Billboard mode (v1.6.0+)
particles.setBillboard(true); // Particles face camera
particles.setBillboard(false); // Particles use geometry orientation
// Geometry (v1.7.0+) - expensive operation, recreates mesh
particles.setGeometry(new THREE.SphereGeometry(0.5, 8, 8));
// Emitter Shape (v1.7.0+)
particles.setEmitterShape('sphere', new THREE.Vector3(2, 2, 2)); // Shape + size
particles.setEmitterShape('box'); // Just shape, keep existing sizeThese setters update uniforms directly (except setGeometry which recreates the mesh), so changes are reflected immediately.
Advanced Features
Ribbon Trails
Create smooth, flowing trails behind particles using GPU position history.
const particles = new GPUParticleSystem({
// ... basic config
trail: {
enabled: true,
segments: 16, // Number of history points
width: 0.2, // Width relative to particle size
updateInterval: 0.02, // Sampling rate (seconds)
fadeAlpha: true // Fade opacity from head to tail
}
});Curves & Gradients
Control properties over particle lifetime using non-linear curves.
import { LifetimeCurve, GradientCurve } from '@interverse/three-particles';
const particles = new GPUParticleSystem({
// Ease-out size
sizeCurve: new LifetimeCurve([
{ p: 0, v: 0.1 },
{ p: 0.2, v: 1.0 },
{ p: 1, v: 0.0 }
]),
// Color gradient: red -> yellow -> smoke
colorGradient: new GradientCurve([
{ t: 0, c: new THREE.Color(1, 0, 0) },
{ t: 0.5, c: new THREE.Color(1, 1, 0) },
{ t: 1, c: new THREE.Color(0.2, 0.2, 0.2) }
])
});Advanced Physics
Enable complex interactions like depth collisions and vector fields.
const particles = new GPUParticleSystem({
// Bounce off scene geometry (requires depth texture)
depthCollisions: true,
bounciness: 0.6,
// Simple floor collision
floorY: 0,
// 3D Vector field for flow effects
vectorField: myVectorFieldTexture3D,
turbulence: 0.5,
});
// Important: Pass depth texture for depth collisions inside render loop
particles.setDepthTexture(depthTexture);Custom Materials
Inject your own TSL-based material for full shader control. Two approaches available:
Option A: Material Injection
Pass a pre-created material and access particleNodes after construction.
import { SpriteNodeMaterial } from 'three/webgpu';
import { mix, color, instanceIndex } from 'three/tsl';
const customMaterial = new SpriteNodeMaterial();
const particles = new GPUParticleSystem({
material: customMaterial,
// ... other config
});
// Build shader using particle nodes
const { velocities, progress, speed } = particles.particleNodes;
// Use helper functions for cleaner code
customMaterial.colorNode = mix(color(0xff0000), color(0xffff00), speed().div(10));
customMaterial.opacityNode = progress().oneMinus();
customMaterial.scaleNode = progress().oneMinus().mul(0.5);Option B: Material Factory (Recommended)
Use materialFactory callback for cleaner access to particle context.
import { SpriteNodeMaterial } from 'three/webgpu';
import { mix, color } from 'three/tsl';
const particles = new GPUParticleSystem({
materialFactory: (ctx) => {
const mat = new SpriteNodeMaterial();
// ctx provides storage nodes + helper functions
mat.colorNode = mix(color(0xff0000), color(0xffff00), ctx.speed().div(10));
mat.opacityNode = ctx.progress().oneMinus();
mat.scaleNode = ctx.progress().oneMinus().mul(0.5);
return mat;
},
// ... other config
});ParticleMaterialContext properties:
positions,velocities,ages,lifetimes,rotations,colors,styles- Storage nodestime,delta- Uniformsindex- Instance index nodeprogress()- Returns lifetime progress (0-1)speed()- Returns velocity magnitudedirection()- Returns normalized velocitystyleIndex()- Returns style index for current particleisStyle(n)- Returns true if particle matches style nstyleCount- Number of defined styles
Multi-Style Particles
Create particles with multiple visual styles (e.g., fire + smoke) in a single emitter.
import { SpriteNodeMaterial } from 'three/webgpu';
import { mix, color } from 'three/tsl';
const particles = new GPUParticleSystem({
styles: [
{ name: 'fire', weight: 3, color: new THREE.Color(0xff3300) },
{ name: 'smoke', weight: 1, color: new THREE.Color(0x333333) }
],
materialFactory: (ctx) => {
const mat = new SpriteNodeMaterial();
// ctx.isStyle(0) returns 1 for fire, 0 for smoke
const isFire = ctx.isStyle(0);
mat.colorNode = mix(color(0x333333), color(0xff3300), isFire);
mat.opacityNode = ctx.progress().oneMinus();
return mat;
}
});Style weights determine spawn distribution: fire (weight 3) spawns 75%, smoke (weight 1) spawns 25%.
Providers
Providers extend particle behavior with modular forces and effects. Add multiple providers to create complex simulations.
Using Providers
import {
GPUParticleSystem,
AttractorProvider,
TurbulenceProvider,
BoidsProvider
} from '@interverse/three-particles';
const particles = new GPUParticleSystem({ /* config */ });
// Add attractor force field
const attractor = new AttractorProvider(4); // max 4 attractors
attractor.addAttractor({
position: new THREE.Vector3(0, 2, 0),
strength: 5
});
particles.addProvider(attractor);
// Add turbulence for organic motion
particles.addProvider(new TurbulenceProvider({
frequency: 0.5,
amplitude: 1.0
}));Available Providers
| Provider | Description | |----------|-------------| | AttractorProvider | Point/area attraction with optional spin | | BoidsProvider | Flocking behavior (Reynolds' Boids) | | TurbulenceProvider | Noise-based turbulent motion | | VortexProvider | Spiral/vortex swirling forces | | WindProvider | Directional wind with gusts | | PathProvider | Guide particles along curves | | MouseInteractionProvider | Mouse/touch push/pull interaction | | DepthCollisionProvider | Bounce off scene geometry |
AttractorProvider
Creates gravitational/magnetic point attractors that pull particles.
import { AttractorProvider } from '@interverse/three-particles';
const attractor = new AttractorProvider(8); // max 8 attractors
// Simple gravity point
attractor.addAttractor({
position: new THREE.Vector3(0, 5, 0),
strength: 10
});
// Spinning attractor (vortex-like)
attractor.addAttractor({
position: new THREE.Vector3(3, 0, 0),
strength: 5,
spinAxis: new THREE.Vector3(0, 1, 0),
spinStrength: 2.0
});
// Update attractor position at runtime
attractor.updateAttractor(0, { position: new THREE.Vector3(0, 8, 0) });
particles.addProvider(attractor);AttractorConfig:
position: Vector3- Attractor world positionstrength: number- Pull force (higher = stronger)spinAxis?: Vector3- Axis for orbital spinspinStrength?: number- Spin force multiplierfalloff?: 'linear' | 'inverse' | 'inverseSq'- Distance falloff (default: inverseSq)
BoidsProvider
Implements Craig Reynolds' flocking algorithm with three core rules: separation, alignment, and cohesion. Enhanced with goal seeking and boundary avoidance.
import { BoidsProvider } from '@interverse/three-particles';
const boids = new BoidsProvider({
// Core Reynolds' rules
separationWeight: 1.5, // Avoid crowding
alignmentWeight: 1.0, // Match neighbor velocity
cohesionWeight: 1.2, // Move toward flock center
// Perception
neighborRadius: 2.5, // How far to look for neighbors
separationRadius: 1.0, // Personal space radius
// Movement limits
maxSpeed: 5.0,
maxForce: 1.0, // Max steering force
// Containment
boundSize: 10, // Soft boundary size
boundaryForce: 2.0, // Edge avoidance strength
// Optional goal seeking (for scripted paths)
goalPosition: new THREE.Vector3(5, 0, 0),
goalWeight: 0.5,
// Organic randomness
wanderStrength: 0.3
});
// Adjust at runtime
boids.setSeparationWeight(2.0);
boids.setGoal(new THREE.Vector3(10, 5, 0), 0.8);
particles.addProvider(boids);BoidsConfig:
separationWeight?: number- Avoid crowding neighborsalignmentWeight?: number- Steer toward average headingcohesionWeight?: number- Steer toward average positionneighborRadius?: number- Perception distanceseparationRadius?: number- Personal space distancemaxSpeed?: number- Maximum speed limitmaxForce?: number- Maximum steering forceboundSize?: number- Soft boundary sizeboundaryForce?: number- Edge avoidance strengthgoalPosition?: Vector3- Optional goal to steer towardgoalWeight?: number- Goal seeking strengthwanderStrength?: number- Random organic motion
TurbulenceProvider
Physics-based turbulent forces following fluid dynamics principles. Implements Kolmogorov's energy cascade and Reynolds number effects.
import { TurbulenceProvider } from '@interverse/three-particles';
const turbulence = new TurbulenceProvider({
// Base turbulence
frequency: 0.5,
amplitude: 1.0,
octaves: 3,
friction: 0.02,
// Physics enhancements
velocitySensitivity: 0.3, // More turbulence at higher speeds
intermittency: 0.4, // Puff-like variation over time
intermittencyFrequency: 0.5,
kolmogorovScaling: 0.7, // Follow -5/3 energy law
// Wake effect
flowDirection: new THREE.Vector3(1, 0, 0),
wakeIntensity: 0.5 // Stronger behind flow
});
// Adjust at runtime
turbulence.setVelocitySensitivity(0.5);
turbulence.setKolmogorovScaling(1.0);
particles.addProvider(turbulence);TurbulenceConfig:
frequency?: number- Noise spatial frequencyamplitude?: number- Force strengthoctaves?: number- Noise layers (1-4)friction?: number- Velocity damping (viscosity)velocitySensitivity?: number- Reynolds effect (faster = more turbulent)intermittency?: number- Puff behavior intensityintermittencyFrequency?: number- Puff frequencykolmogorovScaling?: number- 0=flat, 1=physically realistic -5/3 lawflowDirection?: Vector3- Direction for wake calculationwakeIntensity?: number- Turbulence boost in wake regions
VortexProvider
Creates a swirling vortex that pulls particles while spinning them around an axis.
import { VortexProvider } from '@interverse/three-particles';
const vortex = new VortexProvider({
center: new THREE.Vector3(0, 0, 0),
axis: new THREE.Vector3(0, 1, 0), // Spin around Y
strength: 2.0, // Spin force
pullStrength: 0.5, // Pull toward center
radius: 8.0 // Effect radius
});
// Move vortex at runtime
vortex.setCenter(new THREE.Vector3(5, 0, 0));
vortex.setStrength(3.0);
particles.addProvider(vortex);VortexConfig:
center?: Vector3- Vortex center positionaxis?: Vector3- Rotation axis (normalized)strength?: number- Spinning forcepullStrength?: number- Inward pull forceradius?: number- Effect radius (falloff)
MouseInteractionProvider
Enables mouse/touch interaction to push or pull particles.
import { MouseInteractionProvider } from '@interverse/three-particles';
const mouse = new MouseInteractionProvider({
strength: 5.0,
radius: 3.0,
push: true // false = pull particles
});
particles.addProvider(mouse);
// Update mouse position in animation loop
mouse.setMousePosition(mouseWorldPos);WindProvider
Applies directional wind forces with gusts and turbulence for natural outdoor effects.
import { WindProvider } from '@interverse/three-particles';
const wind = new WindProvider({
direction: new THREE.Vector3(1, 0, 0.2), // Wind direction
strength: 2.0, // Base force
gustStrength: 1.5, // Extra burst force
gustFrequency: 0.3, // Gusts per second
turbulence: 0.3, // Random variation
heightFactor: 0.1 // Stronger wind at higher Y
});
// Change wind direction at runtime
wind.setDirection(new THREE.Vector3(-1, 0.2, 0));
wind.setStrength(4.0);
particles.addProvider(wind);WindConfig:
direction?: Vector3- Wind direction (normalized)strength?: number- Base wind forcegustStrength?: number- Additional burst forcegustFrequency?: number- Gust frequency (per second)turbulence?: number- Random position-based variation (0-1)heightFactor?: number- Wind strength increase per unit Y
PathProvider
Guides particles along a predefined path with attraction and alignment forces.
import { PathProvider } from '@interverse/three-particles';
const path = new PathProvider({
pathPoints: [
new THREE.Vector3(-5, 0, 0),
new THREE.Vector3(0, 4, 2),
new THREE.Vector3(5, 1, -1),
new THREE.Vector3(8, 0, 0)
],
attraction: 2.0, // Pull toward path
alignment: 1.5, // Push along path direction
spread: 1.0, // Max distance from path
speed: 1.0
});
// Update path at runtime
path.setPath([...newPoints]);
particles.addProvider(path);PathConfig:
pathPoints?: Vector3[]- Path control points (min 2)attraction?: number- Force pulling toward pathalignment?: number- Force pushing along pathspread?: number- Allowed deviation distancespeed?: number- Movement speed along pathloop?: boolean- Loop back to start
Creating Custom Providers
Extend BaseProvider to create custom force behaviors:
import { BaseProvider, ProviderContext } from '@interverse/three-particles';
import { vec3, Fn } from 'three/tsl';
class WindProvider extends BaseProvider {
name = 'WindProvider';
priority = 30;
getForceNode(ctx: ProviderContext) {
return Fn(() => {
// Simple wind force
return vec3(1.0, 0, 0.5).mul(2.0);
})();
}
}API Reference
GPUParticleSystemConfig
interface GPUParticleSystemConfig {
// Core
maxParticles?: number; // Default: 100000
emissionRate?: number; // Particles per second
lifetime?: number; // Particle lifetime in seconds
loop?: boolean; // Continuous emission
// Geometry
particleGeometry?: THREE.BufferGeometry; // Custom geometry
billboard?: boolean; // Face camera (default: true)
// Emitter Shape
emitterShape?: 'point' | 'box' | 'sphere' | 'mesh' | 'line';
emitterSize?: THREE.Vector3;
emitterMesh?: THREE.Mesh; // Emit from surface of mesh
// Visual
texture?: THREE.Texture;
textureSheet?: TextureSheetConfig; // Sprite sheet config
colorStart?: THREE.Color;
colorEnd?: THREE.Color;
sizeStart?: number;
sizeEnd?: number;
opacityStart?: number;
opacityEnd?: number;
// Curves
sizeCurve?: LifetimeCurve | CurvePreset;
opacityCurve?: LifetimeCurve | CurvePreset;
colorGradient?: GradientCurve;
// Physics
velocity?: THREE.Vector3;
velocityVariation?: THREE.Vector3;
gravity?: THREE.Vector3;
drag?: number;
turbulence?: number;
vectorField?: THREE.Data3DTexture;
// Trails
trail?: TrailConfig;
// Collisions
depthCollisions?: boolean;
bounciness?: number;
floorY?: number;
// Quality
sorted?: boolean; // Back-to-front sorting
softParticles?: boolean; // Depth-aware fading
softness?: number; // Soft edge distance
frustumCulled?: boolean; // GPU frustum culling
// LOD
lod?: LODConfig[];
}Performance Tips
- Set
maxParticlesappropriately - Allocates GPU memory upfront. - Use
sorted: falseunless alpha blending requires strict ordering. - Control Trail Segments: Higher segments = more memory and vertices. default(8) is usually good.
- Disable
softParticlesif not needed, as it adds a depth read overhead. - Pool particle systems - Reuse systems for ephemeral effects instead of creating/destroying.
Browser Support
| Browser | Status |
|---------|--------|
| Chrome 113+ | ✅ Full support |
| Edge 113+ | ✅ Full support |
| Firefox Nightly | ⚠️ Experimental (enable dom.webgpu.enabled) |
| Safari 18+ | ⚠️ Experimental |
License
MIT © Interverse Engine
