水体渲染

【Siggraph 2016】Rendering Rapids

2024-06-07  本文已影响0人  离原春草

今天学习的是Uncharted 4在Siggraph 2016上分享的他们的水体技术方案。

相关要点总结如下:

  1. 河流通过Wave Particle FBM的方式来实现水波的模拟
  2. Mesh组织方式为CDLOD,离线模式统计出需要绘制的base quads,运行时基于距离完成base quad的细分与tessellation
  3. 河流的流动效果通过控制不同octave的流动速度来实现,降低低频信号的流速,提高高频信号的流速
  4. 船体对水体的影响是通过胶囊体模拟实现的,这里还通过离屏buffer计算了粒子特效的作用,最终将这俩还有displacement的作用结合在一起,完成水体的高度的控制
Page2 Page3 Page4 Page5

介绍一下Uncharted的历史

Page6

PS3

Page7 Page8 Page9

PS4时代Uncharted 4

Page10

Uncharted 4中水体在游戏体验中占有重要作用。

Page11

需求:支持各种各样的水体,包括小水坑、覆盖较大范围的河流以及风卷浪涌的海洋。

Page12

这里会聚焦在河流的渲染上,不过相关技术在河流跟海洋都是共用的。

Page13

在此前的游戏中已经有大量关于海洋的渲染方案出来,基本上是基于FFT+Wave Particle方案来的,已经比较成熟了。

而河流部分则主要通过一些复杂的shader计算来模拟,mesh表面大多是不带displacement的,少量会有一些顶点的displacement。

而在顶点的驱动上,Positional based fluids的实时模拟方案算是其中比较表现较好的,但是依然计算消耗很高,而这正是本文的关注点。

Page14

河流的渲染主要需要覆盖四个方面的内容:

  1. 首先是水波的模拟,关注的是顶点如何在实时完成offset以得到尽可能逼真的河流效果
  2. 接着是河流的mesh组织逻辑
  3. 第三个是河流的shading效果
  4. 最后是性能
Page15

想着先看看真实的河流效果,看能不能有一些启发

Page16

但分析下来发现实在是过于复杂了。。。

Page17

最后从家后的一条水沟的水流中得到了启发,这个特征相对简单,更为聚焦。

Page18

水流虽然是混乱无规律的,但是仔细观察就会发现水波其实是按照一定的规律在重复的,而重复特征在实现上就是一个循环。

可以将水流的水波看成是函数的输出,函数的输入为源头的水流与各种外界输入(风等),函数的处理逻辑为水流流过的地形,只要这两块是稳定的,那么输出就应该是基本稳定的(即不断重复)。

除了重复这个特征外,对上面的水体分析,我们还可以有如下的一些结论:

  1. 有standing wave,有backward wave
  2. 细节水波叠加在宏观水波之上(高频叠加在低频上,FBM)

这里顺便说一句,河流湖泊等水体通常会采用FBM(不论是基于程序化噪声perlin noise还是基于离线计算得到的波形贴图)来模拟。

Page19

通过对海洋的水波跟河流的水波特征进行分析,我们就知道,两者的共性都是高低频的叠加,不同的是海洋是全局的叠加策略,而河流则是局部的叠加,即不同区段的水波表现会有所不同,因此叠加参数,输入的基础噪声会有所不同。

Page21

一个实现策略是尝试将海洋的方案复用到河流上,那怎么来模拟水体在河床上的移动效果呢?

这里给出的方案是采用一个基于水波的动画,将之沿着流动方向进行传播,实现上可以采用Wave Particle。

Page22

Wave Particle之前在Uncharted 3中就用过,这种方案具有实现简单快速、效果控制比较直观且方便的特点,这里我们来看下单个波形的计算公式,假设t\in[-\pi, \pi],同时有:
x = t-\beta sin(t) \\ y = \frac{1}{2}(cos(t)+1)

就可以得到高度y随着空间某个方向的偏移值x的映射关系,上图给出了不同的\beta取值下的波形效果,可以看到\beta越大,波峰越陡峭。

Page23

Uncharted3就开始通过Wave Particle来模拟海洋水面的波动效果:

  1. 通过多层不同频率与振幅的Wave Particle(类似FBM)来实现高低频搭配效果
  2. 在一块区域(grid)使用多个Wave Particle,彼此之间叠加之后实现海洋波形的模拟

这里给出了实现的一些细节:

  1. 一块由32x32个顶点组成的区域大概需要300个wave particle(这里没有考虑分层)。
  2. 单块区域采用上述模拟方法看起来还可以,但是如果每个区域都这样做效果就不太好了,这里的解决方法就是采用前面说的FBM方式
  3. 提供了多个彼此独立的用于控制Wave particles效果的参数
Page24

海洋会包含多个区域,如果每个区域都单独模拟的话,消耗会很高,因此这里多个区域之间会复用模拟输出的displacement结果

不过为了提供一些区分度,这里还增加了一个叫做strain的概念用来表达每块区域(grid)的torsion:

d_1 = length((\vec {v_{i+1,j}} - \vec {v_{i, j}}) \times (\vec {v_{i,j+1}} - \vec {v_{i, j}})) \\ d_2 = length((\vec {v_{i,j+1}} - \vec {v_{i+1, j+1}}) \times (\vec {v_{i+1,j}} - \vec {v_{i+1, j+1}})) \\ r = 0.25 \\ strain = 1.0 -r*\frac{d_1+d_2}{scaleX * scaleZ}

这个数值可以用于检测水波的尖端(cusp),拿到这个数值后,就可以在对应位置生成对应的特效(浮沫?)。

Page25

下面对FFT跟Wave Particle(简称WP,下同)方案做一个对比

Page26

FFT的问题:

  1. 需要一个相对大尺寸的grid(顶点数更多)
  2. 美术同学在效果调整上会相对困难一点,而WP则因为是多层组成的,因此允许美术同学对每一层的效果进行调控
Page27

FBM设置,一共四层,每一层都是上一层的两倍。这里也说到了,如果下一层跟上一层不是整数倍的关系,可以避免各个地方的效果的重复感

Page28

将多个grid的displacement叠加在一起,就得到了最终的displacement。

Page29 Page30

前面介绍了海洋的Wave Particle实现逻辑,解析来看看怎么将这套方案用在局部的河流模拟上。

这里的想法是通过控制每个频率的grid滚动的速度来实现模拟:降低低频信号的滚动频率,提高高频信号的滚动频率。

Page31

这里展示了前面不同grid流速控制的结果,得到了一种高频细节在低频水体上流动的效果,不过暂时还缺少了控制河流转向的方法。

Page32

转向控制想通过flowmap来实现。

Page33

Flowmap之前用在法线的扰动上,以得到一种水体流动视觉上的模拟(没有用来控制displacement)。

Page34 Page35

这里对实现逻辑做了回顾。

Page36

如果要用来实现displacement的控制以实现displacement层面的流动效果,就需要基于同样的思路来控制不同频率grid的scrolling方向。

为了得到更精细的控制,这里还增加了一个振幅(height)贴图。

Page37

这里给出了实现的伪代码:

  1. 通过flowmap来控制WP采样的位置
  2. 得到的displacement之后再叠加一个全局heightmap
Page38

这里展示了通过flowmap实现一个circular的流动效果的截图。

Page39

这里给出了经过上述逻辑得到的河流效果截图,其中为每一层的grid提供了单独的WP振幅控制贴图,通过一个RGBA贴图来存储对应的数据。

虽然也可以为每个grid设置单独的流向,但是试过之后发现效果也就那样,所以就算了。

Page40

接下来看下mesh是怎么组织的。

Page41

这里介绍了两种之前用过的方案:

  1. Projected Grid:需要通过Tessellation来避免视角移动导致的抖动
  2. Clipmap:因为PS4硬件的特性,不规则的计算逻辑效率会有明显下降,同时相邻clip之间也没有做衔接,存在裂缝
Page42

这里给出了Mesh方案的两点要求:

  1. 顶点要保持稳定,不论是LOD切换还是相机切换,都不能存在明显的跳变
  2. GPU友好
Page43

这里想要采用CDLOD来实现,CDLOD除了衔接平滑等优点之外,还可以很自然的用来实现河流这种局部数据(控制部分节点的显示即可)

Page44

四叉树管理,每一层都是上一层的两倍,这里可以通过一套切换距离来控制什么时候需要显示哪个层级的数据。

这里选择mesh patch的尺寸是17x17个顶点,这个数字在平衡内存、顶点密度上具有最佳的表现(没给理由)。

Page45 Page46 Page47 Page48 Page49

这里给出了如何基于切换距离来得到CDLOD的节点细分层级的图示逻辑。

Page50

用颜色标注出了需要衔接的区域

Page51

基于上述区域得到需要做过渡的节点,注意,并不是与前述区域相交的顶点才需要,同时,与这些顶点共用父节点的节点也同样需要。

Page52

前面得到了CDLOD需要展示的节点(层级),每个节点直接以Quad形式发送到GPU,GPU中会通过tessellation将之转换为17x17的patch。

Page53

Tessellation完成后的效果。

Page54

多层之间自然衔接

Page55

将CDLOD用在河流上,流程给出如下:

  1. 选定一个quad覆盖范围(这里的quad我们称之为base quad),离线烘焙出一个稀疏的四叉树,用于覆盖整条河流
  2. 运行时基于距离等来决定每个quad的细分层级
  3. 基于quad的boundingbox来实现视锥剔除
  4. 在GPU中完成每个细分后的quad的tessellation,之后在VS中完成displacement的计算
Page56

这里给出了河流的覆盖区域,用一套base quad(固定覆盖范围,没有细分)来覆盖。

Page57

经过细分并displace之后得到如图效果。

Page58

加上其他数据

Page59

接下来看看物件与水体的交互实现逻辑。

Page60

将交互的物件用胶囊体表示(只需要几个参数即可表达),之后在CPU中获取到与之存在交互关系的base quads。

在GPU中通过某种方式拿到受影响的base quads以及对应的胶囊体,完成对应的形变计算。

Page61 Page62 Page63 Page64 Page65 Page66 Page67

这里用图示效果做了实现逻辑的展示。

Page68

特效对水体的形变,走的是另一套逻辑,类似于角色与泥地、雪地的交互,会先将特效的作用绘制到一张RT(RGB,32bit,R11G11B10)中,用于表示特效对水体的offset影响,之后在绘制水体的时候对这张贴图进行采样实现控制。

Page69

贴图是垂直投射到水面上的,整体的计算逻辑分为几个步骤(都是通过compute shader实现的?):

  1. 在base quads确定了对应的层级,完成细分之后,得到更细粒度的quads(这些quad会被tessellated成17x17的顶点),这里将这些quads称为water boxes
  2. 通过相交检测,拿到与船体相交的water boxes列表
  3. 在displacement计算完成后,会对与船体相交的box的所有顶点做形变处理,将之降低到与船体底部持平的高度
Page70

这是整体的计算流程:

  1. 先计算WP的displacement
  2. Particle的作用绘制到一个离屏Buffer中
  3. 完成box quads的tessellation,并基于WP的displacement以及water offset buffer实现顶点的高度变换,得到一个顶点array
  4. 另外用一个compute shader来对被船体影响到的box的顶点进行形变处理,最终的结果被用来进行绘制
Page71

水体的锯齿问题是一个需要关注的挑战点,具体可以参考KeXu的TAA分享。

Page72

最后看看整体的生产管线。

Page73

这里并没有想象的那么简单

Page74

Houdini用来生成水体的height map跟flow map

Page75

首先是河流的base mesh:

  1. 控制水体的覆盖范围、高低走势
  2. paint各个区域的水体属性,比如速度流向之类的
Page76

得到的base mesh

Page77 Page78

Houdini中的FLIP模拟,输出base mesh相关信息与flowmap

Page79

河流各个区域的效果控制参数众多

Page80

生成四张贴图,alpha mask用于控制哪些区域不需要水体(或者需要渐隐)

Page81

还有两张额外的贴图:

  1. 幅度贴图,控制该区域的WP grid的振幅
  2. 用于对各个位置的WP做精细控制Wave modulation贴图
Page82

最终结果

Page83

美术同学工作流

Page84

载具在水中的效果

上一篇下一篇

猜你喜欢

热点阅读