这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
15.2 性能的度量与剖析
一帧 6.7 秒,时间到底花在哪?本节把 320x240 基线的单帧耗时拆成主光线求交、阴影光线求交、着色算术、光线生成、命中处理五块,用计数而不是直觉解释它;实测阴影光线求交独占 53.68%,比主光线求交还贵,因为阴影光线大多要遍历到底去证明这一点没被挡住。
posts
这里是全站文章的归档入口,下面是按时间倒序排列的全部文章。若想按主题浏览,请从首页的专题分类进入。
一帧 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‰)。再给出反直觉的结论:判据一抓不到「两侧同错」。
把 CPU 定点与浏览器 WebGL2 之间的差异拆成三类可归因的来源:光栅化规则(顶左规则与子像素精度)、插值方式(屏幕空间仿射对透视校正)、浮点精度(Q6/Q16.16 对 float32)。每类配一个只动一个变量的实验,并用 MAE ≤ 1.0、PSNR ≥ 30 dB、覆盖归属差异 ≤ 20‰ 三条声明容差加一个必须被拒绝的负例来收口。
光栅化与光追是两条完全独立的成像路径:一个在屏幕 Q6 空间用整数边函数判覆盖,一个在世界空间用 Möller–Trumbore 求交。同一份场景下两条路径的覆盖掩码各 43731 像素、逐像素对称差 4 个,且这 4 个全落在到最近正向边距离为 0 Q6 的轮廓边上;采样点挪半个像素,同一判据立刻变差到 160。
同一份 scene_spec.json 交给 Python 与 TypeScript,像素缓冲 sha256 同为 213b4130…、五个计数逐条相同。把相机基从行换成列,命中像素从 43731 掉到 21519,两侧却依旧逐字节相同——跨语言一致抓不到「两边一起错」。再把 BigInt 换成 Number,右移只有 32 位,命中像素静默变成 8484。
一张图是几万次累加拼出来的:整数加法三序零差异,破坏结合律的是 fmul 的截断(结合律反例 1005/4096)。重排三角形重建 BVH 后图像摘要一字不变而访问节点数变了;全局 RNG 流下换遍历顺序 3049/3072 像素改变,逐像素流则逐字节相同。
把 Möller–Trumbore 的误差写成一条闭式不等式,在 43731 条命中主光线上逐条核对:界恒成立,最紧处实测/界 = 0.746916。改一处舍入规则,41254 条命中的 t 就会分叉。真正决定自交的是垂直误差 Δt·sinθ,最大 241.47 LSB,而非沿光线的 Δt。
知道每个像素的误差量级之后,就可以让它自己决定什么时候停。本节实测三条停止准则(逐像素方差、相对误差阈值、预算上限)各跑两个最小样本数版本,量出各自停在哪、总样本数、最终误差,以及两种失效模式——四样本全零会让方差估计变成 0,于是最早停、错得最狠。
误差只在有参考解时才有意义,可参考解本身也是估计量。本节用四条互相独立的量法给它造不确定度:批均值的标准误、对半拆、换种子族的第二条参考解、以及只取全程无遮挡像素的确定性网格求积。实测 1024 样本/像素的参考解相对标准误 0.005729,对半拆比值 2.045、换族比值 2.219,都与理论吻合。
噪声不是「看起来脏」这种观感描述,而是一个可算的整数:同一像素在 N 个样本下是个随机变量。本节先把面光源半径取 0、N = 1 退化回卷三基线的逐字节锚点,再用 256 样本扫掠与 8 批参考解量出收敛速率——实测 log-log 斜率 -0.5027,理论值 -0.5。
有了求交点,法线靠差分、软阴影靠沿光取样,两者都不是免费午餐:差分步长 h 太大会跨过棱角、太小会被定点量化吃掉,实测中心差分在 h=64 时均值 0.0342 度;软阴影有一个系统性平面偏置 k*sin(theta),k 必须超过 1/sin(theta)≈1.732 才能让开阔地面回到满亮度。
sphere tracing 每一步走多远由场值自己决定,于是三件事必须量:迭代次数怎么分布、容差 eps 取多少、步长放大到几倍会出事。3072 条主光线的实测是解析命中 2048、marching 命中 1995、真漏交 0;无地面场景里场放大 1.1 倍只穿透不漏交,1.25 倍漏交 303.0303‰,1.5 倍直接 1000‰。
距离场要被 sphere tracing 当步长信任,必须同时满足下界性与 Lipschitz 界。本节用 40000 对随机点估计八个场的 Lipschitz 常数:球、盒、平面、交、差都落在 1000.0‰ 附近,高度场却到 1084.1‰;下界性实测球/盒/平面/并集的最大 f-d_true 恰为 0,高度场 648 个采样点里 426 个高估,最坏 3909 Q16.16。
程序化场景要能当资产用,靠的不是看起来像,而是可断言:同一 seed 必须给出同一场景。本节把 fBm 位移与噪声分档材质接进既有 scene_spec,基线帧 sha256 逐字节锚定 213b4130…,程序化场景的几何 digest 与帧 sha256 在同 seed 重跑下不变、换 seed 必变。
一个 octave 的噪声撑不起地形,fBm 按 gain^k 的振幅把它叠起来。本节用左移采样坐标实现 lacunarity=2、用 Q16.16 连乘 gain,全程整数:4 层高度场幅 132654,octaves=1 时逐位等于底座值噪声,与 8 层参照解的最大偏差 7551,结构函数斜率收敛到 0.533。
噪声在本章不是纹理而是几何的生成器,所以同一个 (ix, iz, seed) 必须给出同一个整数。本节用同一个 32 位混合哈希派生出值噪声与梯度噪声:128 点扫描线上均值 31041、标准差 14919,16384 个格点低 16 位的碰撞率 117.4‰,整数插值与 float64 参照的最大偏差只有 1 个 Q16.16。
底座的 trace 把 depth 一路传下去,却不把入射方向传给 shade。本节写一个与它逐字同构、只多传一个 dirv 的 trace_rec,给着色器加一层受深度门控的镜面反射:深度 1 的输出与卷三基线 sha256 逐字节相同(退化为直接光照),每加一层 MAE 与变化像素都单调下降。
相机三根基向量必须从 mat_look_at 的视图矩阵取行(第 0/1/2 行即 s / u / -f)。底座原先取列,把 up 的 z 与 back 的 y 各反一号,相机是歪的。判据是主光线命中掩码与卷一光栅化覆盖掩码的逐像素差:取行 43731 只差 4 个,取列 21519 差 22212。像素再经 NDC 与 4:3 宽高比合成主光线方向。
光线就是 o + t·d,求交就是把这个参数式代进一个隐式方程解出 t。本节用同一条探针光线对球、地面、三角形三种原语各求一次交:76800 条主光线归一化残差最大 5 Q16.16,命中 43731 个像素,三种 F(p) 残差分别是 712、0、-29。