大家好,很高兴能有机会同大家分享关于移植UE5 Hillside示例项目的体积云至URP下的一次技术实践。
最近由于项目需要,在URP底下做了一次复现UE Hillside体积云效果的尝试,于是花了一些时间,研究学习了一下相关技术内容,下面是未添加后处理的最终实现效果。下面将和大家分享下具体内容。
- 渲染流程分析
首先,为了弄清楚Hillside体积云的实现原理,我们利用RenderDoc抓帧进行了渲染流程分析,涉及到的流程如下图所示,大致可分为下采样深度图、体积云绘制、体积云重建、体积云上采样及混合等关键流程,并添加体积光、大气散射及高度雾等后处理。
值得注意的是,出于性能考虑,体积云的绘制在1/4 x 1/4分辨率RT上进行,仅渲染1/2 x 1/2分辨率目标RT中2x2像素区域内的一个像素,剩下的三个像素则通过重投影的方式用历史帧结果重建得到,再得到1/2 x 1/2分辨率目标RT的重建结果后,进一步通过上采样得到全分辨率下的体积云。
2. 关键流程的基本原理
为了实现每个关键流程,我们深入UE引擎源码并总结出需要关注的代码文件:
ScreenPass.cpp、DownsampleDepthPixelShader.usf:定义下采样深度图的逻辑;
VolumetricCloudRendering.cpp、VolumetricCloud.usf:定义体积云绘制的逻辑;
VolumetricRenderTarget.cpp、VolumetricRenderTarget.usf:定义体积云重建、上采样及混合的逻辑。
2.1 下采样深度图
DownsampleDepthPixelShader.usf中的Main函数包含了下采样深度图的Shader代码,首先是对CameraDepthRT进行下采样降分辨率得到半分辨率的CheckerboardMinMax深度图,其在后续流程的场景深度计算时能更好地覆盖深度范围,避免渲染时出现遮挡物体边缘漏光及闪烁等问题的发生。如下所示是CheckerboardMinMax的示意图及代码,其中深色像素存储了原CameraDepthRT 2x2像素中的最小值,浅色像素存储了2x2像素中的最大值,下面也展示了不同下采样方式与全分辨率渲染下的效果对比。
不同下采样方式与全分辨率的效果对比:(a)全分辨率;(b)CheckerboardMin/Max下采样;(c)Min or Max下采样。
2.2 体积云的绘制
接下来就是体积云的绘制,采用的是在屏幕空间进行Raymarching,计算视线与云层底部和顶部的交点,在这段距离上分布采样点进而计算云的散射光照、透过率以及深度等。
UE的体积云系统设计的比较灵活,将体积云密度场建模部分通过Cloud Material接口开放给用户去实现,而体积云光照建模部分则被进行了封装,其中的调控参数则以VolumetricCloud组件以及Cloud Material模块暴露给用户。
体积云的密度场建模:打开Cloud Material蓝图后可以发现,Hillside密度场建模的整体思路上是根据采样点的世界坐标以及相对的海拔高度去采样Perlin-Worley噪声生成基础形状的云,再用Worley噪声对云进行细节上的侵蚀与扰动,之后用CloudPattern纹理控制水平方向上不同类型的云的覆盖情况,用CloudProfile纹理控制不同类型的云在竖直方向上的密度分布,最后再用一张CloudMask为不同类型的云增加额外的密度控制以及一张预烘焙好的3D纹理来实现雷暴天气下的自发光效果,最终材质输出Albedo、Emissive、Extinction、AO等参数用于后续体积云的光照计算,具体组件及材质的使用可以看UE官方的文档[2]。
我们通过Renderdoc获取到相应的纹理资源后,再根据Cloud Material内节点的关系编写了该项目体积云密度场的Shader代码,并将材质的设置参数显示在编辑器面板供调节,而输出参数则被用于体积云的光照计算,下面是体积云材质的部分参数以及几张粗略地调整参数后不同类型云及其混合的效果图。
体积云的光照建模:下面就是体积云的光照建模部分,UE中计算体积云光照的Shader代码可以在VolumetricCloud.usf中的MainCommon函数中找到,cpp端则是调用了VolumetricCloudRendering.cpp的FSceneRenderer::RenderVolumetricCloudsInternal函数。体积云采用的是体渲染,通过建模散射积分方程来解算云的光照效果,详细的体渲染原理及UE源码分析大家可以看这位大佬的文章[1],我们在此不再赘述。
光照计算上采用了UE的做法,并增加了一些微调,我们仅考虑了太阳这个单一平行光源的情况,建模的主要部分可分为单次散射光照、多重散射光照以及环境光照等。云的单次散射光照采用Lerp两个HG函数作为相位函数,Vis阴影项只考虑了云自身的体积阴影,未考虑其他不透明物体产生的阴影,计算公式如下:
多重散射可以更好地突显出体积云的细节及阴影,让云的体积感和真实感增强,计算时通过叠加N次不同倍频衰减、散射系数以及偏心率下的单次散射光照结果来近似多重散射光照,计算公式及效果如下所示:
考虑到UE中天空环境光是通过采样大气散射的天空光纹理获得,而URP中没有计算大气散射天空光的流程,我们选择采样环境光探针获取天空光和地面光并添加参数用来调节天空光和地面光的颜色及强度,再根据相对海拔高度对二者进行插值得到最终的环境光,同UE一样我们为天空光添加了高度梯度使得云的顶部受到的天空光更多,代码及效果如下:
2.3 体积云的重建
在渲染流程分析中我们也提到体积云的重建是利用当前帧1/4 x 1/4的绘制结果去更新半分辨RT的2x2像素中被绘制的像素,利用历史帧结果去重建剩余3个未被绘制的像素,具体Shader源码可在VolumetricRenderTarget.usf的ReconstructVolumetricRenderTargetPS函数中找到,cpp端则是调用VolumetricRenderTarget.cpp的ReconstructVolumetricRenderTarget函数,重建算法的思路上也比较简单,大致分为以下几步:
1.当前像素为被绘制的像素,则直接用绘制的结果进行更新;
2.剩余的3个未被绘制的像素,首先根据速度矢量计算出其历史帧UV并判断UV的有效性,若历史帧UV超过范围不能使用,则直接在1/4 x 1/4的绘制结果上进行线性插值得到重建结果;若历史帧UV有效,进一步判断当前像素的深度与历史帧像素的深度差异,若深度差异过大则使用与当前像素深度差异最接近的相邻绘制结果来作为重建结果,否则将历史结果Clamp至相邻绘制结果的Min/Max包围盒内作为重建结果。
2.4 体积云的上采样及混合
得到了1/2 x 1/2分辨率的重建结果后,我们还需对重建结果进行一次上采样的操作才能与CameraColorRT进行混合,上采样算法采用的是Bilateral upsampling,其Shader代码可以在VolumetricRenderTarget.usf中的ComposeVolumetricRTOverScenePS函数中找到,cpp端则是调用VolumetricRenderTarget.cpp的ComposeVolumetricRenderTargetOverScene函数。
我们实现的时候,发现UE中Bilateral upsampling模块代码与TAA和waterSystem耦合较重,各种宏分支很多,对比之下,Unity HDRP管线中Bilateral upsampling算法实现相对简洁干净,因此在我们的项目中借鉴了HDRP中的实现,具体的代码可以参考Unity HDRP管线中的VolumetricCloud.compute中UPSAMPLE_KERNEL的Pass。
到这里,关于此次实践的分享已经结束,最后感谢大家能抽出宝贵时间耐心看完,也欢迎大家能留言讨论提出意见与分享经验。
参考引用
- 《虚幻引擎体积云系统剖析》:
https://zhuanlan.zhihu.com/p/645281439 - UE体积云组件:
https://dev.epicgames.com/documentation/en-us/unreal-engine/volumetric-cloud-component-in-unreal-engine - UE官方的Hillside示例项目可在虚幻商城里免费下载到完整工程
- HDRP体积云:
https://docs.unity3d.com/Packages/com.unity.render-pipelines.high-definition@17.0/manual/index.html
欢迎加入我们!
感兴趣的同学可以投递简历至:CYouEngine@cyou-inc.com