16.2 三种消费的联调
同一份 scene_spec.json 在 Python 定点、TypeScript 定点、浏览器 WebGL2 三处消费:S 与 P 成片 sha256 逐字节相同(0d997343…,43731 覆盖像素),S 与 W 按声明容差比对为 MAE 0.5067、PSNR 47.03 dB、覆盖归属差 0.0781‰;负例(片元颜色 ×0.5)MAE 21.3528 被同一判据拒绝。
tag
同一份 scene_spec.json 在 Python 定点、TypeScript 定点、浏览器 WebGL2 三处消费:S 与 P 成片 sha256 逐字节相同(0d997343…,43731 覆盖像素),S 与 W 按声明容差比对为 MAE 0.5067、PSNR 47.03 dB、覆盖归属差 0.0781‰;负例(片元颜色 ×0.5)MAE 21.3528 被同一判据拒绝。
把卷二前面十五节的环节首尾相接成一条从规格到成像的链:S 层在 320x240 场景上逐级核对十二个缓冲契约,w 不变量 487/487 成立、重心权重和违反 0 次、覆盖 43731 像素,成片 sha256 与卷二基线逐字节一致;P 层用 BigInt 重算同一链路复现同一摘要,负例(顶点顺序交换)必须让摘要分叉。
把同一份场景交给 WebGL2 渲染,再与 S 层定点基线逐像素比对:MAE 0.5073、PSNR 46.95 dB、覆盖归属差异 0.08‰,与 12.1 的 CPU 浮点镜像 0.4963 落在同一噪声地板上。不一致像素按「共享边归属 0.0% / 插值精度 11.3% / 舍入 88.6%」三类归因,并如实交代本机后端是 ANGLE/SwiftShader 软件实现,不是真实 GPU。
sha256 是「全等判据」,只在「全等是合理期望」时成立——浮点实现的 sha256 与基线不同,而它的 MAE 只有 0.4963。本节给出 MAE / RMSE / PSNR / 最大通道差四个标量的定义与各自盲区:整幅 +1 与 4 像素 +64 的 MAE 完全相同,只有 RMSE 与最大通道差分得开;覆盖归属差异与颜色误差是两条独立判据,必须分两栏报告。
调色的整数形态是查表:对比度绕 128 旋转、色温用逐通道偏置,合成三张 256 项的一维 LUT;饱和度写成 luma + (s·(in-luma) >> 16)。本节量化 8 位 LUT 的往返代价与三维 LUT 的节点数误差,并给出 W 层浮点着色器与 S 层整数结果的声明容差比对(含被拒绝的负例)。
泛光不是「把亮的地方涂糊」,而是一条四步整数链:Rec.601 亮度做整数阈值提取亮部、每级减半的 2×2 盒式降采样、整数双线性上采样、以及权重和恰好为 ONE 的合并。本节扫描级数 2/4/6、阈值 64/96/128、强度 0.25~2.0,并给出金标准泛光帧的 sha256。
后处理的第一步是用核卷积。本节把卷积写成全整数:Q16.16 核权重之和必须恰好等于 ONE,否则整帧会整体偏亮或偏暗;边界有 clamp/wrap/mirror 三种;半径 r 的二维核拆成两次一维可分离卷积,半径 8 时实测快 8.86 倍。Python 与 TypeScript 在四个半径下的模糊缓冲 sha256 逐字节相同。
把 CPU 定点与浏览器 WebGL2 之间的差异拆成三类可归因的来源:光栅化规则(顶左规则与子像素精度)、插值方式(屏幕空间仿射对透视校正)、浮点精度(Q6/Q16.16 对 float32)。每类配一个只动一个变量的实验,并用 MAE ≤ 1.0、PSNR ≥ 30 dB、覆盖归属差异 ≤ 20‰ 三条声明容差加一个必须被拒绝的负例来收口。
有了求交点,法线靠差分、软阴影靠沿光取样,两者都不是免费午餐:差分步长 h 太大会跨过棱角、太小会被定点量化吃掉,实测中心差分在 h=64 时均值 0.0342 度;软阴影有一个系统性平面偏置 k*sin(theta),k 必须超过 1/sin(theta)≈1.732 才能让开阔地面回到满亮度。
把阴影偏差解析地推出来:深度 1 LSB 对应 80 个 Q16.16 世界单位,纹素世界尺寸 20480、tan(theta)=1.4142,故解析上界 28963,而实测每像素所需偏差中位数只有 1、p95 184。再扫出偏差权衡曲线:bias 从 0 加到 4096,自遮挡从 22360 降到 0,阴影脱离从 17 升到 459;分辨率 64² 到 512² 自遮挡降 38 倍。
噪声的终点是可用的纹理。本节把 fBm 与距离场组合成云、木纹、石纹三张 64×64 贴图,全部走整数:距离用 fsqrt、环带与脉线用整数取模、颜色是 8 位通道的整数混合。判据是像素缓冲的 sha256——Python 与 TypeScript 合并缓冲同为 4ef471f3…,共 36864 字节逐字节相同。
单层噪声太规整,fBm 把它叠成分形。本节用整数实现 octaves / lacunarity / gain 三个旋钮,在 0.125 世界单位步长上扫描 1/2/4/8 层:归一化粗糙度 0.0405 → 0.0509 → 0.0714 → 0.0918 单调上升,方差反而从 13653 降到 8481;再做整数域扭曲,看细节被拧成条带。
站内那篇讲的是大地形 LOD、分块与流式加载的工程实现,本节把噪声函数本身拆成可逐值断言的整数。值噪声与 Perlin 共用同一个整数哈希,只在格点上放标量值还是梯度向量:128×128 实测两者均值 32138 与 98,相邻差分均值 4703.5 与 7464.1,功率谱峰值分别落在第 0 与第 2 个频带。
从最近邻到三线性:双线性用四个 Q16.16 权重累加后一次 >> 32 降精度;mip 链是逐级 (a+b+c+d) >> 2,奇数尺寸要决定最后一行列是保留还是截断;LOD 用整数位长求 floor(log2)。对 4×4 超采样参考解,最近邻 MAE 54.41、mipmap 14.15、三线性 10.56。
把 sRGB 接进卷二着色链路并量出代价:8 位量化误差最大 218 Q16.16、均值 40.10,往返不变量残差 0 灰度级;线性域取中点与 sRGB 域取中点最多差 61 灰度级;端到端「线性着色 + 编码」与「直接在 sRGB 域着色」MAE 15.57、PSNR 24.28 dB。
把 Reinhard、扩展 Reinhard(白点)与 ACES filmic 近似三条色调映射曲线写成全整数,喂同一份《烛影山谷》HDR 辐射度(曝光 6.0 后 max 4.33),实测输出曲线与 8 位直方图:Reinhard 均值 165.89 不裁剪、扩展 Reinhard 裁掉 156 像素、ACES 均值 213.43。
把 sRGB 传递函数固化成整数查找表:解码表 256 项一次生成后冻结,编码用整数二分在边界表上反查,Python 与 TypeScript 读同一份整数故逐位一致。实测 8 位往返残差 0 灰度级、线性往返最大误差 292 Q16.16,并给出线性段与幂段的分段边界。
6.1 的 BRDF 和 6.2 的参数映射都默认反射能量不超过入射能量,但 BRDF 本身给不出这个保证。本节把 ∫f·cosθ·dω≤1 变成一个可跑的整数实验:GGX 各粗糙度的残差表、alpha=1 处的解析锚点 1-ln2、单次弹射丢掉 69.3% 能量、Kulla-Conty 补偿把它补回 1,以及 kD=1-F 为什么守不住掠射角。
金属度-粗糙度工作流只用两个标量描述一个表面:基础色在绝缘体时是漫反射 albedo、在金属时是镜面 F0,粗糙度平方后映射成 GGX 的 alpha。本节扫过粗糙度 0/0.25/0.5/0.75/1,量出 NDF 峰值从 809 掉到 0.32、半高角从 0.76 度张到 90 度,并说明为什么高光宽度要用 NDF 半高角而不是 BRDF 的半高全宽。
微表面理论把镜面反射拆成 D、G、F 三个因子:GGX 分布 D 决定高光形状,Smith 几何项 G 补偿微面互相遮挡,Schlick 菲涅尔 F 决定反射比例。本节在 Q16.16 里把它们写成纯整数,用投影圆盘方向集验证 ∫D(n·h)dω≈1,并量出 alpha=0.25 时镜面波瓣的半高全宽是 40.74 度。