水是渲染行业永恒的话题,关于水的算法太多太多了,比如我博客中的Shader板块,有一个简易版水的算法。最近项目原因,需要一个出彩的水,刚好有个同事阅读过顽皮狗在GDC2012和Siggraph2016上分享的《神秘海域3》与《神秘海域4》中河流的实现方法,然后实现了Unity版本。我拿过来拜读后收益匪浅,必须要翻译到博客中。以下为本人针对Siggraph2016中《神秘海域4》PPT的全文翻译和个人理解。

Rendering rapids in Uncharted 4

《神秘海域4》是一部第三人称动作冒险类游戏,在全世界冒险和寻找财富。这不是一个关于水的游戏,但是各种形式的水是这个游戏的一个主要元素。

《神秘海域4》中各种情况下的水各种各样,从小池塘,到蔓延的河流,到暴风雨中的大海。

这份PPT是关于《神秘海域4》中的水是如何绘制的,我会主要讲如何处理河流,但其实这些技术针对绘制河流以及大海,都是通用的。

《神秘海域4》共有5个目标:1.从PS3移植到PS4,2.支持河流和大海,3.支持各种类型、各种情况(比如平静的、暴风雨中的、急流等),4.高质量的Shading和geometric细节,5.与船之间的交互以及通过特效实现的变形/形态受损。

关于在游戏中渲染水,以前做了很多的工作。目前,渲染海洋已经很常见,也有很多游戏使用了很好的技术去实现它(比如实时FFT,Wave particles ),这一块非常成熟了。然而,渲染河流却没有那么先进,我们已知的大多数技术,都是依赖复杂的Shader和顶点动画,有一些位移,但是大多数都是平坦的,我们今天主要讲河流。当然,也有很多的模拟解决方案可以创建出来很好的海洋和河流/急流,特别是由Macklin在2013年Siggraph提出的Positional Based Fluids,这个方案很好,且可以实时实现复杂惊艳的流体运动。但是由于运算量过大,还不太适合游戏。因为在游戏中设备以及FPS的原因,每帧的运算能力有限,且用于模拟计算的运算能力更少。

在游戏中绘制河流,需要处理四个方面:1.Wave animation,2.River geometry(mesh),3.Surface appearance(shading),4.性能,所有的工作都要在有限的时间内完成。这张PPT主要集中讲解1.动作,2.几何体,4.性能上。因为3.效果在很多其他论文和以前的工作中讲述过。

下图可以作为渲染河流/急流的目标,它的表面很复杂,上面有很多细节和湍流运动,所以我们可以从这张图片得到灵感。不过,它虽然很好,但是里面运动太复杂,无法从中总结出规律来实现算法。

Water

虽然在河流中绘制很多复杂状况是一个很好的目标,但是非洲河流和其他水上录像并非能给我们的解决方案带来灵感。所以,其实真正的灵感是来自于我在洛杉矶遇到了一场罕见的暴雨,看到了处于风暴中的水道。由于洛杉矶很干旱,暴雨很罕见,所以一定要利用好这次机会,我在我屋后的Ballona Creek 拍到了很多很好的,关于快速移动的水的照片,这些给了我很好了灵感,去用程序绘制河流。

Water

当我们去想河流中的水是如何运动的时候,我们总是会去认为它的运动是混乱的。然而,它实际上是重复且连续的,所以我们可以认为其中有很多重复的因子,或者说有个循环,所以我们可以利用这些现象去用程序构建我们河流中的运动。

通过看图片中急流的现象,总结出来以下4点:1.混乱的,但是有一个连续的节奏,2.运动看上去有些重复,3.有站立的波浪和向后的波浪,小波浪叠在大波浪上面,4.高频波浪叠在低频波浪上面。

构建运动中的水的程序模型:当构建海洋的时候:使用多种波浪(Gerstner、sine等),频率、振幅、相位各不相同,但是这些波浪会影响海洋的全局globally。当构建河流的时候:也有类似的波浪,但是每个都只会影响河流外表的局部。(Patrick:说实话,globally和local的区别我还是没懂,貌似是海洋的水会移动,但是河流的水只会上下晃?)

把海洋的技术运用在河流中,需要完成以下两点:1.将global运动转成local运动,2.运动中应该包含了什么?实际做的时候,就需要使用类似于动作的波浪,然后在河流的方向上采用它,也就是wave particles。

在神海3中,我们使用了GDC 2012中提出的Water Technology of Uncharted Wave Particles算法去实现海洋,而非FFT算法,也没有使用Gerstner 波(globally)。这是因为这个算法又快,又容易被TA理解。Wave Particle首次是在Siggraph 2007中被提出来的。算法曲线如下图所示。其中3条曲线,从外到内,波峰越来越尖,对应的β从0到0.5到1。

Water Water

太久没看公式,第一眼看到这个公式有点晕,算了下后对这个公式和曲线终于有点印象,其中x为横坐标、y为纵坐标。

Water Water

为了实现海洋的运动,我们创建了一个可重复的位移网格,Wave Particle会覆盖在这个网格中,每个粒子会在网格中生成一个小的起伏,当有足够的粒子的时候,我们创建的这个伪循环的运动就看起来像一个开阔的海洋的运动了。除了计算位移网格,我们还会计算相应的normal。这块网格的大小不会很大(因为会用到tessellaton),在这里我们使用的是一块32*32个顶点的网格,每个网格包含300个粒子。一个单一的网格运动起来还行,但是这样并不像一片大海,如果我们用多层网格,就好像八度音阶一样,那样就看起来好多了。每个网格的参数都是独立的,我们可以控制粒子移动的快慢和整体海面的翻滚程度。一般先控制海面的翻滚程度,再控制移动速度。这些参数类似于实时FFT中的“wind velocity”参数,但是这个能更容易的被TA进行控制。当我们针对这些网格采样的时候,会当做他们是同一个表面使用了重复的贴图,然后会把它们堆积在一起。使用多个独立的小网格的优势在于当它们的尺寸不成倍数关系的时候,不用考虑到repeat,这种情况经常发生。

Water

除了Wave Particle之外,还要计算normal和应变值(网格单元的扭曲,是用于寻找wave的尖的,这个值将会被特效使用,去判断应该在海洋的哪个位置绘制粒子)。应变值的计算公式为:d1 = length(cross(vi+1,j-vi,j,vi,j+1-vi,j)),d2 = length( cross(vi,j+1-vi+1,j+1,vi+1,j-vi+1,j+1)),r=.25 // parameter that can be adjusted .25->1000,strain = 1.0-(r*(d1+d2)/(scaleX * scaleZ))(Patrick:这个公式是真没看懂,但是应该有用哇)

Water

下面要详细聊Wave Particle,首先先把Wave Particle和FFT做一下对比,先放一个FFT的效果图,从图中可以看出来FFT就三个参数,尺寸,风速和波峰的尖锐程度。还有动态版本的。可以随意调整一下参数看看效果,相同情况下尺寸越大,波浪看起来越不明显,反之就越明显越多细节,相应的还可以改变风的方向和速度,以及波峰的尖锐程度,就可以很轻易的看到对应的效果。

Water

使用Wave Particle的话比FFT更简单,且只有一个频率。FFT的话包含了多种频率和更为复杂的外观。但是这一点的话,我们可以使用叠加多层网格的方式来弥补,每层网格的频率和缩放比例是独立的,这样做的好处是我们可以对每层网格更加可控,更灵活。相比较下来,FFT的话需要一个大的网格来包含所有的频率,且TA也很难去操控这些频率。

通过下图就可以看到Wave Particle是如何叠加多层的。(在这里我们叠加了4层,每一层的尺寸都是2倍关系)。如果我们选择大小不是彼此的倍数,我们就避免了重复的视觉模式。

Water

然后,我们只需要把所有的可扩展网格层中的某些(x,z)点的位移加在一起,组成的结果就是3D位移矢量表面。

Water

海洋表面叠加了四层wave particle,每一层都会有一个不同的频率、缩放比例和旋涡速度。

Water

下一步,我们就要研究如何对Wave Particle进行修改来模拟河流。我们可以通过控制翻滚程度和频率控制每个网格,下面,我们就通过降低网格频率来模拟站立的波浪,加速高频率网格的翻滚去模拟流动的水。

基于我们的海洋使用的技术是采样自wave particle的位移网格,而且每个都可以独立控制。也就是说每个都可以单独的缩放、滚动、拥有自己的频率。在实验中,我们将一个高频网格放在一个低频网格上,这样就营造出了小波浪叠在大波浪上面的感觉。然而虽然这样看起来像是河流了,但是我们依然无法控制河流流动,所以我们需要局部调整。也就是使用流技术。

下面要使用的流技术用在了神秘海域:德雷克的宝藏中,并发布在了Real-Time Rendering 2010上。Shader中以矢量场的方式用到了法线图/流图,是一种用于绘制流水的常见技术。

所以纹理流基本上是纹理按照矢量场的移动,当纹理移动了一段时间后,我们可以将其重置到其初始位置。我们在两个相位偏移的纹理之间进行混合。如果使用一张生物结构的纹理,看上去就更好了。

Water Water

下面要做的就是把流动和move particle技术合并在一起。从shader层次上进行合并,将每个网格按照流动的方向不同的速度进行合并,使用振幅图来调节河流的部分地方。

将流技术与wave particle合并后,我们就可以非常精确的控制波浪的方向。比如,我们可以使用一个圆形的流图去生成一个漩涡。

Water Water

如下的图中使用了4层位移网格,每一层的缩放比例不同,速度也不同,使用了一张RGBA纹理去控制每层网格的振幅,还可以通过更多的RGBA纹理去控制每层网格的流向。

Water

通过以上这些算法,完成了顶点动画,核心算法就是根据水流动的方向,在wave particle纹理中采样,再根据振幅等算出实际顶点偏移,加到顶点位置上,造成顶点移动。

下面要说的技术点,是Mesh重建(tessellaton登场)

投影网格(屏幕空间mesh)需要细分的非常好,以防走样(因为使用粗糙细分的时候,当镜头出现又大又高的浪的时候就会出现走样),但是这样的话运算量过大,且不适合拥有不同高度的河流。而且在PS3中的神海3中用到的几何剪切图,由于是不规则的且不具备几何过渡技术,所以也不适合当前PS4的GPU。

所以要用到Mesh LOD技术,有如下特定:1.顶点的稳定性,也就是说在切换LOD的时候,一个顶点必须保持它原有的位置,即使相机旋转了。2.顶点少了,GPU也省事了。3.支持有边界和无边界的物件(河流和海洋)

2010年Filip Strugar有篇文章,Continuous Distance-Dependent Level of Detail for Rendering Heightmaps,就是说根据距离的远近使用不同的LOD,简称CDLOD,是用于地形绘制的,使用四叉树结构,支持到 tessellaton的patch级别。

CDLOD的数据结构很简单,共有四层,每层都是根据距离摄像机的位置进行细分的,每层是按照两倍关系逐层变化的。从摄像机开始,可以看到根据距离关系绘制了四层曲线,用来决定何时要使用更高一层。每层都将包含一套网格用于作为tessellation的基础网格。我们使用的网格是每个quad 为17*17,这是由于考虑到内存等原因。每层网格都会根据波浪独立进行位移。

Water Water Water Water Water Water Water Water

一个最基本的quad会在GPU中tessellation成一个17*17的网格,这个规格可以改变,我们选择这个规格是考虑到内存、计算量的平衡。

Water

下面这张图可以看到一个quad被细分为17*17的样子。每个quad的间距在这个图中进行了放大,是为了更清晰的看到17*17

Water

下面这张图可以看到真正用于制作波浪,没有gap的mesh

Water

总结一下河流中的CDLOD:1.从河流的几何体中抽取出一套四边形,2.根据距离来决定四边形的LOD,3.根据高度图来进行摄像机Cull,4.根据XZ来对每个四边形进行细分,5.使用高度图和波浪图来改变顶点高度。

下面是制作河流的工作流:1.先有一堆四方形,这个时候还没有对其进行细分,也没有根据距离进行LOD;2.细分,并根据距离LOD,然后根据高度图和波浪图修改其高度;3.下面就就跟前后物件进行融合,加上最终的特效和后处理。

Water Water Water

虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2018 年 1 月 31 日发表,原文链接(http://geekfaner.com/unity/blog10_Water.html)