今天学习的是顽皮狗在GDC 2012上关于水体渲染的分享,照例对文中的一些关键内容做一个简单总结:
- 通过一个shader将反射、折射、浮沫、churn(水下的剧烈扰动)、基于深度的着色都覆盖进去了,且所有参数都是美术同学可控制的
- 基于flow map(两次采样并混合)实现了水流效果,其中spline用来标注流动方向,用2D贴图来标注各个点的流动速度、浮沫、相位等数据(以grid的顶点为单位)
- 水体mesh的没有用效果较差的perlin噪声,也没有用美术同学控制比较痛苦的FFT,也没有完全用随着octave数目上升性能消耗急剧上升的gerstner wave,而是用了4层gerstner wave,4层wave particle以及一层大波浪来实现
- mesh的数据结构用的是clipmap,不过做了一些针对水体的处理优化,包括T-Joints的消除、mesh的blending以及分patch的绘制处理逻辑(利用SPU的并行特性进行加速)
- 由于mesh是以patch组织的,因此剔除也可以基于patch来做,包括2 pass的剔除、portal culling等
- 为了避免水波侵入船体,这里会以船体的甲板来构建box,对超出box的顶点将之压低
- 水下的雾效是直接用一个面片(顶点随波浪顶点一起起伏)实现的
- 水体上某个位置的高度查询不是通过对mesh ray casting得到的,而是采用类似牛顿寻根的迭代算法得到
- 水体的渲染只用了2.7ms,消耗比较高的部分为tessellation+wave displacement,大概花了8ms
![](https://img.haomeiwen.com/i19200103/a1c6f93b61cff3f8.jpg)
深海是动作冒险游戏,核心是战斗跟探索而非水上gameplay。
场景种类多样,包括丛林、神庙、洞穴、废墟等,里面会夹杂各类水体,如水坑、溪流、河流、池塘湖泊、大海等。
水体元素对游戏玩法有着较为重要的作用,在一代代的作品中也在持续提升。
![](https://img.haomeiwen.com/i19200103/f8a1c83b6588c242.jpg)
![](https://img.haomeiwen.com/i19200103/11a520ba5f64cb90.jpg)
![](https://img.haomeiwen.com/i19200103/0e670f128ad12834.jpg)
![](https://img.haomeiwen.com/i19200103/ccb925c6d16816a8.jpg)
水体的表现形式多样,从小尺寸的水坑到大尺寸的海洋
![](https://img.haomeiwen.com/i19200103/ff2c94a0417c084c.jpg)
![](https://img.haomeiwen.com/i19200103/d60624902fa43a32.jpg)
Gameplay也有跟水体有关的玩法,比如浸水之后的衣物潮湿
![](https://img.haomeiwen.com/i19200103/cec9cfb6a04384b9.jpg)
在水体上的移动速度也会导致不一样的表现
![](https://img.haomeiwen.com/i19200103/dd8dc8fb4457c2c6.jpg)
水体的效果需要跟游戏画面的风格相匹配。
![](https://img.haomeiwen.com/i19200103/ace40e6a48c978a9.jpg)
这里参考过很多此前的工作,其中Scientific visualization的工作在后续的工作中有很好的体现。
![](https://img.haomeiwen.com/i19200103/6459de292c142942.jpg)
水体的实现覆盖了多种效果:
- 水流:通过(基于顶点或像素)滚动的UV,或者基于向量场生成的法线贴图来实现
- 折射:基于水深来抖动和着色
- 浮沫:在梯度场上给一个阈值,超出阈值的即添加浮沫效果
- 剧烈的扰动:同样是基于深度的着色,这里会使用浮沫贴图来对水体的深度进行调制,之后进行混合,以实现volumetric效果的模拟。
所有的参数都需要支持美术同学的控制。
![](https://img.haomeiwen.com/i19200103/314104adbcd27e2a.jpg)
![](https://img.haomeiwen.com/i19200103/d03b180ce4566dec.jpg)
水体的渲染有如下的一些要点:
- 使用bump贴图来对法线进行扰动
- 基于扰动的法线来计算得到fresnel项,并基于这个数据完成折射与反射的混合
- 在折射、反射效果之上,再叠加浮沫,这里还可以将浮沫的效果跟折射数据做混合来实现泥浆效果
- 基于剧烈的扰动(churn)来实现volumetric效果
- 折射的着色跟当前着色点处的折射光线到水底的距离(深度)有关:
- 这个深度的着色函数可以是线性的,也可以是指数的
- 基于深度的着色可以受到剧烈扰动(churn)的影响,这里的做法是用浮沫贴图的某个通道数据(因为受海浪高度影响,这里其实也可以理解成基于高度)来对深度颜色(depth color,水体因为深度不同而呈现的颜色,通过查表得到)与扰动颜色(churn color,暂时不清楚这个颜色代表的是啥)进行混合(不是太明白这里的含义)
- 在浮沫移动(被海浪的高度所调制)的时候,浮沫的颜色就会发生变化,看起来就像是浮沫在滚动
- 反射数据也可以直接添加到折射数据之上,而不是跟折射数据进行混合
再次重申,所有的参数都可以交由美术同学控制。
![](https://img.haomeiwen.com/i19200103/05337f51259dda39.jpg)
接下来一层层的解析下水体着色中的各个关键因素。
![](https://img.haomeiwen.com/i19200103/ca2486d3ec2dbfd4.jpg)
去除了bump之后的效果长这样:
![](https://img.haomeiwen.com/i19200103/b2fde0a7683a79b8.jpg)
叠加bump的效果,可以看到海面高光扰动细节更为丰富真实
![](https://img.haomeiwen.com/i19200103/1ce44d5c000c3e74.jpg)
前面说过,折射跟反射是通过fresnel系数完成混合的(物理公式),先看下只有折射效果的样子
![](https://img.haomeiwen.com/i19200103/7900f21a755f0a09.jpg)
再基于水深来对水体的基色与折射做一个混合,这里没有叠加bump的扰动效果
![](https://img.haomeiwen.com/i19200103/727bd504f3bff46f.jpg)
叠加扰动后效果大概是这样的
![](https://img.haomeiwen.com/i19200103/ecb31c48d3c52e80.jpg)
再加上软影效果
![](https://img.haomeiwen.com/i19200103/1d38241c294b3aa8.jpg)
接着再基于海浪的幅度来对叠加浮沫。
foam跟churn会在水面之上跟之下叠加一个贴图(水下的浮沫效果就是水流扰动效果?),这俩都会受flow map的影响。
![](https://img.haomeiwen.com/i19200103/4842836784a32bec.jpg)
加上高光效果
![](https://img.haomeiwen.com/i19200103/89b090e3f6581ad4.jpg)
最终效果如下图所示
![](https://img.haomeiwen.com/i19200103/08d7d1f9eca4ddeb.jpg)
接下来看看法线贴图是怎么处理的,这是水体渲染着色的关键。
![](https://img.haomeiwen.com/i19200103/3510652c5a91f1a7.jpg)
Flow shader主要是基于Flow Visualization using Moving Textures方法(文中介绍了如何基于贴图的UV调整来实现流动效果,下面也会简介实现细节)实现的,主要的思想是基于向量场来对mesh的UV、Displacement等属性进行扰动对流(advect)
![](https://img.haomeiwen.com/i19200103/6bfbb1634ea96802.jpg)
这里给出了flow实现的一个例子,在这个例子中,会使用VS来计算三角面片之间blend的数值,需要注意,当我们想让水流往东边流动,那UV就应该朝着西边移动。
可以看到,这里的flow需要完成两张贴图的采样,每张贴图的采样UV都会随着时间线性增长,同时,两张贴图的UV相差0.5个相位,同时两者混合的权重相加等于1,某个贴图的权重最高(=1.0)的时候,正好是其UV处于0.5的时候。
![](https://img.haomeiwen.com/i19200103/f2e86cfe524acbd1.jpg)
这里可以看到,流动方向不需要细化到像素级别,只要到grid的顶点级别即可
![](https://img.haomeiwen.com/i19200103/cae8a0fcc7c60512.jpg)
除了基础的流动效果之外,还可以添加其他的效果,这里通过一个second fetch来实现额外的流体效果。
![](https://img.haomeiwen.com/i19200103/3cf0feb5b6a1a0a8.jpg)
这里给出了优化flow效果的几种方法,听起来有点没头没脑
![](https://img.haomeiwen.com/i19200103/785f978f7645d8d1.jpg)
这里来看下Flow Shader的实现逻辑:
- 先是基于当前时间g_time计算出两张贴图采样时的相位差fTime
- 基于相位差,得到当前UV的偏移flowUV1/2
- 基于偏移的UV对flow map进行采样,得到tx1/2
- 按照前面的理解,将采样得到的扰动信息作为UV的扰动再做一次额外的采样(优化flow效果,可选)
- 基于前面的混合公式完成两个采样结果的混合
这里的shader是基础版本,如果想要提升效果,还可以尝试一些其他的手段,比如:
- 添加一个起始的偏移,不同贴图的起始偏移可以不一样
- 通过对采样uv再加上0.5个flowDir的偏移来消除flow的distortion(原理是?)
- 对fTime的计算再加上一个offsetPhase来分散相位切换处的水流(spread around,需要在PS中完成)
![](https://img.haomeiwen.com/i19200103/d397c8bfa76a5b44.jpg)
这里展示了神海系列中使用到的maya工具,这个工具会使用spline来标注流动的方向,颜色贴图来标注流动速度(灰度就行了吧,还需要3色吗?)。
此外,如果需要的话,还可以生成foam map以及phase map(displacement需要)
![](https://img.haomeiwen.com/i19200103/fffed87d2e3258a3.jpg)
这里是最终效果
![](https://img.haomeiwen.com/i19200103/ab1866d49f9a418d.jpg)
接下来看看基于flow实现的displacement(水体mesh的高低起伏)。这里说到,水体mesh的顶点是按照各向异性的圆形移动的,不同顶点的相位有所不同(听起来是gerstner wave那一套)。
如果为每个顶点指定一个相位,那么整片水域就会需要一个相位场,而通过基于flow map来调控这个相位场,使得跟随flow的方向或者垂直flow的方向,就能够实现不同的波浪效果。
![](https://img.haomeiwen.com/i19200103/236ac30c9f27dcc2.jpg)
下面展示在相同的flow作用下,通过调整flow对相位的调节算法来实现不同的波浪效果
![](https://img.haomeiwen.com/i19200103/0854f37bc36c080d.jpg)
![](https://img.haomeiwen.com/i19200103/defcd68e5d094044.jpg)
![](https://img.haomeiwen.com/i19200103/c9ffa0c444bd39fc.jpg)
flow除了用在水体之外,还可以用于一些其他的效果,如沙、云、雪等
![](https://img.haomeiwen.com/i19200103/83f26f7af449d4c7.jpg)
前面的水体效果在深海1/2中是够用了,那3呢
![](https://img.haomeiwen.com/i19200103/6d9b796f4de80da4.jpg)
3中提出了一个玩法设想:在狂风巨浪中有一艘大船,突然转向穿入波涛中。
此前的基于displacement mesh的方式就不太能支持了。
![](https://img.haomeiwen.com/i19200103/013c17bbaa89aa87.jpg)
![](https://img.haomeiwen.com/i19200103/ef242aa213704de9.jpg)
期望实现如下图所示的效果:
![](https://img.haomeiwen.com/i19200103/14426e36e2c686f8.jpg)
emmm...
![](https://img.haomeiwen.com/i19200103/d1b0ee695e144f3f.jpg)
先来明确下需求:
- 水体渲染区域大,且要能支持100+的高浪
- 能够跟船舶等发生交互(不考虑动画实现方案)
- 支持游泳
![](https://img.haomeiwen.com/i19200103/07687fcb1d185d2c.jpg)
海洋的实现中,海浪是程序化生成的(即运行时基于程序控制实现),参数可以调整,且同样的参数输出的效果是固定的,不随时间而变化的,可控性强。
![](https://img.haomeiwen.com/i19200103/18c4d77a6a27a079.jpg)
先来解读下程序化实现的含义。
这里需要找到一个合适的模型来兼顾性能与表现,首先可以肯定的是不能完全走物理模拟来实现,耗费太高。
Perlin噪声实现的方案效果有点差,看起来有点假。
FFT方案效果比较好,但是参数控制起来有点痛苦,美术同学调整会很麻烦。
![](https://img.haomeiwen.com/i19200103/ef20d2747f47067d.jpg)
再来看看参数化控制的含义,这里给了一个函数,参数有:
- uv
- 时间t
- 其他参数
最终输出一个xyz的顶点坐标。
因为期望海洋的任何点的数据都可以通过计算得到,这里就不能使用grid,而是希望能够做到跟任何参数化公式的兼容,因此这里就需要给出一个组合式的海浪系统。
这里需要注意的是,我们这里最终是需要生成一个vector displacement,通过这个方式,我们可以不用高度场就能实现一个效果很漂亮的海浪。
![](https://img.haomeiwen.com/i19200103/a50ee3718f48a9f2.jpg)
最后看看一致性:即希望参数一致的情况下,效果要能实现精准的回放,以满足cutscene的效果需要。
![](https://img.haomeiwen.com/i19200103/ba63e5acc049096b.jpg)
最终选择的波浪是通过Gerstner Wave实现,这种波浪有如下的特点:
- 实现简单,但是高频细节较少
- 如果叠加使用的数目过多(20+),就会导致性能消耗急剧升高
![](https://img.haomeiwen.com/i19200103/07b9d45316db599a.jpg)
FFT虽然效果很真实,但是想要调制出正确的参数就会比较困难,对于美术同学来说,负担尤其重。此外,在grid分辨率较低的时候,还会出现tiling的瑕疵。
![](https://img.haomeiwen.com/i19200103/5963d518c25ff4fd.jpg)
最终这里采用的是Siggraph 2007上提出的Wave Particle方法,但在实现上,跟原文做了一些区分:
- 不再使用点源,而是在一个圆形区域里,放置随机分布的粒子来模拟开放水域波纹扰动的混乱特性
- 在某个速度边界的约束下对未知跟速度做随机,可以产生一个tileable的向量displacement场
![](https://img.haomeiwen.com/i19200103/f3196bf3b4a4edb5.jpg)
这里给出波浪的解析公式,xy(代表的是啥?难道是粒子在xy方向上的位移?)都只受时间驱动,其中x还受一个参数控制,从曲线分布来看,这个数值越大,产生的波浪就越尖锐。
![](https://img.haomeiwen.com/i19200103/0716e8159496f52d.jpg)
释放600个粒子后的海域波纹效果长这样
![](https://img.haomeiwen.com/i19200103/9b93c8929e2319dc.jpg)
Wave Particle方法有如下的几项优势:
- 美术同学调整会非常直观
- 不会出现tiling问题
- 性能好,适合SPU的并行计算
- 能够保持一致性的效果:每个粒子的新的位置是通过初试位置、速度与时间推导而来,不会跳变
![](https://img.haomeiwen.com/i19200103/4acaf5b280e99078.jpg)
采用FBM的思想,通过不同频率、振幅的displacement field来叠加可以得到更为真实的效果。
![](https://img.haomeiwen.com/i19200103/dbc51a2a470b1535.jpg)
这里还采用了类似clipmap的思路来对不同octave数据进行叠加,还可以产生LOD效果。
![](https://img.haomeiwen.com/i19200103/ace3900cec0bb191.jpg)
实际上,这里并没有完全抛弃gerstner wave,而是在四层gerstner wave的基础上叠加了四层wave particle grid
![](https://img.haomeiwen.com/i19200103/337c1981b07b0436.jpg)
还增加了一个flow grid的概念(听起来是以grid的顶点为数据存储的基本单位),在一个grid中完成flow、foam以及振幅因子数据的集成。
下图是只有flow curve作用下的示意图
![](https://img.haomeiwen.com/i19200103/7968e2f488588d1c.jpg)
这里则是在grid中的flow vector作用下的示意图
![](https://img.haomeiwen.com/i19200103/628c1e46e513e32d.jpg)
前面说过,还存储了wave的振幅乘法因子。
![](https://img.haomeiwen.com/i19200103/82291d840e9f84d1.jpg)
最终生成水体mesh的时候,会将foam的数据调制转化为顶点色。
![](https://img.haomeiwen.com/i19200103/de572a365f8583b9.jpg)
看下大图效果
![](https://img.haomeiwen.com/i19200103/51360f9161653edc.jpg)
再来回顾下最初的目标。
![](https://img.haomeiwen.com/i19200103/ff9a11e30246c64f.jpg)
这里还差了大尺寸的海浪的效果。
![](https://img.haomeiwen.com/i19200103/b38ae528940ee9b0.jpg)
所以在之前的两类wave上,再增加一个大尺寸海浪。
这个大尺寸的海浪会由美术同学手动调制后叠加上去,这种海浪通常会用在一些crash wave scene(没太明白这个意思),同时,还会用于阻止玩家游离gameplay area。
![](https://img.haomeiwen.com/i19200103/1aecb80110caa85f.jpg)
大尺寸海浪是通过对一个矩形区域的覆盖处理来实现。
![](https://img.haomeiwen.com/i19200103/7d527f47ae143aa3.jpg)
这个曲线的u方向会被用来模拟一条样条曲线
![](https://img.haomeiwen.com/i19200103/687d6052fe71a471.jpg)
之后沿着v方向对样条曲线进行复制,从而起到将平面拱起的效果。
![](https://img.haomeiwen.com/i19200103/14d2d5a31d290589.jpg)
但是这样得到的波形在两端就会出现裂缝,为了实现平滑的衔接,需要将拱起的波形朝着两遍做抹平处理。
最终的波形就是通过下图中的公式来给出。
![](https://img.haomeiwen.com/i19200103/58702d0072fdd532.jpg)
之后,也可以根据需要对波形进行缩放、平移来实现动画效果。
![](https://img.haomeiwen.com/i19200103/30acccfb7cbb78cd.jpg)
下图展示了通过这种方法实现的大尺寸波浪效果。
![](https://img.haomeiwen.com/i19200103/62e6d567835a6b85.jpg)
![](https://img.haomeiwen.com/i19200103/d2f254ae8cc74da0.jpg)
下图给出了整个波浪系统的计算公式,可以看到,整体的波浪由三部分累加得到:
- 多个不同频率的gerstner wave
- 多个不同频率的wave particle grid
- 美术调制的大尺寸波浪
大尺寸波浪使用的是一个标准的不支持旋转的样条曲线,当然也可以考虑其他的如贝塞尔等曲线,但是代码量会更重。
grid(u, v)函数返回的是一个标量,按照前面的描述,在不同的场合有不同的含义,这里代表的是对波形进行累乘的因子。
![](https://img.haomeiwen.com/i19200103/a7b3b4758c176590.jpg)
接下来看看水体的mesh是如何表达的,总的来说,可以通过多种方法来表达:
- 屏幕空间的projected grid方法,可能会导致锯齿问题(?)
- 也可以使用quasi的projected grid方法,但是在大尺寸的displacement的实现上,就可能满足不了需要(所有projected grid方法都有这种问题吧)。
![](https://img.haomeiwen.com/i19200103/d2b9b1a8d32d189d.jpg)
这里采用的是基于Siggraph 04年的Clipmap方法实现的非常规Clipmap方案,所谓的非常规就是针对水体做了改造。
![](https://img.haomeiwen.com/i19200103/12ef3da01467a2e8.jpg)
主要做了如下的几点改动:
- 为了避免跨level之间衔接的T-joints(裂缝)问题,做了不同的split(划分)方法
- 在多个level之间做了动态混合(目的是?)
- 采用了patch的方式,以进一步利用SPU
![](https://img.haomeiwen.com/i19200103/a3a5f575812d32cf.jpg)
这里给出clipmap的示意图。在相机移动的时候,各级的clipmap也会跟随移动,这里会保证任意点在相机移动前后,采样的数据都是相同的,从而避免抖动以及锯齿问题。
![](https://img.haomeiwen.com/i19200103/295d563afd05d8ad.jpg)
这里将第0级(完整的quad)之外的其他level叫做ring,精度越低,ring的级别越高。
![](https://img.haomeiwen.com/i19200103/d32b15d7aa8a4b40.jpg)
下面截了一堆图用来展示相机移动后ring的包裹范围,应该还介绍了一些其他的内容,但是在备注中没有说到,后面有需要可以从原始视频中找信息。
![](https://img.haomeiwen.com/i19200103/7472526afb4c61f6.jpg)
![](https://img.haomeiwen.com/i19200103/9d1f8c35bf7f99c3.jpg)
![](https://img.haomeiwen.com/i19200103/9422d30223ac5335.jpg)
![](https://img.haomeiwen.com/i19200103/bbd6787f02564980.jpg)
![](https://img.haomeiwen.com/i19200103/8f376c73b6c6bcb0.jpg)
![](https://img.haomeiwen.com/i19200103/d0a403fae8bbd402.jpg)
![](https://img.haomeiwen.com/i19200103/b0240bee926d7a96.jpg)
![](https://img.haomeiwen.com/i19200103/5340624eb2cdd764.jpg)
用单个ring来解释
![](https://img.haomeiwen.com/i19200103/2be491e52791fa65.jpg)
绿色的是原始的patch(一个quad就是一个patch),红色的则是额外添加的patch
![](https://img.haomeiwen.com/i19200103/30021c7d960631e6.jpg)
?没看懂这里想要表达的是什么。
![](https://img.haomeiwen.com/i19200103/89434997e9357190.jpg)
附加的patch可以起到承上启下的作用,以规避两级ring之间的顶点密度不一致导致的裂缝。
![](https://img.haomeiwen.com/i19200103/7b95b55a1c87e46c.jpg)
裂缝的修复是通过如下的顶点layout完成的。
![](https://img.haomeiwen.com/i19200103/5fdce39dc53080af.jpg)
这张图画的不是很明显,在修复裂缝的那一圈顶点或quad之内的顶点会调整其位置,使得网格密度有一个从粗到细的变化,以避免数据跳变太过严重,产生视觉瑕疵。
![](https://img.haomeiwen.com/i19200103/3151982b0234f0b2.jpg)
这里用颜色标注出修复区跟混合区。
![](https://img.haomeiwen.com/i19200103/b7e1542b7c5fc8d3.jpg)
接着来看下,怎么做剔除以提高绘制效率的,整个剔除分为两个pass完成:
- 先基于patch的四个角上的顶点添加一点余量做成box,之后做一次快速的相交测试
- 之后未被剔除的box会经历displacement的处理,就能得到一个更为准确的box,再基于这个数据做一轮新的剔除
![](https://img.haomeiwen.com/i19200103/5c7c781028b00176.jpg)
![](https://img.haomeiwen.com/i19200103/6ab51ae83c0028ae.jpg)
这里给出了clipmap渲染的wireframe模式。
![](https://img.haomeiwen.com/i19200103/cc216eb12c6d4581.jpg)
在舞厅中依然是可以看见水面的。
![](https://img.haomeiwen.com/i19200103/2b084198eb928be8.jpg)
![](https://img.haomeiwen.com/i19200103/04b469bd4b93035b.jpg)
为了优化室内视角下的剔除力度,这里还用了portal剔除的思想。
对于那些浪的尺寸较大的情况,如果不做处理会导致浪穿透甲板进入到室内,这里的做法是用一个box来对wave进行求交测试,当相交的时候,就需要对不可见的部分的顶点做压低处理,将之压平到跟box的底部一致。
![](https://img.haomeiwen.com/i19200103/521005361e9426ef.jpg)
这里还有一个画面需要兼顾,主角会进入到船舱进行破坏。
![](https://img.haomeiwen.com/i19200103/c3d32e6d59cf1ebc.jpg)
船舱中的水体也是采用跟海洋一样的方式进行绘制的,只是shader跟参数会有所不同。
![](https://img.haomeiwen.com/i19200103/0929d48f9985871a.jpg)
这里看下线框模式。
![](https://img.haomeiwen.com/i19200103/a551f413cc2023d6.jpg)
当船翻的时候,还会出现新的情况。
![](https://img.haomeiwen.com/i19200103/c80fa3d5ce8ea17e.jpg)
![](https://img.haomeiwen.com/i19200103/5a92724c7cb64065.jpg)
再看回舞厅。
![](https://img.haomeiwen.com/i19200103/8048180ac7f979cd.jpg)
![](https://img.haomeiwen.com/i19200103/276fef949c1ceac5.jpg)
由于一次性只能绘制一个海洋,因此在游轮上,需要经常的切换不同的参数来绘制不同的水体效果,比如在两帧之间切换shader跟参数。
由于游轮倾斜了90度,所以天光部分就变得困难。船外的水体还需要考虑水上跟水下的部分,此外,还会有一个洪水涌入的环节。
为了用一个平面来完成剔除,还要将船的移动约束在一个平面上。
![](https://img.haomeiwen.com/i19200103/4de2a56f6c4abb30.jpg)
从窗户外可以看到水上跟水下的部分。
![](https://img.haomeiwen.com/i19200103/36edbd3cf0ca5f27.jpg)
洪水涌入部分的实现可以参考GDC 2012中Creating the Flood Effects in U3
![](https://img.haomeiwen.com/i19200103/d792c27fb7de348e.jpg)
这里来看下天光部分,这里是俯视角下的示意图。
![](https://img.haomeiwen.com/i19200103/c1066d8239fa0144.jpg)
同样以patch为粒度进行剔除,不可见部分用红色表示。
![](https://img.haomeiwen.com/i19200103/62278c2506502685.jpg)
之后剔除掉不需要被天光影响的区域,用黄色表示。
![](https://img.haomeiwen.com/i19200103/d84547d806959d6a.jpg)
还依然可见的部分用紫色表示。
![](https://img.haomeiwen.com/i19200103/dc6a5be4a476d0a6.jpg)
对于天光来说,在渲染上,就是通过一个平面剔除来规避天光的影响(绿色部分),而水面mesh顶点则是通过压低到对齐船体box来实现。
![](https://img.haomeiwen.com/i19200103/58634fc04d361048.jpg)
水下的雾效是通过一个‘窗帘’面片(边缘顶点对齐波浪起伏)来模拟的,这个面片会跟随水体而移动。
![](https://img.haomeiwen.com/i19200103/46ebad922f721827.jpg)
![](https://img.haomeiwen.com/i19200103/57e505b584306883.jpg)
通过线框模式可以看得更清楚。
![](https://img.haomeiwen.com/i19200103/160833f4582a5fba.jpg)
![](https://img.haomeiwen.com/i19200103/ada998e67418d5d0.jpg)
这里给出了窗帘的mesh效果。
![](https://img.haomeiwen.com/i19200103/d2fe0c68f4390910.jpg)
![](https://img.haomeiwen.com/i19200103/094d86a2aff5d929.jpg)
对水上物体的浮力的计算,这里会对相交区域的多个点进行采样,并调整对应位置的水体顶点高度。
![](https://img.haomeiwen.com/i19200103/656fbe7a9a7b46ae.jpg)
![](https://img.haomeiwen.com/i19200103/216eee8ff920c582.jpg)
游轮这边的处理要复杂一点:
- 会沿着船身进行采样
- 但是不会对所有的波浪都采样,只选取那些低频的波浪
- 对于船头,会用一个弹簧来模拟
![](https://img.haomeiwen.com/i19200103/7de22f8adbd3fe28.jpg)
在实现过程中,有比较多的需要对单点的水面高度进行查询的需要,比如角色游泳等,而要想获得精确的数据却并不那么容易。
![](https://img.haomeiwen.com/i19200103/5eafa523f6e4cd1b.jpg)
这个问题可以用数学符号抽象一下:对于一个给定的点p<u,v>,需要找到水体对应的世界坐标r(u, y, v)
这里采用的是Ryan Broner的搜索方法,其原理跟Secant方法类似,不过是运算在3D而非2D空间:直接对wave field数据进行搜索,而非构建一个mesh之后走ray casting
![](https://img.haomeiwen.com/i19200103/c876aa36541dac81.jpg)
根据p的uv坐标,我们可以运算出其在波浪方程作用后的世界坐标,如下图所示的1点。
![](https://img.haomeiwen.com/i19200103/fb362635b6f45875.jpg)
将1点的坐标做一下投影,得到新的uv坐标与1点的高度,如下图中橙色的1。
![](https://img.haomeiwen.com/i19200103/065c44f8dd6db4ff.jpg)
从p点到橙色的1点,会有一个数据差。
![](https://img.haomeiwen.com/i19200103/988ec93367e774de.jpg)
将这个数据差反向施加到p点上
![](https://img.haomeiwen.com/i19200103/a80ef950c28ad883.jpg)
得到一个新的点2。
![](https://img.haomeiwen.com/i19200103/fde8e48fe1dfb12f.jpg)
![](https://img.haomeiwen.com/i19200103/6fe2cc20279b6c64.jpg)
基于2点,我们可以通过波浪方程得到其水体上的位置,如红色的2点。
![](https://img.haomeiwen.com/i19200103/75f8d854cd7e7306.jpg)
对红色的点再次做投影
![](https://img.haomeiwen.com/i19200103/a872daf8bf848644.jpg)
得到新的跟p点的差值
![](https://img.haomeiwen.com/i19200103/c1effc95cc722de2.jpg)
同时得到原始2点跟投影2点的差值
![](https://img.haomeiwen.com/i19200103/4c8332187ca0f917.jpg)
基于这两个差值的方向,我们可以直接选取2点的投影点作为3点的位置。
![](https://img.haomeiwen.com/i19200103/b37fcf447bec46ab.jpg)
重复上述过程
![](https://img.haomeiwen.com/i19200103/dfbb8508ec4db9c9.jpg)
当3点的水体位置出现在问号点的右侧(看投影点在p点右侧),就重复上述过程。
![](https://img.haomeiwen.com/i19200103/b78412e39c02f51f.jpg)
![](https://img.haomeiwen.com/i19200103/9d3266837f3b7135.jpg)
![](https://img.haomeiwen.com/i19200103/7a0cb82dd9f8f2d0.jpg)
![](https://img.haomeiwen.com/i19200103/55ec152c5a982b42.jpg)
这里不再是直接将p点反向减去差值,而是将3点反向减去差值,这也可以理解,毕竟红色的3点是3点计算得到的。
![](https://img.haomeiwen.com/i19200103/ebe694104fc922d3.jpg)
![](https://img.haomeiwen.com/i19200103/bbafe21e264d8641.jpg)
经过多轮迭代,最终我们得到了命中问号点的uv坐标与问号点的高度。
![](https://img.haomeiwen.com/i19200103/685096e1e0c72826.jpg)
![](https://img.haomeiwen.com/i19200103/485b1d541e96e167.jpg)
![](https://img.haomeiwen.com/i19200103/da404230550e85d6.jpg)
![](https://img.haomeiwen.com/i19200103/1a5f530b69e90064.jpg)
下面介绍下Mesh是如何计算的:
- 对于每个ring,会跑一个SPU任务来对patch进行处理,这里每个任务会以3为周期处理各个patch(不知道这么做的好处在哪里,实现负载均衡?)
- 通过double buffer来实现数据的输入跟输出
![](https://img.haomeiwen.com/i19200103/da86c79e713f7d9b.jpg)
由于每个任务产生的mesh patch都能很好的缝合周边,因此这里也不需要一个额外的pass来对patch做缝合了。
最终输出的mesh包括了多个mesh部分。
![](https://img.haomeiwen.com/i19200103/5432b23508807d3f.jpg)
整个计算的流程如下图所示:
- 在PPU中完成相机的计算、job的发起,并设置barrier等待所有内容的计算完成
- 在SPU中则负责wave particle的创建、更新(之后就已经有了所需要的displacement grid数据),水面点位的查询,clipmap的计算等
- PPU等待所有的SPU任务完成后就会进入渲染阶段
![](https://img.haomeiwen.com/i19200103/0a937fff7f8c6b77.jpg)
这里给出最终的性能数据:
- wave particle的计算花费了0.9ms
- 点位查询花费0.1ms
- 8ms用于tessellation与波浪的displacement,这里是在5个SPU下计算的,差不多有7个ring的21个任务
- 渲染只花费了2.7ms(大约50+个patch)
- double buffer内存大概是1MB
![](https://img.haomeiwen.com/i19200103/befbbc886d3478a7.jpg)
![](https://img.haomeiwen.com/i19200103/e8f53c9e2eb8dbcf.jpg)
![](https://img.haomeiwen.com/i19200103/0969442ec395d9c7.jpg)
![](https://img.haomeiwen.com/i19200103/ab57a0bd3a623195.jpg)
网友评论