照例对方案的要点做一个总结:
- 方案是面向PC&主机设计的,性能在PC上都算高的(全部特性打开,6.6ms)
- 采用多个cascade构成clipmap、lod来覆盖广阔的区域,同时保障近景的精度
- 波形实现用的是Gerstner Wave的方案,通过多个Gerstner Wave的叠加来实现海洋波形的模拟,同时通过cascade的作用来降低叠加wave过多时的消耗
- 设计了一套通用的波形输入框架,支持各种波形输入的效果叠加:将波形的输入剥离出来,并设计了相应的通用格式,方便实现各种输入数据的叠加
- 参考盗贼之海的shading逻辑,基于wave的波长来调节shading的效果,以实现兼顾近景远景的次表面散射效果的模拟
- 在前面的数据输入框架中,叠加了flowmap,并针对叠加后的问题做了处理,做到了小尺寸、大尺寸波浪与flowmap数据的兼容,基于这个方案可以实现海面上的漩涡效果
17年分享过后,有几个项目开始接入,作者跟另一位兄弟开始把业余时间都投入到这个里面,经过产品的使用与反馈,对项目的效果做了进一步的完善,这次把一些新的进展给大家做一个分享(依然保持开源,这也是一条非常诱人的路啊)。
这里需要强调的一点是,方案目前只是面向PC跟主机。
适配到了多个平台,也适配了不同的风格。
最后5张是为LWRP管线设计的一个demo场景的,下面有个视频。
这里给出了文章大纲:
- 先对业界的方案做一个比对
- 再来介绍一下本次分享的重点,multi-scale & multi-resolution框架,基于这个框架可以根据视角来实现不同的计算LOD,从而可以很好的保障近景的精度与足够的渲染范围。
- 接着来介绍上面的框架的一些优点
- 再接着17年分享的着色逻辑,谈谈19年的进展
- 最后看看性能以及未来的工作
这里给出了一个场景的效果,以及与之相关的水体绘制所需要的输入贴图数据,可以看到,贴图数据总共有五类(每类对应于一列),同类贴图有多张,从上到下分别对应的是覆盖不同范围(相邻翻倍)的数据,从左往右分别是:
- water depth,水体深度贴图,黑色表示0,通常是陆地海拔过高,其实也可以看成是某种clamp版本的场景高度图
- water flow,水体流向图
- displacement,水体形状所需要的贴图
- foam贴图,基于displacement计算得到
- shadow,通过jitter采样可以实现水体的软影、硬影效果
这些图都是运行时计算的,可以支持各种动态效果
接下来看看前人工作,这里不可能囊括所有的方案,只会介绍一些最近成功落地的
波形是水体效果表现优劣的关键,这里对前人的方案做了分类,分别是动画波形与动态模拟波形方案。
动画方案:
- 无状态,预测方便,可以很方便的实现网络同步
- 比如可以通过多个波形(如gerstner wave)的叠加来给出相对真实的表现,但是这个叠加是有限制的,数目过多导致性能问题,数目过少效果又不真实,神海3的做法是将Gerstner wave跟wave particle结合,前者给出大体轮廓,后者给出高频细节,从而实现两者的兼顾(神海4则抛弃了Gerstner wave,直接用了多种不同频率的wave particle叠加来实现)
- 另一种方案则是FFT,借助不同频率的FFT的叠加(分别覆盖不同范围),可以很好的实现高低频信号的兼顾,具有不错的真实感
模拟方案:
- 运行时模拟得到波形结果,天然支持与环境的动态交互效果
- 因为无法tilable,所以随着范围的扩大,性能下降的很快,暂时无法广泛应用
- 这里给了两种代表性的方案(UIWS的涟漪模拟跟浅水方程SWE)
- 关于这块的分析,Jeshke2018的工作做了比较细致的整理
两种方法各有优劣,业界常用的做法是将两者进行结合,动画方案给出大体形状,模拟方案实现交互细节
回到Crest的方案,Crest提供了一套可伸缩的框架:
- 支持对多种波形数据进行快速累加
- 支持一个相对大范围的动态模拟
目前方案中,大体波形是通过对多个Gerstner Wave叠加完成的,理论上也可以换成多个FFT叠加,而细节模拟则是通过对Ripple Simulation方案模拟得到,理论上也可以换成SWE。
接下来看看波形数据是如何使用的:
- 盗贼之海是直接对FFT贴图进行采样来实现波形的模拟
- 刺客信条2012则是将数据写入到一个屏幕空间buffer(projected grid),这种方案天然支持LOD
- Crest则是将多套不同LOD(相同分辨率,覆盖不同范围)贴图结合起来使用,与之类似的,Insomniac在Resistance 2中尝试将多套FFT(不同频率)叠加起来使用,其中奇数序列的贴图会旋转45度
- 神海4的做法则是以wave particle为基底,通过程序化生成的primitive来对波形效果进行雕刻,并添加一个局部的形变贴图用于驱动特效
相比于此前的方案,当前方案主要是将数据抽取出来,做成了一套相对独立的框架,基于这个框架可以很方便的实现数据的添加删除与更新,以实现各种各样的效果。
接下来就来看看这套框架的细节
位移数据用于描述各个点的偏移,水面可以看成是一个平面的grid,基于位移贴图的偏移才形成了丰富的波形效果,因为是3D偏移,因此可以用一个颜色来表示。
前面说了,水体的数据都用贴图来表示,同一种类的贴图具有相同的分辨率,覆盖不同的范围,这里可以用一个texturearray来表示,目前的设计中,每张贴图分辨率为512.
每一张贴图覆盖范围都是前一张贴图的两倍,8张贴图在当前的设计中,最大覆盖范围为8公里(以角色或相机为中心)
左下角的图给了一个debug view,为了避免两级之间过渡的跳变,这里还在接缝处做了blend。
这种设计的好处是,只需要增加一张贴图,就可以将覆盖范围扩大一倍,具有很好的伸缩性。
跟17年介绍的细节一样,随着视角的抬升,mesh的精度会平滑的调整以覆盖更大的范围
这里用一个简单的例子来描述框架是如何工作的。
假设开发者需要在水体上添加一个垂直方向的偏移,比如基于smoothstep函数实现,要怎么做呢?
那就是在场景添加一个quad,之后在shader中返回一个高度数据(计算逻辑写在shader中,比如实现smoothstep),并且将混合参数设置为叠加,之后通过某种方式将这个输出注册到系统中。
之后在运行的时候,系统就会整合各种来源的数据,按照一定的算法进行计算,输出波形效果。
除了波形,shading、foam也可以采用类似的逻辑实现。
基于这种方式,可以为各种不同的gameplay需要提供方便的接入手段。
这里对方案做了个总结,简单来说就是可以适配无限大的范围,具备较好的伸缩性,对数据的输入做了抽象设计,允许各种各样的输入数据,而计算逻辑则是统一的。
这里带有流向的大洞就可以通过上述框架很容易实现,其他的比如standing wave也可以通过同样的方式实现。
接下来看看位移贴图实现的一些细节,进一步介绍框架的优点。
在当前的框架里,支持用多种分辨率来完成水体的绘制。这里以Gerstner Wave举例,在运行时对多个Wave进行累加的时候,遭遇的一个严重问题就是性能问题,但是这个问题在当前框架的实现中将不存在。
这个效果不只是对于Gerstner Wave有效,FFT也一样
Gerstner Wave:
- Analytical Wave
- 每个粒子都是圆形运动,靠经表面的半径大
公式给出如上图所示:
- cos表示高度的偏移
- 水平的偏移用sin表达
通过将多种具有不同振幅不同波长(相邻翻倍)的Gerstner Wave叠加起来以得到符合视觉效果的波形displacement数据。
将相邻两个cascade的数据混合起来(高精度数据覆盖区域小,只与低精度区域的某个quad的数据混合)
这里用几张图解释了高精度跟低精度混合后,会起到一定的细分(平滑)作用,并用了一条一维的曲线做了进一步的说明。
这里演示了采样密度下降后波形随之变得平滑的效果
性能相关:
- 将一些周边的逻辑从循环中移出来,采用SIMD,每次计算处理四个Gerstner Wave,每个Cascade通常包含8个Wave,也就只用循环两次即可
- 每个Cascade的贴图为512的,最终8个Cascade叠加了224个Wave,大概花费的时间给出如上(GTX 1070上耗费0.47ms)
对displacement波形逻辑做一下总结:
- 采用的多分辨率方案不只是适用于Gerstner Wave,其他波形如FFT也同样适用,这种方案可以兼顾性能跟效果,既避免了密度过低带来的锯齿问题,也避免了密度过高导致性能消耗
- 最后的混合pass会使得结果变得平滑,这个虽然在效果上有一定的减损,但总体来说,可以看成是正优化
- 最后,还会通过一个异步pass将数据回读到CPU用于gameplay逻辑,后面也在考虑通过将这个过程放到GPU上来做进一步降低损耗。
接下来看看动态波形模拟部分
波形模拟想要实现上图所示的效果:
- 水面上的物件能够对水面的波形产生影响(涟漪、波纹等)
- 支持近景的交互效果跟远景的交互效果(远处游轮对水体的影响)
这里来介绍一下基本思路:
- 模拟是发生在Height Field上的,作用的结果会叠加到前面的Displacement波形上
- 右侧的debug view给出了模拟的结果,红色、绿色分别表示高度以及垂直方向上的速度
- 通过compute shader完成模拟计算,在前面的多分辨率框架下,每个cascade会dispatch一个shader,彼此独立计算
- 最后在前面displacement的叠加pass中完成结果的应用,无需额外的计算pass
扩散(Dispersion)是一种大尺寸波形传播速度快于小尺寸波形的现象,这个效果有助于增强水体表现的真实感。
默认的波形计算公式中,波形的传播速度是一个常量,这里只需要给每个cascade的波形添加一个变量来控制即可,得到的结果是每个cascade中的波形传播速度是相同的,对标的是该cascade中最小的波长的波形传播速度(虽然效果上有一些影响,但是在游戏项目中来说,是合适的)
在进入细节介绍之前,先来给出前人的实现方案:
- Canabal 2016等项目采用一个大尺寸的拉普拉斯kernel来得到相应效果,并通过一个mipmap金字塔结构来优化性能,但这个实现复杂度会偏高
- Day 2009采用频域模拟的方案实现,需要FFT跟IFFT计算,复杂度跟消耗都偏高
- Jeschke 2018则直接在一个大尺寸的分辨率上进行多次模拟,消耗就无法接受
一些实现细节:
- 会基于当前cascade的scale以及CFL条件中的C来动态调整time step(作用是?)
- Wiki中对CFL有详细描述,简单来说就是一个wave每个timestep不能跨越一个grid的尺寸,上图给了CFL的计算公式(2D)
- 其中参数C可以用于描述计算的稳定性,这里用0.6来进行显式欧拉模拟
- C固定下来后,波形的传播速度跟grid的尺寸就取决于cascade了,基于上面的公式,我们就可以计算出模拟的timestep(也就是频率)
- 在这样的逻辑下,我们就可以天然支持LOD(远处cascade的计算频率就可以变低,在视角抬高后,波形的模拟频率也可以降低)
- 模拟同样是发生在8个cascade上,以512的分辨率进行,覆盖8km的范围,近景处计算频率要提高,大概需要3次模拟,在1070的设备上花费0.6ms
基于上述计算后,我们只能得到波形垂直方向上的交互效果,缺少了水平方向上的扰动。
从真实的角度来看,波形应该同时具备水平跟垂直两个方向上的移动才好
Tessendorf的FFT方案中描述了如何基于Heightfield中计算displacement的方法,第一个公式给出了从频域转空域heightfield的计算逻辑:
- 是频域数据
- exp是不同频率的波形公式,其中k是波形的传播方向,这个向量的长度正好对应波形的波长的倒数
第二个公式给出了水体mesh的顶点在水平方向上的偏移计算方法。
第三个公式通过梯度算子(对第一个公式的x进行微分计算),得到了高度的空间微分项,跟前面第二个公式后面项相比,就差了一个k的倒数,正好是波长,这也是为啥大尺寸的波形容易生成大尺寸的displacement的原因。
上面的公式只是用来解释基本原理与特性,并不打算直接按照这个公式来计算,因为这里不想要走复杂的FFT计算。
不过这里有一个问题,那就是目前模拟得到的wave是多种频率多种波长的波浪混合而成的,我们要怎么知道应该要乘以哪一种波浪的波长?
这里粗暴的采用了当前cascade中的多种波长的平均值,在Nyquist定律的限制下,平均波长导致的结果是displacement会被低估,也就是波形起伏没那么高,这个虽然看起来真实感稍有减弱,但至少不会出现异常的bug。
(Ottosson方案在这里做了带宽的约束,限制了最大的波长,本文方法还没有尝试)
所以这里是为水平方向波形效果增加的一些计算逻辑:
- 按照此前介绍的模拟方法进行计算
- 在将计算结果整合到最终的波形数据之前,先计算出波形的空间微分
- 在空间微分上乘以平均波长以及一个美术同学控制的缩放因子
- 将上面一项结果应用到前面模拟的结果中,并叠加到波形上
右边是添加了水平displacement的效果,恕我直言,除了shading部分(材质、foam)有所变化之外,其他地方并没有明显区别
这里对波形方案的实现逻辑做了总结
这里对Jeschke 2018的Water Surface Wavelets方案做一个补充说明:
- 这种方案将dynamics跟visual details解耦开来,可以用一个较低的分辨率来完成dynamics计算以降低计算成本,不过虽然如此,由于方案还是在单个grid上计算的,所以时间消耗的量级跟其他方法无区别
- 用一个量化的数据对比下,Jeschke方案的GPU消耗为12ms,而本文方案则为0.6ms(view dependent adaptive lod),此外,显存消耗也会跟高
- 在效果上,Jeschke方案支持了较多细节的展示,包括ambient wave撞到边界反弹的效果等
- 如果将Jeschke的方案集成到本文描述的多分辨率框架中,将可以很好的解决原文中消耗过高的问题。
下面看下光照散射计算逻辑
光照散射解决的是光在水体中传播并最终进入人眼的计算问题
这里采取的计算方法与盗贼之海的类似,都是基于水平方向的displacement的长度来的。
为什么散射需要跟水平偏移的距离关联呢,如右图所示,按照Gerstner Wave的模拟,水面上的粒子在水平上的偏移即为其运动轨迹(圆形)的半径,而这个数值与波形波峰的高度、宽度有直接的关联,而这些参数都跟散射的计算有很大的关联。
为了将这套逻辑应用到前面提到的多分辨率计算框架中,这里对displacement做了一个归一化(除以波长),并用归一化的结果来计算散射(为啥?下面会介绍为啥不能直接用displacement)
最开始的计算逻辑中,散射结果只与水面的高度(深度?)有关,这种逻辑下,每当波形条件变化,都需要重新调制参数(?),非常费劲。
而如果直接用盗贼之海中基于displacement的散射计算,就会在远景处得到这样一种不真实的效果。
但是如前所述,如果将displacement除以波长,问题就解决了(原因没解释)。
最后看下流向效果的实现。
流向是一种预定义的水体速度的特性,这里直接将flow设置为整体框架的一种输入数据,并基于这个输入数据来对水体的所有贴图进行处理,以得到更为统一的动态效果。
这里展示了流向数据作用的结果,流向数据是以1s为周期重复的。
但是1s的周期下,在大尺寸的波浪上,会很容易看到波形的循环起伏效果,真实感有较大影响。
将周期调整为4s后,问题有较好改善。
但是这又会导致小尺寸波形拉伸跟锯齿变得严重
最终的做法还是老招数,为不同cascade的波形设置不同的周期,从而实现两者的兼顾,这种方法还能很好的缓解flowmap本身的pulsing(跳动)效果。
最终效果。
对flow效果的实现方案做一个总结。
接下来看看shading部分。
shading也是跟cascade的scale关联的,有如下的好处:
- 可以消解远距离下的pattern & 噪声效果,类似mipmap
- 性能会更好
主要的细节来自于法线贴图,剩下的大尺寸特征则来自于波形,目前最大波长为1km
最后来看看性能表现
这是测试场景,开启了前面说的所有特性,并采用了最高配置
测试数据由Nsight Graphics给出
总体的消耗是84M显存,虽然看起来合理,但还是有一些可以优化的空间:
- 不是所有的项目都需要动态改变的地形的,所以如果不需要的话,深度贴图可以在离线烘焙好
- 并不是所有数据都需要相同的分辨率的,比如foam就可以尝试使用更低一点的分辨率
- 并不是所有数据都需要覆盖8个cascade,同样的foam可以稍微减少几个cascade
- 一些临时数据其实是可以复用的,不用占据那么多空间
- 部分数据不需要那么高的精度
这里是单帧的耗时,所有的计算都是在GPU上完成,花费6.6ms,其中模拟花费2ms,渲染1ms(还有3.6ms呢?)
再来看下另一个场景,采用的是crest的默认设置,覆盖7个cascade,每个cascade贴图的分辨率是256
贴图分辨率降低为原来的1/4,显存耗费也未1/4,同时模拟时间也降低为接近1/4,不过渲染的面数保持不变,所以基本一致。
接下来看看未来的工作
首先,目前是把所有的特性都开了,实际需要的pass应该会比这个少,后续可以优化。
目前的网格覆盖了viewer周边的广大范围,这个对于海洋来说没问题,但是放到局部水体如江河湖等就不太行了。
Dynamic Wave sim还存在另外一个问题,就是在相机移动过快的时候,会使得部分wave消失(原因未描述,只说了评估后认为不算是大问题)
后续工作方向
结论
阴影贴图也是每个cascade有自己的一套数据,在计算阴影的时候会从已有的阴影贴图如CSM中进行采样,之后根据不同的阴影类型(普通高光阴影,还是体积光阴影效果)来设置不同的jitter半径
这里是阴影作用的位置
用红色标识出来
这里给了个视频
这里介绍了这个方案的不足,就是阴影是跟阴影贴图绑定的,如果水体对应的区域对应的阴影贴图数据缺失,就容易看出瑕疵(不过好像仔细看了下,也没发现问题)
阴影部分的总结,可以考虑扩大shadow区域来规避上述问题