Instanced Drawing

Instanced Drawing with drawArraysInstanced

Each draw call costs CPU time in the browser, ANGLE 2,592 and the driver. drawArraysInstanced(mode, first, count, instances) draws the same vertices instances times in one call; the vertex shader tells copies apart with gl_InstanceID, and drawElementsInstanced() does the same for indexed meshes. WebGL 1 offered both through ANGLE_instanced_arrays, without gl_InstanceID. GPU Particles draws a 4-vertex strip 400 times in one call, with corners taken from gl_VertexID.

A whole bookcase of 600 books drawn in one drawArraysInstanced call, each placed and coloured from gl_InstanceIDHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 11px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  canvas { display: block; width: 100%; max-width: 600px; }
  p { max-width: 600px; margin: 4px 0; font-family: monospace; }
</style>
<canvas id="c" width="600" height="320"></canvas>
<p id="out"></p>
<script>
const gl = document.getElementById('c').getContext('webgl2');
const program = gl.createProgram();
for (const [type, src] of [[gl.VERTEX_SHADER, `#version 300 es
uniform float uTime, uAspect;
out vec3 vColor;
float hash(float n) { return fract(sin(n * 12.9898) * 43758.5453); }
void main() {
  // One box: 36 vertices from gl_VertexID (6 faces x 2 triangles).
  int face = gl_VertexID / 6, corner = gl_VertexID % 6;
  vec2 q = vec2[6](vec2(-1), vec2(1, -1), vec2(1), vec2(-1), vec2(1), vec2(-1, 1))[corner];
  vec3 n = vec3[6](vec3(0,0,1), vec3(0,0,-1), vec3(1,0,0), vec3(-1,0,0), vec3(0,1,0), vec3(0,-1,0))[face];
  vec3 u = vec3[6](vec3(1,0,0), vec3(-1,0,0), vec3(0,0,-1), vec3(0,0,1), vec3(1,0,0), vec3(1,0,0))[face];
  vec3 p = (n + q.x * u + q.y * cross(n, u)) * 0.5;
  // Which copy is this? gl_InstanceID picks a shelf, a slot and a random size and colour.
  float id = float(gl_InstanceID);
  float shelf = floor(id / 60.0), slot = mod(id, 60.0);
  float height = 0.7 + 0.3 * hash(id), width = 0.6 + 0.4 * hash(id + 7.0);
  vec3 size = vec3(0.014 * width, 0.08 * height, 0.05);          // half extents
  vec3 place = vec3(-0.93 + slot * 0.031, -0.9 + shelf * 0.19 + size.y, 0.0);
  float tilt = hash(id + 3.0) > 0.93 ? 0.25 : 0.0;          // a few books lean
  p *= size * 2.0;
  p.xy = mat2(cos(tilt), sin(tilt), -sin(tilt), cos(tilt)) * p.xy;
  vec3 palette[6] = vec3[](vec3(0.12,0.37,0.55), vec3(0.36,0.25,0.6), vec3(0.88,0.6,0.06), vec3(0.25,0.49,0.23), vec3(0.71,0.27,0.18), vec3(0.16,0.62,0.56));
  float light = 0.55 + 0.45 * max(dot(n, normalize(vec3(-0.3, 0.4, 0.9))), 0.0);
  float glow = 0.5 + 0.5 * sin(uTime * 2.0 - slot * 0.2 + shelf);   // a wave running along the shelves
  vColor = palette[int(hash(id + 1.0) * 6.0)] * light * (0.8 + 0.3 * glow);
  vec3 world = place + p;
  float c = cos(0.25 * sin(uTime * 0.5)), s = sin(0.25 * sin(uTime * 0.5));
  world.xz = mat2(c, s, -s, c) * world.xz;                      // the bookcase sways gently
  gl_Position = vec4(world.x, world.y * uAspect, -world.z * 0.5, 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);
gl.bindVertexArray(gl.createVertexArray());                 // no buffers at all: IDs build everything
gl.enable(gl.DEPTH_TEST); gl.enable(gl.CULL_FACE);
gl.uniform1f(gl.getUniformLocation(program, 'uAspect'), 1.0);
const BOOKS = 600;
document.getElementById('out').textContent = `drawArraysInstanced(TRIANGLES, 0, 36, ${BOOKS}): 1 draw call, ${36 * BOOKS} vertices`;
function frame(ms) {
  gl.clearColor(0.35, 0.24, 0.15, 1);                       // the bookcase's back panel
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  gl.uniform1f(gl.getUniformLocation(program, 'uTime'), ms / 1000);
  gl.drawArraysInstanced(gl.TRIANGLES, 0, 36, BOOKS);       // 36 vertices, 600 times
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>