Skip to content

High-performance particle system for Three.js WebGPU, with support for React Three Fiber, TresJS, and Threlte

License

Notifications You must be signed in to change notification settings

mustache-dev/Three-VFX

Repository files navigation

✨ Three VFX ✨

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).

Features

  • 🚀 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

Quick Start

React Three Fiber

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>
  )
}

Vanilla Three.js (Experimental)

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)

TresJS / Vue (Experimental)

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>

Threlte / Svelte (Experimental)

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>

How to use

Use the debug panel to design your effect, then copy the generated code and replace it in your code.

API Reference

VFXParticles

The main particle system component.

Basic Props

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
debug boolean false Show interactive debug panel (lazy-loads debug-vfx)

Appearance Props

Prop Type Default Description
size number | [min, max] [0.1, 0.3] Particle size range
colorStart string[] ["#ffffff"] Starting colors (random pick per particle)
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
side Side DOUBLE Face culling: FRONT, BACK, DOUBLE

Physics Props

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
startPositionAsDirection boolean false Use spawn offset as velocity direction (radial emission)
interface FrictionConfig {
  intensity: number // Drag amount
  easing?: 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' // Deceleration curve
}

Emitter Shape Props

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

Geometry Mode Props

Prop Type Default Description
geometry BufferGeometry null Custom particle geometry (switches to instanced mesh mode)
lighting Lighting STANDARD Material: BASIC (unlit), STANDARD (PBR), PHYSICAL (full PBR)
lightingParams LightingParams null PBR material parameters (see below)
shadow boolean false Enable shadow casting/receiving
orientToDirection boolean false Orient geometry to velocity direction
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

lightingParams gives full control over PBR material properties when using lighting: 'standard' or 'physical':

interface LightingParams {
  roughness?: number          // Surface roughness (0 = mirror, 1 = matte)
  metalness?: number          // Metallic factor (0 = dielectric, 1 = metal)
  emissive?: string           // Emissive color hex string
  emissiveIntensity?: number  // Emissive brightness
  envMapIntensity?: number    // Environment map strength
  // Physical mode only:
  clearcoat?: number          // Clearcoat layer intensity
  clearcoatRoughness?: number // Clearcoat roughness
  transmission?: number       // Glass-like transparency
  thickness?: number          // Volume thickness for transmission
  ior?: number                // Index of refraction
  iridescence?: number        // Iridescence effect intensity
  iridescenceIOR?: number     // Iridescence index of refraction
}
<VFXParticles
  geometry={gemGeometry}
  lighting="physical"
  lightingParams={{
    roughness: 0.3,
    metalness: 0.8,
    clearcoat: 1,
    clearcoatRoughness: 0.1,
    iridescence: 1,
    iridescenceIOR: 1.5,
  }}
/>

Stretch Props

Prop Type Default Description
stretchBySpeed StretchConfig null Stretch particles by velocity
interface StretchConfig {
  factor: number // Stretch multiplier
  maxStretch: number // Maximum stretch amount
}

Turbulence Props

Prop Type Default Description
turbulence TurbulenceConfig null Curl noise turbulence
interface TurbulenceConfig {
  intensity: number // Turbulence strength
  frequency: number // Noise scale
  speed: number // Animation speed
}

Attractor Props

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
}

Collision Props

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
}

Trail Props

Prop Type Default Description
trail TrailConfig null Trail rendering via meshline

Requires makio-meshline installed as a peer dependency.

interface TrailConfig {
  segments?: number // Trail resolution (default: 32)
  width?: number // Line width (default: 0.1)
  taper?: boolean | ((t: number) => number) // Width taper (default: true)
  opacity?: number | ((data: TrailOpacityData) => Node) // Opacity control (default: 1)
  length?: number // History in seconds (default: 0.5)
  showParticles?: boolean // Show particles alongside trails (default: true)
  fragmentColorFn?: (data: TrailData) => Node // Per-pixel trail coloring
}

taper controls how the trail width varies from head to tail:

// Default linear taper (thick at head, thin at tail)
trail={{ taper: true }}

// No tapering (uniform width)
trail={{ taper: false }}

// Custom JS callback: t goes 0 (head) → 1 (tail), return width multiplier
trail={{ taper: (t) => Math.sin(t * Math.PI) }} // fat middle, thin ends
trail={{ taper: (t) => Math.abs(Math.sin(t * Math.PI * 4)) }} // wavy

opacity controls trail transparency. As a number it sets global opacity. As a TSL callback it runs per-vertex in the fragment shader with full access to particle data:

// Global opacity
trail={{ opacity: 0.5 }}

// TSL callback with particle data
trail={{
  opacity: ({ alpha, trailProgress, progress, lifetime, position, velocity, size }) => {
    // Fade based on trail position and particle lifetime
    return alpha.mul(trailProgress.oneMinus()).mul(lifetime)
  }
}}

Sorting Props

Prop Type Default Description
sortParticles boolean false Enable back-to-front depth sorting for transparency
sortFrameInterval number null Run sort every N frames (WebGPU only, performance tuning)

When enabled, particles are sorted by distance to camera for correct alpha blending. On WebGPU this uses a GPU bitonic sort; on WebGL fallback it uses a CPU radix sort.

<VFXParticles
  sortParticles
  sortFrameInterval={2}  // Sort every other frame for better perf
  blending="normal"
/>

Rendering Props

Prop Type Default Description
depthTest boolean true Test against depth buffer
renderOrder number 0 Three.js render order (higher = renders on top)

Soft Particles Props

Prop Type Default Description
softParticles boolean false Fade near geometry
softDistance number 0.5 Fade distance in world units

Curve Props

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
curveTexturePath string Path to pre-baked curve texture (faster startup)

Custom Shader Props

Prop Type Description
geometryNode GeometryNodeFunction Geometry-mode vertex position override
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
type GeometryNodeFunction = (data: ParticleData, defaultPosition: 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
}

Texture Props

Prop Type Description
alphaMap Texture Alpha/shape texture
flipbook FlipbookConfig Animated flipbook
interface FlipbookConfig {
  rows: number
  columns: number
}

VFXEmitter

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>

Props

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

Ref Methods

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
}

useVFXEmitter Hook

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>
}

Returns

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
}

useVFXStore

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')

Examples

Fire Effect

<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}
/>

Sphere Burst

<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]}
/>

3D Geometry Particles

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}
/>

Turbulent Smoke

<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,
  }}
/>

Velocity Curves

<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]}
/>

TypeScript

Full TypeScript support with exported types:

import type {
  VFXParticlesProps,
  VFXEmitterProps,
  ParticleAPI,
  SpawnOverrides,
  CurveData,
  TurbulenceConfig,
  CollisionConfig,
  AttractorConfig,
  TrailConfig,
  TrailData,
  TrailOpacityData,
} from 'r3f-vfx'

License

MIT

About

High-performance particle system for Three.js WebGPU, with support for React Three Fiber, TresJS, and Threlte

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 2

  •  
  •