Z赵崇逍Technical Artist / Shader Developer

UE Volumetric Cloud / Custom Cloud Renderer

云与云

云团像了,颜色和阴影却总差一点。

我一开始只想把密度搬进 UE,后来又接了追踪、阴影和重建。做着做着,才把两边的差别一点点理清楚。

新 UE 工程近景实际单帧当前专用路径 · 近景单帧
UE 这边UE 5.8.1 源码与材质接口
游戏这边RDC 实际执行与原生写入
主要聊什么形状 · 光影 · 转镜头时的表现

Technical Comparison

一开始以为,换张云材质就够了

最开始做这片云的时候,我以为找到纹理、把密度公式搬进 UE,剩下的交给原生云就行了。做出来以后,云团的位置和轮廓已经挺像,颜色和阴影却总差一截。镜头一动,云边上的颗粒也很明显。

后来把截帧里的几个阶段串起来,才发现原版自己做了不少东西:距离场除了决定云的形状,还拿来控制步长;太阳阴影有一套三维缓存;云算完以后,还要经过自己的时域重建。这篇就聊这些地方和 UE 自带的云有什么不一样,以及我为什么最后还是写了渲染插件。

Shared Background / Implementation Stages

先看几张同背景的对比图

左边是我用恢复密度做的 UE 原生云,灯光也调过了;右边是游戏参考。两边用同一个背景和显示变换,这里先看画面,不比性能。

UE 原生积分 · 定制密度游戏参考 · 捕获输出

城市是原帧底图,我没有在 UE 里重建它。这几版的采样尺寸、光照和历史处理都有变化,下面再分别说。

Captured Frame / Resolution Chain

参考帧有两层重建

  1. 01 · 专用云主追踪598 × 336 网格
  2. 02 · 专用云时域升采样1196 × 672
  3. 03 · 场景合成云重建与合成有效 1707 × 960
  4. 04 · UE 后处理TSR 最终输出2560 × 1440

这是这张参考帧里的尺寸,换画质档位可能会变。像素少算了多少,也不能直接当成省下的 GPU 时间。

01这里拿来比较的是哪几套东西

下面说的 UE 原生云,是 UE 5.8.1 的 VolumetricCloud 渲染器,不是某个示例关卡里那张云材质。UE 没有规定云只能用哪种噪声,三维贴图、SDF、解析形状都可以放进材质里。

游戏这边,我看的主要是参考截帧里实际跑起来的 Cloudly。它也在 UE 里,但云的追踪、阴影、AO 和重建,用了自己的实现。

我做的复现分了两步。第一步保留 UE 原生渲染,只把恢复出来的密度接进去,先看形状。第二步才把游戏里的追踪和重建接成 RDG 插件。后面几组图也是按这个顺序来的。

环节 UE 5.8.1 原生云 本次捕获的游戏定制云
云形与介质 Volume 材质定义,可用 SDF 或噪声 Base、Alto、Cirrus 分层,布局 SDF 加多级位移
主视线步进 按交段与预算求基础步长,结合空区跳过 粗追踪区间加 SDF 驱动的自适应步长
相函数 两个 HG 项按 Blend 插值 两个 HG 项取 max,并有动态暗边分支
多次散射近似 材质 octave 控制,公开编辑范围最多 2 个额外项 参考帧使用 4 个贡献项及自己的衰减参数
太阳遮蔽 二次体积 raymarch 或 Beer 阴影贴图 近场有限步追踪加远场级联三维光学深度缓存
环境光 引擎天空输入、材质 AO 和地面贡献等 专用天空 LUT、云 AO 与积分后的亮度补偿
云历史 原生 VolumetricRenderTarget 的重投影与重建 颜色、距离、可见性、Aux 和 Info 五组历史
场景集成 使用引擎组件和材质接口 还需要维护自定义资源、参数与生命周期

两边有不少相同的大方向,具体做法又不一样。下面挑对画面影响比较大的几个地方展开。

02都能用 SDF,为什么搬完密度还不够

UE 会在射线上的采样位置计算 Volume 材质,再读取消光、反照率等属性。所以把游戏的 SDF 解码和密度公式放进去,是能工作的。我第一版只改了“这个位置有没有云、云有多密”,追踪和光照都还用引擎的。

游戏这一帧的 Base、Alto 共用一张 512 × 512 × 64 的双通道布局纹理,上面再加两级位移、云底软相交和随高度变化的边缘。近处还有 Hotsure 细节,远一点就慢慢淡掉。这部分大多能在原生材质里写出来。

但原版拿 SDF 做的事更多。它会用距离场决定下一步走多远,配合粗追踪跳过没有云的区间,查询和阴影也会读这套密度。只把最后算出的密度交给 UE,原生追踪器不会连这些规则一起接过去。

最早的材质版:SDF → 密度 → 消光 → UE 原生积分
游戏里的做法:SDF → 密度、步长、粗区间和查询 → 专用积分

所以第一版的轮廓能很快接近参考,细节和开销却还没跟上。云长得像了,射线在云里怎么取样,仍然是两回事。

把恢复密度放入 UE 原生云后的早期原型
最早的原生 UE 云版本。密度换过了,追踪、光照和重建还是引擎的。这张开了较高质量的静态预览,不能拿它看实时性能。

03云边上的几步,走法也不一样

我看的这版 UE 源码,会先按射线穿过云层的长度和采样预算求样本数,再算基础步长 StepT。遇到空位置时,Conservative Density 可以帮它少算昂贵的材质,另外还有 Empty Space Skipping 路径。

游戏这边先跑 CoarseTrace,圈出可能有云的区间。进到区间以后,再根据 GetSteppingSDF、视线方向、离表面有多远、已经连续走了多少空步等条件调整步长,最后加上蓝噪声。哪里可以大步走、哪里要放慢,和它自己的云形表示联系得更紧。

UE 也会跳空区,所以我不会把这个区别写成“UE 固定步长,游戏才会自适应”。要看的还是实际用了哪些信息来选采样位置。

连积分也有差别。UE 的一条主要路径把每一小段介质看成常量,做解析积分;恢复出来的游戏代码里,则有前后样本的梯形项和边界修正。薄云、云边本来就没几个有效样本,位置稍微变一点,透射率就可能不同。

// UE 每段常量介质积分的简化写法
deltaL = T_before * source * (1 - exp(-sigma * stepMeters)) / sigma;

加采样能缓解一部分误差,但不会把这两套走法变成同一套。做到这里,光在材质里继续加参数,已经不太够用了。

04颜色差一截,问题不全在曝光

早期最明显的问题就是颜色。云团看着已经挺像,暗部却总有点不对。我一开始也调了灯光和地面贡献,后面看相函数才发现,这里本来就有差别。

UE 的 SamplePhaseFunction 把两个 HG 相函数按 PhaseBlend 插值。游戏参考帧用的组合是:

UE:      phase = lerp(HG(mu, g1), HG(mu, g2), blend)
参考帧:  phase = max(HG(mu, 0.2), 0.5 * HG(mu, 0.69))

这样一来,观察方向和太阳夹角变化时,两边的响应就不同。同样的 g 值填进去,再调 Blend,也没法普遍得到 max 的结果。原版还有根据局部条件变化的 DarkEdge,云的暗边会跟着变。

多次散射也得一起看。参考帧用了 4 个贡献项,早期 UE 版本是 2 个额外 octave,加上第一项,一共 3 项。UE 编辑器公开的范围是 0 到 2 个额外 octave,不过 Shader 的 MSCOUNT 来自宏,这不是引擎永远只能算三项的硬限制。除了数量,每一项怎么衰减光学深度和散射贡献,也得对得上。

环境光又多了一层区别。游戏从 DistantSkyLightLut 取值,做去色、蓝偏移和云 AO 处理,等整条视线积分结束后,再对直射加环境光的总亮度做一次补偿。原生云的天空输入、云底和地面贡献走的是另外一套公式。

最后这一步很难只靠 Volume 材质补。材质正在算一个采样点,它拿不到这条射线最终累积的 direct、ambient 总和。往 Emissive 里加环境项可以试效果,但和原版最后那次补偿还不是一回事。继续调曝光,则会把亮部和暗部一起带着走。

早期 UE 原生积分在共同背景下的云画面
早期材质版。两边先放到同一个背景里,云自己的偏色和阴影差异才比较容易看。
UE 原生积分经过灯光校准后的结果
调过 UE 灯光和地面贡献之后。颜色接近了一些,相函数、积分和时域算法都还没换。

05阴影里的六步,省下的工作去了哪里

UE 原生云有二次体积 raymarch 和 Beer cloud shadow map 两条路。一个继续沿光线采样,一个查阴影贴图,距离、细节和成本各有取舍。Epic 的说明里也有这两种效果的对比。

游戏参考帧把远近分开了:近太阳方向最多做 6 步局部追踪,后面的采样距离按自己的规则展开;更远的光学深度直接从级联三维纹理里查,再混合前后缓存。它存的是空间中不同位置和高度的遮蔽信息,和直接切到 UE 的 Beer 阴影模式并不一样。

刚看到“6 步”时,很容易觉得阴影已经很便宜了。但三维缓存也要生成、更新和读取,这些工作只是放到了别处。要比性能,不能只数主追踪里那几步。

Cloud AO 也是类似的思路:游戏有独立三维数据,主追踪按位置查询。UE 也有天空光云 AO,不过同样叫 AO,生成和取样未必相同。云内遮蔽、屏幕空间 AO、Lumen GI 和太阳自阴影,排查时还是要拆开。

这次关了 Lumen,噪点还在,所以还得往下找。光凭这个现象,没法直接认定是动态 AO。现在复现里的云内阴影仍有一部分读的是捕获输入,生成后的数据怎么更新、什么时候切换,还没有全部接回原版的做法。

06云算完以后,还没到最终画面

UE 原生云已经有低分辨率追踪、历史重投影和按深度重建、上采样的流程。这些大方向两边都有,差别藏在每一步用的数据里。

游戏参考帧先在 598 × 336 的网格上主追踪,云时域输出是 1196 × 672,再重建到有效 1707 × 960 的场景。到这里还没结束,后面还有 UE TSR,最终输出 2560 × 1440。云自己的重建和整帧 TSR,不能混在一起看。

恢复的云时域阶段一共留了五组历史:颜色、距离、可见性、Aux 和 Info。Aux 存的是首次命中云之后几段距离里的透射率,后面会拿它调整重建。历史被拒绝时,颜色和这些辅助数据还要选到同一个深度合适的当前样本。

这不是多存几张图就完了。假设颜色已经靠历史稳定下来,决定滤波范围的数据却还跟着当前抖动采样跳,后级一样可能出颗粒。最后显示用的平滑结果,如果又塞回下一帧历史,几帧以后也可能越抹越糊。

原生云也在处理这些取舍。它的源码注释里就提到,近处关掉一部分重投影可以隐藏某些问题,但会让云更吵。换成专用重建以后,噪点和拖影也不会自动消失,还是得看具体镜头。

复现过程中云暗部和边缘的噪点反馈
复现中遇到的颗粒,暗部和云边尤其明显。这张是当时的问题截图,不代表 UE 原生云都会这样。

07为什么最后还是写了渲染插件

只想先把云做出来,我觉得原生材质很好用。布局、SDF 解码、位移、密度边缘,都可以先在这里验证,再用引擎自己的质量模式看效果。我最早就是靠这一版把形状理顺的。

但要继续贴近原版,就得改采样位置、相函数组合、整条射线的环境补偿,还有阴影缓存和那五组历史。这些已经超出了改一张云材质能做的事。后来的插件保留 UE 场景和 RDG 调度,把云的几个核心阶段换成恢复的实现,最后再接回场景合成。

麻烦也跟着来了。前后帧相机、曝光、尺寸、场景深度、历史什么时候能用、缓存什么时候换新,都要自己接好。用原生云时引擎已经做掉的一些事,现在得重新检查。

比如 CameraSpeedMeter,看名字很像速度,追到游戏 CPU 的实际写入才发现,它传的是米制帧位移,没有除时间。再比如捕获的 70% 重建变体,不能只把尺寸改成 100% 就当作同一套算法。公式抄对了,传进去的东西不对,画面还是会出问题。

接进新工程后,我关掉原生云绘制,免得两套云叠着画,Lumen 和 Nanite 都保留。云组件现在能控制这套云显不显示,不过风速、天气、太阳和动态曝光还没全部接上。能在关卡里看到云,离完整替掉原生组件还有一段。

08那到底哪套更快

从代码能看出来,游戏把不少重复计算换成了可以反复查的数据:先圈出粗区间,按 SDF 走步,远处阴影读缓存,再靠历史补回低分辨率采样。省下了一部分当帧计算,也多了纹理、生成流程和跨帧更新。

UE 原生也能省这些方向的开销。真要比较,原生云的 Mode、阴影方式、密度复杂度和最后想要的画质都得说清楚。一边开全分辨率高采样做静态截图,另一边用低分辨率加缓存,得出的帧率差说明不了多少。

目前还没有同一 GPU、相近目标画质,并且算上完整缓存更新周期的双方耗时,我先不给“快几倍”的数字。后面要测,会把生成、主追踪、重建和合成分开,再看静止、转镜头和穿云几种情况。

少采样能省计算,剩下的画面得靠过滤和重建补回来。这个配合做不好,先露出来的就是云边上的颗粒。

09现在做到哪了

现在这套云已经能在新 UE 工程里自由看了。网页上的同背景图主要看云形和颜色,实时单帧和连续序列拿来看转镜头。图里的城市是原帧背景,我没有在 UE 里把它重建出来。

专用云路径的共同背景诊断结果
换成专用路径后的静态对照。这张取了采集里的 16 个相位样本做线性平均,内部网格也和原版不同,主要拿来看形状和明暗。

在新工程 1280 × 720 的近景测试里,静止和只转 yaw 的连续画面都没有再出现之前那么强的闪烁,实际看也好了不少。但平移、穿云,以及刚从遮挡后露出来的地方还得继续测。这次补的最终视口采集工具,只是帮我看清输出,没有顺手改去噪算法。

新工程近景静止序列中的实际单帧
新工程近景的一张实际输出,没有离线多帧平均。这里用了预览场景自己的曝光,和上面的诊断图用途不同。
新工程纯 yaw 连续序列中的实际单帧
只转 yaw 的连续序列里截出的一帧。和上一张是静止、运动两组记录,不是修复前后。

如果是做自己项目里的云,我还是会先用原生材质,把造型和光照需求试清楚。这次想尽量贴近游戏里的效果,才继续往它的追踪、缓存和重建里走。接下来要补的还有天气动画、动态阴影和宿主场景适配,尤其是转镜头和穿云时那些容易露馅的地方。

10参考资料

原生实现对照为本机 UE 5.8.1 的 VolumetricCloud.usf、VolumetricRenderTarget.cpp 和材质接口源码;游戏侧以 RDC 实际绑定、预处理 HLSL、资源配置和原生写入证据为准。各阶段画面条件见图注,原游戏图像与资源属于其权利方。