NEKOPARTY STUDIO

Godot高性能完整物理天空+体积云实现(上)

字数统计: 2k阅读时长: 7 min
2026/06/04
loading

前言

白天效果

夜晚效果

此天空实现大量参考了Godot源码主要贡献者ClayJohn在一个开源Repo里的实现,并作出了大量修改和扩展。本文会详细解析他的实现并说明我自己做了什么样的内容以及扩展。

现在的天空包含完整的基于物理的昼夜循环,以及高质量(几乎无噪音)的体积云渲染,且速度极快。

这套实现天空部分比较标准,但是体积云部分的渲染策略非常罕见,性能比起3A游戏的普遍标准是数量级级别的好(ClayJohn自己的代码就是这种实现,我基本抄了),但是也有相当的局限性(我通过一些Trick进行了改进,但是治标不治本),是一个需要取舍的工程问题。

如果你需要一个标准的3A级别的体积云实现,你可以使用GodotAssetLib里面的SunshineClouds这个插件。但是这个插件性能问题不小,且云的降噪写的质量比较差,实际视觉效果可能不理想。如果你没有这么高要求的体积云,其实可以看看我这篇(的下一篇)

整套天空渲染架构有四级渲染管线,这里我们一一分析。由于篇幅所限,体积云会在下一部分详细分析。

天空,透射率LUT

整个大气渲染,其实就是在计算光从大气里的A点到B点还剩多少比例的问题。由于这个量只依赖于两个量,所以可以预计算成一张二维查找表,也就是这里的透射率LUT。计算好之后直接用采样纹理的方式取即可。

光沿路径衰减遵循 Beer–Lambert 定律。设 $\sigma_t(h)$ 是高度 $h$ 处的消光系数(extinction coefficient),单位 $\text{km}^{-1}$,表示每走 1 km 损失的比例。那么沿路径 $A\to B$ 的透射率是消光系数的路径积分再取负指数:

$$
T(A \to B) = \exp!\left(-\int_{A}^{B} \sigma_t\big(h(s)\big), ds\right)
$$

所以ClayJohn的天空实现里的transmittance-lut.glsl实际就是在做这个积分。

透射率原本依赖:起点位置(3D)+ 方向(2D)+ 终点。看似维度很高。但是在球对称大气假设下可以大量简化。可以认为起点位置只和高度有关。这样起点就简化为了一维参数(高度)。假设光只来自于无穷远的太阳,我们就能把终点固定在大气顶。这样方向也就只剩下了一个角度:就是太阳到水平平面角度。

这样就把输入参数简化为了两个变量,从而让查找表做法成为可能。

在实际的操作过程中,LUT 被渲染为了 256 * 64 的图像。UV的x分量是太阳角度的余弦值。UV的y分量是海拔。

除此之外,ClayJohn的实现还有个Trick(可能是更好的方式,并非Trick),就是所有波长相关量都是 vec4,分别对应 630 / 560 / 490 / 430 nm的波长,计算完之后再转换为sRGB。这应该能带来很明显的视觉准确度提升。

在实际的glsl计算中,get_atmosphere_collision_coefficients()这个函数包含了经典的Rayleigh,Mie和臭氧吸收计算。

Rayleigh

get_molecular_scattering_coefficient()中是这么写的

1
return molecular_scattering_coefficient_base * exp(-0.07771971 * pow(h, 1.16364243));

其中,molecular_scattering_coefficient_base = vec4(6.605e-3, 1.067e-2, 1.842e-2, 3.156e-2)。

蓝光(最后一个分量 3.156e-2)系数是红光(6.605e-3)的近5倍,因为蓝光被散射得多,所以天是蓝的。exp(-0.07… * pow(h, 1.16…)) 是对真实大气密度随高度衰减的经验拟合,比简单的指数大气更准确一点。

Mie

Mie主要实现雾霾和地平线泛白的效果。写在get_aerosol_density()函数。

1
return aerosol_base_density * (exp(-h / aerosol_height_scale) + aerosol_background_divided_by_base_density)

参数来自 Guimera et al. (2018) 的物理大气模型。

臭氧吸收

这个效果是让天空顶部更蓝。写在get_molecular_absorption_coefficient()函数

1
2
3
4
h += 1e-4;
float t = log(h) - 3.22261;
float density = 3.78547397e20 * (1.0 / h) * exp(-t * t * 5.55555555);
return ozone_absorption_cross_section * ozone_mean_monthly_dobson * density;

h加了一点防止数值问题。

完成

之后就是RayMarch,并把积分结果写进查找表里。ClayJohn的实现里,这个March了40个Steps。由于只要在开头进行一次计算,所以步数多点也无所谓。

最后计算出的LUT大约是这样的。

LUT结果

我们看一下这个LUT。明确两点:

  1. 从左到右依次代表太阳在地平线(最低)和太阳在顶部(最高)。纵轴代表海拔,越往上越高。

  2. 这张纹理是 RGBA16F,存的是 4 个波长的透射率(630/560/490/430nm),不是 RGB+透明度。第 4 个波长 430nm 正好落在 alpha 通道。不过Godot的预览就这样了我也没招了。

主要中间有一条蓝紫色的窄带,是物理正确的表现,说明臭氧项的作用正确。

sky-view LUT,绘制结果

下面就说明sky-view LUT,以及如何把结果绘制成天空结果。

Sky-view LUT更进一步,其结果是为了说明朝天空某个方向看过去,沿这条视线累积进眼睛的散射光总共是什么颜色。它把整个半球(其实是全方向)的天空颜色预计算成一张 200×100 的小图。运行时无论是云着色器还是天空着色器都查询这个纹理就行了,非常便宜。

实际计算过程在sky-lut.glsl里,把像素映射成一条视线方向:

1
2
3
4
5
6
7
8
9
vec2 uv = vec2(pos) / params.texture_size;
float azimuth = 2.0 * PI * uv.x; // 横轴 = 方位角,整圈 360°

float l = uv.y * 2.0 - 1.0; // [0,1] → [-1,1]
float elev = l*l * sign(l) * PI * 0.5; // 非线性仰角 [-π/2, π/2]

vec3 ray_dir = vec3(cos(elev)*cos(azimuth),
cos(elev)*sin(azimuth),
sin(elev)); // 注意:z 是"上"

可以注意到,仰角用了非线性采样 l*l*sign(l) ,这是因为天空颜色在地平线附近变化最剧烈(回想一下夕阳和日出的效果),而天顶方向几乎是均匀的颜色。这样设计的目的是把更多的纹理密度给到地平线附近,防止地平线附近的纹理密度不够,最后出现明显的Artifact(比如产生条带)

当然了,采样的时候必须还原这个变换。

之后这个Compute Shader就是计算内散射积分。沿视线 Ray-March 30 步,每步把”散射进来的光”累加,同时维护视线自身的透射率。

实际计算每个位置太阳到这个位置光线的结果的时候,就直接查前文的LUT就行了。(当然,按理来说你也可以在这里头直接计算,但是😅)

到达该点的阳光被散射系数和相位函数重新分配方向,其中朝向视线的那部分加进S。两种成分(Rayleigh,Mie)分别算再相加。

除此之外还做了一个能量守恒积分。

1
S_int = (S - S·step_transmittance) / extinction

这是引用自 Hillaire/Frostbite 的实现(太有学术规范了)。

除此之外,还做了多重散射近似。真实大气里光会弹很多次。用两项廉价的近似补上:

1
2
3
4
5
// 地面二次反照
vec4 L_ground = PHASE_ISOTROPIC * omega * (GROUND_ALBEDO/PI) * T_to_ground * T_ground_to_sample * cos_theta;

// 大气多重散射的经验拟合(Hillaire)
vec4 L_ms = 0.02 * vec4(0.217, 0.347, 0.594, 1.0) * (1.0 / (1.0 + 5.0*exp(-17.92*cos_theta)));

整个积分都在 4 波长(630/560/490/430nm) 上进行,上文已经说了。最后用 4×3 矩阵转回可显示的 linear sRGB。不过储存的是高动态范围的结果,在实际采样的时候会用一个经验参数除回去到正常的颜色空间。

之后天空Shader去采样SkyLUT即可。太阳和月亮是天空Shader负责绘制,这部分比较简单就不再叙述了。

最后(某个时段)的天空LUT是这样的:

SkyLut

下篇会详细介绍体积云的实现,是文章的重点。我们下次再见~

CATALOG
  1. 1. 前言
  2. 2. 天空,透射率LUT
    1. 2.0.1. Rayleigh
    2. 2.0.2. Mie
    3. 2.0.3. 臭氧吸收
    4. 2.0.4. 完成
  3. 2.1. sky-view LUT,绘制结果