Chapter 3 · material_pass.wgsl · surface.wgsl
Rebuilding the triangle
Key idea. With the three corners' clip x, y and w as the rows of a matrix M, a pixel's perspective-correct weights are proportional to its device point times M's inverse. Each column of the inverse is a cross product of two rows, and normalising the weights to sum to 1 cancels the determinant, so no corner is ever divided by its w.
A forward pass gets its interpolated attributes from the rasteriser. A compute pass gets only an ID, and must find the triangle, run its corners through the vertex shader's work, and work out how much of each corner the pixel sees.
From an ID to three corners
The material pass clears bit 31, and if it was set adds phase two's first index divided by 3, to get the triangle's place in the frame's draw. That draw's packed indices give three vertices. For each, it calls vertex_output, the very function the lit pass's vertex shader calls, moved into a part both share, surface.wgsl: the packed index names a Meshlet record, which names the Instance, the Meshlet and the Material; the vertex comes from the vertex Arena; and out come the camera-relative position (ADR 0014), the clip position, normal, tangent and UV. Calling the same function is what lets D draw A's image: the corners are computed exactly as A's vertex shader computes them.
Weights without dividing by w
Rasterisers interpolate in screen space and then correct for perspective, dividing by each corner's w. A compute pass could do the same: project each corner, take 2D weights on the projected triangle, and correct each by 1/w. The prototype does not. Write each corner's clip coordinates (x, y, w) as a row of a 3×3 matrix M, and let p = (xndc, yndc, 1) be the pixel centre's device point.
point with weights λ on the triangle, in clip space: λᵀ M
it projects to p exactly when λᵀ M = s p for some s > 0
so λᵀ = s p M⁻¹ ∝ p M⁻¹
column i of M⁻¹ = (row i+1 × row i+2) / det M
normalise λ to sum to 1: s and det M cancel
let rows = mat3x3<f32>(first_corner.clip_position.xyw,
second_corner.clip_position.xyw,
third_corner.clip_position.xyw);
let edges = mat3x3<f32>(cross(rows[1], rows[2]), cross(rows[2], rows[0]), cross(rows[0], rows[1]));
fn barycentric_weights(edges: mat3x3<f32>, point: vec3<f32>) -> vec3<f32> {
let unnormalised = point * edges;
let sum = unnormalised.x + unnormalised.y + unnormalised.z;
return unnormalised / select(sum, 1.0, sum == 0.0);
}
The only division is by the sum of the unnormalised weights. That sum is 0 only where the pixel's ray runs parallel to the triangle's plane, which a neighbour pixel evaluated for a derivative (chapter 4) can reach at a grazing angle; the prototype then leaves the weights unnormalised, and finite. A corner behind the camera has a negative w, and nothing divides by it, so it needs no clipping. The same identity gives the depth along the ray: the plane is met at w = det M / sum.
This is Olano and Greer's 1997 scan conversion in 2D homogeneous coordinates: "three edge functions = M-1 = inverse of 2D homogeneous vertex matrix", and each parameter's "interpolation function = parameter vector * M-1". Their algorithm "avoids costly clipping tests" and handles clipping "by the addition of clip edges, without the need to actually split the clipped triangle". They still add a near clip edge when interpolating, because values such as 1/w can overflow near the eye, and the clip edge lets them store 1/w in fixed point; the material pass works in f32, at pixels whose samples the triangle covers and their neighbours. It needs only the interpolation half. Which pixels a triangle covers is already settled: the depth passes' rasteriser, which does clip, wrote the IDs.