6.1 渲染方程
渲染方程把一个像素的颜色写成第二类 Fredholm 积分方程:出射辐亮度等于自发光加上半球上入射辐亮度的积分。本节逐项拆解它,再用 Neumann 级数展成「每次弹射一项」的几何级数——双板腔真值 1.199936,截断 K 项的相对偏差恰为 (ab)^K;灯箱实测第一阶占 0.592314,第 8 阶之后只剩 0.00781676。
category
渲染方程把一个像素的颜色写成第二类 Fredholm 积分方程:出射辐亮度等于自发光加上半球上入射辐亮度的积分。本节逐项拆解它,再用 Neumann 级数展成「每次弹射一项」的几何级数——双板腔真值 1.199936,截断 K 项的相对偏差恰为 (ab)^K;灯箱实测第一阶占 0.592314,第 8 阶之后只剩 0.00781676。
分层把立方体切成 4×4 格子,低差异序列让点自己避开彼此:同一个 d 球占比积分上,4096 个 Halton 点抵得上 7.4 万个随机点,log-log 斜率从 −0.4980 陡到 −0.7103。但这份红利随维度迅速蒸发,8 维时优势只剩 1.4 倍。
估计量的方差是 σ²/N,误差只能按 1/√N 降。本节把这条定律量成一个小数:7 档 N × 2048 条独立子流,log-log 最小二乘拟合出斜率 −0.4978,落在 −0.5 ± 0.03 内;再把 pdf 写错成 1.1,总误差被常数偏差压平到 −0.2638,而方差那条纹丝不动。
蒙特卡洛积分把「算一个积分」换成「估一个积分」:无偏估计量的均值收敛到真值,偏差则永远停在那里。本节讲清无偏性成立的条件,逐位核对 SplitMix64 的掩码语义——漏掉 `& RNG_MASK` 会让输出涨到 191 位、Q16.16 采样当场失效;再用 3 分箱卡方量出取模的量化偏置:值域 2^4 时 χ² = 15677,而 64 位拒绝采样只有 1.301。
光线树把「反射的光线还能再反射」显式组织成一棵按深度分层的树:depth 只是透传给着色器的参数,形状由着色器决定。实测每层光线 76800 / 44877 / 9232 / 6881 / 6269,代价随深度亚线性增长(每主光线 2.686 → 3.236 条),而在某一层截断会引入单边偏差而非无偏估计。
反射方向 r = d - 2(d·n)n 与 Snell 折射在 Q16.16 下逐位算清:4096 组随机单位向量的单位性偏差最大 55;eta = 1.5 时 3061 组全内反射;临界角公式与整数扫描在水 / 玻璃 / 金刚石三档互证到 1–2 LSB。菲涅尔 x^5 的平方链与逐次相乘实测不逐位相同(429/4096 差 1 LSB)。
硬阴影只有「被挡住」和「没被挡住」两种结果,判据是一条从命中点指向光源的阴影光线。本节把 `off` 是沿法线的偏移向量、不是起点这件事讲透:写错一行,77211 条阴影光线的遮挡数会从 1357 涨到 77210,画面只剩环境光。最后用一张方向光掩码数出像素层面的真阴影 839 个。
把 BVH 打开、关掉、再只去掉 best_t 收缩:三个模式跑同一条代码路径、同一批 76800 条主光线,用可逐条复现的整数计数(访问节点数、三角形测试数)与逐像素比对,回答加速结构到底省掉了什么、又省得够不够多;全部结论只建立在计数上,不做速度断言。
本节拆开 BVH 的构建:划分轴为什么按最长轴选、中位数划分平均花掉 SAH 最优代价的多少倍、叶子容量从 1 扫到 32 时每条光线的访问与测试各是多少,以及排序键的除法为什么必须走 floorDiv 才能在两个宿主上复现同一棵树、而不是只复现节点数。
把 896 个三角形收进一棵扁平整数数组的 BVH:叶子 9 个整数、内部节点 10 个,left 等于负一是整棵树唯一的类型标签。本节立起两条不变量——父盒包含两个子盒、叶盒包含它引用的每个三角形,再把 slab 判据逐轴展开到方向分量为 0 的退化分支,全部落到实跑输出。
自交与容差的边界分析:EPSILON 被两条可测量的边界夹住——下界是地形自交在 eps>=16 清零,上界是场景里最小的可见几何间隙 174 Q16.16;实测丢遮挡在 eps=256 起出现,并解释为什么偏移要沿法线而不是沿光线,以及球自交为何清不干净。
Möller–Trumbore 的除法链在 Q16.16 定点下的量级实测(原始乘积到 2^49、除法分子到 2^39,故 TS 侧必须全程 BigInt);det 趋近 0 时相对误差从 0.061‰ 涨到 46.1426‰ 的扫描,以及真实命中里为什么不能按 det 大小做 epsilon 剔除。
三条解析求交原语——光线-球、光线-平面、光线-三角形的闭式解,连同方向与平面平行、交点在光线背后、方向与三角形共面三个退化情形一律返回 None 的口径;再用真实场景量出解析球与八面体细分网格之间的面化误差,看清网格近似在离线光追里的真实代价。
卷三的交付账本:三卷基线 .rgb 的 sha256 逐个复核,六条计数恒等式(主光线 76800、BVH 访问 3238333 = 主光线 + 阴影光线、掩码对称差 4)逐条对账,六档可运行性各写清证明了什么;另给出已证明 4 条与未证明 5 条(真实 GPU、生产级材质、硬件光追 API 等),以及三条扰动轴的软告警。
把三卷基线的 sha256、计数与容差判据并成一张表:卷一顶点光栅化 bac65977…、卷二逐像素 0d997343…、卷三主光线 213b4130…,三者命中或覆盖都是 43731 像素;再引第 13.3 与第 14.3 节的实测锚给出反直觉结论——列口径真 bug 下两个语言实现仍逐字节相同,只有掩码对称差(4 对 22212)抓得出来。
把 config/scene_spec.json 到像素缓冲的八级链路逐级重放:规格摘要、相机基行口径、76800 条主光线方向摘要、BVH 511 节点高 9、命中 43731 像素、阴影光线 77211(遮挡 1357)、遍历 3238333 节点,末级 sha256 与卷三基线逐字节相同,P 层在 BigInt 里复现每一级摘要。
把分辨率、每像素样本数、最大弹射深度、BVH 叶子容量四条旋钮各转一遍,用「相对参考解的 MAE」对「本机墙钟」给出取舍表;实测样本数在 9 spp 之后饱和(MAE 只再降 1.5463 倍),叶子容量的遍历代价在 leaf=4 处取到最小值,未达预期的组合以软告警列出。
一帧 6.7 秒,时间到底花在哪?本节把 320x240 基线的单帧耗时拆成主光线求交、阴影光线求交、着色算术、光线生成、命中处理五块,用计数而不是直觉解释它;实测阴影光线求交独占 53.68%,比主光线求交还贵,因为阴影光线大多要遍历到底去证明这一点没被挡住。
离线渲染的成片是一串图,不是一张图。本节把共用相机参数接上一条纯整数相机路径,16 帧逐帧渲染并落盘 PNG 序列,用 ffmpeg 合成 GIF/MP4;实测单帧约 1.6-2.0 s、总耗时 29.1 s,逐帧重跑 16/16 帧 sha256 相同。
把三类判据摆在同一帧上:跨语言逐字节一致(sha256 相等,0 容忍)、跨路径像素掩码一致(光追命中掩码对光栅化覆盖掩码的对称差等于 4 个像素)、跨宿主声明容差(MAE ≤ 1.0 / PSNR ≥ 30 dB / 覆盖差 ≤ 20‰)。再给出反直觉的结论:判据一抓不到「两侧同错」。