· 4 分钟阅读 · 1644 字

UV 需要透视校正,深度却不能再除一次 W

一个倾斜三角形本应被已有表面遮挡,软件光栅器却把它画到了前面。固定采样点的正确深度为 0.84375,已有深度为 0.8,采用 LESS 比较应当拒绝写入;如果把深度送进 UV 使用的透视校正函数,结果变成 0.73828125,同一个采样点就通过了深度测试。覆盖范围没有变化,错误来自插值字段的含义。

倾斜纸面棋盘的摄影概念图,表达透视缩短,不作为实验结果

UV 校正恢复的是原表面上的线性属性。深度缓冲接收的通常已经是投影除法之后的值。把所有数值字段放进同一个插值器,会把两份不同的数据合同混在一起。对自建光栅器、CPU 遮挡查询与离线烘焙工具,这个区别直接决定哪些样本有资格覆盖旧结果。

在哪里完成透视除法,决定插值公式

本探针明确采用正 Z 朝前、近面 n=1、远面 f=9、投影深度范围 [0,1] 的约定,不借用某个引擎的默认矩阵。令相机空间深度为 z,clip W=z,clip Z=Az+B,其中 A=f/(f-n)=1.125,B=-fn/(f-n)=-1.125。顶点透视除法后得到 d=Z/W=A+B/z,近面映射到零,远面映射到一。

三角形在相机空间的顶点为 A=(0,0,2)、B=(4,0,4)、C=(0,8,8),投影取 (x/z,y/z),屏幕位置依次为 (0,0)、(1,0)、(0,1)。对应顶点深度为 0.5625、0.84375、0.984375。在屏幕点 (0.25,0.5),重心 λ 为 (0.25,0.25,0.5),直接混合三个 d 即得 0.84375。

这个线性关系可以从齐次组合推出。设原表面重心为 α,投影后的屏幕重心满足 λᵢ=αᵢWᵢ/ΣαⱼWⱼ。采样点深度为:

d = (Σ αᵢ Zᵢ) / (Σ αᵢ Wᵢ)
  = Σ λᵢ (Zᵢ/Wᵢ)

分母已经包含在 λ 与顶点 Z/W 的关系里。对 d 再计算 Σλᵢdᵢ/Wᵢ ÷ Σλᵢ/Wᵢ,就换成了另一种属性定义。所有角点仍然正确,因此只测三个顶点会漏过问题;三个 W 相等时错误也会被公共因子掩盖。

固定深度输入、两条插值路径与射线平面参照计算

让深度入口只接收已经投影的值

研究目录 .tmp/daily-labs/2026-11-02-2000/ 的 depth.mjs 使用 JavaScript double 验证数值合同,完整核心为:

export function depthAt(depths, weights) {
  if (depths.length !== 3 || weights.length !== 3 ||
      depths.some(d => !Number.isFinite(d) || d < 0 || d > 1) ||
      weights.some(w => !Number.isFinite(w) || w < 0) ||
      Math.abs(weights.reduce((a,b) => a+b,0)-1) > 1e-12)
    throw new RangeError('Invalid depth sample');
  // 输入已经除过 W;这里只按屏幕覆盖权重混合。
  return depths.reduce((sum,d,i) => sum+d*weights[i],0);
}

这个函数有意不接收 W,避免调用端把“深度也做一次透视修正”当作通用选项。它接收上游已确定覆盖的屏幕重心;负权重、非有限值、错误长度和明显偏离单位和的输入直接失败。这里的 [0,1] 限制来自本探针选择的裁剪与深度约定,若目标管线允许深度钳制或使用其他范围,应修改接口合同及测试,不能静默夹取越界输入。

相机空间 z 也不能直接代替这个输入。它沿视线表示距离,屏幕内部需要用倒数关系恢复;写入标准深度缓冲的 d 则按上述公式线性变化。反向深度会改变端点映射和比较方向,但在采用同类投影且深度范围映射为仿射变换时,已除 W 的深度仍保留屏幕线性关系。自定义片元深度、对数深度以及任意非线性编码需要重新推导。

用射线求交检验,而非重复同一个插值器

三角形所在平面是 z=2+x/2+3y/4。从原点穿过屏幕点 (sx,sy) 的射线写为 (sx·t,sy·t,t),代入平面得到 t=2/(1-sx/2-3sy/4)。先独立求出交点,再把其 z 代入投影深度公式,就能检验插值结果:

const rayDepth = (sx, sy) => {
  const t = 2 / (1 - sx/2 - 3*sy/4);
  return 1.125 - 1.125/t;
};

固定点的交点 t=4,参照深度为 0.84375。测试保留错误实现,要求它在已有深度 0.8 时错误通过 LESS;正确实现必须被遮挡。这个断言锁住了实际决策差异,而不只是证明两种公式返回不同数字。

随机测试以固定种子 711022 生成 10,000 个三角形内部屏幕点,把超出单位直角三角形的 (x,y) 反射为 (1-x,1-y)。每个点同时走屏幕深度加权与射线平面求交,两条路径最大误差为 1.1102230246251565e-16。它验证了这一固定三角形内部的公式关系,没有验证任意退化三角形的覆盖鲁棒性。

2026-11-02 使用 Node.js 24.18.0 运行,入口执行固定样本、三个角点、等深退化、近远端点、随机求交比较,以及八类无效输入拒绝检查:

cd "$WORK_DIR"
node --check .tmp/daily-labs/2026-11-02-2000/depth.mjs
node .tmp/daily-labs/2026-11-02-2000/depth.mjs
syntax: exit=0
run: exit=0, passed=18, failed=0, skipped=0
samples=10000, elapsed_ms=17.604584
vertex_depths=0.5625,0.84375,0.984375
screen_weights=0.25,0.25,0.5
correct=0.84375, wrong=0.73828125, ray_reference=0.84375
stored_depth=0.8, correct_visible=false, wrong_visible=true
max_error=1.1102230246251565e-16

耗时是本轮 CPU 正确性检查总时间。当前证据没有包含 GPU 光栅器、深度量化、MSAA 或真实帧缓存回读,因此不能据此承诺目标引擎的逐像素一致性。接入时还必须让深度与覆盖使用同一采样位置;拿像素中心深度配上其他子样本的覆盖,会引入本公式之外的差异。

对一条完整光栅化链,裁剪阶段保留齐次位置与原始表面属性,投影阶段生成 Z/W,覆盖阶段提供屏幕重心,深度路径执行线性混合,UV 路径恢复表面属性。分开这些字段的语义,就能在进入遮挡比较前定位本例错误;无需通过偏移深度或调整比较容差来补偿一条错误的插值公式。