Attribute Divisors

Per-Instance Attributes with a Vertex Attribute Divisor

Most instances need their own data, such as a position, color or random seed. vertexAttribDivisor(location, n) sets how often an attribute advances: 0 (the default) per vertex, 1 per instance, n every n instances; the VAO stores it. In GPU Particles, divisor 1 makes instance k read aSeed from floats 4k to 4k + 3 for all four vertices. Forget to reset a divisor to 0 when reusing the location, and every vertex reads the first element.

Per-instance offsets and colours read with vertexAttribDivisor(1) (left), and the same draw with the divisor left at 0 (right)HTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .names { display: flex; max-width: 600px; text-align: center; font-family: monospace; }
  .names div { flex: 1; padding: 4px 2px; }
</style>
<canvas id="c" width="600" height="260"></canvas>
<div class="names"><div>vertexAttribDivisor(1, 1), (2, 1): per instance</div><div>divisor 0: every vertex reads element 0, 1, 2, 3</div></div>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aCorner;     // per vertex: the 4 corners of a cover
layout(location = 1) in vec3 aOffset;     // per instance: x, y and a turn
layout(location = 2) in vec3 aColor;      // per instance: the cover colour
uniform float uTime; out vec3 vColor; out vec2 vUV;
void main() {
  vUV = aCorner;
  float a = aOffset.z + 0.4 * sin(uTime + aOffset.x * 3.0);
  vec2 p = mat2(cos(a), sin(a), -sin(a), cos(a)) * ((aCorner - 0.5) * vec2(0.09, 0.14));
  vColor = aColor;
  gl_Position = vec4(p * vec2(1.0, 2.3) + aOffset.xy, 0.0, 1.0);
}`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; in vec2 vUV; out vec4 c;
void main() { c = vec4(vColor * (vUV.x < 0.12 ? 0.6 : 1.0) * mix(0.8, 1.1, vUV.y), 1.0); }`]]) {
  const s = gl.createShader(type); gl.shaderSource(s, src); gl.compileShader(s);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program); gl.useProgram(program);
// 60 instances: offsets and colours, 3 floats each.
const N = 60, offsets = [], colors = [];
const palette = [[0.12, 0.37, 0.55], [0.36, 0.25, 0.6], [0.88, 0.6, 0.06], [0.25, 0.49, 0.23], [0.71, 0.27, 0.18], [0.16, 0.62, 0.56]];
for (let i = 0; i < N; i++) {
  offsets.push(-0.85 + (i % 10) * 0.19, 0.72 - Math.floor(i / 10) * 0.29, (i * 37 % 11 - 5) * 0.05);
  colors.push(...palette[(i * 7) % 6]);
}
function makeVAO(divisor) {
  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);
  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0, 1, 0, 0, 1, 1, 1]), gl.STATIC_DRAW);
  gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(0);
  [[1, offsets], [2, colors]].forEach(([location, data]) => {
    gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
    gl.vertexAttribPointer(location, 3, gl.FLOAT, false, 0, 0); gl.enableVertexAttribArray(location);
    gl.vertexAttribDivisor(location, divisor);         // stored in the VAO
  });
  gl.bindVertexArray(null);
  return vao;
}
const vaos = [makeVAO(1), makeVAO(0)];
gl.enable(gl.SCISSOR_TEST);
function frame(ms) {
  gl.uniform1f(gl.getUniformLocation(program, 'uTime'), ms / 1000);
  vaos.forEach((vao, pane) => {
    gl.viewport(pane * 300, 0, 300, 260); gl.scissor(pane * 300 + 2, 0, 296, 260);
    gl.clearColor(0.93, 0.91, 0.87, 1); gl.clear(gl.COLOR_BUFFER_BIT);
    gl.bindVertexArray(vao);
    gl.drawArraysInstanced(gl.TRIANGLE_STRIP, 0, 4, N);
  });
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>