Interleaved Buffers

Interleaved Versus Separate Vertex Buffers

Vertex data can be interleaved, one record per vertex in one buffer as in the cover wall, or separate, one tightly packed buffer per attribute as in Vertex Attributes. The stride and offset of vertexAttribPointer() describe either:

The cover layout interleaved (stride 20) and as separate buffers (stride 0)
The cover layout interleaved (stride 20) and as separate buffers (stride 0)

Interleaving keeps each vertex's bytes together and suits static meshes. Separate buffers let you rewrite one attribute, such as animated positions, while the rest stay put. Keep strides and offsets multiples of 4.

The same ribbon from one interleaved buffer and from separate buffers, where only positions are rewrittenHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  .layout { display: flex; max-width: 600px; gap: 10px; }
  .layout > div { flex: 1; }
  .bytes { display: flex; margin: 3px 0; }
  .bytes span { flex: 1; text-align: center; padding: 2px 0; color: #fff; border-right: 1px solid #f7f4ee; font-size: 10px; }
  .p { background: #1f5f8b; } .c { background: #e09a10; }
</style>
<canvas id="view" width="600" height="200"></canvas>
<div class="layout">
  <div><b>Interleaved</b> (stride 20, offsets 0 and 8)
    <div class="bytes"><span class="p">x</span><span class="p">y</span><span class="c">r</span><span class="c">g</span><span class="c">b</span><span class="p">x</span><span class="p">y</span><span class="c">r</span><span class="c">g</span><span class="c">b</span></div></div>
  <div><b>Separate</b> (stride 0, offset 0 each; positions rewritten per frame)
    <div class="bytes"><span class="p">x</span><span class="p">y</span><span class="p">x</span><span class="p">y</span><span class="p">x</span><span class="p">y</span></div>
    <div class="bytes"><span class="c">r</span><span class="c">g</span><span class="c">b</span><span class="c">r</span><span class="c">g</span><span class="c">b</span></div></div>
</div>
<script>
const gl = document.getElementById('view').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
layout(location = 0) in vec2 aPos; layout(location = 1) in vec3 aColor; out vec3 vColor;
void main() { vColor = aColor; gl_Position = vec4(aPos, 0.0, 1.0); }`], [gl.FRAGMENT_SHADER, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 c; void main() { c = vec4(vColor, 1.0); }`]]) {
  const s = gl.createShader(type);
  gl.shaderSource(s, src); gl.compileShader(s);
  gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);

const N = 24;                                   // a ribbon: N pairs of vertices as a triangle strip
function ribbon(x0, t) {
  const pos = [];
  for (let i = 0; i < N; i++) {
    const x = x0 + (i / (N - 1)) * 0.85, y = Math.sin(i * 0.5 + t) * 0.25;
    pos.push(x, y - 0.3, x, y + 0.3);
  }
  return pos;
}
const colors = [];
for (let i = 0; i < N * 2; i++) { const k = i / (N * 2); colors.push(0.12 + 0.76 * k, 0.37 + 0.23 * k, 0.55 - 0.49 * k); }

// Interleaved: x, y, r, g, b per vertex in ONE buffer (static).
const interleaved = [];
ribbon(-0.95, 0).forEach((v, i, all) => {
  if (i % 2 === 0) interleaved.push(all[i], all[i + 1], ...colors.slice((i / 2) * 3, (i / 2) * 3 + 3));
});
const vaoA = gl.createVertexArray();
gl.bindVertexArray(vaoA);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(interleaved), gl.STATIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 20, 0);   // 5 floats = 20 bytes per vertex
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 20, 8);
gl.enableVertexAttribArray(0); gl.enableVertexAttribArray(1);

// Separate: positions in one DYNAMIC buffer, colours in another STATIC one.
const vaoB = gl.createVertexArray();
gl.bindVertexArray(vaoB);
const positionBuffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, N * 2 * 2 * 4, gl.DYNAMIC_DRAW);
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);    // stride 0: tightly packed
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(colors), gl.STATIC_DRAW);
gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 0, 0);
gl.enableVertexAttribArray(0); gl.enableVertexAttribArray(1);

function frame(ms) {
  gl.clearColor(0.97, 0.96, 0.93, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.bindVertexArray(vaoA);
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, N * 2);
  gl.bindVertexArray(vaoB);
  gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);        // rewrite one attribute, colours stay put
  gl.bufferSubData(gl.ARRAY_BUFFER, 0, new Float32Array(ribbon(0.1, ms * 0.003)));
  gl.drawArrays(gl.TRIANGLE_STRIP, 0, N * 2);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>