A naive particle system with 100 000 particles will grind most GPUs to a
halt. This tutorial builds a high-performance system using
BufferGeometry with typed arrays, billboard sprites
rendered via a custom vertex shader, and distance-based level-of-detail
to skip invisible work.
1Flat particle pool with typed arrays
Keep all data in flat Float32Arrays rather than JS
objects. Each index maps to one particle across several parallel
arrays:
const MAX = 100_000; // Parallel typed arrays — one slot per particle
const pos = new Float32Array(MAX * 3); // xyz const vel = new
Float32Array(MAX * 3); // vxvyvz const life = new Float32Array(MAX);
// remaining lifetime (s) const maxLife = new Float32Array(MAX); //
initial lifetime for t=life/maxLife const size = new
Float32Array(MAX); // particle radius const dead = new
Uint8Array(MAX); // 0=alive, 1=dead (for pool) let alive = 0; // count
of live particles let head = 0; // ring-buffer emit cursor //
Initialise all as dead dead.fill(1);
Typed arrays live in contiguous memory, so iterating them is
cache-friendly — much faster than arrays of objects like
{x,y,z,vx,vy,...}.
2Upload to GPU as BufferGeometry
import * as THREE from
'https://cdn.jsdelivr.net/npm/three@0.160/build/three.module.js';
const geo = new THREE.BufferGeometry(); const posAttr = new
THREE.BufferAttribute(pos, 3); const sizeAttr = new
THREE.BufferAttribute(size, 1); const lifeAttr = new
THREE.BufferAttribute(life, 1);
posAttr.setUsage(THREE.DynamicDrawUsage); // hint: updates every frame
sizeAttr.setUsage(THREE.DynamicDrawUsage);
lifeAttr.setUsage(THREE.DynamicDrawUsage);
geo.setAttribute('position', posAttr); geo.setAttribute('a_size',
sizeAttr); geo.setAttribute('a_life', lifeAttr); geo.setDrawRange(0,
MAX); // draw up to MAX points const particles = new THREE.Points(geo,
/* custom material below */); scene.add(particles);
// Skip updates for particles beyond a threshold distance from camera
const camPos = new THREE.Vector3(); function updateLOD(camera) {
camera.getWorldPosition(camPos); for (let i = 0; i < MAX; i++) { if
(dead[i]) continue; const i3 = i * 3; const dx = pos[i3] - camPos.x;
const dy = pos[i3+1] - camPos.y; const dz = pos[i3+2] - camPos.z;
const d2 = dx*dx + dy*dy + dz*dz; // Skip update but still render
particles > 200 units away if (d2 > 200 * 200) continue; // Full
update only for nearby particles // (velocity integration etc. already
skipped by LOD check above) } }
6Additive blending for fire and smoke
// Fire: additive blending — bright where particles overlap //
orange-yellow gradient by life const fireFrag = /* glsl */ ` varying
float v_life; void main() { float r = length(gl_PointCoord - 0.5); if
(r > 0.5) discard; // Colour: white hot → orange → red vec3 hot =
vec3(1.0, 1.0, 0.7); vec3 mid = vec3(1.0, 0.4, 0.0); vec3 cool =
vec3(0.4, 0.0, 0.0); float t = clamp(v_life, 0.0, 1.0); vec3 col = t >
0.5 ? mix(mid, hot, (t - 0.5) * 2.0) : mix(cool, mid, t * 2.0); float
alpha = (1.0 - r * 2.0) * v_life; gl_FragColor = vec4(col * alpha,
alpha); } `; // Smoke: normal (src-alpha) blending with greyscale
darkening over time const smokeFrag = /* glsl */ ` varying float
v_life; void main() { float r = length(gl_PointCoord - 0.5); if (r >
0.5) discard; float grey = 0.1 + v_life * 0.4; float alpha = (1.0 - r
* 2.0) * v_life * 0.6; gl_FragColor = vec4(grey, grey, grey, alpha); }
`;
Combine a fire emitter (additive) with a smoke emitter (alpha blend) —
add the smoke mesh as a separate Points object drawn
after fire to ensure correct sorting.
Frequently Asked Questions
What will I learn in this tutorial?
Build a high-performance particle system in Three.js using BufferGeometry, billboard particles, and level-of-detail switching for 100k+ particles.
What topics are covered in this tutorial?
This tutorial covers: Flat particle pool with typed arrays, Upload to GPU as BufferGeometry, Billboard vertex shader, Emit and recycle particles, Distance-based LOD, Additive blending for fire/smoke.
What tools and technologies does this tutorial use?
This tutorial uses Three.js, BufferGeometry, Shaders, Performance.
How long does this tutorial take?
This tutorial takes approximately 50 minutes to complete.
What prerequisites do I need before starting?
This is a Intermediate-level tutorial — no special preparation beyond basic JavaScript is assumed.