High-performance GPU-accelerated particle system for Three.js WebGPU.
Available for React Three Fiber (R3F), and experimentally for vanilla Three.js, TresJS (Vue), and Threlte (Svelte).
- 🚀 GPU Compute Shaders - All particle simulation runs on the GPU for maximum performance
- 🎨 Flexible Appearance - Sprites, custom geometry, materials, and shaders
- 🌀 Advanced Physics - Gravity, turbulence, attractors, collisions, and more
- 🎯 Multiple Emitter Shapes - Point, Box, Sphere, Cone, Disk, and Edge emitters
- 📊 Curve-based Control - Bezier curves for size, opacity, velocity, and rotation over lifetime
- 🔗 Emitter System - Decoupled emitters that can share particle systems
- ⚡ WebGPU Native - Built specifically for Three.js WebGPU renderer
- 🐢 WebGL fallback – Three VFX targets WebGPU (79% global support) but provides a CPU fallback
Add it to your React Three Fiber project with:
npm install r3f-vfximport { Canvas } from '@react-three/fiber'
import { VFXParticles } from 'r3f-vfx'
function App() {
return (
<Canvas>
<VFXParticles debug />
</Canvas>
)
}Add it to your vanilla Three.js project with:
npm install vanilla-vfximport { VFXParticles } from 'vanilla-vfx'
const particles = new VFXParticles(renderer, { debug: true })
scene.add(particles.renderObject)Add it to your TresJS project with:
npm install tres-vfx<script setup>
import { TresCanvas } from '@tresjs/core'
import { VFXParticles } from 'tres-vfx'
</script>
<template>
<TresCanvas>
<VFXParticles debug />
</TresCanvas>
</template>Add it to your Threlte project with:
npm install threlte-vfx<script>
import { Canvas } from '@threlte/core'
import VFXParticles from 'threlte-vfx/VFXParticles.svelte'
</script>
<Canvas>
<VFXParticles debug />
</Canvas>Use the debug panel to design your effect, then copy the generated code and replace it in your code.
The main particle system component.
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
- | Register system for use with VFXEmitter |
maxParticles |
number |
10000 |
Maximum number of particles |
autoStart |
boolean |
true |
Start emitting automatically |
delay |
number |
0 |
Seconds between emissions (0 = every frame) |
emitCount |
number |
1 |
Particles to emit per burst |
position |
[x, y, z] |
[0, 0, 0] |
Emitter position |
| Prop | Type | Default | Description |
|---|---|---|---|
size |
number | [min, max] |
[0.1, 0.3] |
Particle size range |
colorStart |
string[] |
["#ffffff"] |
Starting colors (random pick) |
colorEnd |
string[] | null |
null |
Ending colors (null = no transition) |
fadeSize |
number | [start, end] |
[1, 0] |
Size multiplier over lifetime |
fadeOpacity |
number | [start, end] |
[1, 0] |
Opacity over lifetime |
appearance |
Appearance |
GRADIENT |
Shape: DEFAULT, GRADIENT, CIRCULAR |
intensity |
number |
1 |
Color intensity multiplier |
blending |
Blending |
NORMAL |
Blend mode: NORMAL, ADDITIVE, MULTIPLY, SUBTRACTIVE |
| Prop | Type | Default | Description |
|---|---|---|---|
lifetime |
number | [min, max] |
[1, 2] |
Particle lifetime in seconds |
speed |
number | [min, max] |
[0.1, 0.1] |
Initial speed |
direction |
Range3D | [min, max] |
[[-1,1], [0,1], [-1,1]] |
Emission direction per axis |
gravity |
[x, y, z] |
[0, 0, 0] |
Gravity vector |
friction |
FrictionConfig |
{ intensity: 0 } |
Velocity damping |
| Prop | Type | Default | Description |
|---|---|---|---|
emitterShape |
EmitterShape |
BOX |
Shape: POINT, BOX, SPHERE, CONE, DISK, EDGE |
emitterRadius |
[inner, outer] |
[0, 1] |
Radius range for sphere/cone/disk |
emitterAngle |
number |
π/4 |
Cone angle in radians |
emitterHeight |
[min, max] |
[0, 1] |
Height range for cone |
emitterDirection |
[x, y, z] |
[0, 1, 0] |
Cone/disk normal direction |
emitterSurfaceOnly |
boolean |
false |
Emit from surface only |
startPosition |
Range3D |
[[0,0], [0,0], [0,0]] |
Position offset per axis |
| Prop | Type | Default | Description |
|---|---|---|---|
geometry |
BufferGeometry |
null |
Custom particle geometry |
lighting |
Lighting |
STANDARD |
Material: BASIC, STANDARD, PHYSICAL |
shadow |
boolean |
false |
Enable shadow casting/receiving |
orientToDirection |
boolean |
false |
Orient geometry to velocity |
orientAxis |
string |
"z" |
Axis to align: "x", "y", "z", "-x", "-y", "-z" |
rotation |
Range3D | [min, max] |
[0, 0] |
Initial rotation per axis |
rotationSpeed |
Range3D | [min, max] |
[0, 0] |
Rotation speed rad/s |
| Prop | Type | Default | Description |
|---|---|---|---|
stretchBySpeed |
StretchConfig |
null |
Stretch particles by velocity |
interface StretchConfig {
factor: number // Stretch multiplier
maxStretch: number // Maximum stretch amount
}| Prop | Type | Default | Description |
|---|---|---|---|
turbulence |
TurbulenceConfig |
null |
Curl noise turbulence |
interface TurbulenceConfig {
intensity: number // Turbulence strength
frequency: number // Noise scale
speed: number // Animation speed
}| Prop | Type | Default | Description |
|---|---|---|---|
attractors |
AttractorConfig[] |
null |
Up to 4 attractors |
attractToCenter |
boolean |
false |
Pull particles to emitter center |
interface AttractorConfig {
position: [x, y, z]
strength: number // Positive = attract, negative = repel
radius?: number // 0 = infinite range
type?: 'point' | 'vortex'
axis?: [x, y, z] // Vortex rotation axis
}| Prop | Type | Default | Description |
|---|---|---|---|
collision |
CollisionConfig |
null |
Plane collision |
interface CollisionConfig {
plane: { y: number } // Plane Y position
bounce?: number // Bounce factor (0-1)
friction?: number // Horizontal friction
die?: boolean // Kill on collision
sizeBasedGravity?: number // Gravity multiplier by size
}| Prop | Type | Default | Description |
|---|---|---|---|
softParticles |
boolean |
false |
Fade near geometry |
softDistance |
number |
0.5 |
Fade distance in world units |
All curves use Bezier spline format:
interface CurveData {
points: Array<{
pos: [x, y] // Position (x: 0-1 progress, y: value)
handleIn?: [x, y] // Bezier handle in (offset)
handleOut?: [x, y] // Bezier handle out (offset)
}>
}| Prop | Type | Description |
|---|---|---|
fadeSizeCurve |
CurveData |
Size multiplier over lifetime |
fadeOpacityCurve |
CurveData |
Opacity over lifetime |
velocityCurve |
CurveData |
Velocity multiplier (overrides friction) |
rotationSpeedCurve |
CurveData |
Rotation speed multiplier |
| Prop | Type | Description |
|---|---|---|
colorNode |
NodeFunction |
Custom color shader |
opacityNode |
NodeFunction |
Custom opacity shader |
backdropNode |
NodeFunction |
Backdrop sampling (refraction) |
castShadowNode |
NodeFunction |
Shadow map output |
alphaTestNode |
NodeFunction |
Alpha test/discard |
type NodeFunction = (data: ParticleData, defaultColor?: Node) => Node
interface ParticleData {
progress: Node // 0 → 1 over lifetime
lifetime: Node // 1 → 0 over lifetime
position: Node // vec3 world position
velocity: Node // vec3 velocity
size: Node // float size
rotation: Node // vec3 rotation
colorStart: Node // vec3 start color
colorEnd: Node // vec3 end color
color: Node // vec3 interpolated color
intensifiedColor: Node // color × intensity
shapeMask: Node // float alpha mask
index: Node // particle index
}| Prop | Type | Description |
|---|---|---|
alphaMap |
Texture |
Alpha/shape texture |
flipbook |
FlipbookConfig |
Animated flipbook |
interface FlipbookConfig {
rows: number
columns: number
}Decoupled emitter component that links to a VFXParticles system.
<VFXParticles name="sparks" maxParticles={1000} autoStart={false} />
<group ref={playerRef}>
<VFXEmitter
name="sparks"
position={[0, 1, 0]}
emitCount={5}
delay={0.1}
direction={[[0, 0], [0, 0], [-1, -1]]}
localDirection={true}
/>
</group>| Prop | Type | Default | Description |
|---|---|---|---|
name |
string |
- | Name of VFXParticles system |
particlesRef |
Ref<ParticleAPI> |
- | Direct ref (alternative to name) |
position |
[x, y, z] |
[0, 0, 0] |
Local position offset |
emitCount |
number |
10 |
Particles per burst |
delay |
number |
0 |
Seconds between emissions |
autoStart |
boolean |
true |
Start emitting automatically |
loop |
boolean |
true |
Keep emitting (false = once) |
localDirection |
boolean |
false |
Transform direction by parent rotation |
direction |
Range3D |
- | Direction override |
overrides |
SpawnOverrides |
- | Per-spawn property overrides |
onEmit |
function |
- | Callback after each emission |
interface VFXEmitterAPI {
emit(): boolean // Emit at current position
burst(count?: number): boolean // Burst emit
start(): void // Start auto-emission
stop(): void // Stop auto-emission
isEmitting: boolean // Current state
getParticleSystem(): ParticleAPI
group: THREE.Group // The group element
}Programmatic emitter control.
function MyComponent() {
const { emit, burst, start, stop } = useVFXEmitter('sparks')
const handleClick = () => {
burst([0, 1, 0], 100, { colorStart: ['#ff0000'] })
}
return <mesh onClick={handleClick}>...</mesh>
}interface UseVFXEmitterResult {
emit(
position?: [x, y, z],
count?: number,
overrides?: SpawnOverrides
): boolean
burst(
position?: [x, y, z],
count?: number,
overrides?: SpawnOverrides
): boolean
start(): boolean
stop(): boolean
clear(): boolean
isEmitting(): boolean
getUniforms(): Record<string, { value: unknown }>
getParticles(): ParticleAPI
}Zustand store for managing particle systems.
const store = useVFXStore()
// Access registered particle systems
const sparks = store.getParticles('sparks')
sparks?.spawn(0, 0, 0, 50)
// Store methods
store.emit('sparks', { x: 0, y: 0, z: 0, count: 20 })
store.start('sparks')
store.stop('sparks')
store.clear('sparks')<VFXParticles
maxParticles={3000}
size={[0.3, 0.8]}
colorStart={['#ff6600', '#ffcc00', '#ff0000']}
colorEnd={['#ff0000', '#330000']}
fadeSize={[1, 0.2]}
fadeOpacity={[1, 0]}
gravity={[0, 0.5, 0]}
lifetime={[0.4, 0.8]}
direction={[
[-0.3, 0.3],
[0.5, 1],
[-0.3, 0.3],
]}
speed={[0.01, 0.05]}
friction={{ intensity: 0.03, easing: 'easeOut' }}
appearance={Appearance.GRADIENT}
intensity={10}
/><VFXParticles
maxParticles={500}
size={[0.05, 0.1]}
colorStart={['#00ffff', '#0088ff']}
fadeOpacity={[1, 0]}
lifetime={[1, 2]}
emitterShape={EmitterShape.SPHERE}
emitterRadius={[0.5, 1]}
startPositionAsDirection={true}
speed={[0.1, 0.2]}
/>import { BoxGeometry } from 'three/webgpu'
;<VFXParticles
geometry={new BoxGeometry(1, 1, 1)}
maxParticles={500}
size={[0.1, 0.2]}
colorStart={['#ff00ff', '#aa00ff']}
gravity={[0, -2, 0]}
lifetime={[1, 2]}
rotation={[
[0, Math.PI * 2],
[0, Math.PI * 2],
[0, Math.PI * 2],
]}
shadow={true}
lighting={Lighting.STANDARD}
/><VFXParticles
maxParticles={300}
size={[0.3, 0.6]}
colorStart={['#666666', '#888888']}
colorEnd={['#333333']}
fadeSize={[0.5, 1.5]}
fadeOpacity={[0.6, 0]}
gravity={[0, 0.5, 0]}
lifetime={[3, 5]}
direction={[
[-0.1, 0.1],
[0.3, 0.5],
[-0.1, 0.1],
]}
speed={[0.02, 0.05]}
turbulence={{
intensity: 1.2,
frequency: 0.8,
speed: 0.3,
}}
/><VFXParticles
maxParticles={1000}
velocityCurve={{
points: [
{ pos: [0, 1], handleOut: [0.1, 0] },
{ pos: [0.5, 0.2], handleIn: [-0.1, 0], handleOut: [0.1, 0] },
{ pos: [1, 0], handleIn: [-0.1, 0] },
],
}}
speed={[0.5, 1]}
lifetime={[2, 3]}
/>Full TypeScript support with exported types:
import type {
VFXParticlesProps,
VFXEmitterProps,
ParticleAPI,
SpawnOverrides,
CurveData,
TurbulenceConfig,
CollisionConfig,
AttractorConfig,
} from 'r3f-vfx'MIT