Instanced Rendering

Instanced Rendering with @builtin(instance_index)

pass.draw(vertexCount, instanceCount, firstVertex, firstInstance) runs the vertex shader vertexCount x instanceCount times; @builtin(instance_index) says which copy is running, starting at firstInstance. Every chapter demo since First Shader Pair has used it to place six covers from one quad. The index can select anything: an array element, a texture layer (Cover Texture Array) or a storage-buffer record:

Six covers from one quad: instance_index picks the columnJavaScript
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32)
    -> @builtin(position) vec4f {
  let q = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1),
                vec2f(0, 1))[v];
  return vec4f(-0.87 + f32(i) * 0.3 + q.x * 0.24, q.y * 0.52 - 0.26, 0, 1);
}

firstInstance offsets the index, so draw(6, 3, 0, 3) draws books 4 to 6; the counts can even come from a GPU buffer (Indirect Draws and Bundles).

Six covers from one quad: instance_index picks the column, the texture layer and the price record, and firstInstance offsets itHTMLLive
<!doctype html>
<style>
  body { margin: 0; background: #f7f4ee; font: 14px system-ui, sans-serif; }
  .stage { position: relative; width: 100%; max-width: 600px; }
  .stage canvas { display: block; width: 100%; }
  .stage canvas + canvas { position: absolute; inset: 0; pointer-events: none; }
</style>
<div class="stage">
  <canvas id="view" width="600" height="340"></canvas>
  <canvas id="labels" width="600" height="340"></canvas>
</div>
<script>
const canvas = document.getElementById('view');
const ink = document.getElementById('labels').getContext('2d');

function showMessage(text) {                     // 2D fallback when WebGPU is missing
  const ctx = canvas.getContext('2d');
  ctx.fillStyle = '#fbeaea'; ctx.fillRect(0, 0, canvas.width, canvas.height);
  ctx.fillStyle = '#8a2b2b'; ctx.font = '18px system-ui, sans-serif'; ctx.textAlign = 'center';
  ctx.fillText(text, canvas.width / 2, canvas.height / 2);
}

const titles = [['The Quiet', 'Harbor', '#1f5f8b'], ['Small Steps', 'to Summits', '#5c4099'], ['Salt and', 'Saffron', '#b5452e'],
                ["Clockmaker's", 'Paradox', '#e0990f'], ['Night', 'Trains', '#2d5d2a'], ['Paper', 'Gardens', '#299e8f']];
const code = /* wgsl */ `
@group(0) @binding(0) var covers: texture_2d_array<f32>;
@group(0) @binding(1) var smp: sampler;
@group(0) @binding(2) var<storage> prices: array<f32>;
@group(0) @binding(3) var<uniform> time: f32;
struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f, @location(1) @interpolate(flat) book: u32 }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
  let q = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1), vec2f(0, 1))[v];
  let hop = max(sin(time * 3 - f32(i) * 0.8), 0.0) * 0.12;              // a wave along the shelf
  let h = 1.1 + prices[i] / 40 * 0.5;                                     // taller for pricier books
  return Out(vec4f(-0.87 + f32(i) * 0.3 + q.x * 0.24, -0.9 + hop + q.y * h, 0, 1), vec2f(q.x, 1 - q.y), i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return textureSample(covers, smp, in.uv, in.book); }`;

async function main() {
  const adapter = await navigator.gpu?.requestAdapter();
  if (!adapter) return showMessage('WebGPU is not available in this browser');
  const device = await adapter.requestDevice();
  const context = canvas.getContext('webgpu');
  const format = navigator.gpu.getPreferredCanvasFormat();
  context.configure({ device, format });

  const covers = device.createTexture({ size: [96, 144, 6], format: 'rgba8unorm',
    usage: GPUTextureUsage.TEXTURE_BINDING | GPUTextureUsage.COPY_DST | GPUTextureUsage.RENDER_ATTACHMENT });
  titles.forEach(([a, b, color], layer) => {
    const art = new OffscreenCanvas(96, 144), g = art.getContext('2d');
    g.fillStyle = color; g.fillRect(0, 0, 96, 144); g.fillStyle = '#f5ecd7'; g.fillRect(0, 44, 96, 46);
    g.fillStyle = '#222'; g.font = 'bold 13px Georgia, serif'; g.textAlign = 'center'; g.fillText(a, 48, 64); g.fillText(b, 48, 81);
    device.queue.copyExternalImageToTexture({ source: art }, { texture: covers, origin: [0, 0, layer] }, [96, 144]);
  });
  const prices = device.createBuffer({ size: 24, usage: GPUBufferUsage.STORAGE | GPUBufferUsage.COPY_DST });
  device.queue.writeBuffer(prices, 0, new Float32Array([14.99, 39.50, 24.00, 18.75, 16.20, 21.30]));
  const time = device.createBuffer({ size: 4, usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST });
  const module = device.createShaderModule({ code });
  const pipeline = device.createRenderPipeline({ layout: 'auto', vertex: { module }, fragment: { module, targets: [{ format }] } });
  const group = device.createBindGroup({ layout: pipeline.getBindGroupLayout(0), entries: [
    { binding: 0, resource: covers.createView() }, { binding: 1, resource: device.createSampler({ magFilter: 'linear', minFilter: 'linear' }) },
    { binding: 2, resource: { buffer: prices } }, { binding: 3, resource: { buffer: time } }] });

  ink.font = '12px ui-monospace, monospace'; ink.fillStyle = '#222';
  ink.fillText('draw(6, 6): instance_index 0-5 picks column, layer and price', 10, 18);
  ink.fillText('draw(6, 3, 0, 3): firstInstance 3, so books 4 to 6 only', 10, 180);
  function frame(now) {
    device.queue.writeBuffer(time, 0, new Float32Array([now / 1000]));
    const encoder = device.createCommandEncoder();
    const pass = encoder.beginRenderPass({ colorAttachments: [{ view: context.getCurrentTexture().createView(),
      clearValue: [0.97, 0.96, 0.93, 1], loadOp: 'clear', storeOp: 'store' }] });
    pass.setPipeline(pipeline);
    pass.setBindGroup(0, group);
    pass.setViewport(0, 24, 600, 150, 0, 1); pass.draw(6, 6);
    pass.setViewport(0, 186, 600, 150, 0, 1); pass.draw(6, 3, 0, 3);
    pass.end();
    device.queue.submit([encoder.finish()]);
    requestAnimationFrame(frame);
  }
  requestAnimationFrame(frame);
}
main();
</script>