UE4之制作高端霸气的干冰体积雾

Summary

制作好看又不会让显卡爆炸的体积特效是近年来游戏开发的热点主题之一。受到去年战争机器5技术演讲和之前Guerrilla关于体积云的论文启发,我决定肝一发我的版本,最终成功利用虚幻引擎4的体积雾做出了软萌的干冰效果:

UE4体积雾の云https://www.zhihu.com/video/1211908332312891392

相关链接

  1. [点此看twitter上的讨论]
  2. [如果你的英语比较利索,可以移步我的博客原文]
  3. 我的TA交流群 1073502821

因为完全通过体积雾和Volume材质实现,今天的整个工程都不需要底层的coding,并且这个效果支持UE4的各种光照特性。

关于我

大家好,我是Asher,Epic Games的Developer Relations Technical Artist,平时我的工作包括帮助各种大小游戏工作室解决技术/美术问题及实现相关引擎功能,有时间也会用引擎制作内容作为演讲和教程的示例。体积雾效一直在我心中有着特殊的位置,加上之前做的这个干冰雾很受欢迎,于是写了这一篇自己都有点后怕的长文,很高兴和大家分享,希望带来启发。

我的 [Twitter] [Bilibili]

Volumetric Fog

Volumetric fog特性在引擎版本4.16时就有了。和一般只支持单方向光和skylight的custom volumetric raymarcher相比,volumetric fog支持一个方向光、任意数量的点光和射灯、skylight,并且支持各种阴影,比如静态阴影,CSM,距离场软阴影,DFAO;它同样支持VLM和粒子灯光。总之volumetric fog和引擎的各方面都契合得很好,性能也优。

目前主要的缺陷是不支持IES、raytracing,和更重要的,出于性能考虑不支持自阴影。本文稍后会提到一些有趣的trick来伪造自阴影。

关于此解析

开始之前先明确一下本特效的性能消耗不低。虽然Volumetric fog本身的定位是当前主流PC/主机硬件,但为了表现出浓雾的边缘细节,我们需要调高它的显示质量。

出于本文的展示目的,我把质量调得比较高,修改了这个cvar(可以在控制台里输入):
r.VolumetricFog.GridPixelSize 4
只改了这个cvar的情况下,最终干冰效果在2080Ti显卡上耗时~4ms(1080p)。

在此高质量配置下(也就是开头视频中的质量),最终效果的性能开销大概是默认volumetric fog的8倍;默认质量配置下(GridPixelSize=8),开销大概是默认volumetric fog的2倍

在学习制作volumetric fog特效时,出于调整效果需要,在你的机器能承受的情况下显然质量开得越高越好。

GridPixelSize = 4 / 8 / 16 的质量对比,点此放大

[点此放大图片]

在本文的最后会有一小节再说优化问题。你可以根据需求调整雾的显示质量,更重要的是,通过本文的解析,你能学习到各种花样控制3D体积雾的手段。在此之上更可以叠加其他的VFX来得到更丰富厚重的环境氛围。

此外,本文的难度也较进阶,如果不是基础较强的TA同学,挑战还是很大的。我尽量详细的提及各方面知识,如果你觉得信息量太大,经常要自己费劲折腾一整天再回来看,这是符合预期的,我看好你。Maximum effort!

Volumetric Fog的区域控制

首先你仍然需要设置指数高度雾启用Volumetric Fog,如果不想要整个场景的雾可以把密度设成0.00001(设成0会禁用)。

设置为Volume domain的材质可以用来描述空间中某一片区域中volumetric fog的Albedo(颜色),Emissive(自发光)和Extinction(不透明度)。举个栗子你可以用一个简单的SphereMask节点定义一个雾球,在材质视图中把WorldPosition连到SphereMask再给Extinction就可以。

把材质给随便一个方盒mesh就可以。不需要对法线有什么操作,材质也不需要设置双面,因为引擎只会取这个mesh的边界作为本地雾的范围。
知道了这个,我们可以玩出各种花样。

3D Noise

下面很自然的一波操作是尝试3D noise,因为对于屏幕上的每块面积[GridPixelSize * GridPixelSize]像素的方格,由于volumetric fog都需要对多个voxels进行计算,我们使用的Volume material需要较高的效率。如果使用自带Noise节点,里面Fast Gradient - 3D Texture是唯一足够快的方法。
上面的材质可以轻松生成这样的雾效:

伪の上帝之光

这一节和我们的想要的最终干冰雾没有直接关系,但这个例子能方便的展示怎样用数学生成好看并且高效的材质效果,包括体积材质。这是受Epic的Sjoerd De Jong同学一段技术演讲的启发,他在视频里提到使用贴图打造人为的god ray来给volumetric fog增加更丰富的细节。

[点此放大]

先算出和光源方向垂直,且互相垂直的两个单位向量,然后用这两个向量作为perin noise的UV输入就可以把perlin noise沿着光源方向‘拉伸’:

再举一个栗子,之前我为了一期影视主题的Unreal Circle技术演讲制作的demo:

这里地上也有一些流动的雾,是我当时制作这个效果的第一次尝试。


Dense Cloud

之前提到过,浓重的volumetric fog一般效果并不好,因为缺乏自阴影。另一方面,它和引擎的各种光照特性都结合得很好。我们可以用各种方法添加人工阴影,然后通过场景灯光和雾的动画来掩盖其缺陷。
总结一下我们需要做到以下几件事情:

  1. 让体积雾感觉上是贴着地表移动
  2. 制作云朵一般的
  3. 自阴影
  4. 平衡性能开销从而使它有见到太阳的机会

Distance Function

在战争机器5的技术讲座中,Colin提到他们给每一个场景都生成了对应的heightmap(高度图),并作为volume材质的mask。这达成了一个非常有用的控制:根据到地面距离控制雾的密度。

通过高度图实现棉花糖飘过山顶

对于本文解析的干冰雾效果,我们会使用Distance Field(距离场)来代替heightmap达到相同的控制目的。Heightmap采样更快一些,但需要制作自定义的工具来生成它,脱离了本文的范畴。

开启Distance Field以后,我们可以直接在材质视图中通过Distance To Nearest Surface节点来访问global distance field数据:

一个小tip,出于范围和精度的平衡,global distance field只会生成最远395.75cm的数据,所以并不适合很大范围的雾效控制(比如我上面做的山顶棉花糖,用的是landscape的heightmap)。

Volume Texture

至此我们了解了Volume materal的作用和它如何与场景结合,下面向大家安利Volume Texture。虚幻引擎4在4.21版本中加入了对真のVolume Texture(也就是3D texture)的支持。工作流上,我们会首先导入一张2D texture,它上面遍布一个正方体volume沿着一个方向的横截面,引擎会自动把它转换成GPU支持的3D texture。点这里看具体步骤

晚霞中的草泥马,你在哪里呀

这段可选阅读,如果你对VDB有兴趣)
引擎同样提供了接口直接读通过自动函数导入volume texture。你可以给UVolumeTexture::UpdateSourceFromFunction()喂一个函数来直接读取vdb文件。这里是我之前尝试的vdb导入插件

auto QueryVoxel = [&](const int32 x, const int32 y, const int32 z, void* ret)
{
	openvdb::Coord xyz(x + Start.X, y + Start.Y, z + Start.Z);

	if (TextureFormat == ETextureSourceFormat::TSF_G8)
	{
		uint8* const Voxel = static_cast<uint8*>(ret);
		Voxel[0] = accessor.getValue(xyz) * 256 * ValueScale;
	};
			if (TextureFormat == ETextureSourceFormat::TSF_RGBA16F)
	{
				half* const Voxel = static_cast<half*>(ret);
		Voxel[0] = accessor.getValue(xyz) * ValueScale;
		Voxel[1] = 0;
		Voxel[2] = 0;
		Voxel[3] = 0;
	}
	// Only listing two formats here
};

VolumeTexture->UpdateSourceFromFunction(QueryVoxel, End.X - Start.X, End.Y - Start.Y, End.Z - Start.Z, TextureFormat);
VolumeTexture->MarkPackageDirty();

return true;

导入支持多种材质格式,一般每个通道用8位就满足特效需求了。Note: 我这里的版本因为引用引擎自带的老版本OpenVDB,只支持导入老版本VDB文件)。

这节介绍了导入静态volumetric数据的方法。为了打造真实自然的云雾流动动画,我们需要在叠加多层3D noise并同时维持美术参数控制的手感。有些类似用2D noise叠加制作水波的normal或者高度。这是一个非常考验TA功底的任务(调参工程师已上线),幸运的是已经有不少FX / tech artists分享过相关解析。我将在下面的段落尽量总结我目前的心得。

Shaping The Cloud With 3D Noises

Author 3D Noises

制作3D noise有很多方法。我会先说明怎样在Houdini里制作它们,然后再介绍直接在虚幻引擎4里制作他们的方法(需要版本4.25或以后)。这么做的考虑是,Houdini制作3D noise的过程更加直观,有现成节点设计就是专门用来干这个,预览什么的也方便很多。然而如果你对Houdini不熟悉,或者想在UE4里实现高效通透的工作流,4.25里新的Volumetrics插件提供了可靠的工具协助你。

Create The Base Layer (with Houdini)

Guerrilla Games提到的Perlin-Worley noise是我测试中发现在美术上最耀眼的noise算法。像之前提的,Houdini提供了能无缝衔接的Perlin和Worley noise节点,用Volume Vop即可实现阶段性的Perlin分形noise:

[点此放大]


[点此放大]

这个for循环稍微有点反直觉,熟悉以后非常方便。实现了这个以后,改一个节点就可以方便的获得阶段性的Worley分形noise:

[点此放大]

在Cop net里乘起来你即可拥有Perlin-Worley noise:

[点此放大]

简单介绍一下,第一个Perlin noise用了一个常见技巧叫fBm(fractal Brownian motion),气氛上就是分形的意思--通过把不同频率的noise叠加在一起获得细节更丰富的结果,同时低频noise的大体形状。

Worley noise算法并不复杂--体素的亮度等于此体素到空间中其他点的次短距离。这个算法会生成软萌的视觉特征。把它和其他的noise叠加,通过参数控制可以让volume像云朵一样分离开,也可以给云朵边缘增加一缕缕通透的的细节

材质和之前的棉花糖山顶差不多,采样函数中采样了Perlin-Worley noise

采样3D noise和2D差不多,只是输入从UV(float2)变为UVW(float3),文档点此

图上你可以看到整体细节比较符合Perlin noise的特征且保持椅子和,同时Worley noise提供了浪花一样的起伏。这提供了非常不错的基础layer,稍后我们会继续修饰它并增加细节和动画。现在,如果你给3D noise的UVW输入加一点panning动画,立刻就会得到不错的流动效果。

Create The Base Layer (within Unreal Engine 4)

这是在引擎内生成3D noise的有趣方法。Ryan Brucks新加入的Volumetrics插件,在本文着笔时仅在dev-main (4.25)分支里可以看到。你可以在 Plugins\Volumetrics\Content\Content\VolumeTextures\Blueprints 目录下找到 BP_Draw_Tiling_volume 这个蓝图。

使用很直接,点击Create Static Texture按钮就可以生成一个uasset,推荐点进这个BP围观具体实现方法。

默认的BP预览试图和生成的3D noise

[点此放大]

主要的函数部分在 M_Encode_Tiling_Noise 里,这个材质描述了BP会生成的3D noise uasset。它并没有自带上文提到的Perlin-Worley noise,但我们可以很方便的修改。

点进去可以看到custom节点接收了一系列输入,并且生成3D noise。输入里的Function参数描述了noise类型。默认只有一个Gradien 3D noise输出。我们可以如图复制这个custom节点并修改对应参数,使得结果为一个Gradient (Perlin) noise和一个Voronoi (Worley) noise相加的结果。点Create Static Texture按钮可以得到和前文Houdini类似的结果。

[点此放大]

[点此放大]

Stacking Noises To Replicate Dry Ice Fog Movement

上面提到,Worley noise在整体云朵形状控制和打造细节上都很好用。与2D texture类似的时,我们可以把样式和流动速度不同的多层3D noise加/乘起来,从而获得更动态的流动效果。为了方便说明,我这里先用2D noise举例:

2层2D noise叠加

我们想要在3D空间中实现类似的变化,使得云雾能自然的流动。然而,3D noises的性能开销很大。举个栗子512^3的volume texture约等于12k*12k的2D texture,同时引擎目前不支持3D texture streaming (但支持mipmap)。屏幕上每[GridPixelSize*GridPixelSize]个像素都要进行多次voxel的计算,每次计算都要进行多次3D texture的采样,如果3D texture比较大,对显卡温度的贡献是相当杰出的。

一个更好的策略是叠加三层(而不是两层)比较低分辨率的3D noise(比如128^3)。这三层noise每一层都比前一层的频率高很多,叠加后能在维持细节丰富度的同时让肉眼感觉不到pattern的重复。

分别为R\G\B通道
叠加三层低分辨率的noise,为了便于理解,仍然全是2D的

可以看到细节确实比之前弱,但同时增加的一层layer使得整体流动的复杂度有所提升,并且看不出任何重复的pattern。
我们把这三层3D noise编码到一个Volume Texture的RGB通道里以节省显存。

感觉如果是Minecraft方块也不错呢
MF_CloudSample

[点此放大]

调了一会参数后,你可以成功得到这个东西:

然后你要在群里(1018846968)喊了这个怎么做的我对着教程做不行啊学不会实物和宣传不符。
这里就是对TA美术要求很高的部分,需要付出很多时间不断的尝试,调参调到意识模糊,才会对3D noise的行为产生一些感觉,并没有捷径。

我能给的一些tip是,开始时保持简单干净,先做好整体基础再加细节。我花了很多时间仅用两层noise来模拟干冰雾的流动(也就是上面说到volume texture的R、G通道)。
另外,让流动方向稍微偏离地表切线方向也很重要,因为distance field再持续的淡去地表上方的noise,稍微偏离一些可以增加noise的变化。

“再加入一点细节”

逗我呢这叫一点?Calm down,我能理解你的心情。但这两个动图的区别其实只有两块:

增加l两层3D noise,其中一层是上面的B通道,用法类似不再多说。另一层是一个更细腻的‘效果器’类似物,用来增加表面细节的锐度。
阴影,Offset阴影和DFAO。

Add Detail – Wispy Edges

在又增加了一层(G通道の)Worley noise以后你大概可以获得类似左边这张图的程度(除了阴影)

[点此放大]

到此为止大致形状已经定型了,但仍然缺乏锐利的边缘纹理。这里可以另外制作一层Worley noise放进A通道,也可以尝试复用前面的通道,来加入高频的细节,得到中间这幅图的类似物。因为频率高,重复度也高,但因为之前的三层noise叠加已经让雾效变化非常丰富,肉眼基本难以识别重复的pattern。

接着我们想要让靠近地面的部分柔和一些,不要锐度这么高,一是看起来错落对比视觉效果更好,二是如果你去搜真实干冰雾的视频,高处的雾扰动确实更乱一些,三是地面附近的雾如果对比度太高,volumetric fog渲染切片的artifacts会更明显:

[点此放大]

想要实现细节的渐隐控制,我们可以很方便的用distance field的值去lerp此细节层的强度。

Add Detail – Curl Noise (Optional)

另一个增加雾效性格很棒的方法是使用无散度卷曲noise(divergence-free curl noise)来对空间进行一些扭曲。强度比较高时你可以得到风格化很强的扭曲效果,比较低时可以给整体形状增加一些微妙的变化。当然这会额外消耗一次3D texture采样,所以见仁见智了。

在Volumetrics插件的 plugin/Content/VolumeTextures/Textures/VT_CurlNoise 路径下有一个线程的curl noise供同学们尝试。UVW同样接入world position。

[点此放大]

Curl noise强度:无 / 低 / 高 / 小浣熊干脆面


Shading

阴影 – Offset Shadow

首先,之前说过volumetric fog不支持自阴影。但我们可以在volume材质里对输入的Albedo染色来假造。当然也有一些问题,主要是在环境的真阴影中看起来好像哪里不对(因为直接光并没有照进来,但还是会有假的直接光阴影)。这个问题可以用对场景distance field的raymarching来部分解决,本文先不详细说了。

Offset shadow(偏移阴影),简单说来,就是这个:

这段字看起来很实,并且在画布很自然的留下一个投影。现在如果我们把下面个阴影叠在文字上,并且模糊一下,会得到:

这样的感觉,这就是我们将要对雾效做的操作。对Volume材质中采样的每一个空间点A,我们把它想光源方向偏移一段距离L,得到点B,然后以点B为UVW同时采样干冰雾MF_CloudSample,如果B的密度是0,那么说明干冰雾自己没有挡住光线射到点A的路径,A就还是亮的颜色,如果B的密度大于0,我们把点A的Albedo染色变暗一些,程度由B的密度决定。

[点此放大]

由于UE4的volumetric fog实现本身就加入了temporal抖动,亮部和暗部的界限并不是非常明显。你可以尝试调整L的长度来获得更自然的观感:

L从0到200cm的渐变

Offset Jitter

此外我还想了一个不错的方法,即上面UseJitteredOffsetShadow true的部分。我们对L在一定范围内进行高频随机的抖动,Temporal超采样会把阴影在抖动范围内柔化。显然这么做也会产生一些artifacts,但最终结果效果很好。抖动的代码包在上图的custom节点里:

int3 randpos = int3(WorldPosition.xy, View.StateFrameIndexMod8);
float rand =float(Rand3DPCG16(randpos).x) / 0xffff;
return rand;

Shadow – DFAO

另外一个理所当然并且很漂亮的自阴影方法是Distance Field Ambient Occlusion,在这里显然和引擎自带的DFAO实现不一样,但思路是一样的。

[点此放大]

实现很简单--voxel离最近的表面越近,那么颜色越暗。上图的Curve Atlas节点是引擎4.20加入的功能,可以方便的以给定的颜色曲线(包含RGBA通道)生成出一小块texture,并且在编辑器下可以实时修改。用这个功能来实现DFAO让我们能很方便直观的调整颜色曲线和不透明度曲线。

[点此放大]

优化

我还在实验一些优化的想法,比如预烘培的阴影、更有效加速密度为0的voxel渲染的方法。现在我先提一下比较重要的几点。

首先需要理解Volumetric Fog的几个cvar配置。Volumetric Fog最终是渲染成3D texture,然后拉伸填满到摄影机的视锥里,如图。渲染深度可以在通过 ExponentialHeightFog actor - Volumetric Fog - View Distance 调节。

[点此放大]

用GPU Visualizer(快捷键 ctrl+shift+, )可以看到,1080p分辨率下本工程的3D texture分辨率为 480x270x128

从何而来?让我们先看我用的cvar配置:

r.VolumetricFog.GridSizeZ 128 (默认值 128)
这是沿着深度轴(Z)的分辨率

r.VolumetricFog.GridPixelSize 4 (default 8)
这是用来定义屏幕XY轴平面和屏幕像素分辨率的比例,4的意思是在 1920×1080 屏幕分辨率下,雾效XY平面的分辨率为 480x270。

GridPixelSize 4 / 8 / 16 对比

[点此放大]

这些是你能用快速用质量换性能的主要cvar,比如把GridPixelSize从4打到8可以把渲染时间降到1/4。
同样的把GridSizeZ从128降到64可以削减一半的渲染时间,但由于纵深分辨率不够,temporal jitter会产生一闪一闪的artifact。为了缓解可以增加
r.VolumetricFog.HistoryWeight (default 0.9)
的数值,让temporal超采样保留更多的历史信息,但另一方面这又会降低雾效运动的清晰度。

最后还有一项效果明显的优化是,使用distance field的值来决定当前voxel是不是需要采样3D texture并做任何计算。材质视图的If节点并不能实现shader的动态分支,但Custom节点的代码可以,我们从distance field的值计算出一个Alpha值作为入参来避免对空voxel进行不必要的采样和计算。

[branch]

// Simplified code. Ideally we should immigrate most calculations from the material graph into the if scope

[branch]
if(DF < Alpha)
{
  return float4(
    Texture3DSample(Tex, TexSampler, UVW0).r,
    Texture3DSample(Tex, TexSampler, UVW1).g,
    Texture3DSample(Tex, TexSampler, UVW2).b,
    Texture3DSample(Tex, TexSampler, UVW3).a
  );
}
else
{
   return float4(0.f, 0.f, 0.f, 0.f);
};

恭喜你看完了

能看完我们都值得互相拍肩,真不容易,这也是我第一次写这么长的解析,如果有什么疑问敬请留言。再强调一下,这个工程陆陆续续花了我一个月才做到这个程度,所以不用急,慢慢来,每一段搞懂了都是赚到!

下期我may or may not继续这个课题,不过我会在Epic Games上海办公室出至少一期官方直播针对本特效做一些操作演示,期待投喂。

然而后面的知乎文你们想我讲啥呢?目前几个可能的主题:

  1. Niagara进阶特效,比如这个,或者这个
  2. 新的多层地形Landmass系统,之前的技术演讲
  3. 狂霸酷拽的进阶shader特效,比如这个
阿姆斯特朗回旋加速喷气式能量甲https://www.zhihu.com/video/1212401145722888192

可以留言投票~ 期待下期再见!

Asher Zhu
Tech Artist, Epic Games
http://Asher.GG

编辑于 07-07

文章被以下专栏收录