一 、综述
大气散射是对现实世界大气现象的描述,其影响包括不限于白天和傍晚天际线的大气颜色变化,丁达尔现象的产生,人眼对于地球大气层的视觉感受。而对于追求“真实”的CG领域来说 大气散射则显得尤为重要,决定了大气层的真实与否。我们通常用 ray-march,path-tracing 方法来研究大气散射「光线在大气中是如何传输的」。

当前系统实现了一套基于物理的高级大气散射渲染管线,参考UE 引擎 [Hillaire 2020] 方案。 [Hillaire 2020] 是在 [Bruneton08] 基础上的创新,这篇论文最后达到的效果为:
- 在保证效果跟path tracing的GT接近的情况下,比以前的预计算LUT,实时采样LUT [Bruneton08]的效率只稍微慢一点;
- 有一定的LUT要素,但是LUT完全可以实时计算(大气参数可以实时修改,而且不是分帧更新之类的伪实时,可以做到一帧搞定);
- 在iPhone6s上可以在每帧1ms以内搞定(包括了LUT计算);
- 做了一些近似假设,从而可以用数值方法计算所有高阶散射之和;而不是像之前的方法只计算一定的阶数就结束。在大气稀薄的情况下(高阶散射更加重要)效果提升非常明显。
二、物理模型与散射理论

光线在大气中是如何传输
我们可以从光线传输方程、宏观几何模型、微观介质属性三个维度来快速回顾下**基于物理的大气散射模型 **,更详细的理论基础详见链接文章。
光线传输方程与积分 (Light Transport)
构建光路
我们首先来讨论光线在大气中是如何**传输**的。如下图所示,即使一条从摄像机出发的光线没有直接命中太阳,它也能为当前像素带来颜色的贡献。考虑最简单的 单次散射 光路就能很快想明白为何天空不是纯黑色的,因为本来不应该经过视线的光由于空气粒子的反弹进入了视线,形成光路。

光线在空气中主要会发生两种物理现象:散射和透射。散射是指一束光和大气层中的微粒发生碰撞之后,众多粒子各奔东西四散而逃的物理现象。我们把逃逸到视线方向进而产生颜色贡献的光照能量记作

透射(Transmittance)描述的是光在介质中穿行所造成的能量衰减,记作

考虑上面提到的单次散射。这样一条简单的光路由 “透射-散射-透射” 三部分组成,它对于当前着色像素的贡献是
太阳光首先经过
衰减,剩余 能量 在某点发生散射,向视线方向散射出
能量 随后在到达视线的路途中发生
衰减 最终剩余的能量为

这只是 “视线-大气层” 连线中某点散射形成的一条光路。事实上在大气层内太阳光可以被看做是平行光,因此需要对视线方向做积分来重建无数条光路的总和:

我们已经说明了光线在大气中是如何传输的,但是并没有构建关于散射S、透射T的具体表达函数,所以我们要进一步地探究散射和透射的背后的物理模型。
具体到物理模型,当我们计算大气最终颜色时,实际上是在求解光线传输方程(基于 Beer-Lambert 定律)。
透射率 (Transmittance)
透射率
散射(Scattering)
Scattering,
在大气渲染中,我们通过 “散射系数” 和 “相位函数” 来描述光线散射的现象。散射系数

剩下的能量并非完全进入我们的视线,而是会四下逃窜。因此相位函数

将散射系数和相位函数简单相乘就能得到在某点发生一次散射之后,逃逸到某个方向上的能量大小:
Extinction
Absorption Coefficient
Scattering Coefficient
Phase Function ,
大气层并非完全均匀,大气分子的密度随着高度的增加而减少。越少的分子数目意味着发生散射的概率越低。此外大气对红、黄、蓝三种波长的光有着不同的散射量。因此散射系数通常是波长和高度的函数:
对于空气这种介质,它的密度通常随着海拔的增高而降低。密度的高低反应在数值上就是散射概率的减少,散射概率的减少对应着散射后剩余能量的增加。因此可以用海平面(Y=0)处的散射系数和海平面 h 处的高度密度衰减函数
单次内散射积分 (Single In-Scattering)

In-Scattering,
简单来说:视线透射率 × 散射相函数 × 太阳光透射率。


多重散射补偿 (Multi-Scattering)


Luminance,
此外,
仅仅计算单次散射会导致天空背光面和底部能量严重丢失(死黑)。在我们的架构中,我们通过预计算 Multi-Scattering LUT,利用基于球面随机采样的积分方法,将二阶及以上的散射能量补回系统,从而实现能量守恒和极其柔和的全局天光过渡。
宏观几何与介质密度模型
我们在物理上将大气层抽象为一个包裹着星球的非均匀介质球壳。
- 几何定义:星球具有一定的半径
(如地球约为 6360 km),大气层具有厚度 (约为 60 km)。我们所有的积分计算都建立在这个以地心为原点的球坐标系中。 - 密度衰减模型:大气不是均匀的,受重力影响,越靠近地表密度越高。我们采用指数衰减模型来描述介质密度
随海拔高度 的变化:
- 这里的
是标高(Scalar Height),表示密度衰减到海平面密度 (约 36.8%)时的高度。不同的介质拥有不同的标高。
微观物理介质属性
光线穿过大气时,会与三种主要介质发生交互,空气分子 [瑞利散射] 和气溶胶粒子 [米氏散射],臭氧;在特定球形层内,每种介质的散射比例 S 由散射系数 (Scattering)和相位函数( Phase Function )决定,同时考虑了气溶胶的吸收,为简化暂忽略折射率随高度变化引起的光线弯曲。
1. 瑞利散射 (Rayleigh Scattering)
这是由空气分子(氧气、氮气)引起的散射。其颗粒尺寸远小于可见光波长。
- 波长依赖性:瑞利散射的强度与光波长的四次方成反比(
)。由于蓝光波长短,被散射的概率远高于红光,这就是白天天空呈现蓝色的根本原因。 - 相函数:瑞利相函数是前后对称的,光子向前后散射的概率大致相同。
- 标高:分子集中在大气层,标高
设定为 8000 m。
2. 米氏散射 (Mie Scattering)
由大气中的气溶胶、灰尘、水滴(即雾霾)引起,颗粒尺寸与可见光波长接近。
- 波长独立性:米氏散射对波长几乎没有偏好,因此散射出的光是白色的(导致天空发白、起雾)。
- 相函数与双 HG 模型:米氏颗粒具有极其强烈的前向散射特性。为了精确拟合太阳周围极其明亮的日晕(Halo)并保留一定的后向散射,我们在系统中引入了更高级的双 Henyey-Greenstein (Double HG) 相函数,而不是传统的单 HG 模型:
- 这里
控制强烈的前向日晕, 补偿后向散射。 - 标高:气溶胶集中在地表对流层,标高
设定为 1200 m。
3. 臭氧吸收 (Ozone Absorption)
臭氧不参与散射,但会强烈吸收光能。
- 视觉贡献:它主要吸收黄绿光。在日出日落时分,光线穿透大气路径极长,黄绿光被彻底吸收,留下的红蓝光混合,赋予了天顶和地影区迷人的紫色调。
- 密度分布:臭氧集中在平流层,因此我们不使用指数衰减,而是使用一个中心高度在 25000 m 左右的线性帐篷分布(Tent Distribution)。
VolumetricScattering
1 | |
Transmittance
1 | |
三、4-LUTs 架构与数据流
大气渲染的组成
具体应用到游戏渲染里,我们一般会把大气渲染分为两个部分:
- 天空背景的渲染,即skybox,
- 大气透视的渲染,也是我们俗称的大气雾效。
在渲染的时候需要考虑光线入射方向s上的遮挡(阴影),通常使用传统的shadow map就可以了。这样我们就可以得到god ray效果。
首先来考虑单次散射的情况。
Sky Background 天空背景
[Rendering] 基于物理的大气渲染可以直接拿来渲染天空盒。其中,第0级散射可以理解成对“sun disk(太阳圆盘)”的渲染。
Aerial Perspective 大气透视

大气透视(aerial perspective)是渲染场景里物体距离摄像机远近的很重要的效果,也是所谓的雾效。与渲染天空盒略微不同的是,我们还需要考虑反射地表颜色等。
于是一共需要考虑 2 类光照:
- 地表颜色经过路径衰减后的光照
- 路径上由于其他点内散射到相机路径上的光照
图示 考虑了在A点、沿着观察路径AB接收到的光照。这部分光照可以分为两个部分:一是B点反射的地表颜色Rb经过路径AB衰减后的光照,也就是第0级反射,二是路径AB上由于散射贡献的光照:
Precomputed LUTs
大气散射渲染数学上是一个多重积分求解问题,为了将
| 查找表名称 | 物理意义 | 存储内容 | |
|---|---|---|---|
| Transmittance | 透射率 | 记录不同高度、角度光线穿过大气的剩余比例。 | ![]() |
| Multi-Scattering | 多重散射 | 补偿单次散射丢失的能量,防止背光处死黑。 | 50 倍渲染效果 |
| Sky View | 天空颜色 | 结合单次与多重散射,存储相机看到的最终天空色。 | ![]() |
| Aerial Perspective | 空气透视 | 3D Froxel 结构,存储场景深度对应的雾效颜色与透射率。 | ![]() |

之后利用 Transmittance,Multi-Scattering 预积分,再 Ray March 得到最终的 SkyView LUT 。
从 不同渲染坐标到物理空间的映射 是这套实现中值得关注的部分
星球空间
基于物理的大气散射渲染中,我们首先需要考虑真实世界与游戏世界坐标系的转换:
- 世界空间 (World Space):Unity 默认空间。
- 星球中心空间 (Planet-Centric Space):所有物理积分的基础空间。
- 变换逻辑:假设星球中心位于
或通过偏移量计算。在 Compute Shader 中,我们需要将相机高度转换为相对地心的距离 。 - 高度计算:
r = WorldPos.y - SeaLevel + PlanetRadius
- 变换逻辑:假设星球中心位于
另外,在本项目中需要频繁的计算 相机与大气层是否相交 (用于早期退出),代码实现如下:
RayIntersectSphere
1 | |
Transmittance LUT
为了在有限的贴图分辨率下获得更高的精度,系统在 Helper.hlsl 中实现了精细的参数映射:
- 映射目标:将
映射到 UV。 - 天顶角余弦



UvToTransmittanceLutParams
1 | |
GLSL (展开查看 36 行完整代码)
1 | |
Multi-Scattering LUT
[Hillaire 2020]论文作者在计算Multiple Scattering时,认为高阶散射非常低频,从此出发使用了一些简化,包括:
- 大于等于2阶的散射,将Rayleigh散射和Mie散射视为为各向同性的散射
- 计算某点的大于2阶的Scattering时,认为该点周围任意一点的Illuminance与其相同
第一点,对于1阶散射,我们认为Rayleigh、Mie散射是正常的,但是在之后的阶数,我们认为相位函数对于任何方向都是相同的值
第二点,在计算某点的Scattering时,空间中任意一点的某一阶的Scattering都视作和该点相同。因为参与计算的点需要与该点有视线链接 [Line of sight],从整个地球的视角来看确实是Neighboring points。这一条近似看上去是非常粗略的,但是看起来是有效的。
实际流程中,Multiple Scattering LUT的计算可以一个pass输出到一张32*32的2D LUT完成。只需要2D的原因是因为上述简化中我们使用了各向同性Phase Function,不需要存储相机跟光照方向的关系,只需要存储不同高度、不同光照角度的情况即可。
在pass中,论文中提到,首先计算
其中
转移函数
第二步,在上述计算完毕后,计算当前像素的Transfer Function
使用
这里可能会对
根据简化1、2,我们认为在计算当前像素时,
即
可以发现,相比(2)式,(7)式中省去了地面反射。作者表示确实是忽略了多重散射照射到地面形成反射的贡献,不过保留的话也没办法化简出这么一个
多重散射结果
有了
根据数学公式,因为
最后,就可以将

TransmittanceToTopOfAtmosphere
1 | |
Sky View LUT
在实际的渲染流程中,首先根据相机位置渲染至一张低分辨率的Sky-View LUT上,后期再合成到 SkyBox 上。
Sky-View LUT中包含了当前相机位置接收到的各个角度的Luminance。计算时根据像素对应的视线方向直接做Raymarch得到结果。

坐标映射
Sky View LUT 使用极坐标映射来存储从相机视角出发的全天空颜色:
- UV 到视线方向 (ViewDir):
theta(天顶角):通过(1.0 - uv.y) * PI映射。phi(方位角):通过(uv.x * 2 - 1) * PI映射。
- 视线方向到 UV:利用
atan2和asin将 3D 方向反解回 2D UV 坐标。

1 | |
作者观察到高频视觉信息特征在地平线附近更为显著,为了更精准地呈现这些特征 [天空中大部分的散射现象是低频的,除了靠近地平线部分会变得高频],在计算纹理坐标
$$ v=0.5+0.5sign(l)\sqrt{\frac{\lvert l \rvert}{\pi /2}} ,with \space l \in[-\pi/2,\pi/2] $$

在论文中的测试,PC上只需要200*100的分辨率效果就足够。另外太阳本身是不会渲染在图里的,因为属于高频特征。会在后面再合成上去。
需要注意的是,这里跟 [Bruneton08] 已经不一样了,[Bruneton08] 中不会有任何实时的Raymarch,都是LUT查找搞定;而这篇论文中,不管怎么样,在这一步实时Raymarch都是少不了的(其他的Raymarch步骤可以看情况跳过),这也是相比老方法会稍微慢一点的原因。
MultiScattering
1 | |
- Aerial Perspective LUT
除了背景的大气,相机和远处物体之间的空气透射也是画面的重要组成部分。

具体的算法思路如下:
- 分割Camera Frustum(和Cluster Rendering中的分割一样),计算每个格子到Camera方向的In-Scattering和Transmittance,保存在Volume Texture中;
- 在Volume Texture的z轴方向上根据Transmittance累加In-Scattering,使得每一个单元格保存的是该单元格到Camera的Luminance;
- 在Opaque渲染之后,做一次Post Processing,采样上述的Volume Texture,对场景中的物体添加Aerial Perspective;
- Transparent物体渲染时在VS中采样Volume Texture添加Aerial Perspective。

AerialPerspectiveLUT
1 | |
四、渲染实现与性能优化策略
我们在 URP 管线中通过 ScriptableRendererFeature (Atmosphere.cs) 进行了无缝集成,将复杂的体积积分运算解耦到 Compute Shader (Atmosphere.compute) 中,最终通过四个低分辨率的 LUT (Look-Up Table) 实现了高性能的全天候天空与空气透视渲染。这套系统为后续整合体积云、体积光等提供了极其统一的物理光照基础。在工程实践中,通过 LutDebugView 提供了直观的运行时调试 Debug 功能,允许我们实时观察四张查找表的生成状态。
渲染调度
分帧更新 (Time-Slicing/Amortization)
在 Atmosphere.cs中,我们错开了不同 LUT 的更新频率。例如,透射率和多重散射由于变化极其缓慢,设定为每 16 帧更新一次;空气透视 LUT 每 6 帧更新;Sky View LUT 每 4 帧更新;而极其耗时的环境光 (GI) 则每 120 帧刷新。这在保证视觉连贯性的同时,大幅压榨了 GPU 算力。
RenderFeature 调度实现如下
Atmosphere.cs
1 | |
天空盒渲染
Skybox.shader 并不直接进行积分,而是通过 ViewDirToUV 采样预生成的 Sky View LUT。
星空与月亮:
在天空颜色基础上,根据太阳位置(SunDirToDayTime)混合星空贴图,并利用相位计算渲染月亮圆盘。
- 需要关注的是采样月亮圆盘 在整个 SkyBox 中的贴图拉伸情况。
CSkybox.shader
1 | |
空气透射
这里实现了对不透明物体的后处理方式添加Aerial Perspective
- 半透明物体待实现
Aerial Perspective
1 | |
五、总结与后续迭代优化
总结来说,这套物理理论不仅是对天空色彩的数学还原,更是构建现代化游戏环境的基架。我们将这套庞大的积分运算通过 4张 LUT(Transmittance, Multi-Scattering, Sky View, Aerial Perspective)进行巧妙降维。这使得我们的渲染管线能够在极低的运行时代价下,为我们复杂的游戏场景,提供绝对物理正确的光照与深度遮挡(体积雾)基准。
代码仓库如下:
因为时间与精力的限制,后续我们可以考虑在以下几个方向进行深入优化:
- RenderGraph 现代化重构: Unity 6 的核心是 RenderGraph。我们目前的
ScriptableRenderPass仍在使用传统的RTHandle手动管理内存。将其重构为 RenderGraph API,利用其自动的资源生命周期管理和 Pass 裁剪,能更好地应对未来复杂的依赖关系(如与体积云、大面积草海的交互)。 - 体积阴影 (Volumetric Shadows) 的接入: 目前的系统在计算大气散射时并没有考虑地形或云层的遮挡。我们可以在生成 Sky View LUT 和 Aerial Perspective LUT 的 Raymarching 循环中,加入对 Directional Light Shadow Map(或级联阴影 CSM)的采样,从而实现物理正确的上帝光 (God Rays) 和云隙光 (Crepuscular Rays)。
与体积云 (Volumetric Clouds) 的深度耦合** [ 已完成 ]:** 空气透视系统应当包裹体积云。在渲染云层时,我们需要使用 Transmittance LUT 来计算阳光穿透大气的衰减,并在云层最终渲染阶段,采样 Aerial Perspective LUT 让云层融入远处的雾气中,形成“环境系统”的完美闭环。

50 倍渲染效果
