Listen up. If you're building modern applications, understanding Particles in Three.js 3D WebGL is non-negotiable. This is where simple logic turns into intelligent behavior.
1Why Particles Instead of Meshes
Welcome to Particles! If you want to create rain, snow, stars, or explosions, you shouldn't use thousands of Meshes. That will crash the browser.
Each Mesh carries its own geometry, material, and a separate draw call, so spawning thousands of them multiplies CPU overhead just to keep track of matrices and materials ā the GPU rarely gets the chance to actually help. THREE.Points collapses all of that into a single geometry and a single draw call, letting the GPU render tens of thousands of vertices as points in one pass.
This is the same reason particle effects in games never use individual sprites or meshes per flake or spark; the draw-call count, not the vertex count, is usually what limits real-time frame rate in WebGL.
// Thousands of objects = lag.
// Particles = buttery smooth.3D Scene rendered. Objects: 4, Draw Calls: Optimized.
2The THREE.Points Object
Instead of Meshes, we use THREE.Points. Points render a geometry not as solid faces, but as individual, disconnected vertices.
A THREE.Points object takes the exact same two constructor arguments as a Mesh ā a geometry and a material ā but it changes how the renderer interprets that geometry's vertex data. Instead of connecting vertices into triangles via an index buffer, the GPU is told to draw each vertex as its own independent point sprite.
Because there are no faces to compute, Points skips normal calculation and face culling entirely, which is part of why rendering huge particle counts stays cheap compared to equivalent mesh-based geometry.
const particles = new THREE.Points(geometry, material);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
3Building Particle Geometry with BufferGeometry
To create the geometry for particles, we usually use an empty BufferGeometry and manually populate it with thousands of random X, Y, Z coordinates.
Each particle needs exactly three numbers in the position array ā one each for X, Y, and Z ā so a system of 1,000 particles requires a Float32Array of length 3000. That flat array is then attached to the geometry with setAttribute('position', new THREE.BufferAttribute(positions, 3)), telling Three.js to read the array in groups of three floats per vertex.
Generating the coordinates randomly (often with Math.random() * range - range/2) is what gives particle effects like starfields or explosions their organic, scattered look rather than a uniform grid pattern.
const geometry = new THREE.BufferGeometry();
// array of 3000 numbers (1000 particles * 3 axes)
const positions = new Float32Array(3000); 3D Scene rendered. Objects: 4, Draw Calls: Optimized.
4Configuring PointsMaterial
For the material, you must use PointsMaterial. This material specifically controls the size, color, and texture of the points.
The size property sets how large each point renders on screen, measured in world units by default (or pixels if sizeAttenuation is turned off) ā it's the equivalent of scale for a Mesh, but applied per-vertex instead of per-object. The color property tints every particle uniformly unless you also supply per-vertex colors via a color BufferAttribute.
Unlike MeshStandardMaterial, PointsMaterial ignores lighting entirely by default, since points have no surface normals for light to bounce off ā this keeps particle rendering fast, which matters when you're drawing thousands of them every frame.
const material = new THREE.PointsMaterial({
size: 0.05,
color: 0xffffff
});3D Scene rendered. Objects: 4, Draw Calls: Optimized.
5Why PointsMaterial Is Required
When creating a Points object, which specific material class MUST you use to define the size of the dots? The answer is PointsMaterial ā it's the only built-in material with a size property honored when paired with THREE.Points.
Using a MeshBasicMaterial or MeshStandardMaterial with a Points object won't throw an error, but it also won't respect a size setting the way you'd expect, since those materials were designed around triangle faces, not point sprites.
PointsMaterial also exposes particle-specific options like sizeAttenuation (whether distant particles shrink with perspective) and map for applying a texture to each point, none of which exist on mesh-oriented materials.
const mat = new THREE.???({ size: 0.1 });3D Scene rendered. Objects: 4, Draw Calls: Optimized.
6The Sparkles Helper in React Three Fiber
Let's see it in action in React Three Fiber! We'll use the @react-three/drei helper called <Sparkles>, which wraps everything up beautifully.
Under the hood, <Sparkles> builds its own BufferGeometry, populates a position attribute with count randomly distributed points inside a bounding volume defined by scale, and animates them with a small built-in shader ā all the manual BufferGeometry and PointsMaterial setup from the previous sections is handled for you.
Props like size, speed, and color map directly onto the concepts you just learned: size controls point scale, speed drives the built-in per-frame animation, and color tints the material, making Sparkles a fast way to prototype particle effects before writing custom shader-based systems.
import { Sparkles } from '@react-three/drei';
<Canvas>
<Sparkles count={1000} scale={10} size={2} speed={0.4} />
</Canvas>3D Scene rendered. Objects: 4, Draw Calls: Optimized.
7Layering Multiple Particle Systems
Behold, a galaxy! Move the camera around to see the depth of these 2,000 floating particles.
Stacking two <Sparkles> instances with different count, scale, and color values ā as this example does with cyan and pink layers ā creates visual depth that a single uniform particle system can't achieve, since each layer moves and scales independently. Combined with OrbitControls, orbiting the camera reveals parallax between the layers, reinforcing the illusion of a 3D volume rather than a flat particle plane.
This layering technique is common in real starfield and galaxy shaders: a dense, small, fast layer for foreground detail and a sparser, larger, slower layer for background atmosphere.
import { Sparkles, OrbitControls } from '@react-three/drei';
const App = () => {
return (
<Canvas camera={{ position: [0, 0, 8] }}>
<color attach="background" args={["#050510"]} />
<OrbitControls autoRotate autoRotateSpeed={0.5} />
{/* 2000 tiny glowing stars! */}
<Sparkles
count={2000}
scale={12}
size={3}
speed={0.2}
color="#00F0FF"
/>
<Sparkles
count={1000}
scale={10}
size={4}
speed={0.4}
color="#FF0099"
/>
</Canvas>
);
};
render(<App />);3D Scene rendered. Objects: 4, Draw Calls: Optimized.
8Texturing Particles with map and alphaMap
If you want to map a texture to each particle (like a snowflake or a star image), you use the map or alphaMap property on the PointsMaterial.
map replaces the flat, circular default point shape with the actual pixels of your texture, while alphaMap uses a grayscale image purely to control per-pixel transparency ā useful when you want a soft, glowing falloff at the edges of each particle instead of a hard-edged square or circle.
Both properties expect a loaded THREE.Texture, typically produced with THREE.TextureLoader().load(), and combining map with transparent: true is what lets particle sprites like sparks or snowflakes blend naturally against whatever is behind them instead of rendering as opaque squares.
const starTexture = textureLoader.load('star.png');
const material = new THREE.PointsMaterial({
map: starTexture,
transparent: true
});3D Scene rendered. Objects: 4, Draw Calls: Optimized.
9Enabling Transparency for Textured Particles
If you assign an image with a transparent background to a particle's map, which property MUST be set to true on the material? The answer is transparent ā without it, Three.js ignores the alpha channel of the texture entirely and renders the image's transparent areas as solid black or white squares.
Setting transparent: true tells the renderer to respect per-pixel alpha values and blend the particle against the scene behind it, rather than treating every pixel as fully opaque.
This is easy to forget because the material will still compile and render without errors ā the particles will just look wrong, showing hard rectangular edges instead of soft, natural shapes.
new THREE.PointsMaterial({ map: myImage, ???: true });3D Scene rendered. Objects: 4, Draw Calls: Optimized.
10Animating Particles: CPU Updates vs Custom Shaders
Animating custom particles is tricky. You either have to update the Float32Array in the CPU every frame (slow), or write a custom Shader to move them on the GPU (fast but advanced).
Updating positions on the CPU means looping over every particle in JavaScript each frame, writing new X/Y/Z values back into the Float32Array, and then setting geometry.attributes.position.needsUpdate = true so Three.js re-uploads the buffer to the GPU ā this works fine for hundreds or low thousands of particles but quickly becomes a bottleneck beyond that.
For systems with hundreds of thousands or millions of particles, the animation logic instead moves into a custom vertex shader (via ShaderMaterial or RawShaderMaterial), where the GPU computes each particle's position in parallel every frame ā no JavaScript loop, no CPU-to-GPU buffer re-upload, just raw parallel throughput.
// For massive systems (1M+ particles), you must use Custom Shaders.3D Scene rendered. Objects: 4, Draw Calls: Optimized.
11Particle Systems Recap
Great! You now know how to render massive swarms of objects efficiently using Points. Let's move on to making the 3D world interactive!
To recap: THREE.Points pairs a BufferGeometry full of vertex positions with a PointsMaterial (or custom shader) to render thousands of particles in a single draw call, and you can texture, color, and animate them either on the CPU for smaller counts or on the GPU for massive systems.
Next up is the Raycaster, which lets users click and hover over objects in the 3D scene ā the foundation for adding interactivity like selecting a mesh or, yes, even picking individual particles out of a swarm.
// ⨠Magic unlocked!3D Scene rendered. Objects: 4, Draw Calls: Optimized.
12Step-by-Step Breakdown
Welcome to Particles! If you want to create rain, snow, stars, or explosions, you shouldn't use thousands of Meshes. That will crash the browser.
Instead of Meshes, we use THREE.Points. Points render a geometry not as solid faces, but as individual, disconnected vertices.
Which Three.js class is used instead of a Mesh when you want to render thousands of individual vertices as standalone points (particles)?
- āParticles
- āPoints
- āDots
To create the geometry for particles, we usually use an empty BufferGeometry and manually populate it with thousands of random X, Y, Z coordinates.
For the material, you must use PointsMaterial. This material specifically controls the size, color, and texture of the points.
When creating a Points object, which specific material class MUST you use to define the size of the dots?
- āMeshBasicMaterial
- āParticleMaterial
- āPointsMaterial
Let's see it in action in React Three Fiber! We'll use the @react-three/drei helper called <Sparkles>, which wraps everything up beautifully.
Behold, a galaxy! Move the camera around to see the depth of these 2,000 floating particles.
If you want to map a texture to each particle (like a snowflake or a star image), you use the map or alphaMap property on the PointsMaterial.
If you assign an image with a transparent background to a particle's map, which property MUST be set to true on the material?
- āalpha
- ātransparent
- āopacity
Animating custom particles is tricky. You either have to update the Float32Array in the CPU every frame (slow), or write a custom Shader to move them on the GPU (fast but advanced).
Great! You now know how to render massive swarms of objects efficiently using Points. Let's move on to making the 3D world interactive!
Level Up š
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Respect prefers-reduced-motion for Particle Animations
Continuously animated particle systems (snow, sparks, drifting stars) can trigger discomfort for users with vestibular disorders and should be paused or replaced with a static version when the user's OS has requested reduced motion.
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
if (reduceMotion) sparklesRef.current.speed = 0;SEO Implications
- 1
Particle Effects Render Inside an Opaque Canvas
A THREE.Points system, like all WebGL output, is drawn to a <canvas> element that search engine crawlers cannot parse for content ā the SEO value comes entirely from the surrounding page copy that explains what the particle effect demonstrates, not from the particles themselves.
Best Practices
Dispose Particle Geometries and Textures on Unmount
A BufferGeometry's position array and any textures loaded for PointsMaterial live on the GPU and are not automatically freed when a component unmounts ā call geometry.dispose(), material.dispose(), and texture.dispose() explicitly, or use React Three Fiber which handles this for you.
Prefer a Single Large Points Object Over Many Small Ones
Rendering 5,000 particles as one THREE.Points object costs a single draw call; splitting the same 5,000 particles across 50 separate Points objects costs 50 draw calls for no visual benefit. Batch particles into as few geometries as the effect allows.
Frequent Bugs
Updating the position Float32Array on the CPU every frame, but the particles never visibly move.
Three.js caches buffer attributes for performance. After mutating the position array in place, you must set geometry.attributes.position.needsUpdate = true so the renderer knows to re-upload the buffer to the GPU that frame.
Real-World Examples
Ambient Background Effects in Landing Pages
Marketing and portfolio sites often use a low-count, slow-moving THREE.Points field (via <Sparkles> or a hand-built BufferGeometry) as an ambient background layer behind hero text, giving a sense of depth and motion without pulling focus or tanking frame rate on lower-end devices.
<Sparkles count={80} scale={6} size={1.5} speed={0.15} color="#8888ff" />