5.3 环境贴图与球谐
环境贴图把整个天空的入射光存下来,球谐用 9 个系数(L0/L1/L2)把它压成可解析重建的方向函数。本节用整数 Fibonacci 格点做投影,得到 9 个 Q16.16 系数,重建辐照度与直接积分只差 2‰;但太阳圆盘这类高频特征装不进 9 个系数,L2 带被它拉偏,反而在低频背景上引入振铃。
tag
环境贴图把整个天空的入射光存下来,球谐用 9 个系数(L0/L1/L2)把它压成可解析重建的方向函数。本节用整数 Fibonacci 格点做投影,得到 9 个 Q16.16 系数,重建辐照度与直接积分只差 2‰;但太阳圆盘这类高频特征装不进 9 个系数,L2 带被它拉偏,反而在低频背景上引入振铃。
SSAO 从深度缓冲重建视空间位置与法线,再在半球上撒一组固定方向做遮挡估计。方向集固定会让误差呈低频带纹,把它按一张确定性的 4×4 旋转表逐像素转开,误差被打散成高频噪声,再用 3×3 盒式模糊压回去。本节量出 4/8/16/32/64 采样的收敛曲线、旋转前后的粗糙度,以及模糊对内部像素误差的削减,并在 TypeScript 侧逐字节复现。
环境光遮蔽衡量一个点周围半球有多大比例能看见天空,是环境光里最便宜的一阶近似。本节先构造一个能解析求解的最小场景——平面上方悬一个球,正下方那点的遮蔽恰为 1 减去 r/h 的平方——再用一组固定的 64 条整数方向做逐像素数值估计,量出它与解析值的误差、整幅 AO 图的 MAE,最后把 AO 作为乘性项接进着色,并在 TypeScript 侧复现同一张 AO 图。
把 32x32 的 8 位法线贴图还原成 128x128 世界法线:解压 n=(v<<17)/255-ONE 后未归一化模长落在 [0.9905,1.0024],归一化后偏差最大 2,与连续法线夹角最大 0.7753°。W 层用 WebGL2 片元着色器做同一件事,与 S 层按声明容差比对 MAE 0.5068 / PSNR 51.08 dB / 覆盖差异 0‰,负例被同一判据拒绝。
切线空间从三角形的三个顶点位置与三个 UV 解出:标准 UV 下 denom=65536、T=(2,0,0)、B=(0,0,2),手性 w=-1;把 u 反向令 denom=-65536 后 T 翻成 (-1,0,0)、w 变 +1。相邻面平均出的切线 T_avg=(1,0.5,0) 模长 1.1180、n·T_avg=0.5,Gram-Schmidt 后回到正交单位基。
法线不能直接用模型矩阵 M 变换:非均匀缩放 diag(2,1,1) 下 (M·t)·(M·n) 实测 +3.0000 而非 0,改用逆转置 (M⁻¹)ᵀ 后恒为 +0.0000;纯旋转时两者相等,错误只在非均匀缩放下暴露。4096 个单位法线扫描给出归一化后的模长不变量 |n| ∈ [ONE−5, ONE]。
材质的线性混合只要满足不变量 w_A+w_B==ONE 就仍能量守恒(五组权重实测 65533/65479/65426/65373/65321 全 ≤ ONE,权重和 =2·ONE 时涨到 130854);清漆层朴素相加 78640 越界、按 (1-F) 缩放后 60946 守恒。并用混合材质渲染一帧,跨宿主 sha256 逐字节相同。
把 Lambert、Phong、Blinn-Phong 三种 BRDF 写成全整数:1/π=20861,Phong 归一化 9/π=187749,Blinn 归一化 3/π=62583,指数 (R·V)^16 用 4 次平方。实跑能量:Lambert 65533、归一化 Phong 65321、归一化 Blinn 69886(超过 ONE),未归一化 Phong 只 22800。
把 BRDF 的半球反射率写成固定的 512 方向整数估计:cosθ_k=(2k+1)/128 是精确有理数、方位只用 {0,±1,±√2/2},全程无浮点三角函数。∫cosθ dω 的估计逐位等于 π(残差 0),Lambert 能量 65533(999.9542‰),残差 3。
同一份场景规格在 Python 与 TypeScript 两条 CPU 路径下产出逐字节相同的像素缓冲。把着色器写成可参数化形式,五个负例各只动一处口径——降精度改用除法、插值后不归一化、逐项钳制、1/π 取 20860、albedo 四舍五入——每个都让 sha256 分叉,七个摘要两两不同,退化档复现卷二基线。
把光照拆成环境 / 漫反射 / 镜面 / 衰减四张灰度图,再加一张合成图。故意把衰减写成 r²/(r²+d)(分母忘了平方),衰减通道 43731/43731 个像素全变、最大差 47042,而环境 / 漫反射 / 镜面三通道逐字节相同——缺陷被精确定位到衰减那一行。
三种光源共用一套光照方向构造与衰减接口:方向光无衰减、点光源 r²/(r²+d²)、聚光再乘锥软化。同一场景下受光像素 959.8‰ / 953.3‰ / 70.1‰;衰减的两种整数写法在 178/1280 个距离上分叉、最大差 1 个 Q16.16,d=range 处恰好 0.5。
一份材质表 JSON 驱动三种宿主:Python 建材质对象、TypeScript 建同构对象、HTML 内联同一条材质。三处解析的六个字段逐字段相同(对账表 S/W/P 三列一致),规范 digest c456c8c1… 两侧相同;改一个字段后帧 digest 与内联值同步变化,四次扰动全部同步。
材质的每个开关都让着色器分叉:4 开关理论 16 组合、实际只用 9;8 开关理论 256、实际 23,爆炸比 11.13×。变体键用整数位编码去重,与 frozenset、关键词串去重同数;键漏掉 shadow 位则去重数从 9 塌缩到 7。逐个渲染实际变体,不同键必给不同帧。
材质是六个定点参数而非浮点结构体。继承用不动点松弛解析,与输入顺序无关:叶子 gold_trim_dark 逐字段来源确定,解析 digest 074b01f1… 在打乱输入后不变,自指父指针报错;叶子材质驱动一帧,改被继承字段帧变、改旁系帧不变。
把「哪个抗锯齿方案更好」写成一条可复算的判据:先用参考解的梯度阈值圈出边缘像素,只在边缘上算 MAE,再配上 PSNR。64×64 圆盘的 296 个边缘像素上,无抗锯齿 → 4 spp SSAA → 8 帧 TAA → 16 spp SSAA 三级判据全部成立,而把覆盖率乘 0.5 的错实现被同一条判据拒绝。
抖动不减少量化误差,它把误差从低频的「带状」搬到高频的「噪声」。本节把 0..255 的斜坡量化到 8 级,比较无抖动、Bayer 有序抖动、Floyd–Steinberg 误差扩散与 16×16 蓝噪声:总误差几乎不变,低频带状分量从 6.4844 压到 1.2 上下;蓝噪声更把阈值矩阵的低频功率占比压到同尺寸白噪声的 1/243。
时间抗锯齿的全部机制可以压成三个整数:帧间权重 w = ONE//(k+1)、确定性的 Halton 子像素抖动、历史帧的 3×3 邻域裁剪。本节在解析圆盘上实测——静止相机下 1→16 帧的 MAE 从 1.9163 收敛到 0.3881;每帧微动 0.25 px 时,裁剪把重影误差从 9.2405 压到 4.929。
同一份场景、同一套顶点变换、同一个 Lambert 公式,只把「插值颜色」换成「插值法线再逐像素求值」:覆盖掩码与深度缓冲逐像素相同(几何封版),颜色却有 41261 个像素不同(537.2526‰),MAE 3.5764、PSNR 37.06 dB、最大通道差 48。
Phong 高光要构造反射向量 R = 2(N·L)N − L 再算 R·V 的幂次,Blinn-Phong 改用半步向量 H。本节用 66 个观察方向实测两者的叶宽、最大差与运算量,把 x^32 的平方链(5 次 fmul)与 31 次逐次相乘逐位比对,并推翻「Blinn-Phong 不会照亮背面」这句流传很广的话。