DEV Community

Cover image for Building a Custom GPU-Accelerated Particle System with WebGL and GLSL Shaders
HexShift
HexShift

Posted on

Building a Custom GPU-Accelerated Particle System with WebGL and GLSL Shaders

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

Sentry image

Make it make sense

Make sense of fixing your code with straight-forward application monitoring.

Start debugging →

Top comments (0)

Sentry image

Make it make sense

Make sense of fixing your code with straight-forward application monitoring.

Start debugging →

👋 Kindness is contagious

Please consider leaving a ❤️ or a friendly comment if you found this post helpful!

Okay