@interpolate(type, sampling) sets how a @location value is blended across a triangle. The type is perspective (the default), linear (a straight screen-space blend) or flat (none). The sampling is center (the default), centroid (inside the covered samples, avoiding extrapolation at multisampled edges) or sample (which runs the fragment shader per sample); flat takes first (the default) or either. Integer outputs must be flat, and @interpolate(flat, center) failed. The same uv travels three ways through a quad whose top edge is three times farther away:
<canvas id="gpu" width="480" height="120"></canvas>
<script type="module">
const code = /* wgsl */ `
struct Out { @builtin(position) pos: vec4f,
@location(0) uvP: vec2f, // perspective, center: the default
@location(1) @interpolate(linear) uvL: vec2f, // straight screen-space blend
@location(2) @interpolate(flat) uvF: vec2f, // the first vertex's value
@location(3) @interpolate(flat) panel: u32 }
@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 w = 1 + uv.y * 2; // the top edge is three times farther
let x = -0.66 + f32(i) * 0.66 + (uv.x - 0.5) * 0.6 / w;
return Out(vec4f(vec2f(x, uv.y * 1.8 - 0.9) * w, 0, w), uv, uv, uv, i);
}
@fragment fn fs(in: Out) -> @location(0) vec4f {
let uv = array(in.uvP, in.uvL, in.uvF)[in.panel];
let check = select(1.0, 0.7, (u32(uv.x * 6) + u32(uv.y * 6)) % 2 == 1);
return vec4f(vec3f(0.08 + 0.8 * uv.x, 0.4, 0.75 - 0.6 * uv.y) * check, 1);
}`;
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 pipeline = device.createRenderPipeline({ 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, 3), pass.end(); // panels: perspective, linear, flat
device.queue.submit([encoder.finish()]);
await device.queue.onSubmittedWorkDone();
window.__done = true;
</script>
Perspective (left) shrinks the checker rows toward the far edge as a real floor would; linear (middle) bends them where the two triangles meet, the affine-texturing artifact of 1990s consoles; flat (right) paints each triangle with its first vertex's value. Both stages must declare the same mode.