WorldEnvironment 里切到物理天空(PhysicalSkyMaterial),太阳高度角一拖,天边泛出橙红色、头顶保持深蓝——这不是 LUT 贴图,是 shader 里实时积分大气散射方程的结果。这篇拆 servers/rendering/renderer_rd/shaders/sky.glsl 的实现:Preetham 模型在 GPU 上怎么算得动,以及那些参数背后的物理量。
单次散射的物理方程
太阳光进大气后发生两类散射:Rayleigh(空气分子,强度 $\propto 1/\lambda^4$,所以蓝天)和 Mie(气溶胶,前向性强、与波长弱相关,所以雾霾和日晕)。某点 $\mathbf x$ 看向方向 $\mathbf d$ 的辐射亮度是沿视线的积分:
$$ L(\mathbf d) = \int_0^{s_{max}} \Big[\beta_R e^{-\tau_R(s)} P_R(\cos\theta) + \beta_M e^{-\tau_M(s)} P_M(\cos\theta, g)\Big] \cdot E_{sun} \cdot e^{-(\tau_R + \tau)(s_{sun})}\, ds $$
四项的直觉:$\beta$ 是散射系数(多容易散)、$e^{-\tau}$ 是透射率(光从太阳走到散射点、再从散射点走到相机,各自被衰减多少)、$P$ 是相位函数(往你这个方向偏的概率,$\theta$ 是相对方太阳的角度)、$E_{sun}$ 是太阳辐照度。 sunset 之所以红,是因为日落时 $s$ 和 $s_{sun}$ 都变长,$e^{-\tau_R}$ 对短波(蓝)衰减远大于长波(红),剩下的主要是红光。
精确积分是 nested 的(每视线步还要沿太阳方向再积一次光学深度),实时不可行。
Preetham 近似:把积分折进解析式
Preetham 1999 的贡献是把整坨积分拟合成少量解析公式:透射率用 Chapman 函数近似(球面大气下的光学深度有闭式解),Rayleigh/Mie 的角分布用 $\cos\theta$ 的多项式,整体系数按太阳天顶角查一组经验系数。Godot 的 sky.glsl 基本按这个路子实现:
- 光学深度:对视线段和太阳段各算一次 Chapman 近似,O(1) 开销拿到 $e^{-\tau}$。
- 多重散射:单次散射的式子会让背光面死黑(真实天空的亮蓝有很大部分是多 scattering)。Godot 加一个
multi_scatter参数做经验补偿——不是物理多散射,是"给暗部补一个按透射率衰减的环境项",视觉上等价于把多次散射的平均效果折算进去。 - 臭氧 与 地面:
use_ozone开关加一层臭氧吸收(Chappuis 带,日落时增强红色);低于地平线的视线段 clamp 到地面,地面反照率项ground_color给地平线附近加一点反弹光。
这也让夜空不是纯黑有了出处:即使太阳在天平以下 $-18°$(天文暮光结束),散射项仍然非零,加上默认环境光最低值,物理天空给出的夜天亮度约 $10^{-4}$ cd/m² 量级——比纯黑亮几个数量级,这正是"夜晚场景不开灯也微微可见"的原因。
为什么不做预计算 LUT
Unreal 的方案是把散射积分烘成 2D/3D LUT(Bruneton 模型),运行时查表,质量更高且支持多散射的准确版本。Godot 选了每帧每像素直接算解析式,理由是:sky 通常只占屏幕一小部分(除非飞模拟游戏),解析式的几十个 ALU 指令比 LUT 的一次双线性采样 + 生成 LUT 的每帧成本(太阳一动 LUT 就要重烘)更省;且 Godot 的 sky 还支持程序化云、星空等附加层,LUT 方案要为此扩展表维度。代价是精度——多散射是经验补偿,极地低太阳角的色偏不如 Bruneton 方案准。
一图流
实战推论
sun_disk_scale和物理无关,是太阳盘的艺术缩放;物理准确的太阳角直径只有 0.53°,默认 1.0 就是这个值,调大是风格化需要。- 低太阳角出"脏橙色"多半是 Mie 系数过大(
mie_coefficient)或turbidity偏高——这两个参数实际控制"空气有多脏"。 - 夜戏不要直接把
energy_multiplier拉到 0:物理天空的夜晚亮度本来就接近但非零,直接归零会让环境光采样和雾计算失去参照,出现"有雾无环境"的诡异画面。
本文基于 Godot 4.x 源码:servers/rendering/renderer_rd/shaders/sky.glsl、scene/resources/sky.h(PhysicalSkyMaterial/ProceduralSkyMaterial)、servers/rendering/renderer_rd/environment/sky.cpp(Sky RD 后端)。