Hotdry.

Article

大气散射天空渲染:日落渐变与行星大气层的GPU实现路径

基于Rayleigh-Mie散射与raymarching,详解天空颜色随时间变化的实现、日落橙红渐变的 transmittance 计算,以及行星大气层的 ray-sphere intersection 建模与 LUT 优化路径。

2026-05-12systems

在大气散射渲染领域,理论原理与工程落地之间存在显著鸿沟。昨天我们探讨了 Rayleigh-Mie 大气散射的 WebGL 通用实现框架,今天我们将视角聚焦到三个具体工程问题:天空颜色随太阳角度的动态变化、日落时橙红渐变的精确模拟,以及行星大气层的壳式建模。这三个问题构成了实时天空渲染的核心挑战,也是游戏与可视化项目中经常遇到的痛点。

Rayleigh-Mie 散射的天空颜色建模

理解天空为何呈现蓝色,是构建可信大气渲染系统的基础。Rayleigh 散射定律指出,光线与大气分子的相互作用强度与波长的四次方成反比 —— 这意味着短波长(蓝光)的散射效率远高于长波长(红光)。在 Shader 中,这一特性通过 Rayleigh 散射系数向量体现:

const vec3 RAYLEIGH_BETA = vec3(5.5e-6, 13.0e-6, 22.4e-6);

当阳光进入大气层,蓝光在大气分子间反复散射,最终从各个方向进入观察者眼中,形成我们所见的 "天空蓝"。然而,仅有 Rayleigh 散射还不够 —— 它产生的蓝色过于饱和,缺乏真实大气中那种微妙的渐变与深度感。

Mie 散射解决了这个问题。它描述光线与较大气溶胶粒子(尘埃、水滴、污染物)的相互作用,特点是前向散射占主导,产生围绕光源的明亮光晕。在日落时分,当太阳靠近地平线,Mie 散射产生的光晕效果尤为显著,形成天空中那种朦胧的橙黄色边缘。实现 Mie 散射需要两个关键函数:

float miePhase(float mu) {
    float gg = MIE_G * MIE_G;
    float num = 3.0 * (1.0 - gg) * (1.0 + mu * mu);
    float den = 8.0 * PI * (2.0 + gg) * pow(max(1.0 + gg - 2.0 * MIE_G * mu, 1e-4), 1.5);
    return num / den;
}

float mieDensity(float h) {
    return exp(-max(h, 0.0) / MIE_SCALE_HEIGHT);
}

其中 MIE_G 控制前向散射的偏心程度,通常取 0.76 到 0.8 之间的值可以实现较为自然的太阳光晕效果。

臭氧吸收是另一个被初学者忽视的因素。臭氧层位于平流层上方,它不散射光线,但会选择性吸收红光与部分紫光。这种吸收效应使得天空在日落时呈现特有的紫色调,在高纬度地区尤为明显。工程实现中,臭氧密度函数通常定义为:

float ozoneDensity(float h) {
    float ozoneCenterHeight = 25.0;
    float ozoneWidth = 15.0;
    return max(0.0, 1.0 - abs(h - ozoneCenterHeight) / ozoneWidth) * 0.6;
}

日落橙红渐变:light marching 与 transmittance 计算

实现可信的日落效果,远非简单改变天空颜色那么简单。真正的挑战在于:当日光穿过大气层到达观察者时,路径上的分子密度差异会导致不同波长光的衰减程度各不相同。

在 raymarching 框架中,每个采样点需要 "询问":从太阳到此处,有多少光线能够穿透大气?答案通过嵌套的 light marching 循环获得:

vec3 lightMarch(float start, float sunY) {
    float denom = max(sunY + 0.15, 0.04);
    float maxDist = (ATMOSPHERE_HEIGHT - start) / denom;
    float stepSize = max(maxDist, 0.0) / float(LIGHTMARCH_STEPS);
    
    float odR = 0.0, odM = 0.0, odO = 0.0;
    for (int i = 0; i < int(LIGHTMARCH_STEPS); i++) {
        float t = (float(i) + 0.5) * stepSize;
        float h = start + t * sunY;
        if (h < 0.0 || h > ATMOSPHERE_HEIGHT) continue;
        
        odR += rayleighDensity(h) * stepSize;
        if (uMieEnabled) odM += mieDensity(h) * stepSize;
        if (uOzoneEnabled) odO += ozoneDensity(h) * stepSize;
    }
    return vec3(odR, odM, odO);
}

结合 view direction 上的光深,总 transmittance 通过 Beer's Law 计算:

vec3 sunOD = lightMarch(h, sunDirection.y);
vec3 tau = BETA_R * (viewODR + sunOD.x)
         + BETA_M_EXT * (viewODM + sunOD.y)
         + BETA_OZONE_ABS * (viewODO + sunOD.z);
vec3 transmittance = exp(-tau);

当日落时,光线穿越大气层的路径急剧增长,蓝光在长路径上几乎完全散射殆尽,而红光因散射效率低得以保留更多,这正是天边呈现橙红色的物理原因。当太阳完全没入地平线后,只剩下散射到高空中的少量蓝光,形成 "蓝色时刻"(blue hour)特有的深蓝紫色天空。

工程实践中,建议设置 sunAngle uniform 范围为 [0, π],其中 0 表示天顶,π/2 表示地平线。通过调整这个参数,可以观察到天空颜色从正午的浅蓝、到下午的金黄、再到日落的橙红、直至夜幕降临后的深蓝紫的连续变化。

行星大气层建模:ray-sphere intersection 与深度重建

将天空渲染从平面背景升级为行星大气层,需要解决两个核心问题:大气壳的几何定义,以及与场景几何的深度交互。

首先,使用 ray-sphere intersection 计算观察射线与大气球壳的交点:

vec2 raySphereIntersect(vec3 rayOrigin, vec3 rayDir, vec3 center, float radius) {
    vec3 oc = rayOrigin - center;
    float b = dot(oc, rayDir);
    float c = dot(oc, oc) - radius * radius;
    float discriminant = b * b - c;
    if (discriminant < 0.0) return vec2(-1.0);
    float sqrtDisc = sqrt(discriminant);
    float t0 = -b - sqrtDisc;
    float t1 = -b + sqrtDisc;
    return vec2(min(t0, t1), max(t0, t1));
}

对于行星场景,还需要检测射线是否先击中星体表面 —— 如果存在这一交点,应将大气层的光线终止点设置为该表面而非大气外缘。这保证了大气渲染不会出现在行星 "内部" 的错误位置。

深度重建是连接 post-processing 效果与 3D 场景的桥梁。通过从深度缓冲区读取数据并结合投影矩阵逆,可以重建每个像素对应的世界空间位置:

vec3 getWorldPosition(vec2 uv, float depth) {
    float clipZ = depth * 2.0 - 1.0;
    vec2 ndc = uv * 2.0 - 1.0;
    vec4 clip = vec4(ndc, clipZ, 1.0);
    vec4 view = projectionMatrixInverse * clip;
    vec4 world = viewMatrixInverse * view;
    return world.xyz / world.w;
}

在行星尺度上,必须使用对数深度缓冲(logarithmicDepthBuffer)来避免深度冲突。由于大气层厚度相对于行星半径极小(地球大气层约 100km,而地球半径约 6371km),普通浮点深度在高空中几乎无法区分大气与星体表面。

LUT 优化路径与关键参数配置

直接 raymarching 的方法虽然直观,但计算成本极高。以 24 步主循环加 16 步 light marching 计算,每像素需要约 400 次采样。在 1080p 分辨率下,这已经接近数十亿次的计算量,难以满足实时渲染需求。

Sebastian Hillaire 在 2020 年提出的 LUT 优化方案将昂贵的散射计算预计算为纹理:三张 LUT 分别存储 transmittance(250×64)、sky view(天空颜色)、以及 aerial perspective(场景雾效)。

Transmittance LUT 的生成逻辑如下:沿 x 轴变化光线角度(mu ∈ [-1, 1]),沿 y 轴变化高度(planetRadius → atmosphereRadius),对每个像素执行 raymarching 并存储结果。这种纹理使得后续的天空渲染只需查表而非重新计算。

工程实现中需要注意几个关键参数:

参数 典型值 说明
RAYLEIGH_SCALE_HEIGHT 8.0 km 大气标高,决定密度衰减速率
MIE_SCALE_HEIGHT 1.2 km Mie 散射粒子分布高度
ATMOSPHERE_HEIGHT 100 km 大气层外缘高度
PRIMARY_STEPS 24 主 raymarching 步数
LIGHTMARCH_STEPS 16 光线步数

对于火星大气模拟,需要调整参数为:rayleighScaleHeight = 11.1、rayleighBeta = vec3 (0.019, 0.013, 0.0057),同时禁用臭氧吸收(ozoneBetaAbs = vec3 (0.0))。这种调整会产生特有的橙红色大气与蓝色日落 —— 与地球截然不同的视觉特征。

日食与遮挡的扩展处理

在实际场景中,行星大气渲染还需要处理天体遮挡带来的光照变化。例如当地球遮挡太阳时,大气层中的散射光量急剧减少,观察者看到的是被 "剪影" 化的暗红日出效果。实现这一效果的 sunVisibility 函数需要考虑三种情况:天体完全在太阳前方、部分遮挡、以及完全在太阳后方。核心算法通过角距(angular separation)与角半径(angular radius)的比较来确定遮挡程度。

总结

本文从工程实现角度探讨了大气散射渲染在天空颜色变化、日落渐变、以及行星大气建模三个维度的问题。核心方法论包含:使用 Rayleigh/Mie/Ozone 三分量散射模型建模大气光学特性;通过嵌套 light marching 计算 transmittance 以实现日落橙红效果;利用 ray-sphere intersection 定义行星大气壳的几何边界;以及通过 LUT 预计算方案优化实时渲染性能。

资料来源:Maxime Heckel - On Rendering the Sky, Sunsets, and Planets

systems

内容声明:本文无广告投放、无付费植入。

如有事实性问题,欢迎发送勘误至 i@hotdrydog.com