Three.js Particles & Particle Systems — Complete Guide to Points, Sprites, Physics & Effects
Three.js particle systems create thousands of tiny objects that move, fade, and change color to simulate natural phenomena — rain, snow, fire, smoke, stars, galaxies, and magic effects. They’re what turns a static scene into a living world.
What You’ll Learn
By the end of this tutorial, you’ll build custom particle systems using THREE.Points, control individual particle behavior with BufferGeometry attributes, create canvas-generated textures, implement physics (gravity, wind, attractors), build rain/snow/fire effects, and generate an interactive spiral galaxy with real-time parameter controls.
Why Particles Matter
Particles create atmosphere. A few thousand floating specks turn an empty scene into a starry night. A rain effect makes a cityscape feel alive. In Durga Antivirus Pro, the 3D threat dashboard uses particle systems to show network traffic: benign traffic flows as slow green particles, suspicious activity as fast orange streaks, and detected threats as red bursts that expand and fade — giving analysts an instant visual read of network health.
flowchart LR
A[Points + PointsMaterial] --> B[BufferGeometry Attributes]
B --> C[Position: XYZ per particle]
B --> D[Color: RGB per particle]
B --> E[Size: Custom per particle]
C --> F[Canvas Textures]
D --> F
F --> G[Particle Physics]
G --> H[Gravity / Wind / Attractors]
H --> I[Effects]
I --> J[Rain]
I --> K[Snow]
I --> L[Fire]
I --> M[Galaxy Generator]
M --> N[Performance Optimization]
style A fill:#44aa88,color:#fff,stroke:none
Points & PointsMaterial — The Particle Foundation
THREE.Points renders a set of vertices as particles. Unlike Mesh, which forms solid surfaces, Points draws each vertex as an independent dot.
// 1. Create a BufferGeometry to hold particle positions
var geometry = new THREE.BufferGeometry();
var count = 1000;
var positions = new Float32Array(count * 3); // 3 floats per vertex (X, Y, Z)
// 2. Fill with random positions within a 100-unit cube
for (var i = 0; i < count * 3; i++) {
positions[i] = (Math.random() - 0.5) * 100;
}
// 3. Set the position attribute (3 components per vertex)
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
// 4. Create a PointsMaterial (unlit — particles don't need lights)
var material = new THREE.PointsMaterial({
color: 0xffffff,
size: 0.5,
sizeAttenuation: true // Particles shrink with distance (perspective)
});
// 5. Create the Points object and add to scene
var particles = new THREE.Points(geometry, material);
scene.add(particles);Why Points instead of Sprites? THREE.Points renders all particles in a single draw call — the GPU draws them all at once. THREE.Sprite creates a separate Object3D per particle — each one adds CPU overhead. Use Points for thousands of particles. Use Sprites only when each particle needs independent rotation or scale.
PointsMaterial Properties
| Property | Type | Default | Description |
|---|---|---|---|
color | Color | 0xffffff | Base tint color |
size | number | 1 | Particle size (pixels or world units based on attenuation) |
sizeAttenuation | boolean | true | Shrink with distance (perspective) |
map | Texture | null | Custom texture (round gradient for soft particles) |
transparent | boolean | false | Enable transparency |
opacity | number | 1 | Overall opacity |
blending | Blending | NormalBlending | AdditiveBlending for glow effects |
depthWrite | boolean | true | Set false for transparent particles |
vertexColors | boolean | false | Use per-vertex colors from geometry attributes |
BufferGeometry Attributes for Particles
Each particle can have custom position, color, and size:
Position — Where Each Particle Lives
var count = 5000;
var positions = new Float32Array(count * 3);
for (var i = 0; i < count; i++) {
var i3 = i * 3;
var radius = 20;
var theta = Math.random() * Math.PI * 2;
var phi = Math.acos(2 * Math.random() - 1);
positions[i3] = radius * Math.sin(phi) * Math.cos(theta);
positions[i3 + 1] = radius * Math.sin(phi) * Math.sin(theta);
positions[i3 + 2] = radius * Math.cos(phi);
}
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));Color — Per-Particle Colors
var colors = new Float32Array(count * 3);
for (var i = 0; i < count; i++) {
var i3 = i * 3;
var color = new THREE.Color();
color.setHSL(Math.random(), 0.8, 0.5 + Math.random() * 0.5);
colors[i3] = color.r;
colors[i3 + 1] = color.g;
colors[i3 + 2] = color.b;
}
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
// Enable vertexColors on the material
var material = new THREE.PointsMaterial({
vertexColors: true, // Use per-vertex colors
size: 0.3,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});Custom Canvas Texture — No External Images
A canvas-generated texture avoids file loading and works offline:
function createParticleTexture() {
var canvas = document.createElement("canvas");
canvas.width = 64;
canvas.height = 64;
var ctx = canvas.getContext("2d");
// Radial gradient: bright center → transparent edge
var gradient = ctx.createRadialGradient(32, 32, 0, 32, 32, 32);
gradient.addColorStop(0, "rgba(255,255,255,1)");
gradient.addColorStop(0.3, "rgba(255,255,255,0.8)");
gradient.addColorStop(1, "rgba(255,255,255,0)");
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 64, 64);
return new THREE.CanvasTexture(canvas);
}
var texture = createParticleTexture();
var material = new THREE.PointsMaterial({
size: 0.5,
map: texture,
transparent: true,
blending: THREE.AdditiveBlending,
depthWrite: false
});Why canvas textures? Loading textures from files causes a delay (network request) and can fail. A canvas texture is instant, always available, and customizable. The radial gradient creates soft, glowing particles that look much better than hard squares.
Animated Particles — Updating Positions Each Frame
var positions = geometry.attributes.position.array; // Reference to existing buffer
var count = positions.length / 3;
function animateParticles() {
var time = Date.now() * 0.001;
for (var i = 0; i < count; i++) {
var i3 = i * 3;
var angle = time + i * 0.01;
var radius = 10 + Math.sin(time + i * 0.1) * 5;
positions[i3] = Math.cos(angle) * radius;
positions[i3 + 1] = Math.sin(angle * 0.5) * 3;
positions[i3 + 2] = Math.sin(angle) * radius;
}
geometry.attributes.position.needsUpdate = true; // ⚠️ Tell Three.js to re-upload
}Why needsUpdate = true? Three.js uploads geometry data to the GPU once. If you modify the position array but don’t set needsUpdate, the GPU still uses the old data. Setting it to true triggers a re-upload.
Particle Physics
Gravity
var velocities = [];
for (var i = 0; i < count; i++) {
velocities.push({
x: (Math.random() - 0.5) * 0.1,
y: (Math.random() - 0.5) * 0.1,
z: (Math.random() - 0.5) * 0.1
});
}
function applyGravity(positions, velocities, gravity, delta) {
for (var i = 0; i < count; i++) {
var i3 = i * 3;
velocities[i].y -= gravity * delta; // Pull particles down
positions[i3] += velocities[i].x * delta;
positions[i3 + 1] += velocities[i].y * delta;
positions[i3 + 2] += velocities[i].z * delta;
}
geometry.attributes.position.needsUpdate = true;
}Particle Recycling (Respawn)
When particles fall below a threshold, reset them to the top:
function recycleParticles(positions, velocities, height) {
for (var i = 0; i < count; i++) {
var i3 = i * 3;
if (positions[i3 + 1] < height) {
positions[i3] = (Math.random() - 0.5) * 20;
positions[i3 + 1] = 10 + Math.random() * 5;
positions[i3 + 2] = (Math.random() - 0.5) * 20;
velocities[i].x = (Math.random() - 0.5) * 0.05;
velocities[i].y = 0;
velocities[i].z = (Math.random() - 0.5) * 0.05;
}
}
}Galaxy Generator — Spiral Particles
A spiral galaxy creates thousands of particles arranged in arms:
function createGalaxy(parameters) {
var count = parameters.count || 20000;
var arms = parameters.arms || 3;
var radius = parameters.radius || 20;
var spin = parameters.spin || 1.5;
var randomness = parameters.randomness || 0.4;
var insideColor = new THREE.Color(parameters.insideColor || 0xffaa00);
var outsideColor = new THREE.Color(parameters.outsideColor || 0x4488ff);
var positions = new Float32Array(count * 3);
var colors = new Float32Array(count * 3);
for (var i = 0; i < count; i++) {
var i3 = i * 3;
var r = Math.pow(Math.random(), 1.5) * radius;
var armAngle = (r / radius) * spin * Math.PI * 2;
var armOffset = (i % arms) / arms * Math.PI * 2;
var spread = Math.pow(Math.random(), 3) * randomness;
positions[i3] = Math.cos(armAngle + armOffset) * r + Math.cos(spread * Math.PI * 2) * spread;
positions[i3 + 1] = (Math.random() - 0.5) * 0.4;
positions[i3 + 2] = Math.sin(armAngle + armOffset) * r + Math.sin(spread * Math.PI * 2) * spread;
var mixFactor = r / radius;
var mixed = insideColor.clone().lerp(outsideColor, mixFactor);
colors[i3] = mixed.r;
colors[i3 + 1] = mixed.g;
colors[i3 + 2] = mixed.b;
}
var geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
var material = new THREE.PointsMaterial({
size: 0.08, vertexColors: true, transparent: true,
blending: THREE.AdditiveBlending, depthWrite: false,
sizeAttenuation: true, map: createGlowTexture()
});
return new THREE.Points(geometry, material);
}Common Mistakes
1. Allocating a new Float32Array each frame
Creating new Float32Array() every frame causes garbage collection stutters. Mutate the existing array and set needsUpdate = true.
2. Using Sprites for thousands of particles
Each Sprite is an Object3D with CPU overhead. For large particle counts, use Points (single draw call).
3. Forgetting depthWrite: false with AdditiveBlending
Additive particles with depthWrite: true render incorrectly — they block each other instead of blending.
4. Not using sizeAttenuation
Particles stay the same size regardless of distance, breaking the sense of depth and perspective.
5. Modifying attributes without needsUpdate
Changes to the typed array have no visual effect until geometry.attributes.position.needsUpdate = true.
6. Same random seed producing clusters
Without proper randomization, particles clump together. Ensure your random distribution covers the desired space evenly.
7. Mixing additive and normal blending in the same Points object
All particles in one Points object must share the same material/blending. Use separate systems for different blend modes.
Practice Questions
Q1: What’s the difference between THREE.Points and THREE.Sprite for particles? A: Points renders all particles in one draw call (GPU-efficient). Sprites are individual Object3Ds (CPU-heavy). Use Points for thousands, Sprites only when each needs independent rotation/scale.
Q2: Why do transparent particles need depthWrite: false?
A: With depthWrite enabled, transparent particles write to the depth buffer and block other particles behind them. Disabling it allows proper additive/normal blending.
Q3: What does needsUpdate = true do?
A: It tells Three.js to re-upload the BufferAttribute to the GPU. Without it, changes to the typed array are invisible.
Q4: How do you create a soft, round particle texture without loading an image? A: Use a canvas with a radial gradient (bright center → transparent edge), then create a CanvasTexture from it. No file loading needed.
Q5: How many particles can Three.js handle? A: With Points, 50,000-100,000 runs well on modern devices. Beyond 200,000, use custom ShaderMaterial or GPU compute.
Challenge: Build a firework effect: particles shoot upward from a point, then explode outward in random directions with gravity pulling them down. Use color gradient (white → yellow → orange → dark) based on particle age. Particle count: 500 per burst.
FAQ
Try It Yourself
Copy this complete HTML file. It’s an interactive Galaxy Generator with real-time sliders for arms, spin, speed, particle count, and colors.
<!DOCTYPE html>
<html>
<head>
<title>Galaxy Generator — Three.js Particle System</title>
<style>
body { margin: 0; overflow: hidden; font-family: sans-serif; }
#controls {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
background: rgba(0,0,0,0.7); padding: 16px 24px; border-radius: 8px;
color: white; display: flex; gap: 20px; flex-wrap: wrap;
justify-content: center; backdrop-filter: blur(8px);
font-size: 13px; z-index: 10;
}
#controls label { display: flex; flex-direction: column; align-items: center; gap: 2px; }
#controls input[type="range"] { width: 100px; cursor: pointer; }
#controls input[type="color"] { width: 40px; height: 30px; border: none; cursor: pointer; }
#controls span { font-size: 11px; opacity: 0.8; }
</style>
</head>
<body>
<div id="controls">
<label>Arms <input type="range" id="arms" min="1" max="8" value="4" step="1"><span id="armsVal">4</span></label>
<label>Spin <input type="range" id="spin" min="0.2" max="3" value="1.2" step="0.1"><span id="spinVal">1.2</span></label>
<label>Speed <input type="range" id="speed" min="0" max="0.01" value="0.003" step="0.001"><span id="speedVal">0.003</span></label>
<label>Size <input type="range" id="size" min="0.02" max="0.5" value="0.08" step="0.01"><span id="sizeVal">0.08</span></label>
<label>Particles (K) <input type="range" id="count" min="5" max="50" value="20" step="1"><span id="countVal">20</span></label>
<label>Inside <input type="color" id="insideColor" value="#ffaa00"></label>
<label>Outside <input type="color" id="outsideColor" value="#4488ff"></label>
</div>
<script type="importmap">
{ "imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.170.0/examples/jsm/"
}}
</script>
<script type="module">
import * as THREE from "three";
import { OrbitControls } from "three/addons/controls/OrbitControls.js";
var scene = new THREE.Scene();
scene.background = new THREE.Color(0x050510);
var camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(15, 8, 20);
var renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
new OrbitControls(camera, renderer.domElement);
function createGlowTexture() {
var canvas = document.createElement("canvas");
canvas.width = 128; canvas.height = 128;
var ctx = canvas.getContext("2d");
var g = ctx.createRadialGradient(64, 64, 0, 64, 64, 64);
g.addColorStop(0, "rgba(255,255,255,1)");
g.addColorStop(0.15, "rgba(255,255,255,1)");
g.addColorStop(0.5, "rgba(255,255,255,0.3)");
g.addColorStop(1, "rgba(255,255,255,0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, 128, 128);
return new THREE.CanvasTexture(canvas);
}
var galaxy, parameters = { count: 20000, arms: 4, radius: 20, spin: 1.2, randomness: 0.4, randomnessPower: 3, insideColor: "#ffaa00", outsideColor: "#4488ff", size: 0.08, speed: 0.003 };
var clock = new THREE.Clock();
function generateGalaxy() {
if (galaxy) { scene.remove(galaxy); galaxy.geometry.dispose(); galaxy.material.dispose(); }
var count = parameters.count;
var positions = new Float32Array(count * 3);
var colors = new Float32Array(count * 3);
var colorInside = new THREE.Color(parameters.insideColor);
var colorOutside = new THREE.Color(parameters.outsideColor);
for (var i = 0; i < count; i++) {
var i3 = i * 3;
var r = Math.pow(Math.random(), 1.5) * parameters.radius;
var armAngle = (r / parameters.radius) * parameters.spin * Math.PI * 2;
var armOffset = (i % parameters.arms) / parameters.arms * Math.PI * 2;
var spread = Math.pow(Math.random(), parameters.randomnessPower) * parameters.randomness;
positions[i3] = Math.cos(armAngle + armOffset) * r + Math.cos(spread * Math.PI * 2) * spread * r * 0.3;
positions[i3 + 1] = (Math.random() - 0.5) * 0.4;
positions[i3 + 2] = Math.sin(armAngle + armOffset) * r + Math.sin(spread * Math.PI * 2) * spread * r * 0.3;
var mix = r / parameters.radius;
var c = colorInside.clone().lerp(colorOutside, mix);
colors[i3] = c.r; colors[i3 + 1] = c.g; colors[i3 + 2] = c.b;
}
var geometry = new THREE.BufferGeometry();
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
geometry.setAttribute("color", new THREE.BufferAttribute(colors, 3));
var material = new THREE.PointsMaterial({ size: parameters.size, vertexColors: true, transparent: true, blending: THREE.AdditiveBlending, depthWrite: false, sizeAttenuation: true, map: createGlowTexture() });
galaxy = new THREE.Points(geometry, material);
scene.add(galaxy);
}
generateGalaxy();
["arms","spin","size","count"].forEach(function(id) {
document.getElementById(id).addEventListener("input", function() {
var v = id === "arms" ? parseInt(this.value) : id === "count" ? parseInt(this.value) * 1000 : parseFloat(this.value);
parameters[id] = v;
document.getElementById(id + "Val").textContent = this.value;
generateGalaxy();
});
});
document.getElementById("speed").addEventListener("input", function() {
parameters.speed = parseFloat(this.value);
document.getElementById("speedVal").textContent = this.value;
});
["insideColor","outsideColor"].forEach(function(id) {
document.getElementById(id).addEventListener("input", function() {
parameters[id] = this.value;
generateGalaxy();
});
});
function animate() {
requestAnimationFrame(animate);
if (galaxy) { galaxy.rotation.y += parameters.speed * 0.3; }
renderer.render(scene, camera);
}
animate();
window.addEventListener("resize", function() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
</script>
</body>
</html>What’s Next
| Topic | Description | Link |
|---|---|---|
| Post-Processing | Bloom and effects that enhance particles | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-post-processing/ |
| Animation | Advanced animation for particle motion | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-animation/ |
| Models & Loading | Adding particles to loaded 3D scenes | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-models-loading/ |
| Interactivity & UI | Interactive particle systems | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-interactivity-ui/ |
| Geometries & Materials | Understanding BufferGeometry | https://tutorials.dodatech.com/frontend/libraries/threejs/threejs-geometries-materials/ |
| WebGL | The underlying graphics API | WebGL |
| JavaScript | Language fundamentals | JavaScript |
You’ve mastered Three.js particles — from Points and BufferGeometry attributes to physics simulations and procedural galaxy generation. The same particle techniques power the traffic flow visualization in Durga Antivirus Pro’s threat dashboard, where green (safe) and red (threat) particles create an intuitive real-time network health display.
What’s Next
Congratulations on completing this Threejs Particles tutorial! Here’s where to go from here:
- Practice daily — Consistency is more important than long study sessions
- Build a project — Apply what you learned by building something real
- Explore related topics — Check out other tutorials in the same category
- Join the community — Discuss with other learners and share your progress
Remember: every expert was once a beginner. Keep coding!
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro