All Lessons
Lesson 6

Particle Systems

Code

const canvas = document.getElementById('glCanvas');
const gl = canvas.getContext('webgl');
gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);

const vsSource = `
  attribute vec3 aPosition;
  attribute vec3 aVelocity;
  attribute float aLife;
  uniform float uTime;
  varying lowp float vLife;
  
  void main() {
    vec3 pos = aPosition + aVelocity * uTime;
    pos.y -= 0.5 * 9.8 * uTime * uTime;
    gl_Position = vec4(pos, 1.0);
    gl_PointSize = 6.0 * aLife;
    vLife = aLife;
  }
`;

const fsSource = `
  varying lowp float vLife;
  
  void main() {
    lowp vec2 coord = gl_PointCoord - vec2(0.5);
    lowp float dist = length(coord);
    if (dist > 0.5) discard;
    
    lowp vec3 color = mix(
      vec3(1.0, 0.3, 0.0),
      vec3(1.0, 1.0, 0.0),
      vLife
    );
    lowp float alpha = (1.0 - dist * 2.0) * vLife;
    gl_FragColor = vec4(color, alpha);
  }
`;

function initShaderProgram(gl, vsSource, fsSource) {
  const vertexShader = loadShader(gl, gl.VERTEX_SHADER, vsSource);
  const fragmentShader = loadShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const shaderProgram = gl.createProgram();
  gl.attachShader(shaderProgram, vertexShader);
  gl.attachShader(shaderProgram, fragmentShader);
  gl.linkProgram(shaderProgram);
  return shaderProgram;
}

function loadShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  return shader;
}

const numParticles = 200;
const particles = [];

class Particle {
  constructor() {
    this.reset();
  }
  
  reset() {
    this.position = [0, -0.5, 0];
    const angle = Math.random() * Math.PI * 2;
    const speed = 0.5 + Math.random() * 0.5;
    this.velocity = [
      Math.cos(angle) * speed,
      2 + Math.random(),
      Math.sin(angle) * speed * 0.3
    ];
    this.life = 1.0;
    this.age = 0;
  }
  
  update(dt) {
    this.age += dt;
    this.life = Math.max(0, 1 - this.age / 2);
    if (this.life === 0) this.reset();
  }
}

for (let i = 0; i < numParticles; i++) {
  particles.push(new Particle());
}

const positionBuffer = gl.createBuffer();
const velocityBuffer = gl.createBuffer();
const lifeBuffer = gl.createBuffer();

const shaderProgram = initShaderProgram(gl, vsSource, fsSource);
const positionAttrib = gl.getAttribLocation(shaderProgram, 'aPosition');
const velocityAttrib = gl.getAttribLocation(shaderProgram, 'aVelocity');
const lifeAttrib = gl.getAttribLocation(shaderProgram, 'aLife');
const timeUniform = gl.getUniformLocation(shaderProgram, 'uTime');

let lastTime = 0;

function render(time) {
  time *= 0.001;
  const dt = time - lastTime;
  lastTime = time;
  
  particles.forEach(p => p.update(dt));
  
  const positions = [];
  const velocities = [];
  const lives = [];
  
  particles.forEach(p => {
    positions.push(...p.position);
    velocities.push(...p.velocity);
    lives.push(p.life);
  });
  
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.DYNAMIC_DRAW);
  gl.vertexAttribPointer(positionAttrib, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(positionAttrib);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, velocityBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(velocities), gl.DYNAMIC_DRAW);
  gl.vertexAttribPointer(velocityAttrib, 3, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(velocityAttrib);
  
  gl.bindBuffer(gl.ARRAY_BUFFER, lifeBuffer);
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(lives), gl.DYNAMIC_DRAW);
  gl.vertexAttribPointer(lifeAttrib, 1, gl.FLOAT, false, 0, 0);
  gl.enableVertexAttribArray(lifeAttrib);
  
  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  
  gl.useProgram(shaderProgram);
  gl.uniform1f(timeUniform, 0);
  gl.drawArrays(gl.POINTS, 0, numParticles);
  
  requestAnimationFrame(render);
}

render(0);

Preview