Canvas 2D is fine for a few hundred particles, but when you want thousands — even millions — of particles moving independently, you need the GPU. In this article, we’ll build a custom particle system using raw WebGL and GLSL shaders to handle the heavy lifting. Fully hardware-accelerated, buttery smooth.
Why WebGL for Particles?
WebGL allows:
- Rendering 10,000+ particles at 60fps
- Parallel computation via vertex shaders
- Full control over physics, color, size, and trails
Step 1: Set Up a Basic WebGL Context
Start by creating a WebGL rendering context:
const canvas = document.getElementById('glcanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
alert("WebGL not supported");
}
Step 2: Define the Vertex Shader for Particle Positions
Each particle is just a single point processed by the GPU:
const vertexShaderSource =
attribute vec2 a_position;
uniform float u_time;
void main() {
float moveX = sin(u_time + a_position.y) * 0.2;
float moveY = cos(u_time + a_position.x) * 0.2;
gl_Position = vec4(a_position + vec2(moveX, moveY), 0, 1);
gl_PointSize = 2.0;
}
;
Step 3: Create the Fragment Shader for Coloring
The fragment shader controls how each particle looks:
const fragmentShaderSource =
precision mediump float;
void main() {
gl_FragColor = vec4(1, 0.5, 0.0, 1); // Orange particles
}
;
Step 4: Initialize Buffers and Animate
Load particle positions into a buffer and draw them each frame:
function createShader(gl, type, source) {
const shader = gl.createShader(type);
gl.shaderSource(shader, source);
gl.compileShader(shader);
return shader;
}
function createProgram(gl, vertexShader, fragmentShader) {
const program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
return program;
}
const vertices = new Float32Array(10000 * 2).map(() => Math.random() * 2 - 1);
const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
const vShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
const fShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
const program = createProgram(gl, vShader, fShader);
gl.useProgram(program);
const positionLocation = gl.getAttribLocation(program, "a_position");
const timeLocation = gl.getUniformLocation(program, "u_time");
gl.enableVertexAttribArray(positionLocation);
gl.vertexAttribPointer(positionLocation, 2, gl.FLOAT, false, 0, 0);
function render(time) {
gl.clear(gl.COLOR_BUFFER_BIT);
gl.uniform1f(timeLocation, time * 0.001);
gl.drawArrays(gl.POINTS, 0, 5000);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
Pros and Cons
✅ Pros
- Extreme performance for huge particle counts
- Highly customizable physics and visuals
- Direct control over GPU rendering
⚠️ Cons
- Steeper learning curve than Canvas 2D
- Debugging shaders can be tedious
- Cross-browser quirks, especially on mobile
🚀 Alternatives
- Three.js: Easier abstraction layer over WebGL
- PixiJS: 2D renderer with fast particle support
- regl: Functional, minimal WebGL abstraction
Summary
When you need insane particle counts or dynamic, real-time visuals in the browser, WebGL is the go-to. By moving particle position calculations onto the GPU, you unleash the raw parallel power of graphics hardware—and the browser becomes your playground.
If this was useful, you can support me here: buymeacoffee.com/hexshift
Top comments (0)