计算机图形学 11:沿管线定位一张错误图像
立方体的位置、轮廓与阴影都正常,一个原本背光的侧面却出现了棋盘。检查最终颜色只能知道结果变了,无法立即判断是纹理、法线、光方向还是颜色编码出了问题。把同一个可见样本分别画成深度、法线和UV,能缩小排查范围;再用数值检查验证阶段之间的关系,才能确认原因。
本篇复用第10篇的地面、立方体、相机与光源,不再复制一套渲染器。正确路径输出四张诊断图,错误路径只在照明时混用局部法线与世界光方向。几何、纹理查询和阴影比较全部相同,因此图像差异有一个明确来源。
中间结果需要有明确含义
当前累计管线先变换顶点,在齐次空间裁剪,随后除w、映射视口、判断覆盖并比较深度。通过测试的样本保存局部位置、面法线和对象编号。着色阶段据此求UV、查询纹理与阴影,在线性域计算明暗,最后编码为sRGB。
这些阶段对应常见图形管线的变换、光栅化、逐样本着色与可见性职责。但这里的执行次序和数据结构属于CPU教学实现,不描述GPU一定采用的内部调度,也没有实现全部可编程阶段。
test_scene.hpp从第10篇提取原有场景提交代码,仍是地面两片、盒子十二片三角形。shadow_check.cpp与新增的pipeline_check.cpp调用同一个函数,使用已有DepthImage和SurfaceBuffer。提取后复跑第10篇,三档偏移的FP、FN与区域计数均保持不变。
场景采用的局部到世界变换是正交旋转,所以法线可乘相同旋转。非均匀缩放仍须用第09篇的逆转置。本实验没有因为这个特例方便,就删除累计检查里的非均匀缩放和剪切反例。
四张图共享同一份可见表面
相机输出512×512,阴影图64×64,接收点偏移固定0.01。黑色背景表示没有写入表面。四图从同一个SurfaceBuffer生成,不分别运行不同的覆盖或深度逻辑,否则诊断图可能在分析另一张图像。
颜色图给地面与盒子各面添加棋盘。基础纹理64×64,每8个纹素换色,线性灰度为0.2和0.8,采用第08篇的repeat双线性查询。这里固定第0层,没有自动估计纹理足迹或LOD,也没有开启SSAA。它用于集成检查,不宣称消除了远处混叠。
线性照明值采用:
其中V为二值光源可见性,受光为1、阴影为0,T是线性纹理灰度。最后三个通道都编码同一个C,得到灰度图。底色0.05是固定教学参数;当前没有使用高光项,也不把这条直接光公式称为完整物理照明。
深度图直接将相机NDC深度乘255并四舍五入,写入RGB三个通道:
近处小、远处大,因此地面呈连续灰度变化,盒子的可见表面也有自己的深度。背景黑色来自对象编号为0时跳过输出,并不代表背景深度等于近面。底层深度缓冲仍清为1。若要从图中恢复精确深度,8位量化已经不够,应读浮点缓冲或数值日志。
世界法线图将单位法线各分量从[-1,1]映射到[0,1]:
地面与盒顶的法线同向,所以颜色相同,即使它们的高度与照明不同。两个侧面颜色不同,边缘突变符合盒子的硬边几何。本篇使用面法线;不能把这种突变误诊为第09篇平滑法线插值失败。
UV图的接缝是否属于错误
地面局部范围x∈[-4,4]、z∈[-3,3],定义u=(x+4)/8、v=(z+3)/6。盒子每个面单独映射:x法向面取(z+0.5,y),y法向面取(x+0.5,z+0.5),z法向面取(x+0.5,y)。这是一组明确的逐面平面映射,没有外部模型文件或展开工具。
UV由插值后的局部位置计算。每个面上的映射都是仿射函数,因此与先给顶点赋UV、再用相同透视权重插值等价:仿射函数可与权重和为1的加权和交换。这个等价只适用于当前逐面仿射映射,不适用于任意非线性程序纹理坐标。
红通道表示u,绿通道表示v,蓝通道固定0。地面应有连续梯度,三角形内部对角线不应产生跳变;盒子换面时允许重新开始,因此面边缘的接缝是当前参数化的结果。若不先说明UV定义,看到接缝就修改插值可能破坏正确实现。
深度、法线与UV图都直接写诊断码值,没有再经过sRGB编码。它们是数值的可视化标签,不是线性辐射亮度。浏览器显示出来的亮度仅帮助观察,不能将截图通道当作原始浮点值计算误差。颜色图则必须编码一次,这两种输出用途不同。
用重投影检查位置是否仍对应这个像素
保存的位置应当属于当前样本。将它再次转世界坐标、乘相机矩阵并映射视口,应该回到像素中心附近;将其NDC深度与保存深度比较,也应一致。
但第05篇先把屏幕顶点量化到1/256像素格,插值权重基于量化后几何。重投影使用原始顶点所定义的几何,不能要求横纵坐标严格相等。本场景正交投影且覆盖内权重非负,重投影偏差是三个顶点量化误差的加权和,每轴界限为半格,即1/512像素。
实际最大横纵误差为0.00181899662194951像素,小于0.001953125的半格界限;深度最大误差为1.11022302462516×10⁻¹⁶。这里的界限依赖正交、相同权重和当前量化规则,不能直接推广到任意透视场景。
最终覆盖68,584个样本,其中地面64,147个,盒子4,437个。地面被阴影图判暗2,889个,恰等于第10篇同一偏移下2,866个真阳性加23个假阳性。这个相等确认集成没有悄悄换场景或偏移,并没有消除那23个已知边界误判。
所有可见法线的最大单位长度误差为0。这个值来自当前轴向面法线经同一正交变换后的有限样本,不承诺一般模型浮点误差也为零。UV输出同时检查有限性与[0,1]范围,仅容许10⁻¹²量级的舍入。
一个看起来合理的错误路径
现在保持可见表面不变,只把照明调用里的世界法线替换成局部法线,光方向仍在世界空间。两个向量各自都是单位长度,所以“检查normalize是否执行”不会发现错误。
世界光方向由局部(2,1,0)/√5旋转得到,带有正的世界z分量。盒子的局部+z法线若直接与它点积,就得到错误的正余弦;正确做法将两者一起旋转,原先为零的点积仍为零。错误图因此在侧面露出棋盘,容易被误认为贴图偶然改善了细节。
实际38,811个样本的8位RGB与正确图不同,漫反射因子最大绝对差为0.108465228909328。两张图共用深度、UV、纹理值与阴影判断,故这些阶段无法解释差异。比较同一坐标空间中的点积,才直接定位了错误。
这里错误路径只生成对照图,没有替换默认正确计算。颜色不同的像素数受量化与阴影影响,不等于所有法线有误的样本数;部分错误余弦仍可能编码成相同RGB,被遮挡样本也只显示相同底色。
哪项检查回答哪种疑问
累计运行不是仅判断程序有没有崩溃。05检查共享边576个位置的唯一归属;06检查六平面裁剪、近面交点、提交顺序与远面严格比较;07用独立射线平面参考检查透视UV;08用解析面积与已知纹理平均检查采样;09检查法线正交与手算照明;10检查独立遮挡参考与偏移两类错误。11把中间位置重新投影,并核对共享场景计数。
若深度图已经错误,应先查变换、裁剪和可见性;深度正常而UV错误,再查属性和映射;二者正常但颜色错误,继续比较法线、光方向、纹理值和输出编码。这样的顺序减少无关修改,但每一步仍需要其数值证据,不能用肉眼正常作为通过条件。
当前正交场景的w全为1,无法暴露漏掉1/w的错误,所以第07篇透视检查必须继续执行。完整图像与有针对性的微小反例各有用途,不能因集成图看起来正确而删掉后者。
1 | |
输出四张诊断PPM和一张错误空间对照PPM,正文PNG从真实输出转换。原始数值在writing-plans/computer-graphics/evidence/11-cpu.txt。运行没有随机数或性能计时,处理完固定场景即结束;这些CPU证据不计作GPU验证。
练习与自检
练习一:法线长度全部为1,为什么仍不能证明照明正确?本篇错误路径为什么通过长度检查却得到不同颜色?
答案:长度没有表示坐标空间。局部法线与世界光方向属于不同基底,点积不对应真实夹角。将二者变换到同一空间,或都保留在局部空间,才能保持旋转前后的点积。
练习二:若将UV图改成纯白纹理的颜色图,能否继续观察三角形对角线上的UV错误?为什么不能把0.001819像素的重投影差直接判为失败?
答案:纯白纹理对任何UV都返回相同颜色,丢掉了诊断信息。重投影差还包含固定格量化;本次在正交条件下有每轴1/512像素的推导上界,应与这个界限比较。若误差超过上界,才需要检查插值权重、坐标变换或像素中心约定。
系列导航与资料
前篇:10:光源看不见的地方怎样变暗。系列入口。下一篇:12:三角网格怎样保存邻接。
Microsoft:Graphics pipeline,Vertex shader、Rasterizer、Pixel shader、Output-merger阶段说明,2026-09-20再次查阅,用于核对职责边界。公式、具体场景与错误注入由本系列独立构造;各算法来源与逐篇验收见05–10正文及evidence/research-05-08.md、evidence/research-09-11.md。






