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).
Add it to your React Three Fiber project with:
npm install r3f-vfx
import { 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-vfx
import { 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