荒野大镖客 2 中的天气系统

原标题

Creating the Atmospheric World of Red Dead Redemption 2: A Complete and Integrated Solution
整套天气系统渲染的实现,包括大气、云雾、二十四小时、不同天气。

物理基础

Scattering

  • 这里只考虑单条光路上的单次散射。
  • 以单个粒子为视角,当光线与其交互时,大致会发生吸收(转化为热能)、散射和反弹。
  • 散射向相机的部分被称作 in-scattering,剩余的部分被称作 out-scattering。
    • out-scattering 会继续作用于其他粒子并造成多重散射,多重散射较为复杂,此处不做考虑。
    • 以一条光路为视角,远处的粒子发生的 in-scattering 会继续在近处的粒子上发生 out-scattering。
  • Scattered Light:单个粒子上发生的散射,P 为相函数,V 为可见性,L 为入射 Radiance。
  • Extinction:消光系数,由吸收系数与散射系数定义,意为一束光经过单个粒子之后沿着原光路继续传播的占比。
  • Transmittance:透射率 / 透光率,意为一束光经过给定距离之间所有粒子之后沿着原光路继续传播的占比。
  • 总结:单光路上单次散射便可表示为,“着色点到达相机剩余的光”与“光路上所有粒子提供给相机的散射”之和。

模型

贴图 + 参数生成两个通道用于表示两层云在平面上该如何放置,覆盖整张地图。
Cloud Map

高度上的定义。
Cloud Height LUT

细节,两张 2D 的位移贴图 + 一张 3D 的噪声贴图结合风速参数。
Cloud Detail

在高空还有一层简化过的卷云,不过光照模型是一致的。

1
2
3
4
5
6
7
8
float2 c = SampleCloudMap(ray.p);
float3 cloudLut = SampleCloudLUT(altitude);
float density = smoothstep(g_CloudShape.xz + cloudLut.xy, g_CloudShape.yw + cloudLut.xy, c.xy);
float rescale(vMin, vMax, v)
{
return saturate((v – vMin) / (vMax - vMin));
}
density = rescale(noise, CloudLut.z, density);

全局雾 + 局部雾。一张图覆盖所有可玩区域,存储起始高度,衰减距离和密度。
Fog Map
同时也支持可以自由摆放的 Fog Volumes,球型或盒型,自定义颜色,alpha blending 或者 additive。

渲染

近处 frustum aligned volume,远处 raymarching。

相函数

单次散射,波长无关,trick 近似多重散射。
Phase Function

  • 叠加多级的 Henyey-Greenstein 函数,每次迭代 各向异性 g 减小。
  • 使用艺术导向的权重 ω0\omega_0ω1\omega_1 以及 消光系数 σext\sigma_{ext} 来混合。
  • 两次迭代。

依旧缺乏向后的散射,clamp 至由消光系数和感知调整参数影响的 Lambertian BRDF 1/PI。
不知道这个感知调整参数具体是什么玩意儿

1
2
3
4
5
float ApplyBackScattering(float phase, float extinction)
{
float v = 1/PI * rescale(BACK_SCATTER_MIN, BACK_SCATTER_MAX, extinction);
return max(phase, v);
}

可见性

主要是 Shadow Map。
对于云,则进行对太阳/月亮的消光采样,通过天气参数控制采样长度。

对地形的 Height Map 做 Raymarch,记录相交的高度,Ray length 用于调整 threshold。
Terrain Shadow Map

云的 Shadw Map 存储为 ESM(Exponential Shadow Map),透射率加权射线长度,滤波存为 Mip Map。
Cloud Shadow Map

1
2
3
4
5
6
7
8
9
float transmittance = 1;
float depth = maxDepth;
while (ray.t < maxDepth && transmittance > 0.001)
{
float density = SampleCloudDensity(ray.p + ray.d * ray.t);
transmittance *= exp(-density * stepLength);
depth = lerp(depth, ray.t, pow(transmittance, 4));
ray.t += stepLength;
}

环境光

复用上面 Cloud Shadow Map 的可见性。

对于 Raymarching:

  • 将无云的 Rayleigh 和 Mie 散射加权平均
  • 渲染至低分辨率的抛物面贴图上
  • Parallel Reduction
  • 太阳/月亮方向加权

对于 Frustum Volume:

  • 采样 Irradiance Probe,存储了 sky light + indirect bounce light * AO

Local Lights

  • 采样 light cluster volume
  • Shadow Map 做可见性
  • 单级 HG 相函数

闪电

没有细讲,比较暴力的实现。

  • 每一击闪电添加一个点光源
  • 指数衰减 + 相函数
  • 简化的局部消光(用于近似阴影)

散射波瓣可视化

Scattered Light Overview

Frustum Voxel Grid

Frustum Voxel Grid

  • 动态可调深度(<160m)

三个 volume

  • Shadow Volume
  • Material Volume
  • Scattered Light and Extinction Volume
  • 分辨率 160 x 88 x 64

Shadow Volume

  • 格式:R16F
  • 存储从 Shadow Map,Cloud Shadow Map 和 Terrain Shadow Map 采样到的直接光阴影项
  • Temporal Filtering

Material Volume

分为两个子 Volume 存储不同参数。
Material Volume

  • 混合顺序:Additive -> Alpha -> Particles
    这样可以用一个大号的 Additive 包围整个建筑,然后用 Alpha 镂空室内,然后还支持用 Particles 表示的爆炸/火灾。
  • 对散射和吸收系数做 Temporal Filtering
  • 与风速交互

Scattered Light Volume

  • 采样 Material 和 Shadow Volume

  • 累计直接光(太阳/月亮/局部光)和环境光(来自 irradiance probe)的 Radiance

  • 格式:RGBA16F,RGB: Scattered light,A: Extinction

  • 一个 slice marching pass 计算每个 slice 的 in-scattering 和 transmittance

  • 无时间性的混合,动态光源拖影明显

  • 使用抖动查找和 TAA

Raymarch

Frustum Volumes 分辨率有限,无高频信息,使用时间性的混合依旧不稳定。
Raymarch 内存更小但是运行开销更大,剔除了雾, irradiance probe 和局部光。

  • 射线长度由屏幕深度,与地平面/穹顶的解析解决定

  • 击中云的基本形状之后步长减半并回退一步,直到达到迭代最大次数

  • 以此做为积分的开始位置

  • 离开云之后恢复初始步长

  • 射线起始位置 = frustum volume 的最后一个 slice 之后 + offset(blue noise)

  • 目标是半屏幕分辨率

  • 还是太慢了,故不对所有像素投射光线,吧计算分布到 4 帧

Reconstruction

  • 每次只投射 2 x 2 tile 中的一条光线,并重投影上一帧中跳过的像素
  • 采样之前的 buffer 时将结果 clamp 至 3 x 3 的临近像素内
  • 放弃与当前像素深度差异过大的样本
  • 无历史信息时使用最近的 Raymarch 结果

Raymarch Reconstruction

Ray Placement

先对深度进行棋盘下采样,这是一种交替存储 tile 内最大值与最小值的下采样方法,有助于保存细节。

  1. 假设以:“左上、右上、右下、左下”的顺序投射光线,在特定的情况下会遇到问题。例如在 Frame 2 我们试图重建左下的像素,会发现九个可信的 Raymarch 结果全部与其深度相差过大,即都应当被丢弃。这时这个像素便会失去在时间上的可信度。在这里其实可以反推出一个理想中光线分布方式应当是覆盖了尽可能大的深度范围的。
    Ray Placement

  2. 又提出了一种不统一从左上角开始的分布方式,依旧不能适配所有情况。
    Ray Placement 2

  3. 在 2 的基础上微调。比较中心 Ray 和周围八个 Ray 的深度,如果相差无几,就将 min 上的 Ray 移到 max 上去,反之亦然。
    移动时每个 tile 内应该都有两个选项,具体怎么移动好像没有细说

Full Resolution Upscale

Dithered Upscale
  • 4 个采样点 + blue noise
  • 深度加权
TAA
  • 卷积核宽度由透射率和深度决定,让远处云不会被处理的太糊。
    原文是:… width … is chosen by the transmittance weighted depth of the ray of this pixel.
  • 查找 in-scattering / transmittance accumulation volume 应用同样的原则。

参考

有一篇结合了逆向的文章,还在 UE 里复现了一遍:
荒野大镖客2天气系统云雾分帧处理细节

集成

Sky Scattering

  • 基于 Precomputed Atmospheric Scattering [Bruneton07]
  • 改进自 Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite [Hillaire16]
  • 支持 earth shadow 这啥?
  • 随时间段更新
  • Raymarch 之后采样一次,而非每个采样点都采样 Sky Scattering

Light Shaft / God Ray

  • 分离 Sky Scattering 的可见性项
  • 每次 Raymarch 累计可见性,通过样本覆盖面积和透射率加权
  • Raymarch 之后通过 光线长度进行归一化。
  • 物理不正确,但是开销小效果好

Sky Irradiance Probes

Sky Irradiance Probes

  • 云雾对大气提供的环境光的遮挡
  • 维护 32 x 32 个 probe,每 256 x 256 平方米一个
  • 高度和方向由 height map 和 bent normal map 决定
  • 编码为三阶球谐函数
Sky Irradiance
With Scattering / Transmittance
With Direct Light

优化:

  • 可视的 Probe 投射 32 条光线,不可见投射 16 条
  • 每个线程一条射线
  • 不考虑云的高频噪声侵蚀
  • 不考虑彩虹 / 闪电
  • 长步长,无需细化
  • Shared memory parallel reduction

Reflection Probes

  • 在相机位置生成低分辨率的 Cube Map
  • 存储 scattering 和 transmittance
  • 和 Sky Irradiance Probe 一样简化的 Raymarch

Water Reflections

  • SSR 不包含半透明 / 体积效果
  • 取而代之使用简化的 Raymarch + temporal blend