VAOs in WebGL 2

Vertex Array Objects in WebGL 2

Switching meshes means resetting every attribute's pointer and enable flag. A vertex array object (VAO) records that state: while it is bound, each vertexAttribPointer(), enableVertexAttribArray() and ELEMENT_ARRAY_BUFFER binding is stored in it, and binding it later restores them all (the pointers remember their buffers; the ARRAY_BUFFER binding itself is not stored). The sixth helper part wraps this:

webgl-helper.js, sixth part: a vertex array object for one interleaved bufferJavaScript
// Section 3.5: record an interleaved float buffer's layout (and optional indices) in a VAO
GLH.vao = function (gl, attributes, data, layout, indices) {   // layout: [[name, size], ...]
  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);
  GLH.buffer(gl, data);
  const stride = layout.reduce((sum, [, size]) => sum + size * 4, 0);
  let offset = 0;
  for (const [name, size] of layout) {
    if (name in attributes) {                 // skip attributes the linker removed
      gl.vertexAttribPointer(attributes[name], size, gl.FLOAT, false, stride, offset);
      gl.enableVertexAttribArray(attributes[name]);
    }
    offset += size * 4;
  }
  if (indices) GLH.buffer(gl, indices, gl.ELEMENT_ARRAY_BUFFER);   // stored in the VAO
  gl.bindVertexArray(null);
  return vao;
};

With it, a mesh's setup is one GLH.vao() call and drawing it is bindVertexArray() plus drawElements(), as Split-Screen shows. Programs sharing a VAO need matching attribute locations (layout(location = n)), and a VAO should be unbound before you set up other buffers, or you will silently rewrite its element binding.

Two meshes with different layouts, each restored by one bindVertexArray() call per frameHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; }
  canvas { display: block; width: 100%; max-width: 600px; }
</style>
<canvas id="view" width="600" height="260"></canvas>
<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 aPosition;   // shared locations let one VAO serve many programs
layout(location = 1) in vec3 aColor;
uniform vec2 uOffset; out vec3 vColor;
void main() { vColor = aColor; gl_Position = vec4(aPosition + uOffset, 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);
  if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
  gl.attachShader(program, s);
}
gl.linkProgram(program);
gl.useProgram(program);

// Record an interleaved float buffer's layout (and optional indices) in a VAO.
function makeVAO(data, layout, indices) {         // layout: [[location, size], ...]
  const vao = gl.createVertexArray();
  gl.bindVertexArray(vao);
  gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
  gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW);
  const stride = layout.reduce((sum, [, size]) => sum + size * 4, 0);
  let offset = 0;
  for (const [location, size] of layout) {
    gl.vertexAttribPointer(location, size, gl.FLOAT, false, stride, offset);   // stored in the VAO
    gl.enableVertexAttribArray(location);                                      // stored in the VAO
    offset += size * 4;
  }
  if (indices) {
    gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, gl.createBuffer());                 // stored in the VAO
    gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
  }
  gl.bindVertexArray(null);                      // unbind before touching other buffers
  return vao;
}

// Mesh 1: a book (indexed quad, position + colour interleaved).
const book = makeVAO([-0.15, -0.85, 0.12, 0.37, 0.55,  0.15, -0.85, 0.12, 0.37, 0.55,
                       0.15, 0.35, 0.3, 0.6, 0.8,    -0.15, 0.35, 0.3, 0.6, 0.8],
                     [[0, 2], [1, 3]], [0, 1, 2, 0, 2, 3]);
// Mesh 2: a star as a triangle fan with a different vertex count and no indices.
const star = [0, 0, 1, 0.9, 0.5];
for (let i = 0; i <= 10; i++) {
  const a = Math.PI / 2 + i * Math.PI / 5, r = i % 2 ? 0.12 : 0.3;
  star.push(Math.cos(a) * r * 0.45, Math.sin(a) * r, 0.88, 0.6, 0.06);
}
const starVAO = makeVAO(star, [[0, 2], [1, 3]]);

const uOffset = gl.getUniformLocation(program, 'uOffset');
function frame(ms) {
  const t = ms * 0.001;
  gl.clearColor(0.97, 0.96, 0.93, 1);
  gl.clear(gl.COLOR_BUFFER_BIT);
  for (let i = 0; i < 4; i++) {
    gl.bindVertexArray(book);                    // one call restores pointers, enables and indices
    gl.uniform2f(uOffset, -0.75 + i * 0.5, 0);
    gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);
    gl.bindVertexArray(starVAO);                 // switch mesh: nothing else to reset
    gl.uniform2f(uOffset, -0.75 + i * 0.5, 0.62 + 0.05 * Math.sin(t * 3 + i));
    gl.drawArrays(gl.TRIANGLE_FAN, 0, 12);
  }
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>