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.
<!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>