js-webgl-perspective-correct-attribute-interpolation
Done. Since the workspace contained only the problem spec (no codebase to patch), I diagnosed the root cause, implemented the fix as a complete software-rasterizer core, and verified it numerically with an independent ground truth (Möller–Trumbore ray-triangle intersection, so the fix and truth can't share a bug).
/workspace/SOLUTION.md — the solution writeup: title, root-cause analysis, exact fix (canonical formulas, full reference implementation, WebGL/GLSL translation notes), and the verification section with the real measured numbers./workspace/solution/reference.js — runnable fixed pipeline (Node ≥ 20, zero deps)./workspace/solution/verify.js — verification harness; run with cd solution && node verify.js (exit 0, deterministic).The renderer interpolated post-divide per-vertex values linearly in window space. Only w and 1/w are affine in window coordinates; UVs, normals, and view-depth w = -z_view are not, so grazing-angle geometry (big w spread per triangle) shears textures, drifts normals, and produces a depth error ~25,000× the true surface separation near the near plane.
Per fragment, with window barycentrics λ:
invW = Σλᵢ/wᵢ (exact, linear in window space)
attr = (Σλᵢ·aᵢ/wᵢ) / invW (perspective-correct)
depth = (1/near − invW)/(1/near − 1/far) (w-buffer: perspective-consistent space)
| Test | Buggy | Fixed |
|---|---|---|
| Grazing ground quad UV shear | max error 0.63 UV units | ≤ 2.1e-14 |
| Normal drift, camera 2→0.25 above surface | mean 9.5° → 21.5° (max 24°→33°) | ≤ 1.7e-6° |
| Depth error, near=0.01, 1e-4 separation | up to 2.53 units | ≤ 1.6e-12 |
| 24-bit depth-buffer winner test | 19.1% wrong pixels | 0% |
A bonus finding documented in §2.2/Test 3c: lerping NDC z per fragment is itself exact (~1e-14), which confirms the z-fighting comes specifically from lerping post-divide view-depth without the 1/w term — the w-buffer is the robust perspective-consistent replacement and reuses the same 1/w already needed for attributes.
# Evidence - Problem class: js-webgl-perspective-correct-attribute-interpolation - Model: openrouter/deepseek/deepseek-v4-flash-0731 - Solved: 2026-09-03T22:21:16.627Z - Verification: solution produced by pi in sandbox; see signatures.json
{"description": "A WebGL renderer interpolates per-vertex UV and normal attributes linearly in window space instead of weighting by 1/w, so textured quads shear and lighting normals drift as the camera approaches, while depth is stored post-divide without the perspective term, causing near-plane z-fighting. Rewrite the pipeline to interpolate attribute/w and 1/w across each triangle and recover attributes per fragment, keeping the depth value in a perspective-consistent space. Validate with a ground-plane texture viewed at grazing angle and a depth-stress scene whose near plane is 0.01.", "environment": "node20", "language": "js", "model": "openrouter/deepseek/deepseek-v4-flash-0731", "problem_class": "js-webgl-perspective-correct-attribute-interpolation", "provider": "openrouter", "solved_at": "2026-09-03T22:21:16.627Z", "version": "20"}