Flying Pages

A Flying-Book-Pages Particle System for BookNest

The page runs 768 pages for 150 frames (2.5 simulated seconds), each frame one dispatch and one instanced draw, then signals the capture; it fetches pages.wgsl from Billboard Particles:

demos/ch04/flying-pages.html: 768 pages simulated and drawn on the GPUHTMLLive
<canvas id="sky" width="480" height="270"></canvas>
<script type="module">
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const COUNT = 768, { STORAGE, UNIFORM, COPY_DST } = GPUBufferUsage;
const init = new Float32Array(COUNT * 8);          // 8 floats per page, all zero...
for (let i = 0; i < COUNT; i++) init[i * 8 + 6] = -3 * i / COUNT;   // ...and staggered ages
const pages = device.createBuffer({ size: init.byteLength, usage: STORAGE | COPY_DST });
device.queue.writeBuffer(pages, 0, init);
const sim = device.createBuffer({ size: 16, usage: UNIFORM | COPY_DST });
const code = await (await fetch('pages.wgsl')).text();
const module = device.createShaderModule({ code });
const format = navigator.gpu.getPreferredCanvasFormat(), context = sky.getContext('webgpu');
context.configure({ device, format, alphaMode: 'opaque' });
const update = device.createComputePipeline({ layout: 'auto', compute: { module } });
const blend = { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' };   // premultiplied
const draw = device.createRenderPipeline({ layout: 'auto', vertex: { module },
  fragment: { module, targets: [{ format, blend: { color: blend, alpha: blend } }] },
  primitive: { topology: 'triangle-strip' } });
const group = (pipeline) => device.createBindGroup({ layout: pipeline.getBindGroupLayout(0),
  entries: [pages, sim].map((resource, binding) => ({ binding, resource })) });
const [updateGroup, drawGroup] = [group(update), group(draw)];   // pages.wgsl: Section 4.18.3
let frame = 0;
function tick() {
  const u = new Float32Array([1 / 60, frame / 60, sky.width / sky.height, 0]);
  new Uint32Array(u.buffer)[3] = frame;
  device.queue.writeBuffer(sim, 0, u);
  const encoder = device.createCommandEncoder(), compute = encoder.beginComputePass();
  compute.setPipeline(update), compute.setBindGroup(0, updateGroup);
  compute.dispatchWorkgroups(COUNT / 64), compute.end();   // simulate: no read-back
  const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear',
    storeOp: 'store', view: context.getCurrentTexture().createView(),
    clearValue: [0.07, 0.16, 0.29, 1] }] });
  pass.setPipeline(draw), pass.setBindGroup(0, drawGroup);
  pass.draw(4, COUNT), pass.end();                   // one instanced draw, 768 quads
  device.queue.submit([encoder.finish()]);
  if (++frame < 150) requestAnimationFrame(tick);    // 2.5 simulated seconds, then stop
  else device.queue.onSubmittedWorkDone().then(() => { window.__done = true; });
}
requestAnimationFrame(tick);
</script>
Browser output of Listing 4.99
Browser output of 99

Pages stream up from the emitter at the bottom center, fan out, turn over and drift down as they fade. Per frame the CPU writes 16 bytes and records two passes, whatever the count; 768 keeps the figure readable.