Instancing and dynamic offsets compose: the offset chooses a group of objects, and the instance index an object inside it. Instanced Shelf draws three shelves of six books with three calls: each setBindGroup(1, shelfGroup, [i * 256]) moves the shelf's uniform window (its height and filter), and draw(6, 7) draws the shelf's covers plus its plank, all reading the same catalog storage buffer. The pattern scales to materials (one offset per material, instances per mesh) and split screens (one offset per camera): an offset per kind of thing, an instance per thing.
<!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 code = /* wgsl */ `
struct Book { price: f32, rating: f32, inStock: f32 }
struct Shelf { y: f32, maxPrice: f32, minRating: f32 }
@group(0) @binding(0) var<storage> books: array<Book>;
@group(1) @binding(0) var<uniform> shelf: Shelf; // moved by the dynamic offset
struct Out { @builtin(position) pos: vec4f, @location(0) color: vec3f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
let q = vec2f(f32(v & 1), f32(v >> 1));
if (i == 6) { // instance 6: the shelf plank
return Out(vec4f(q.x * 1.46 - 0.46, shelf.y - 0.3 + q.y * 0.05, 0, 1), vec3f(0.55, 0.38, 0.22));
}
let b = books[i];
let on = b.inStock > 0 && b.price < shelf.maxPrice && b.rating >= shelf.minRating;
let tints = array(vec3f(.12, .37, .55), vec3f(.36, .25, .60), vec3f(.88, .60, .06), vec3f(.71, .27, .18), vec3f(.25, .49, .23), vec3f(.16, .62, .56));
let color = mix(vec3f(0.93, 0.91, 0.87), tints[i], select(0.2, 1.0, on)); // misses fade out
return Out(vec4f(-0.42 + f32(i) * 0.24 + q.x * 0.2, shelf.y - 0.25 + q.y * 0.46, 0, 1), color);
}
@fragment fn fs(in: Out) -> @location(0) vec4f { return vec4f(in.color, 1); }`;
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 { STORAGE, UNIFORM, COPY_DST } = GPUBufferUsage;
const SLOT = device.limits.minUniformBufferOffsetAlignment;
const books = device.createBuffer({ size: 72, usage: STORAGE | COPY_DST }); // price, rating, stock x 6
device.queue.writeBuffer(books, 0, new Float32Array([14.99, 4.6, 1, 39.50, 4.3, 1, 24.00, 4.8, 0, 18.75, 4.1, 1, 16.20, 4.5, 1, 21.30, 4.4, 1]));
const shelfData = new Float32Array(3 * SLOT / 4); // three shelves, SLOT bytes apart
[[0.64, 1e9, 0], [-0.03, 20, 0], [-0.69, 1e9, 4.5]].forEach((s, i) => shelfData.set(s, i * SLOT / 4));
const shelves = device.createBuffer({ size: shelfData.byteLength, usage: UNIFORM | COPY_DST });
device.queue.writeBuffer(shelves, 0, shelfData);
const V = GPUShaderStage.VERTEX;
const sceneLayout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: V, buffer: { type: 'read-only-storage' } }] });
const shelfLayout = device.createBindGroupLayout({ entries: [{ binding: 0, visibility: V, buffer: { hasDynamicOffset: true } }] });
const module = device.createShaderModule({ code });
const pipeline = device.createRenderPipeline({ layout: device.createPipelineLayout({ bindGroupLayouts: [sceneLayout, shelfLayout] }),
primitive: { topology: 'triangle-strip' }, vertex: { module }, fragment: { module, targets: [{ format }] } });
const scene = device.createBindGroup({ layout: sceneLayout, entries: [{ binding: 0, resource: { buffer: books } }] });
const shelfGroup = device.createBindGroup({ layout: shelfLayout, entries: [{ binding: 0, resource: { buffer: shelves, size: 12 } }] });
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, scene);
for (let i = 0; i < 3; i++) {
pass.setBindGroup(1, shelfGroup, [i * SLOT]); // an offset per kind of thing...
pass.draw(4, 7); // ...an instance per thing
}
pass.end();
device.queue.submit([encoder.finish()]);
ink.font = 'bold 14px system-ui, sans-serif'; ink.fillStyle = '#222';
['All books', 'Under $20', 'Rated 4.5+'].forEach((t, i) => ink.fillText(t, 14, 62 + i * 113));
ink.font = '11px ui-monospace, monospace'; ink.fillStyle = '#555';
[0, 1, 2].forEach((i) => ink.fillText(`setBindGroup(1, g, [${i * SLOT}])`, 14, 80 + i * 113));
ink.fillText('draw(4, 7)', 14, 94); ink.fillText('draw(4, 7)', 14, 207); ink.fillText('draw(4, 7)', 14, 320);
}
main();
</script>