环境光遮蔽(AO)和屏幕空间间接光(SSIL)在 Godot 里是两个开关,但翻 effects/ss_effects.cpp 会发现它们共享同一套采样框架——SSIL 本质是把 SSAO 的"可见性积分"换成"光能积分"。这篇拆 4.x 的 SSAO/SSIL 实现:采样数学、半分辨率流水线、以及那串 blur/upsample 各自在防什么。
从 AO 到 SSIL:换被积函数而已
SSAO 的模型: shading point $\mathbf p$ 处,法线 $\mathbf n$ 的上半球被几何遮挡的比例。对半球内方向 $\boldsymbol\omega$ 采样深度点 $\mathbf q$,若 $\mathbf q$ 在表面内(深度更大),则该方向被挡。写成积分:
$$ \text{AO}(\mathbf p) = 1 - \frac{1}{\pi} \int_{\Omega^+} V(\mathbf p, \boldsymbol\omega)\, (\mathbf n \cdot \boldsymbol\omega)\, d\boldsymbol\omega $$
SSIL 把可见性 $V$ 换成 radiance:不挡的方向往屏幕里看,把看到的像素颜色 $L(\mathbf q)$ 当成"从那个方向来的间接光":
$$ L_{ind}(\mathbf p) = \frac{1}{\pi} \int_{\Omega^+} (1 - V(\mathbf p, \boldsymbol\omega))\, L(\mathbf q)\, (\mathbf n \cdot \boldsymbol\omega)\, d\boldsymbol\omega $$
同一组采样点、同一个深度比较,一个统计"挡没挡"、一个累加"挡到的东西亮不亮"。所以 Godot 把它们合在一个 shader 族里(ssao.glsl 带 USE_SSIL 变体),成本也是共享的:SSAO 纯标量输出,SSIL 多写三个颜色通道。
这个近似的硬伤写在公式里:$L(\mathbf q)$ 是屏幕上看到的光,屏幕外(被挡住的)几何的 radiance 一律缺失——所以 SSIL 只对"从屏幕可见物体反弹的光"有效,角落里的间接光它永远给不出。这是屏幕空间方法的天花板,不是实现问题。
spiral 采样:为什么不用随机旋转
每像素采 $N$ 个点(low/medium/high/ultra 对应 8/12/16/20),如果纯随机,需要每像素一个旋转的 noise 纹理,否则相邻像素采样 pattern 相关,结果是低低频噪点。Godot 用的是螺旋线采样:采样点沿阿基米德螺旋铺在上半球投影盘上,第 $i$ 个点距中心 $r_i = \frac{i}{N}$、角度 $\theta_i = i \cdot \theta_{step}$,$\theta_{step}$ 取 $2\pi \cdot 0.618$(黄金角)。黄金角让相邻采样的角度分布尽量均匀而无周期——不用 noise 纹理、不用随机数、相邻像素天然去相关。这是业界(HBAO/Intel 的示例)的标准做法,Godot 原样采纳。
采样半径由 radius 参数控制,但半径要按深度线性缩(近处 1 世界单位的 AO 在远处可能不足半个 texel),shader 里用 radius * depth 做像素空间偏移,同时 clamp 到最大像素数防远处闪。
半分辨率流水线:三个 pass 各防什么
pass 1: 半分辨率渲染 AO/IL 缓冲(ssao.glsl)
pass 2: 深度感知双边 blur(ssao_blur.glsl,半分辨率)
pass 3: bilateral upsample 到全分辨率(混入全分辨率深度判断)
为什么不直接全分辨率算?成本:$N$ 次采样的 sphere-project 重建(每个采样点要从深度 buffer 重建世界坐标做比较)是全链最贵的 ALU,半分辨率直接省 4 倍。但半分辨率单独 upsample 会产生泄漏——低分辨率的 AO 块边界糊掉,本该遮蔽的物体边缘漏光。所以 blur 和 upsample 都带深度权重:相邻像素深度差超阈值就不混合。pass 3 拿半分辨率结果和全分辨率深度对照,边缘处直接采用全分辨率深度重建的 fallback,代价是边缘一圈 AO 略弱——这是性能换质量的标准取舍,"AO 发虚"的观感多半来自这里,不是采样数不够。
一图流
实战推论
- AO 有噪点先加采样数(8→16),不要先开 blur 半径——spiral 的噪是采样不足,blur 只能糊。
- SSIL 不该和 SDFGI 同开:两者都产间接光,SSIL 的屏幕空间部分会被 SDFGI 重复计算,表现为亮部过曝。官方默认配置里也是互斥推荐。
intensity和power是两个参数:前者缩放结果,后者做曲线($AO' = AO^{1/power}$ 形式),想"加深角落"调 power,想整体变暗调 intensity,混调会反复横跳。
本文基于 Godot 4.x 源码:servers/rendering/renderer_rd/effects/ss_effects.cpp、servers/rendering/renderer_rd/shaders/ssao.glsl、ssao_blur.glsl(半分辨率流水线与深度权重)。