SIGGRAPH 粗读丨看看《地平线:西之绝境》燃烧海岸DLC中的云层渲染(中)


3楼猫 发布时间:2024-12-27 21:33:22 作者:Hakumen Language

前言

上一篇中,作者以相对简短(其实也挺充实)的方式带我们回顾了Nubis体积云系统的前置技术路线,及其优缺点。
简单概括来说,本身对于远处观看的和近处观看的体积云,渲染上自然有着不同的设计——在上篇中分别翻译为垂直轮廓法封包法的两种建模方式,其数据源格式、采样方式、是否支持光照、是否支持演进(可以理解为通过数学推算的动画,例如随风移动)等维度上都有区别。而且旧的两种云建模方式在制作上都是相对不友好的,不容易直接在制作阶段预览到结果。
这次的内容就会正式展开Nubis3系统中的体积云系统,或者说是体素云系统——上一篇中作者提到了,这一技术在最终要在成品游戏的DLC中交付给玩家是有一定挑战的,这次让我们一起看看这个方案落地的设计和实现。
文章还是以翻译原文的讲稿为主,并且由于原文的篇幅很多地方较长,会进行适当的精简和概括。由于图文内容量都很大,这次分为了上中下三篇——这是其中的中篇;打星号的部分则是我个人的补充说明。

1 体素云建模——Voxel Clouds Modeling

经历了短短几个月的快节奏开发后,我们得以圆满完成了这一任务——没有计划上的延误。
这里开始介绍我们为沉浸式(immersive)的实时体积云准备的解决方案。
回顾一下我们的光线步进(ray-march)的过程:
  • 我们在(从摄像机)开始追踪时并不确定具体云的位置。
  • 当命中云层时我们开始采样云的密度。
  • 直接光能量计算——需要一个(让人懊恼的,annoyingly)昂贵的第二条ray-march射线朝向光源。
  • 之后计算环境光的能量(*基于概率估算)。
  • 最终需要累加次一级的光源的能量(*基于概率估算)。
  • 之后我们对于每一个单位步长中都重复这一步骤,直到密度达到不透明(或出了云层)才停止march过程。
None of these operations are terribly efficient or were designed to work efficiently with voxels, but our approach to voxel cloud rendering replaces and optimizes almost every one of them with faster better voxel–based methodologies. Let’s start this overhaul at the beginning.
这些步骤都不是特别高性能、或是面向高效采样体素而设计,但我们的体积云渲染方案替换和优化了其中几乎每个步骤,使之变成更快的基于体素的方法。让我们从头开始介绍这次大型的改造。
开发一个高效的渲染与光照解决方案离不开稳固的建模方法,因为3A级实时图形技术中,你需要做(很多)“如何做”以及“消耗多少内存”的决策,(这些决策)可能会提升性能也可能影响性能。
回顾我们的2.5D云方案的密度采样,其中从2D建模数据中构建了3D云的体积——在提高其分辨率之前。
在一个基于体素的方法中,整个昂贵的操作可以被移到shader外,但这意味着需要能开发一种建模云的方式——存储并高效地通过采样器来访问它们。
There are many tedious and unrealistic ways to model voxel clouds by hand but the most promising approach that I found the most success with in the past was using fluid simulation to “grow” clouds.
尽管也有许多乏味而不现实的手工(建模体积云)的方式,但以过去的经验我探索出的最靠谱的方式是——通过流体模拟器来“生成”云。
在我们2014年未最终使用(un-shelving)的流体模拟实验的基础上进行“人造云层”,似乎会是一个不错的开始,但我们需要一种能将流体模拟数据导入体积云的方法。(*原文用了一个生造词,“frankenclouding”)
We developed a set of authoring tools in Houdini, that we call Atlas. At its heart, Atlas is a compositing pipeline for voxel data with various ways to generate or source and manipulate the data.
我们在Houdini中开发了一组资源加工工具——它被我们称为Atlas。在其内核中,Atlas是一组能将体素数据以多种方式进行生成、存储(作为来源)及操作的工具链组合。
例如,我们的一个大气环境艺术家Bryan Adams,从游戏中的长颈兽模型建立了一个体积云(如图)。
One of the things we learned early on was that multiple voxel grid boxes bounded us both figuratively and literally. Keeping them in memory and switching between them in the inner loop of a ray-march was not ideal and lead to slow performance.
其中我们很快总结出的一件经验就是——多重体素格对我们来说是一种限制(无论从比喻意义上还是字面意思上)。将其保留在内存中并在一次ray-march过程中(在其中)切换,本身就是不理想并会导致低性能的。
*这里作者又用了bounded这个词来做了一次双关,感觉又是一种特有的冷笑话了(上篇里也有过)。而且这个限制并不是被体积边界限制了,更多还是(同一空间里)多重体积这个数据设计导致的。
因此,下一步的思路就是当整个云的形体被构造后,我们将每立方米(every cubic meter)的云的密度信息写入一个密度体素栅格(dense voxel grid)中。
这将解决建模云过程的第一个挑战,并将2.5D的云采样中相对开销大的部分替换掉——如果不是引入了使人崩溃的几个问题的话...
第一个问题是我们要面对的是天量的体素栅格,这肯定会有内存瓶颈。
游戏的DLC区域面积有4平方公里,而我们需要大约500米垂直高度的空间来放置云层——从地面到空中。由于玩家可能飞跃云层,因此我们也需要足够的体素栅格的精度以保存细节信息。
即使放低期望并进行估算,也需要2048x2048x256尺寸的体素栅格以保证每2平方米的精度——玩家的飞行坐骑的翼展就有大约2米,因此这已经是比较极限的数据估计了。
这个密度的栅格需要很多内存,并且会因此导致更长的ray-march时间——因为潜在需要消耗更大的内存带宽。之前的经验告诉我们这样不可行。
也有一些稀疏的体素格式,能仅在有物体的区域内存储一些(相对)更高精度的数据,但需要一些间接定位(indirection)的步骤,这会增大每次采样的开销。对于已经有相当大开销的ray-march来说这是很致命的——并且由于时间也很有限,我们决定搁置这种方案。
我们决定从BC4压缩格式,8米的精度开始。(*就是暂时不考虑稀疏体素这个方式了,另外这里8米应该是长宽,高度应该还是1米)
让我们来总结一下体积云建模的技术路径:
  • 我们通过Houdini中一款称为Aero云解算模拟器来生成云。
  • 之后我们修改并将其整合到我们的“人造云层”中。
Then store them in obscenely low resolution voxel grids to be sampled at render time. It sounds like its hardly a solution to our goals, but…
  • 最终我们可耻地将密度数据以较低分辨率存储在体素栅格中,并在渲染时对其进行采样。听起来这差不多也算是一种解决方案,然而...
*最后一句作者用了很多作为技术分享不太常见的语气词,比如obscenely和sounds like、hardly,可见到这一步勉强实现的效果确实不太让人满意。obscenely这个词直译比“可耻”可能还严重一点,也比较奇怪,原意我这里就不写了,感兴趣可以去查一下。
*体素阶段精度的不足,后续作者团队通过利用节省出的性能空间进行了视觉效果精度上的弥补。

2 采样密度——Sampling Density

Past experience had taught us that balancing work between memory accessing and instructions in the compute shader can yield better performance on the GPU, so we chose to solve this in the density sampler itself as we have done in the past.
过去的经验教育了我们,在compute shader中平衡内存的使用和指令的调用能获得GPU端更更好的性能表现,因此我们在解决这个采样密度的问题时也参照了这一思路。
回顾一下之前(非体积采样)的方案:
  • 将2D的NDF模型数据解压成空间轮廓。
  • 之后“超精度”这一空间轮廓——通过以高频噪声侵蚀的方式。
问题是——当采样体素数据时,这一超精度方法还能生效么?答案是肯定的。
比起明确定义出每个体素的(高分辨率的)密度,我们的方法是从一个低清晰度的基于体素的空间轮廓进行“超精度”——使用一个新的细节噪声。
这使我们得以规避内存瓶颈——通过分流了一部分工作至超精度指令的方式,正如之前在2.5D云中做的那样。
*这里还没有具体描述细节噪声的结构,但给出了体素块的尺寸和形状。
这里用对比图简单做一个预览:上方的图是初始的空间轮廓渲染后的形态,下方的图是超精度之后的结果。
空间轮廓横截面(Cross Section)

空间轮廓横截面(Cross Section)

The Dimensional Profile is generated from a signed-distance field of the cloud to ensure that we get a gradient from outside to inside.
空间轮廓生成自一个有向距离场(缩写成SDF),以确保能得到一个从外到内的梯度。
我们的Atlas工具允许用户建模(被称为)Nubis体素数据场的数据,缩写成NVDF。作为对空间轮廓数据的补充,制作者能建立额外的NVDF,例如:
  • 细节类型(Detail Type),用来混合从纤细到波浪状的噪声细节。
  • 密度缩放(Density Scale),在噪声应用于超精度时适当降低密度。
这些数据使用BC6格式压缩(*之前的文章介绍过),支持3个通道——每个纹素(texel)占用1字节。
下面让我们看看体素云采样函数在超精度过程中的具体实现,以及新的3D噪声的一些特色。
我们的密度采样器中的第一步是:采样空间轮廓NVDF数据。
之所以第一步执行它,因为如果结果为0,则可以跳过后续关于云采样的步骤;如果空间轮廓是非0值,则我们将云模型数据传入超精度函数。
在深入超精度函数前,我们需要先来看看新的3D云细节噪声。
让我们看看这组实景定时拍摄(*原文中是较长一段视频)——虽然当时如果用了三脚架(Tripod)会使拍摄更稳定,不过目前也足以展示重点了。大家都看过类似的镜头,然而为了更好理解流体的特性,最好从不同的视角来观察它。
尤其是上下颠倒(观察)时,它看起来更像泼入水中的牛奶。从这个视角更容易想象水蒸气(water vapor)所受的压力和云整体受的挤压。这一点是我们建模细节噪声时所重视的细节。
*这里作者采用流体类比的思路来观察,让人觉得既吃惊又合理。
回顾一下,我们将云的细节分类为了波浪状的和纤细的。
当水蒸气进入一个冷空气区域时,它会更容易(原文是effectively)地向外被挤出。
The billows that we see are the result of some of that expanding vapor punching through weak spots in this squeezing force.
我们看到的波浪状效果是一些扩散的蒸气,在这类挤压力下穿透稀薄的部分后的结果。
现在,在脑中倒装这一过程,就能得到一个更纤细的荷叶边(scalloped)的形体。
这一过程发生在水蒸气蒸发,同时周围的空气挤压蒸气时。
这里展示了另一组延时拍摄——云稀薄得就像网一样(*原视频后面云逐步消散了)。
此时,空气的扰动就起到了更决定性的作用,为云的形体带来了额外的扭曲。
小结一下:
  • 在密度降低处,表现为多层的纤细细节和卷曲(curly)的扰动。
  • 在密度增加处,表现为多层波浪状的视觉。
在之前的方法中,我们使用反相的Worley噪声来生成波浪状的细节,但我们不得不重复采样很多层以获得类似云的效果——而不是堆叠的球体。
这次我们决定使用Houdini中的Alligator噪声(如图),既有着和之前的噪声相似的结构,又有着更像云的不均匀的空隙(lacunarity)。
对于纤细的细节,之前我们使用了Perlin-Worley混合噪声——不过,这对于超精度生成的过程来说还不够细致。
作为替代,我们从一个反相的alligator噪声开始,并使用一个卷曲噪声将其扭曲成需要的形态——这被我们称为Curly-Alligator噪声。(*Alligator直译是短吻鳄,这里属于专有指代)
我们生成的这些3D纹理是4通道128x128x128的体素。
前两个通道分别存储低频和高频的Curly-Alligator噪声,而后两个通道则是低频和高频的Alligator噪声——讲座的最后会介绍一些辅助产生这些3D噪声的工具。
我也乐于分享一个生成Alligator噪声的工具的源码(*图中链接),它是由SideFX Software提供的。
了解了这些3D噪声的具体格式后,我们可以回到超精度部分的具体实现了。
第一步是(类似2.5D云中一样)使我们的细节噪声卷动——我们通过添加一个“风偏移量”来改变采样位置,以实现这一效果。(*类似UV动画,只不过是三维的)
下一步我们依据MIP级别(根据摄像机距离计算)来采样3D噪声。这能提升大约15%的性能,并且一个合适的MIP级别并不会导致结果上可察觉的影响。
由于采样器会被频繁调用,因此能省一点是一点(原文是every little bit helps)。
下面让我们看看如何定义纤细和波浪状的细节噪声(在超精度函数内部)。
首先,让我们看看纤细云的照片。
注意在密度没那么高的部分,有一些非常高频的细节;而在更致密的区域有着低频的纤细结构。我们想要在超精度的过程中模仿这种关系。
回顾我们的空间轮廓NVDF,可以看到它已经提供了一个从外到内的梯度。
*虽然箭头是向外的,但原文描述的方向确实是from the outside to the inside;箭头更多可能是表达受挤压扩散的方向。
为了模仿这一点,我们可以简单地基于空间轮廓来对低频和高频纤细噪声做混合。
*lerp函数就是线性插值,看多了的应该都不陌生。
*三张图依次是:只有高频、只有低频、混合噪声的结果。
下面来看一些波浪状云层的照片。
注意到在靠近内核的部分有着低频的波浪状形体,而靠近表面则有着高频的波浪状形体。
对于波浪状形体,我们希望在边缘(相对核心)采样更多的球状结构。同样的我们也是基于空间轮廓来混合低频和高频的噪声。
*同样,三张图依次是:只有高频、只有低频、混合噪声的结果。
之后我们基于类型数据(type data),在采样位置对两种噪声(的混合结果)再做混合。
*三张图从上到下依次是:只有纤细噪声、只有波浪噪声、两者基于类型数据混合。
这在远距离时运作良好,但仍然无法符合我们想要飞跃云层的目标——在近处仍相当缺乏细节。因此,我们开发了一种重用高频噪声的方式,以在必要时添加一些更高频的噪声,同时不再进行额外的高开销的噪声采样。
我们创建了一种被称为双折叠噪声的算法,以获得两种类型各自对应的更高频噪声。
*具体公式如图,基础思想就是展开到-1和1的范围后,又通过取绝对值abs“折叠”,之后pow取多次方。
之后我们基于细节类型数据对两种类型的更高频噪声进行混合,最后再基于到摄像机的距离和之前(远距离情况下)的噪声云层进行混合。
*上面两张图展示了:未增加细节和增加了细节后的不同结果。
这里展示了飞跃云层的过程中这些细节噪声是如何生效的——并且可以看出这一效果是无缝切换的。(*因为都是连续插值的)
现在我们明白了噪声的混合及运作过程,让我们将其整合到函数中:
  • 首先,我们使用一个重映射(remap)函数来侵蚀云的空间轮廓。
  • 之后我们将超精度密度(up-rezed density)乘上密度缩放。
  • 最后有一个使结果显得更锐利的方式是通过pow函数——更多地在低密度区域进行这项处理能使这部分提高清晰度。
  • 之后我们将其作为超精度的结果返回。
*解说稿没有完全覆盖公式的每一步,但是参照各处的命名一致性应该比较清晰了。
总结一下:
从结果来说,这套实现是一种能从地面到空中近距离的无缝体积云渲染方案,它有着简化后的采样函数,既回避(sidesteps)了高内存使用同时又能提供足够高的细节度。
我们的空间轮廓能提供相对低精度的每8米一样本的精度,通过一些超精度方法能提升至(相当于)0.5米的精度。
解决方案的关键是平衡内存(从1米降至8米)和指令调用(引入一些噪声采样)来获得相对更好的性能。
不过我们还远没有结束这一改造(overhaul)过程,因为在最差的情况下,960x540像素的渲染会消耗10毫秒——而在地面观看时,我们需要和其它的一些资源一起分配性能预算。从影视特效或宣传动画的角度来说或许足够了,但作为一个实时渲染的游戏来说不能达到3A标准。
*最后一句及后面有一个bonus页,作者展示了一些在致密栅格采样(dense grid sampling)方面的探索。不过截至这一段落为止作者并没有明确后续是否要应用进一步提升精度的方案。

3 光照计算——Lighting

光照,让人哭笑不得的是(ironically)——它也是体积ray-march步骤中开销最重的部分之一。在它的面前我们的任务似乎是不可完成的:既需要节省大量的性能,又要提高质量以支持飞过云层时的效果。
回顾我们之前的介绍,我们把光照分解为直接散射(Direct Scattering)、环境散射(Ambient Scattering)和次级光源(Secondary Sources)三部分。
其中直接散射的计算开销非常大,因为需要向光源发射第二级的ray-march采样射线(*而且是逐采样点的)——我们也尝试了各种可能来加速或替换第二级的ray-march采样。
直到我们以体素的角度来考虑这一问题,之前有一个看起来比较高傲(loftier)的点子就显得有些合理了。
主程(Principal Tech Programmer)Nathan Vos一直想将光采样射线与视线采样射线中解耦,不过直到使用了体素方案后,改动的复杂度才降到了合适的范围。
在我们的新方案中:
  • 前2次采样的方法沿用之前的方案。
  • 但剩余的采样在一个单独的pass中被预计算,并存储于256x256x32尺寸的体素栅格中。
这有助于保持云的近处表面的细节,并提供一种更弥散(diffuse)的视觉效果。需要明确的是,预计算是发生在ray-march的步骤之外的。
Here is how it works: If a voxel has cloud in it, then a ray is marched between that voxel and the boundaries of the voxel grid and density is accumulated at each step.
这里展示了它(预计算的体素栅格)运作的方式:如果一个体素中有云的数据,则向光源发出一根射线,直到达到定义的边界,而过程中的每一步都对密度做累加。(*这里density因为是采样的光照,可以理解成通透程度。但后面还是简化翻译成密度。)
我们发现这大约减少了40%的渲染时间。这项操作自身消耗在0.1-0.2毫秒之间,取决于在一天里的时间以及射线到太阳的距离。这一开销是平摊到8帧之后的结果。
作为节省性能之后的补充,我们终于能渲染远距离云内阴影了。
这里(上面的图)是之前的方案中256米,10条光采样射线的结果。
下面的图则是基于体素的光照方案的结果。这是一次难得的场合,基于性能的优化得出了更好的效果。(*下面的图虽然看着更黑了,看似少了一些细节,但是明暗关系更“对”了一些)
切换到体素方案也为我们带来了一些自由,以固化(solidify)我们建模多重散射效果的方式——对应云的内发光到暗边缘的效果。让我们深入看看这一实现。
让我们看看休斯顿的Natural History博物馆展出的水晶样本。
It’s interesting because you can peer deeply into the core through the little gaps between each crystal. If you blur your eyes, you can almost see what looks like a lump of lighter material under the surface.
有趣的是你能从水晶针的微小缝隙中观察到其内核部分——如果你让视觉稍微模糊点,你就能把它近似看作(暗色)表面下的一小块发光材质。
很难不观察到其中的相似点。在云的表面边界处,有着更少的材质(*这里指实际的水蒸气、冰晶等)——使入射光线更容易散射到随机的方向,因而不容易进入人眼。
因而你能想象云内部有这样一个概率场——朝向观察者的内散射,随着深度越深就越是各向同性(isotropic)或全方向的(omnidirectional)。这种透射向表面的光照很像隔着一层厚棉花的闪光灯。
或是像水晶体结构由内而外发出的光。
从结果上观察这种散射的潜在规律,能更有助于我们建模实时散射效果。
*从自然界的实际效果观察,是这个作者很重要的一种思路。虽然实际上设计这种概率场是一种近似,但视觉上又很“真实”。
幸运的是,我们已经有了这种体积描述。让我们以图中的云为例。
下方的图展示了它的空间轮廓——让我们把这作为(散射)概率场的基础。
先让我们将这片云对齐到视线和太阳的中间。
然后取这个角度中心位置的一个切片作为样本。
这里是切片从初始角度进行观察的效果。
这里展示了我们使用空间轮廓作为散射参数的效果。为了模拟光散射被云体吸收的情况,我们额外应用了一个beer-lambert衰减曲线。(*上图是无衰减的,下图是模拟了衰减的)
这一曲线值在靠近太阳和云的内部的位置更大,以模拟前述的内部光散射效果。
让我们看看实际交付版的游戏中云光照的效果。
*上图是没考虑多重散射估计的,下图是应用了这一算法的。
*这是太阳在观察者背后的情况。上图是没有多重散射估计的,下图是应用了这一算法的。
就像在2.5D云中的那样,我们也使用空间轮廓作为估算环境光散射的概率场。
*上图是没有计算环境光贡献的,下图是计算了的。
由于我们(基于体素的)预计算光照采样的方案运转不错,我们终于考虑改进环境光照的计算方式,从概率估计变成采样累加。
当我们以射线采样预计算天空光照时,我们获得了能局部调整环境光强度的参数,使得天空被其它云遮挡的区域会计算更少的光强度。
在计算时,我们就直接乘上这一累加后的密度系数(*负值的exp,见图中公式)。
*上图展示了无方向性(旧算法)的环境散射效果,下图展示了基于体素光的方向性环境光散射。
而灯光之类的次级光源我们还是基于概率场来估算。
首先,我们定义了一个基于球体的潜在能量范围,并乘上一个模拟出的非均匀(non-homogeneous)密度系数。
这里展示了游戏中的一处效果。
由于这是一次大改造,因此确保云的渲染在一日循环中的始终有效是很重要的。这里让我们执行一个循环以进行观察。
那些远距离云上的阴影也极大提升了观感。
最后总结一下:
  • 我们解耦了光线的ray march过程,预计算并存储在一个体素栅格中的累加密度结果集合中。
  • 通过这一过程(相对原来)减少了每帧40%的开销,我们把这些性能空间用来添加远距离云的阴影。
  • 我们也使用这一方案来改进了环境光的效果。
  • 额外地,我们的一些光照估算方式被简化成直接查找(体素)空间轮廓。
有时就是这样,改良一项效果会开启其它解决方案的门。

结语

存储空间和指令调用(或算法复杂度)的平衡是作者提出的一项很有实践智慧的视角。在给定的需求——飞入云层作为大前提下,体素数据的使用就不可避免。在此基础上,云的建模从体素的结构和性能指标开始考虑,并在后续的密度采样和光照计算尽量发挥体素(对画面)带来的优势来改进渲染管线。
另一个很重要的视角就是——从物理光学中观察。毕竟这些方案无论显得如何“真”,其实中间不乏估算与trick;但很多时候,使用概率场或概率密度来计算光照或采样等,很类似数学拟合后的结果——其结果首先是要有合理的曲线,其次就是不会发生能量凭空增加的情况。只有做到了这一点,在实现全天候光照的时候才能有相对准确的结果。
这一篇读到的这三部分应该也算是Nubis3体积云系统的核心,下周更新的剩余的部分会包含一些系统整合方面的介绍。

最后是资料链接:
Nubis3: Methods (and madness) to model and render immersive real-time voxel-based cloud 1080P PPTX PDF


© 2022 3楼猫 下载APP 站点地图 广告合作:asmrly666@gmail.com