First Shader Pair

Compiling BookNest's First WGSL Shader Pair

This module combines the section: a Cover struct carries position, uv and a flat book index from the vertex to the fragment entry point; the vertex shader sizes one quad per instance by page count; a helper function shades a gradient, spine band and rating bar; and the fragment shader uses discard for a folded corner:

demos/ch04/first-shader-pair.html: BookNest's covers from one vertex and fragment pairHTMLLive
<canvas id="gpu" width="480" height="150"></canvas><pre id="log"></pre>
<script type="module">
const code = /* wgsl */ `
const tints = array(vec3f(.08, .40, .75), vec3f(.18, .49, .20), vec3f(.94, .42, 0),
                    vec3f(.42, .11, .60), vec3f(.22, .28, .31), vec3f(.78, .16, .16));
const books = array(vec4f(312, 4.6, 1, 0), vec4f(428, 4.3, 1, 0), vec4f(256, 4.8, 0, 0),
  vec4f(198, 4.1, 1, 0), vec4f(344, 4.5, 1, 0), vec4f(176, 4.4, 1, 0)); // pages, rating, stock
struct Cover { @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) b: u32) -> Cover {
  let uv = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1),
                 vec2f(0, 1))[v];
  let corner = vec2f(-0.93 + f32(b) * 0.31, -0.9);
  return Cover(vec4f(corner + uv * vec2f(0.26, books[b].x / 240), 0, 1), uv, b);
}
fn shade(tint: vec3f, uv: vec2f, rating: f32) -> vec3f {
  if (uv.y < 0.08) { return select(vec3f(1), vec3f(1, .8, .2), uv.x < rating / 5); }  // rating
  return tint * (0.75 + 0.25 * uv.y) * select(1.0, 0.6, uv.x < 0.12);   // gradient and spine
}
@fragment fn fs(in: Cover) -> @location(0) vec4f {
  if ((1 - in.uv.x) + (1 - in.uv.y) * 3 < 0.3) { discard; }            // a folded corner
  let book = books[in.book];
  let color = shade(tints[in.book], in.uv, book.y);
  let grey = vec3f(dot(color, vec3f(0.3, 0.59, 0.11)));
  return vec4f(select(grey, color, book.z == 1), 1);                   // grey if out of stock
}`;
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const module = device.createShaderModule({ label: 'shelf.wgsl', code });
const { messages } = await module.getCompilationInfo();
log.textContent = `shelf.wgsl: ${messages.length} compilation messages`;
const context = gpu.getContext('webgpu'), format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const pipeline = await device.createRenderPipelineAsync({ layout: 'auto',
  vertex: { module }, fragment: { module, targets: [{ format }] } });
const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear', storeOp: 'store',
  view: context.getCurrentTexture().createView(), clearValue: [0.96, 0.94, 0.90, 1] }] });
pass.setPipeline(pipeline);
pass.draw(6, 6), pass.end();                      // 6 vertices x 6 instances, one per book
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
window.__done = true;
</script>
Browser output of Listing 4.47
Browser output of 47

The module compiled with no messages, and one draw of six instances produced the shelf, with "Salt and Saffron" grey because it is out of stock. Buffers and Bind Groups feeds it the catalog buffer of Shared Book Struct instead of constants.