Observe · Change · Recompute · Explain
先读证据链,再读公式
Read-only · exact teaching model
每段代码都对应实验中的一个可观察量:空间、精度、像素工作、提交成本和可复现报告。
这一课怎么学
- 观察
- 预测
- 改一个量
- 看证据
- 再读代码
Shader anatomy is a data-flow contract
Find the coordinate-space boundary before changing a formula.
// Graphics Debugger Suite — executable teaching model
type Space = "tangent" | "object" | "world" | "view";
const normal = normalize(TBN * decodeNormalMap(rgb));
const correctedNormal = normalize(inverseTranspose(model) * vec4(normal, 0.0)).xyz;
const stage = { vertex, varying, fragment };
shaderError = !normalDecode || (worldScale !== 1.0 && !inverseTranspose);
Normal maps need decode and a basis
Tangent → world is not a naming convention; it is a matrix operation.
type Space = "tangent" | "object" | "world" | "view";
const normal = normalize(TBN * decodeNormalMap(rgb));
const correctedNormal = normalize(inverseTranspose(model) * vec4(normal, 0.0)).xyz;
const stage = { vertex, varying, fragment };
shaderError = !normalDecode || (worldScale !== 1.0 && !inverseTranspose);
Depth precision depends on range
The near/far ratio and world scale explain the symptom before the fix.
const depthRatio = log(far / near) / log(1000000.0);
const precisionError = precision === "f16" ? depthRatio * (worldScale / 24.0) : depthRatio * 0.04;
const reversedZ = 1.0 - depth / far; // put precision where the camera needs it
Overdraw and submissions trade cost
A prepass can lower fragment work while adding CPU and depth cost.
const overdraw = layers * (depthPrepass ? 0.62 : 1.0);
const effectiveDrawCalls = instancing ? ceil(drawCalls * 0.38) : drawCalls;
const cpuMs = 0.18 + effectiveDrawCalls * 0.035 + (depthPrepass ? 0.42 : 0.0);
const gpuMs = resolutionScale * resolutionScale * (0.45 + overdraw * 0.31);
const bandwidthMb = resolutionScale * resolutionScale * (8.0 + variants * 1.7) * (1.0 - atlasCoverage * 0.35);
A report is a reproducible experiment
Keep the parameters with the evidence so an interview answer can be verified.
reportId = `${scene}-${debugMode}-${hash(config)}`;
risk = max(shaderError, precisionError, cpuMs > targetMs, gpuMs > targetMs);