Particle system for ThreeJS.
RendererType.INSTANCED) — removes gl_PointSize hardware limit, ideal for large particles or high particle counts.RendererType.TRAIL) — continuous ribbon trails behind particles with configurable width, opacity, and color tapering.RendererType.MESH) — render each particle as a 3D mesh (debris, gems, coins) using GPU instancing with full 3D rotation and simple directional lighting.npm install @newkrok/three-particles
Include the script directly in your HTML:
<script src="https://cdn.jsdelivr.net/npm/@newkrok/three-particles@latest/dist/three-particles.min.js"></script>
<!-- or -->
<script src="https://unpkg.com/@newkrok/three-particles@latest/dist/three-particles.min.js"></script>
Here's a basic example of how to load and use a particle system:
// Create a particle system
const effect = {
// Your effect configuration here
// It can be empty to use default settings
};
const system = createParticleSystem(effect);
scene.add(system.instance);
// Update the particle system in your animation loop
// Pass the current time, delta time, and elapsed time
updateParticleSystems({now, delta, elapsed});
// Update configuration at runtime without recreating the system
system.updateConfig({
gravity: -9.8,
forceFields: [{ type: 'DIRECTIONAL', direction: { x: 1, y: 0, z: 0 }, strength: 5 }],
// Collision planes — kill, clamp, or bounce particles off surfaces
collisionPlanes: [
{ position: { x: 0, y: 5, z: 0 }, normal: { x: 0, y: -1, z: 0 }, mode: 'KILL' },
],
});
The library works seamlessly with React Three Fiber. No additional wrapper package is needed — use createParticleSystem directly with React hooks:
import { useRef, useEffect } from "react";
import { useFrame } from "@react-three/fiber";
import {
createParticleSystem,
Shape,
type ParticleSystem,
} from "@newkrok/three-particles";
import * as THREE from "three";
function FireEffect({ config }: { config?: Record<string, unknown> }) {
const groupRef = useRef<THREE.Group>(null);
const systemRef = useRef<ParticleSystem | null>(null);
useEffect(() => {
const system = createParticleSystem({
duration: 5,
looping: true,
maxParticles: 200,
startLifetime: { min: 0.5, max: 1.5 },
startSpeed: { min: 1, max: 3 },
startSize: { min: 0.3, max: 0.8 },
startColor: {
min: { r: 1, g: 0.2, b: 0 },
max: { r: 1, g: 0.8, b: 0 },
},
gravity: -1,
emission: { rateOverTime: 50 },
shape: { shape: Shape.CONE, cone: { angle: 0.2, radius: 0.3 } },
renderer: {
blending: THREE.AdditiveBlending,
transparent: true,
depthWrite: false,
},
...config,
});
systemRef.current = system;
groupRef.current?.add(system.instance);
return () => {
system.dispose();
};
}, [config]);
useFrame((_, delta) => {
systemRef.current?.update({
now: performance.now(),
delta,
elapsed: 0,
});
});
return <group ref={groupRef} />;
}
// In your R3F Canvas:
// <Canvas>
// <FireEffect />
// </Canvas>
Key points:
useEffect to create and dispose the particle systemuseFrame to drive updates each frame (call system.update() instead of updateParticleSystems() for per-system control)system.instance to a <group> ref so R3F manages the scene graphuseEffect that calls system.dispose()Optional GPU-accelerated particle simulation via Three.js WebGPU renderer and TSL (Three Shading Language). Offloads all per-particle physics and modifiers to GPU compute shaders, enabling 50K-350K+ particles at interactive frame rates.
three/webgpu)// 1. Enable WebGPU support (once, before creating any particle system)
import { enableWebGPU } from "@newkrok/three-particles/webgpu";
enableWebGPU();
// 2. Create a WebGPU renderer
import * as THREE from "three/webgpu";
const renderer = new THREE.WebGPURenderer({ antialias: true });
await renderer.init();
// No special outputColorSpace handling needed — the library follows the
// standard three.js linear workflow (user colors sRGB, shader math linear,
// renderer converts on output). Leave outputColorSpace at its default
// (SRGBColorSpace).
// 3. Create a GPU-accelerated particle system
import { createParticleSystem, SimulationBackend } from "@newkrok/three-particles";
const system = createParticleSystem({
simulationBackend: SimulationBackend.AUTO, // GPU if WebGPU available, else CPU
maxParticles: 100000,
// ... rest of your config (same API as CPU)
});
scene.add(system.instance);
// 4. In your render loop — dispatch compute before rendering
function animate() {
system.update({ now: performance.now(), delta, elapsed });
if (system.computeNode) {
renderer.compute(system.computeNode);
}
renderer.render(scene, camera);
}
For fine-grained control, you can also use registerTSLMaterialFactory() to selectively register individual WebGPU functions — see the full API reference.
| Value | Behavior |
|---|---|
AUTO (default) |
GPU compute if WebGPU renderer detected, else CPU |
CPU |
Always JavaScript update loop (works with any renderer) |
GPU |
Request GPU compute; falls back to CPU if renderer lacks compute support |
updateConfig() applies on the next frameWebGPU is fully opt-in and non-breaking:
enableWebGPU() not called (or no TSL factory registered), the library uses GLSL shaders (WebGL path)simulationBackend: 'GPU' but WebGPU is unavailable, it silently falls back to CPUAutomatically generated TypeDoc: https://newkrok.github.io/three-particles/api/
All RGB values in particle configs (startColor, backgroundColor) are
sRGB — the same convention used everywhere else in three.js. Pass the
value a color picker gives you (e.g. { r: 1, g: 0, b: 0 } for pure red)
and the renderer will display it correctly.
Internally the library decodes these to linear for shader math and relies
on the renderer's standard output pass to convert back to sRGB on the way
to the framebuffer. No special outputColorSpace setup is required; the
three.js default (SRGBColorSpace) works.
User-supplied color map textures should also be tagged as sRGB
(texture.colorSpace = THREE.SRGBColorSpace) — this is also the
three.js default for color textures loaded via TextureLoader.
The colorOverLifetime feature uses a multiplier-based approach (similar to Unity's particle system), where each RGB channel curve acts as a multiplier applied to the particle's startColor.
Formula: finalColor = startColor * colorOverLifetime
⚠️ Important: To achieve full color transitions, set startColor to white { r: 1, g: 1, b: 1 }. If any channel in startColor is set to 0, that channel cannot be modified by colorOverLifetime.
Example - Rainbow effect:
{
startColor: {
min: { r: 1, g: 1, b: 1 }, // White - allows full color range
max: { r: 1, g: 1, b: 1 }
},
colorOverLifetime: {
isActive: true,
r: { // Red: full → half → off
type: 'BEZIER',
scale: 1,
bezierPoints: [
{ x: 0, y: 1, percentage: 0 },
{ x: 0.5, y: 0.5, percentage: 0.5 },
{ x: 1, y: 0, percentage: 1 }
]
},
g: { // Green: off → full → off
type: 'BEZIER',
scale: 1,
bezierPoints: [
{ x: 0, y: 0, percentage: 0 },
{ x: 0.5, y: 1, percentage: 0.5 },
{ x: 1, y: 0, percentage: 1 }
]
},
b: { // Blue: off → half → full
type: 'BEZIER',
scale: 1,
bezierPoints: [
{ x: 0, y: 0, percentage: 0 },
{ x: 0.5, y: 0.5, percentage: 0.5 },
{ x: 1, y: 1, percentage: 1 }
]
}
}
}