今天学习的是Uncharted 4在Siggraph 2016上分享的他们的水体技术方案。
相关要点总结如下:
- 河流通过Wave Particle FBM的方式来实现水波的模拟
- Mesh组织方式为CDLOD,离线模式统计出需要绘制的base quads,运行时基于距离完成base quad的细分与tessellation
- 河流的流动效果通过控制不同octave的流动速度来实现,降低低频信号的流速,提高高频信号的流速
- 船体对水体的影响是通过胶囊体模拟实现的,这里还通过离屏buffer计算了粒子特效的作用,最终将这俩还有displacement的作用结合在一起,完成水体的高度的控制
介绍一下Uncharted的历史
PS3
PS4时代Uncharted 4
Uncharted 4中水体在游戏体验中占有重要作用。
需求:支持各种各样的水体,包括小水坑、覆盖较大范围的河流以及风卷浪涌的海洋。
这里会聚焦在河流的渲染上,不过相关技术在河流跟海洋都是共用的。
在此前的游戏中已经有大量关于海洋的渲染方案出来,基本上是基于FFT+Wave Particle方案来的,已经比较成熟了。
而河流部分则主要通过一些复杂的shader计算来模拟,mesh表面大多是不带displacement的,少量会有一些顶点的displacement。
而在顶点的驱动上,Positional based fluids的实时模拟方案算是其中比较表现较好的,但是依然计算消耗很高,而这正是本文的关注点。
河流的渲染主要需要覆盖四个方面的内容:
- 首先是水波的模拟,关注的是顶点如何在实时完成offset以得到尽可能逼真的河流效果
- 接着是河流的mesh组织逻辑
- 第三个是河流的shading效果
- 最后是性能
想着先看看真实的河流效果,看能不能有一些启发
但分析下来发现实在是过于复杂了。。。
最后从家后的一条水沟的水流中得到了启发,这个特征相对简单,更为聚焦。
水流虽然是混乱无规律的,但是仔细观察就会发现水波其实是按照一定的规律在重复的,而重复特征在实现上就是一个循环。
可以将水流的水波看成是函数的输出,函数的输入为源头的水流与各种外界输入(风等),函数的处理逻辑为水流流过的地形,只要这两块是稳定的,那么输出就应该是基本稳定的(即不断重复)。
除了重复这个特征外,对上面的水体分析,我们还可以有如下的一些结论:
- 有standing wave,有backward wave
- 细节水波叠加在宏观水波之上(高频叠加在低频上,FBM)
这里顺便说一句,河流湖泊等水体通常会采用FBM(不论是基于程序化噪声perlin noise还是基于离线计算得到的波形贴图)来模拟。
通过对海洋的水波跟河流的水波特征进行分析,我们就知道,两者的共性都是高低频的叠加,不同的是海洋是全局的叠加策略,而河流则是局部的叠加,即不同区段的水波表现会有所不同,因此叠加参数,输入的基础噪声会有所不同。
一个实现策略是尝试将海洋的方案复用到河流上,那怎么来模拟水体在河床上的移动效果呢?
这里给出的方案是采用一个基于水波的动画,将之沿着流动方向进行传播,实现上可以采用Wave Particle。
Wave Particle之前在Uncharted 3中就用过,这种方案具有实现简单快速、效果控制比较直观且方便的特点,这里我们来看下单个波形的计算公式,假设,同时有:
就可以得到高度y随着空间某个方向的偏移值x的映射关系,上图给出了不同的取值下的波形效果,可以看到越大,波峰越陡峭。
Uncharted3就开始通过Wave Particle来模拟海洋水面的波动效果:
- 通过多层不同频率与振幅的Wave Particle(类似FBM)来实现高低频搭配效果
- 在一块区域(grid)使用多个Wave Particle,彼此之间叠加之后实现海洋波形的模拟
这里给出了实现的一些细节:
- 一块由32x32个顶点组成的区域大概需要300个wave particle(这里没有考虑分层)。
- 单块区域采用上述模拟方法看起来还可以,但是如果每个区域都这样做效果就不太好了,这里的解决方法就是采用前面说的FBM方式
- 提供了多个彼此独立的用于控制Wave particles效果的参数
- 单个particle的移动速度控制波形的锐度
- 整个区域的滚动方向控制流动速度
海洋会包含多个区域,如果每个区域都单独模拟的话,消耗会很高,因此这里多个区域之间会复用模拟输出的displacement结果
不过为了提供一些区分度,这里还增加了一个叫做strain的概念用来表达每块区域(grid)的torsion:
这个数值可以用于检测水波的尖端(cusp),拿到这个数值后,就可以在对应位置生成对应的特效(浮沫?)。
下面对FFT跟Wave Particle(简称WP,下同)方案做一个对比
FFT的问题:
- 需要一个相对大尺寸的grid(顶点数更多)
- 美术同学在效果调整上会相对困难一点,而WP则因为是多层组成的,因此允许美术同学对每一层的效果进行调控
FBM设置,一共四层,每一层都是上一层的两倍。这里也说到了,如果下一层跟上一层不是整数倍的关系,可以避免各个地方的效果的重复感
将多个grid的displacement叠加在一起,就得到了最终的displacement。
前面介绍了海洋的Wave Particle实现逻辑,解析来看看怎么将这套方案用在局部的河流模拟上。
这里的想法是通过控制每个频率的grid滚动的速度来实现模拟:降低低频信号的滚动频率,提高高频信号的滚动频率。
这里展示了前面不同grid流速控制的结果,得到了一种高频细节在低频水体上流动的效果,不过暂时还缺少了控制河流转向的方法。
转向控制想通过flowmap来实现。
Flowmap之前用在法线的扰动上,以得到一种水体流动视觉上的模拟(没有用来控制displacement)。
这里对实现逻辑做了回顾。
如果要用来实现displacement的控制以实现displacement层面的流动效果,就需要基于同样的思路来控制不同频率grid的scrolling方向。
为了得到更精细的控制,这里还增加了一个振幅(height)贴图。
这里给出了实现的伪代码:
- 通过flowmap来控制WP采样的位置
- 得到的displacement之后再叠加一个全局heightmap
这里展示了通过flowmap实现一个circular的流动效果的截图。
这里给出了经过上述逻辑得到的河流效果截图,其中为每一层的grid提供了单独的WP振幅控制贴图,通过一个RGBA贴图来存储对应的数据。
虽然也可以为每个grid设置单独的流向,但是试过之后发现效果也就那样,所以就算了。
接下来看下mesh是怎么组织的。
这里介绍了两种之前用过的方案:
- Projected Grid:需要通过Tessellation来避免视角移动导致的抖动
- Clipmap:因为PS4硬件的特性,不规则的计算逻辑效率会有明显下降,同时相邻clip之间也没有做衔接,存在裂缝
这里给出了Mesh方案的两点要求:
- 顶点要保持稳定,不论是LOD切换还是相机切换,都不能存在明显的跳变
- GPU友好
这里想要采用CDLOD来实现,CDLOD除了衔接平滑等优点之外,还可以很自然的用来实现河流这种局部数据(控制部分节点的显示即可)
四叉树管理,每一层都是上一层的两倍,这里可以通过一套切换距离来控制什么时候需要显示哪个层级的数据。
这里选择mesh patch的尺寸是17x17个顶点,这个数字在平衡内存、顶点密度上具有最佳的表现(没给理由)。
这里给出了如何基于切换距离来得到CDLOD的节点细分层级的图示逻辑。
用颜色标注出了需要衔接的区域
基于上述区域得到需要做过渡的节点,注意,并不是与前述区域相交的顶点才需要,同时,与这些顶点共用父节点的节点也同样需要。
前面得到了CDLOD需要展示的节点(层级),每个节点直接以Quad形式发送到GPU,GPU中会通过tessellation将之转换为17x17的patch。
Tessellation完成后的效果。
多层之间自然衔接
将CDLOD用在河流上,流程给出如下:
- 选定一个quad覆盖范围(这里的quad我们称之为base quad),离线烘焙出一个稀疏的四叉树,用于覆盖整条河流
- 运行时基于距离等来决定每个quad的细分层级
- 基于quad的boundingbox来实现视锥剔除
- 在GPU中完成每个细分后的quad的tessellation,之后在VS中完成displacement的计算
这里给出了河流的覆盖区域,用一套base quad(固定覆盖范围,没有细分)来覆盖。
经过细分并displace之后得到如图效果。
加上其他数据
接下来看看物件与水体的交互实现逻辑。
将交互的物件用胶囊体表示(只需要几个参数即可表达),之后在CPU中获取到与之存在交互关系的base quads。
在GPU中通过某种方式拿到受影响的base quads以及对应的胶囊体,完成对应的形变计算。
这里用图示效果做了实现逻辑的展示。
特效对水体的形变,走的是另一套逻辑,类似于角色与泥地、雪地的交互,会先将特效的作用绘制到一张RT(RGB,32bit,R11G11B10)中,用于表示特效对水体的offset影响,之后在绘制水体的时候对这张贴图进行采样实现控制。
贴图是垂直投射到水面上的,整体的计算逻辑分为几个步骤(都是通过compute shader实现的?):
- 在base quads确定了对应的层级,完成细分之后,得到更细粒度的quads(这些quad会被tessellated成17x17的顶点),这里将这些quads称为water boxes
- 通过相交检测,拿到与船体相交的water boxes列表
- 在displacement计算完成后,会对与船体相交的box的所有顶点做形变处理,将之降低到与船体底部持平的高度
这是整体的计算流程:
- 先计算WP的displacement
- Particle的作用绘制到一个离屏Buffer中
- 完成box quads的tessellation,并基于WP的displacement以及water offset buffer实现顶点的高度变换,得到一个顶点array
- 另外用一个compute shader来对被船体影响到的box的顶点进行形变处理,最终的结果被用来进行绘制
水体的锯齿问题是一个需要关注的挑战点,具体可以参考KeXu的TAA分享。
最后看看整体的生产管线。
这里并没有想象的那么简单
Houdini用来生成水体的height map跟flow map
首先是河流的base mesh:
- 控制水体的覆盖范围、高低走势
- paint各个区域的水体属性,比如速度流向之类的
得到的base mesh
Houdini中的FLIP模拟,输出base mesh相关信息与flowmap
河流各个区域的效果控制参数众多
生成四张贴图,alpha mask用于控制哪些区域不需要水体(或者需要渐隐)
还有两张额外的贴图:
- 幅度贴图,控制该区域的WP grid的振幅
- 用于对各个位置的WP做精细控制Wave modulation贴图
最终结果
美术同学工作流
载具在水中的效果