GPU Particles
Render massive particle counts (10k-1M+) efficiently using GPU instancing and custom shaders.
Quick Start import { useRef, useMemo } from 'react'; import { useFrame } from '@react-three/fiber'; import * as THREE from 'three';
function Particles({ count = 10000 }) {
const points = useRef
const positions = useMemo(() => { const pos = new Float32Array(count * 3); for (let i = 0; i < count; i++) { pos[i * 3] = (Math.random() - 0.5) * 10; pos[i * 3 + 1] = (Math.random() - 0.5) * 10; pos[i * 3 + 2] = (Math.random() - 0.5) * 10; } return pos; }, [count]);
return (
Rendering Approaches Approach Particle Count Complexity Use Case Points 10k - 500k Low Simple particles, stars Instanced Mesh 1k - 100k Medium 3D geometry particles Custom Shader 100k - 10M High Maximum control Points Geometry
Simplest approach—each particle is a screen-facing point sprite.
Basic Points function BasicPoints({ count = 5000 }) { const positions = useMemo(() => { const pos = new Float32Array(count * 3); for (let i = 0; i < count; i++) { const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); const r = Math.cbrt(Math.random()) * 5;
pos[i * 3] = r * Math.sin(phi) * Math.cos(theta);
pos[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
pos[i * 3 + 2] = r * Math.cos(phi);
}
return pos;
}, [count]);
return (
Points with Texture function TexturedPoints({ count = 5000 }) { const texture = useTexture('/particle.png');
return (
Custom Attributes
Add per-particle data like color, size, velocity:
function ColoredParticles({ count = 10000 }) { const { positions, colors, sizes } = useMemo(() => { const pos = new Float32Array(count * 3); const col = new Float32Array(count * 3); const siz = new Float32Array(count);
for (let i = 0; i < count; i++) {
// Position
pos[i * 3] = (Math.random() - 0.5) * 10;
pos[i * 3 + 1] = (Math.random() - 0.5) * 10;
pos[i * 3 + 2] = (Math.random() - 0.5) * 10;
// Color (HSL to RGB)
const color = new THREE.Color();
color.setHSL(Math.random(), 0.8, 0.5);
col[i * 3] = color.r;
col[i * 3 + 1] = color.g;
col[i * 3 + 2] = color.b;
// Size
siz[i] = 0.05 + Math.random() * 0.1;
}
return { positions: pos, colors: col, sizes: siz };
}, [count]);
return (
Custom Shader Particles
Maximum control over particle appearance and animation:
const vertexShader = ` attribute float aSize; attribute vec3 aColor; attribute float aAlpha;
uniform float uTime; uniform float uPixelRatio;
varying vec3 vColor; varying float vAlpha;
void main() { vColor = aColor; vAlpha = aAlpha;
vec4 mvPosition = modelViewMatrix * vec4(position, 1.0);
// Size attenuation
gl_PointSize = aSize * uPixelRatio * (300.0 / -mvPosition.z);
gl_Position = projectionMatrix * mvPosition;
} `;
const fragmentShader = ` varying vec3 vColor; varying float vAlpha;
void main() { // Circular particle float dist = length(gl_PointCoord - 0.5); if (dist > 0.5) discard;
// Soft edge
float alpha = 1.0 - smoothstep(0.4, 0.5, dist);
gl_FragColor = vec4(vColor, alpha * vAlpha);
} `;
function ShaderParticles({ count = 50000 }) {
const points = useRef
const { positions, sizes, colors, alphas } = useMemo(() => { const pos = new Float32Array(count * 3); const siz = new Float32Array(count); const col = new Float32Array(count * 3); const alp = new Float32Array(count);
for (let i = 0; i < count; i++) {
pos[i * 3] = (Math.random() - 0.5) * 20;
pos[i * 3 + 1] = (Math.random() - 0.5) * 20;
pos[i * 3 + 2] = (Math.random() - 0.5) * 20;
siz[i] = 10 + Math.random() * 20;
const color = new THREE.Color();
color.setHSL(0.6 + Math.random() * 0.2, 0.8, 0.5);
col[i * 3] = color.r;
col[i * 3 + 1] = color.g;
col[i * 3 + 2] = color.b;
alp[i] = 0.3 + Math.random() * 0.7;
}
return { positions: pos, sizes: siz, colors: col, alphas: alp };
}, [count]);
useFrame(({ clock }) => { points.current.material.uniforms.uTime.value = clock.elapsedTime; });
return (
Animated Particles Position Animation in Shader // Vertex shader with animation attribute vec3 aVelocity; attribute float aPhase;
uniform float uTime;
void main() { vec3 pos = position;
// Simple oscillation pos.y += sin(uTime * 2.0 + aPhase) * 0.5;
// Velocity-based movement pos += aVelocity * uTime;
// Wrap around bounds pos = mod(pos + 10.0, 20.0) - 10.0;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0); gl_PointSize = 10.0 * (300.0 / -mvPosition.z); gl_Position = projectionMatrix * mvPosition; }
CPU Animation (for dynamic systems)
function AnimatedParticles({ count = 10000 }) {
const points = useRef
const velocities = useMemo(() => { const vel = new Float32Array(count * 3); for (let i = 0; i < count; i++) { vel[i * 3] = (Math.random() - 0.5) * 0.02; vel[i * 3 + 1] = (Math.random() - 0.5) * 0.02; vel[i * 3 + 2] = (Math.random() - 0.5) * 0.02; } return vel; }, [count]);
useFrame(() => { const positions = points.current.geometry.attributes.position.array as Float32Array;
for (let i = 0; i < count; i++) {
positions[i * 3] += velocities[i * 3];
positions[i * 3 + 1] += velocities[i * 3 + 1];
positions[i * 3 + 2] += velocities[i * 3 + 2];
// Wrap around
for (let j = 0; j < 3; j++) {
if (positions[i * 3 + j] > 5) positions[i * 3 + j] = -5;
if (positions[i * 3 + j] < -5) positions[i * 3 + j] = 5;
}
}
points.current.geometry.attributes.position.needsUpdate = true;
});
// ... geometry setup }
Instanced Mesh Particles
For 3D geometry particles (not just points):
function InstancedParticles({ count = 1000 }) {
const mesh = useRef
useEffect(() => { for (let i = 0; i < count; i++) { dummy.position.set( (Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10, (Math.random() - 0.5) * 10 ); dummy.rotation.set( Math.random() * Math.PI, Math.random() * Math.PI, 0 ); dummy.scale.setScalar(0.05 + Math.random() * 0.1); dummy.updateMatrix(); mesh.current.setMatrixAt(i, dummy.matrix); } mesh.current.instanceMatrix.needsUpdate = true; }, [count, dummy]);
useFrame(({ clock }) => { for (let i = 0; i < count; i++) { mesh.current.getMatrixAt(i, dummy.matrix); dummy.matrix.decompose(dummy.position, dummy.quaternion, dummy.scale);
dummy.rotation.x += 0.01;
dummy.rotation.y += 0.01;
dummy.updateMatrix();
mesh.current.setMatrixAt(i, dummy.matrix);
}
mesh.current.instanceMatrix.needsUpdate = true;
});
return (
Buffer Geometry Patterns Sphere Distribution function spherePositions(count: number, radius: number) { const positions = new Float32Array(count * 3);
for (let i = 0; i < count; i++) { const theta = Math.random() * Math.PI * 2; const phi = Math.acos(2 * Math.random() - 1); const r = Math.cbrt(Math.random()) * radius; // Cube root for uniform volume
positions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
positions[i * 3 + 1] = r * Math.sin(phi) * Math.sin(theta);
positions[i * 3 + 2] = r * Math.cos(phi);
}
return positions; }
Galaxy Spiral function galaxyPositions(count: number, arms: number, spin: number) { const positions = new Float32Array(count * 3);
for (let i = 0; i < count; i++) { const armIndex = i % arms; const armAngle = (armIndex / arms) * Math.PI * 2;
const radius = Math.random() * 5;
const spinAngle = radius * spin;
const angle = armAngle + spinAngle;
// Add randomness
const randomX = (Math.random() - 0.5) * 0.5 * radius;
const randomY = (Math.random() - 0.5) * 0.2;
const randomZ = (Math.random() - 0.5) * 0.5 * radius;
positions[i * 3] = Math.cos(angle) * radius + randomX;
positions[i * 3 + 1] = randomY;
positions[i * 3 + 2] = Math.sin(angle) * radius + randomZ;
}
return positions; }
Grid Distribution function gridPositions(countPerAxis: number, spacing: number) { const count = countPerAxis ** 3; const positions = new Float32Array(count * 3); const offset = (countPerAxis - 1) * spacing * 0.5;
let index = 0; for (let x = 0; x < countPerAxis; x++) { for (let y = 0; y < countPerAxis; y++) { for (let z = 0; z < countPerAxis; z++) { positions[index * 3] = x * spacing - offset; positions[index * 3 + 1] = y * spacing - offset; positions[index * 3 + 2] = z * spacing - offset; index++; } } }
return positions; }
Performance Tips
Technique Impact
Use Points over InstancedMesh 5-10x faster for simple particles
GPU animation (shader) vs CPU 10-100x faster at scale
Disable depthWrite Faster blending
Use Float32Array Required for buffers
Frustum culling (default on) Skip off-screen
Optimal Settings
File Structure particles-gpu/ ├── SKILL.md ├── references/ │ ├── buffer-patterns.md # Distribution patterns │ └── shader-examples.md # Complete shader examples └── scripts/ ├── particles/ │ ├── basic-points.tsx # Simple points setup │ ├── shader-points.tsx # Custom shader particles │ └── instanced.tsx # Instanced mesh particles └── distributions/ ├── sphere.ts # Sphere distribution ├── galaxy.ts # Galaxy spiral └── grid.ts # Grid distribution
Reference references/buffer-patterns.md — Position distribution patterns references/shader-examples.md — Complete particle shaders