【Siggraph 2018】The Technical Art
今天分享的是《盗贼之海》在Siggraph 2018上的TA技术,这里照例做个总结:
- 项目基于UE4实现,基于延迟管线绘制
- 瓶颈主要在内存与CPU
- 水体波形采用FFT方案,包括foam也是,shading部分采用的是偏风格化的方案
- SSS效果通过FFT生成的displacement数据来fake
- FFT的foam过于杂乱,这里加了模糊叠加了贴图之后效果才好起来
- 最坏情况下(全海洋)GPU耗时为7.86ms
- 针对甲板上的浅水滩还做了专门设计,包括运行时的水流模拟、蒸发等
- 通过mask的方法实现了水体与物件的交互效果(包括河流中物件对流向的影响,瀑布中物体对水体的分割等)
- 云层采用模型的方案来实现,做了一系列的trick来提升整体表现,总体耗时1.32ms(或许移动端也可以尝试这个方案)
- Vertex Dynamics上介绍了绳索的简单模拟方案、触手的缠绕实现逻辑以及闪电的实现策略,具体细节不做展开
![](https://img.haomeiwen.com/i19200103/73f0aac34593f52f.png)
![](https://img.haomeiwen.com/i19200103/e9abad44f355b100.png)
![](https://img.haomeiwen.com/i19200103/bfa1f3af191e53d9.png)
![](https://img.haomeiwen.com/i19200103/b42732ec272a53eb.png)
如何在一个风格化项目上兼顾风格化跟PBR两者的优点?
![](https://img.haomeiwen.com/i19200103/9507e1264c9ee4a5.png)
![](https://img.haomeiwen.com/i19200103/3c8697ee75190037.png)
项目基于UE4实现,在4.10后停止合入引擎的更新,目标平台主要为XBox跟PC,采用的是延迟渲染管线
![](https://img.haomeiwen.com/i19200103/215f322f23c99bf8.png)
光影部分主要采用的是UE自带的方案,项目本身需要支持TOD,采用的是基于LPV的实时GI算法。
从项目跑测来看,主要的瓶颈大多在内存、CPU上,GPU则基本上是够用的。
![](https://img.haomeiwen.com/i19200103/09fc7d5cd0304f38.png)
本文主要涵盖三大主题:
- 水体
- 云层
- 顶点效果
![](https://img.haomeiwen.com/i19200103/4c51ed10d6bb1a72.png)
采用的是FFT的波形模拟方案,shading则采用的是非PBR的方式
![](https://img.haomeiwen.com/i19200103/0d2c6e494db78621.png)
这里的一个要点是想要将真实效果的流体模拟与风格化的画面效果相结合:
- 波形采用的是逼近真实效果的模拟算法
- shading部分,如foam跟sss则采用的风格化方案
![](https://img.haomeiwen.com/i19200103/8dee6fd313b6c5d0.png)
具体而言:
- 借助FFT生成的wave数据,标注出wave的side部分(这是哪个部分?顶端?)
- 假设wave的side部分将具有更高的散射光照
结合上述部分与dot结果得到最后的SSS效果
![](https://img.haomeiwen.com/i19200103/414909ebe2431ab5.png)
![](https://img.haomeiwen.com/i19200103/662f3b09b8ea66dc.png)
foam则采用的是传统的雅克比矩阵方法得到其作用位置(即水波尖端开始碰撞重叠的区域)
![](https://img.haomeiwen.com/i19200103/19040b526ffa327c.png)
示意图1
![](https://img.haomeiwen.com/i19200103/2e167da2c257b5a7.png)
示意图2
![](https://img.haomeiwen.com/i19200103/1907fd877f700989.png)
直接按照FFT方案实现的foam效果太过杂乱(?)
![](https://img.haomeiwen.com/i19200103/57a283f63a1f6351.png)
在上述基础上叠加一个渐进式的模糊(具体一点?)
![](https://img.haomeiwen.com/i19200103/e7344b0401200dfa.png)
之后将这个结果跟foam贴图采样结果相混合
![](https://img.haomeiwen.com/i19200103/d84dac4fdd8b9697.png)
叠加颜色
![](https://img.haomeiwen.com/i19200103/7a08749942cbb4c0.png)
模拟storm效果
![](https://img.haomeiwen.com/i19200103/50a3060a14fe9b81.png)
XBox one在最坏情况(全场景都是水体)下总体的GPU耗时为7.86ms。
![](https://img.haomeiwen.com/i19200103/fd3a4bd84d1195ae.png)
表面浅水效果如甲板上的水体,这里还做了特别处理。
总的来说,因为上图的一些原因,浅水部分的模拟相对而言会简单一些。
![](https://img.haomeiwen.com/i19200103/bb1256eb41677e11.png)
在盗贼之海的船体尺寸下,基于美术同学提供的heightfield贴图,可以用较低的消耗实现较为高精度的模拟
![](https://img.haomeiwen.com/i19200103/b51621cad6ec5524.png)
这里的模拟是基于2007年的一篇文章实现,整体计算逻辑在PS中完成,总体需要两个pass:大致是将水域分割成2D的cell,各个cell会计算相邻cell的水体高度,并基于水往低处流的原理进行逐帧的更新计算。
![](https://img.haomeiwen.com/i19200103/110bd526297391d7.png)
第一个pass中:
- 会先计算各个cell外流的部分,并写入到一张RGBA贴图中
- 表面斜率会调整相邻cell的局部差值(基于法线来对相邻cell的高度进行增减)
![](https://img.haomeiwen.com/i19200103/4844739a055b21f2.png)
第二个pass中对每个cell计算其流入流出部分:
- 减去本身流出部分
- 加上相邻cell流入部分
- 加上cell含水时用于保湿的基本值
- 考虑水体蒸发的损耗等
![](https://img.haomeiwen.com/i19200103/92f6330b213b0323.png)
![](https://img.haomeiwen.com/i19200103/a9d0a62bbbf9624b.png)
得到height field之后,会计算对应的法线贴图,之后对甲板的材质做修改,使之看起来像是叠加了一层水一样。
之后会使用水体的深度以及固定的湿度来对甲板、水体的法线做插值扰动
![](https://img.haomeiwen.com/i19200103/8e0ef6d18d9783d3.png)
![](https://img.haomeiwen.com/i19200103/83d193b074808bac.png)
这部分总体GPU消耗为0.2ms。
![](https://img.haomeiwen.com/i19200103/614015f56e2e36e0.png)
下面看看水流效果,包括水体的自然流动以及与场景物件的交互。
交互部分主要是通过生成一张相交mask来实现。
![](https://img.haomeiwen.com/i19200103/73282bb75f930797.png)
首先要得到物件与水体相交的mask,写的有点没太看懂(从水下往上看,以水面作为远裁剪平面是不是就行了?或者找到水上部分,将之投影到水面上,这个方法对于上大下小的mesh不适用)
![](https://img.haomeiwen.com/i19200103/10c160a2ba60e1d2.png)
这里给了个示意图
![](https://img.haomeiwen.com/i19200103/d1c120d565a330a0.png)
![](https://img.haomeiwen.com/i19200103/7012ca0970faf5e2.png)
![](https://img.haomeiwen.com/i19200103/92f16bff4e6bb373.png)
拿到mask之后,会通过additive的blend方式跟上一帧的结果进行混合。
之后会基于一个离线生成flowmap对这个混合后的结果进行advect(推送)跟模糊。
![](https://img.haomeiwen.com/i19200103/d71ce2f221c7b887.png)
将这个方案用于瀑布生成
![](https://img.haomeiwen.com/i19200103/d6dab15cdedec344.png)
最后看下可交互的foam实现方案。
这里跟前面可交互水体效果一样,需要做一遍深度的比对来找到需要生成交互foam的区域,同样用一个mask表示
![](https://img.haomeiwen.com/i19200103/a374901c8c58c62b.png)
![](https://img.haomeiwen.com/i19200103/43993909708cc3d9.png)
这里是最终结果
![](https://img.haomeiwen.com/i19200103/e329d3702ffc6902.png)
水体流动效果总体耗时比较低,不超过0.1ms。
![](https://img.haomeiwen.com/i19200103/0379d8bdc1b97813.png)
下面看下云的实现逻辑。
![](https://img.haomeiwen.com/i19200103/5d7e111027fcd580.png)
![](https://img.haomeiwen.com/i19200103/bb9f22ba9bbe9e69.png)
目标云效
![](https://img.haomeiwen.com/i19200103/0806dbe5de4cd07d.png)
总体需求:
- 风格化效果
- 支持3D展示(旋转视角不穿帮)
- 支持动态变化
- 形状可控性高
- 性能消耗不能过高
美学层面的要求就两点:
- 具有较好的可读性
- Geometric
![](https://img.haomeiwen.com/i19200103/2077432847b65944.png)
显示参考Paths of Hate的效果,采用模型来实现,为了得到fluff蓬松效果,还添加了billboard。
![](https://img.haomeiwen.com/i19200103/77aae3c830dfee1f.png)
这里的一个关键问题是如何平衡性能跟效果
![](https://img.haomeiwen.com/i19200103/f2507fde9415601e.png)
这里的做法是将模型渲染到一张单独RT中,采用顶点光照,所以计算成本比较低
![](https://img.haomeiwen.com/i19200103/28417ee920f1fbdf.png)
![](https://img.haomeiwen.com/i19200103/aeffbc85baa4590e.png)
降低到1/4分辨率,并做了一次高斯模糊(基于深度调整了高斯模糊计算公式中的标准差,可以看成是一种低成本模仿DOF的方案)
![](https://img.haomeiwen.com/i19200103/c0e2d55875a974a9.png)
光照结果写入到一张4通道的RT中(为啥?)
![](https://img.haomeiwen.com/i19200103/a667315b34608b8d.png)
在云层的depth上做一次box blur,从而可以在mid-level的云层上
![](https://img.haomeiwen.com/i19200103/56f6a5739f51e56b.png)
之后在距离相机500m的位置绘制一个quad,把前面的云层RT结果弄上来
![](https://img.haomeiwen.com/i19200103/390a111392ae7cf3.png)
为了节省性能,这里只对有共享的像素做计算
![](https://img.haomeiwen.com/i19200103/be07635950866647.png)
基于前面模糊过的depth,我们可以反投影得到一个模糊后的世界空间坐标,这个在边缘会不太稳定,所以不适合用来做光照,但是可以用于计算雾效
![](https://img.haomeiwen.com/i19200103/797ed9ec74521db4.png)
接下来需要用到一张噪声cubemap,低频信号存储在RG通道,高频信号存储在BA通道
![](https://img.haomeiwen.com/i19200103/eb1a10b47f6e4e1b.png)
之后基于depth来对噪声进行混合,通过这种方式来实现远处的云层会朝着地平线弯曲,营造一种天穹的效果。
上图中有部分区域是黑色的,这是因为这里移除掉了高度低于某个范围的区域的噪声(主要是为了避免影响到detail cloud效果,具体参考原始视频)。
![](https://img.haomeiwen.com/i19200103/1404845548bf1b68.png)
将噪声应用到云层上,通过alpha做一下处理,可以让远处的云朵看起来更锐利
![](https://img.haomeiwen.com/i19200103/b299cdf316466c88.png)
之后应用红绿通道的光照结果,得到上图效果。
![](https://img.haomeiwen.com/i19200103/e7bbf875f4a5f143.png)
最后叠加雾效
![](https://img.haomeiwen.com/i19200103/4f11d0df69bd9b17.png)
![](https://img.haomeiwen.com/i19200103/db50c895c0c03686.png)
![](https://img.haomeiwen.com/i19200103/78545906ea7283ce.png)
虽然之前担心相机的缩放会导致效果的异常,但实际测试发现表现还都可以接受。
![](https://img.haomeiwen.com/i19200103/d37745c9df857fb8.png)
总体GPU耗时1.3ms
![](https://img.haomeiwen.com/i19200103/6504d9e5f7ffd9fa.png)
这里还展示了这种方法可以支持局部光源的照明效果
![](https://img.haomeiwen.com/i19200103/5e077110b78e065e.png)
同时还能实现云层内部的闪电照明效果
![](https://img.haomeiwen.com/i19200103/a799bcad92512250.png)
给太阳开了个洞,来得到一个相对好看的screenshot,有点trick。。。
![](https://img.haomeiwen.com/i19200103/991289d3b21295d9.png)
后续的优化点
![](https://img.haomeiwen.com/i19200103/5c26961362ee3f17.png)
展示了用于近景的雾效
![](https://img.haomeiwen.com/i19200103/7b676f329c4d4929.png)
在远景效果上如果移动过快,会有较为明显的扭曲。
同时多个不同深度堆叠的时候,在模糊作用下,会有较为明显的异常,还会抖动。
![](https://img.haomeiwen.com/i19200103/89b4d062c6f9e078.png)
下面看下vertex dynamics(找不到一个契合的词,就不翻译了)
![](https://img.haomeiwen.com/i19200103/5be8e2f7ce7556d2.png)
绳子的需求:
- 角色可以直接调整桅杆高度,从而带动绳子的变化
- 桅杆是可以断掉的,绳子也要有相应表现
- 数目多
- 模拟成本高(屏幕空间同时存在的绳子segment多达数百)
![](https://img.haomeiwen.com/i19200103/d191f7ee2d726aeb.png)
一个想法是用一个简单的公式来实现对绳子的模拟,比如输入起点、终点以及绳子的长度,就能得到绳子各个segment的位置(悬挂的绳子是一段抛物线)。
![](https://img.haomeiwen.com/i19200103/bc6e7e9bd57ef961.png)
实际上绳子下垂构成的是双曲线,这是有一个可用的公式的。但这是一个超验公式(transcendental,?),没有解析解。
![](https://img.haomeiwen.com/i19200103/d9e27a3644bd043b.png)
虽然shader中内置了双曲线求解函数,但是执行效率过低。
但是绳子的模拟本质上是一个mesh形变,所以只需要逐顶点计算,成本较低
而前面的复杂公式可以只针对单个绳子的segment执行一次,可以考虑用数值解来逼近。
![](https://img.haomeiwen.com/i19200103/8bd1c6660214919f.png)
如图所示,需要求解的就是y = f(x, H, L),即给定H,L,得到横坐标x处的y值。
这里可以将AB两点的水平方向距离做一个归一化,从而减少一个变量
![](https://img.haomeiwen.com/i19200103/72a43662682d7d0e.png)
这里给出具体的求解伪代码(本文采用的迭代次数是20)。
![](https://img.haomeiwen.com/i19200103/aa5af41803b8268c.png)
展示了效果。
![](https://img.haomeiwen.com/i19200103/b6c85d719d5280ca.png)
有了位置,还需要法线,好在双曲线的微分方程比较简洁,不用额外再求取数值解。
![](https://img.haomeiwen.com/i19200103/2aa3f71370c45079.png)
另外一个问题是,如果不做处理,直接用uv采样得到的贴图会有明显的拉伸。
![](https://img.haomeiwen.com/i19200103/4b1600062610cd91.png)
要解决上述问题,需要获取到arc的长度(?),而这个数值计算起来就过于复杂了,且需要计算两次(一次当前点,一次起点,两次相减得到结果)。
![](https://img.haomeiwen.com/i19200103/2c123cd2d22f728c.png)
- 接下来把绳子的segment链接起来
- 两个点的距离直接决定绳子的长度
- 绳子上两点相对位置变化的时候,将这个length offset传导出去,就能得到正确的结果(没有展开)
- 把绳子拉紧的时候,直接变成直线,以解决精度不足时的异常表现
![](https://img.haomeiwen.com/i19200103/83153c6c57e4755c.png)
这里展示了绳子的动画效果
![](https://img.haomeiwen.com/i19200103/477840e1dd1f939c.png)
随着距离的拉远,为了提升视觉效果,会将绳子改成简单的半透渲染,同时启用persson经典的wire AA算法来优化锯齿表现。
![](https://img.haomeiwen.com/i19200103/356cac76b2dedd0d.png)
![](https://img.haomeiwen.com/i19200103/55f6d9d2d037ea69.png)
性能优化上,超验公式是离线算好的,之后在渲染的时候采用实例化的方式一次性完成渲染。
![](https://img.haomeiwen.com/i19200103/005efac5fbda7503.png)
Tentacle(触手)的实现方案有如下的一些困难:
- 在CPU侧执行Anim Graph过于低效
- 逻辑十分复杂
- 还需要避免触手穿模
![](https://img.haomeiwen.com/i19200103/68ac0081939ff953.png)
借鉴了Kautzman 2016的方案:
- 使用houdini来驱动FEM模拟
- 将结果烘焙成顶点数据,并存储到贴图中(顶点动画,也可以参考神海方案,将结果转成动画数据,走蒙皮逻辑播放,不过这里提到说盗贼之海的CPU过于紧张,不想加重这块负担,所以用了顶点动画,将消耗放到了GPU)
- 运行时读取贴图得到法线跟位置数据
![](https://img.haomeiwen.com/i19200103/b7c511db39b4ef68.png)
通过动画数据重用来实现加速(总体有19个缠绕动画)
![](https://img.haomeiwen.com/i19200103/76617259fcba72ff.png)
每次缠绕可以拆分为多个阶段,每个阶段都导出了一份数据,在运行时,只需要将这些数据按照权重插值即可实现平滑过渡。
![](https://img.haomeiwen.com/i19200103/b5d80beac3e95424.png)
给出了一些更多的细节
![](https://img.haomeiwen.com/i19200103/a62883dd343380e3.png)
展示了不同的缠绕效果
![](https://img.haomeiwen.com/i19200103/8c4fb110692992ff.png)
这里的一个trick是将blend shape烘焙到切线空间。
一种常用的低成本实现mesh状态变化的方案是将blend shape烘焙到UV上,但如果物件是蒙皮的,带有动画的,将每一帧的数据都烘焙下来成本有点高。
这里给出的策略是存储每个顶点以及切线坐标的displacement数据,之后就能够借助蒙皮算法实现免费的形变,简单来说就是动画蒙皮逻辑继续执行,只是在执行的结果上叠加顶点色、UV等通道存储的偏移信息(具体细节参考视频)。
![](https://img.haomeiwen.com/i19200103/8811659c2cfde2c6.png)
这里给了一个最终实现的动画效果。
![](https://img.haomeiwen.com/i19200103/35f6a75785804129.png)
最后看下闪电的实现
![](https://img.haomeiwen.com/i19200103/80b1ba9c6605c2ae.png)
会通过houdini生成多个L-System的数据,之后将各个顶点的数据(到起点的距离,方便做动画,哪条分支是主干,方便加亮)保存下来,这里会保存多个版本
![](https://img.haomeiwen.com/i19200103/09f5652238026869.png)
![](https://img.haomeiwen.com/i19200103/0acc2c15ec324e43.png)
![](https://img.haomeiwen.com/i19200103/c5db15a351a09047.png)
这个是效果图:
- 先是沿着闪电的路径逐渐生成闪电,同时每个segment的发光耗时是固定的,当走到末端时,起点可能快熄灭了
- 走完全程之后,再给主干一个加粗加亮的高光(从无到有到无)
![](https://img.haomeiwen.com/i19200103/a300eac4283d2541.png)
总结
![](https://img.haomeiwen.com/i19200103/bc557bcf5319fd08.png)
![](https://img.haomeiwen.com/i19200103/e8682e24f9ed6217.png)
![](https://img.haomeiwen.com/i19200103/bb334ed43268ca0f.png)
![](https://img.haomeiwen.com/i19200103/ee43aadafb14f06a.png)
![](https://img.haomeiwen.com/i19200103/5ce6fce009caf647.png)
![](https://img.haomeiwen.com/i19200103/2bd8c27c5d3b2d17.png)
参考
[1]. The Technical Art of Sea of Thieves
[2]. Sea of Thieves: Tech Art and Shader Development | Valentine Kozin | GDC 2019