Color Targets and Blending

Color Targets, Blending and Write Masks

fragment.targets has one entry per @location output: a format matching the attachment, an optional blend with separate color and alpha components (srcFactor, dstFactor, operation), and a writeMask. The same three premultiplied covers at 70% opacity under four targets:

Replacing, alpha blending, additive blending and a red-only write maskHTMLLive
<canvas id="gpu" width="600" height="110"></canvas>
<div id="names" style="display: flex; width: 600px; font: 13px Arial"></div>
<script type="module">
const code = /* wgsl */ `
const tints = array(vec3f(.08, .40, .75), vec3f(.94, .42, 0), vec3f(.78, .16, .16));
struct Out { @builtin(position) pos: vec4f, @location(0) @interpolate(flat) color: vec4f }
@vertex fn vs(@builtin(vertex_index) v: u32, @builtin(instance_index) i: u32) -> Out {
  let uv = array(vec2f(0, 0), vec2f(1, 0), vec2f(0, 1), vec2f(1, 0), vec2f(1, 1),
                 vec2f(0, 1))[v];
  let k = i % 3;                                  // three overlapping covers per panel
  let corner = vec2f(-0.94 + f32(i / 3) * 0.5 + f32(k) * 0.1, 0.1 - f32(k) * 0.3);
  return Out(vec4f(corner + uv * vec2f(0.22, 0.75), 0, 1), vec4f(tints[k] * 0.7, 0.7));
}                                                 // premultiplied: rgb already times alpha
@fragment fn fs(in: Out) -> @location(0) vec4f { return in.color; }`;
const device = await (await navigator.gpu.requestAdapter()).requestDevice();
const context = gpu.getContext('webgpu'), format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
const module = device.createShaderModule({ code });
const over = { srcFactor: 'one', dstFactor: 'one-minus-src-alpha' };   // premultiplied "over"
const add = { srcFactor: 'one', dstFactor: 'one' };
const targets = { 'no blending': { format },
  'alpha blending': { format, blend: { color: over, alpha: over } },
  'additive': { format, blend: { color: add, alpha: add } },
  'writeMask RED': { format, blend: { color: over, alpha: over }, writeMask: 1 } };  // RED
names.innerHTML = Object.keys(targets).map((t) =>
  `<span style="flex: 1; text-align: center">${t}</span>`).join('');
const encoder = device.createCommandEncoder();              // a dark clear shows the sums
const pass = encoder.beginRenderPass({ colorAttachments: [{ loadOp: 'clear', storeOp: 'store',
  view: context.getCurrentTexture().createView(), clearValue: [0.10, 0.12, 0.16, 1] }] });
for (const [i, target] of Object.values(targets).entries()) {
  pass.setPipeline(device.createRenderPipeline({ layout: 'auto',
    vertex: { module }, fragment: { module, targets: [target] } }));
  pass.draw(6, 3, 0, i * 3);
}
pass.end();
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
window.__done = true;
</script>
Browser output of Listing 4.61
Browser output of 61

Without blend the last cover replaces what is below. Premultiplied "over" (one, one-minus-src-alpha) mixes them, additive (one, one) brightens overlaps for glows and particles, and writeMask: 1 (GPUColorWrite.RED) left green and blue at the dark clear value. Other operations are subtract, reverse-subtract, min and max.