【GDC 2015】An Introduction to Realistic Ocean Rendering through FFT

今天学习的是GDC 2015上分享的基于FFT的海洋渲染方案,其中通过参考知乎上的相关资料,对基于FFT的海洋模拟实现逻辑做了相对清晰的推导与总结:

  1. 通过贴图来给出某个2D的频域信号,其中贴图的uv代表两个维度的频率,数值代表该信号的振幅
  2. 基于上述频域信号,可以通过逆变换得到随时间变化的2D空域信号,这个变换过程需要在运行时完成(涉及到时间,当然也可以烘焙多张,保证首尾衔接的话即可)
  3. 频域信号用的是菲利普频谱,基于这个频谱可以在运行时计算各个频率的信号数值,也可以在离线完成计算。运行时计算的好处是:
  • 可以根据当前环境的状态比如风向等来调制得到更拟真的效果
  • 可以运行时调整模拟参数,实现海洋效果的动态变化

具体可以参考实现细节:

page01
page02
page03

游戏中的海洋效果:有刺客信条III以及刺客信条黑帆

page04

Crysis的海洋效果

page05

War Thunder的海洋效果

page06

海洋奇缘

page07

泰坦尼克号

page08

这些游戏跟电影有如下的一些共同点

  1. 都是基于FFT来完成模拟的
  2. 模拟的输入是一个频谱,之后对这个频谱做逆向的离散傅里叶变换就得到时域(空域)信号
  3. 时域信号是一个随时间变化的高度displacement数据
page09

为啥是FFT:DFT耗时太高了,时间复杂度是O(n^2),而FFT的时间复杂度则是O(nlog_2(n))

在GPU中计算FFT的算法是Cooley-Tukey(蝴蝶)算法。

page10

傅里叶时域(还是频域?)贴图的分辨率应该是多少呢?正常情况下128 ~ 512就够了,泰坦尼克号跟Waterworld也就只用来2048,超过这个尺寸可能会导致浮点精度问题。

这里选用的分辨率是512。

page11

傅里叶变换的公式(可以参考[4]了解一下具体的推导过程)是:
F(\omega) = \int_{-\infty}^{\infty} f(t) e^{-i\omega t}dt

逆变换公式为:
f(t) = \frac{1}{2\pi}\int_{-\infty}^{\infty} F(\omega) e^{i\omega t}d\omega

在计算机中这个积分公式是没有解析解的,只能转换为离散的累加形式。

page12

再来看离散形式的变换与逆变换,这里的N是说将2\pi分割成N份,N越大,我们得到的频域信号就越丰富,信号就越逼真,模拟越接近,但是计算复杂度也就越高:
F(k) = \sum_{t=0}^{N-1} f(t) e^{-i2\pi \frac{k}{N} t}

f(t) = \sum_{k=0}^{N-1} F(k) e^{i2\pi \frac{k}{N} t}

page13

参考[1],DFT的计算其实就是一个矩阵的运算,而这个矩阵运算则可以通过排列得到如上图所示的蝴蝶累加算法,从而可以很好的实现加速计算。

page14

这里IFFT的实现是通过compute shader完成的,采用的是Radix-2算法(参考[3]),细节就不说了,网上有大把的开源实现。

page15

上面介绍的是一个一维函数的FFT实现,那么回到水体模拟上,这个公式是如何应用起来的呢?

我们知道,傅里叶变换其实是通过将信号拆解为正弦波的方式来完成对信号的表达的,每个正弦波都有一个频率,一个信号的频域表示,其实就是其拆解后得到的正弦波的振幅与频率的表示,频域信号在某个频率上的取值就是当前对应的时(空)域信号在对应频率上的振幅。

回到海洋这边来,其实海浪可以看成是由空间上无穷个2D的正弦波信号叠加而成,同样的,这个信号在频域中的表示就是海浪在不同频率的正弦波上的振幅。不同的是,这个频率是2D的,有X&Z两个方向的频率,同样的,我们的频域信号也是2维的,即在X & Z上具有不同频率的信号的振幅。

用公式来表达,先不考虑海洋波形随着时间波动的变化,我们先看下某个时间点的海洋波形,这个波形刚说了,可以看成是无穷多个2D的正弦波组成的,在空域上,可以表示为h(x, z),即在坐标(x, z)处的水体高度。

这个水波对应的频域信号,我们可以表示为\tilde{h}(w_x, w_z),两者可以通过傅里叶变换(空域->频域)与逆变换(频域->空域)实现转换,在海洋的实现逻辑中,我们的输入通常是频域信号,且只能用IDFT的方式来完成计算,因此按照前面的说明,这个逆变换的公式给出如下:
h(\vec{x}) = \sum_{\vec{k}} \tilde{h}(\vec{k}) e^{i\vec{k}\vec{x}}

其中\vec{k}是频域上的2D向量,对应于前面的(w_x, w_z),或者用k做前缀,将之改写为\vec{k} = (k_x, k_z),根据我们FFT频域模拟的尺寸,这里有k_x = \frac{2\pi n}{L_x}, k_z = \frac{2\pi m}{L_z},n跟m分别是频域贴图(贴图的uv代表着两个方向的频率,对应的数值对应的是这个频率的幅度)的水平跟垂直的分辨率,也就是我们在频谱上的水平跟垂直方向上以\frac{2\pi}{L_x},\frac{2\pi}{L_z}为单位采样的点数,其中L_x, L_z又分别为海面X or Z方向上的patch尺寸,根据[4]中的解释,我们的采样点是分布在原点两侧的

m \in \{-\frac{M}{2}, -\frac{M}{2} +1, ... , \frac{M}{2} - 1\} \\ n \in \{-\frac{N}{2}, -\frac{N}{2} +1, ... , \frac{N}{2} - 1\}

在这样的采点策略下,针对海洋波形,我们就总计拿到了MxN个频域的信号(或者说正弦波),通过他们之间的叠加与逆变换得到海洋的波形,通常情况下,这两个数值越大,波形精度越高,模拟越逼真。

\vec{x}则是空域上的2D信号,对应的是当前点的xz坐标,同样的,在计算displacement的时候,我们也是针对离散点进行的,即\vec{x}也是在海平面上以\frac{L_x}{N},\frac{L_z}{M}为单位采样NxM个点(虽然通常N=M),同样的,在相同的覆盖范围下,这俩数值越大,顶点之间的密度就越大,水体精度就越高,当然计算消耗也同样会增加。

另外需要注意的是,指数上的两个向量的乘法是点乘,即有:
\vec{k}\vec{x} = k_x x + k_z z
在这个公式下我们就有当某个维度固定住的时候,我们得到的就是一个一维的信号。

在这个基础上,如果我们考虑水波随着时间变化的过程,那么我们可以将前面的频域信号改写为时间的函数,即有:

h(\vec{x},t) = \sum_{\vec{k}} \tilde{h}(\vec{k}, t) e^{i\vec{k}\vec{x}}

page16

接下来对这个公式做一个拆解。

page17

里面最大的问题是频域信号怎么得到,这里就没有介绍具体的背景,而是直接给出了结论,海洋波形的频域信号通常使用的是菲利普频谱,其公式为:
\tilde{h}(\vec{k}, t) = \tilde{h_0}(\vec{k})e^{i\omega(k)t}+\tilde{h_0}^*(-\vec{k})e^{-i\omega(k)t}

这个公式中\tilde{h_0}\tilde{h_0}^*是共轭复数(两个实部相等,虚部互为相反数的复数互为共轭复数),k则是\vec{k}的模,\omega(k)是一个函数,其计算公式为:
\omega(k)=\sqrt{gk}

这里的g是重力加速度。

page18

再来看\tilde{h_0}(\vec{k}),有如下的计算公式:
\tilde{h_0}(\vec{k}) = \frac{1}{\sqrt 2}(\xi_r + i \xi_i)\sqrt{P_h(\vec{k})}

\xi_r, \xi_i是相互独立的随机数,均服从均值为0,标准差为1的正态分布。

page19

P_h(\vec{k}) = a\frac{e^{-\frac{1}{(kL)^2}}}{k^4}|\vec{k}\cdot\vec{\omega}|^2

这个公式中的\vec{\omega}是归一化后的风向(2D),L的计算公式为:
L=\frac{V^2}{g}

这里的g依然是重力加速度,V则是风速。

按照[4]的描述,这里的公式是基于统计给出的经验公式,并无过多的解析或物理含义在里面,可以不用过于纠结其来源。

page20
page21
page22

这里对公式中各个变量的含义做了基本的介绍,这里补上前面遗漏的项,前面介绍过的这里就不再介绍:

  • a是全局的海浪的振幅
page23

基于高度偏移的公式h(\vec{x},t) = \sum_{\vec{k}} \tilde{h}(\vec{k}, t) e^{i\vec{k}\vec{x}},我们可以通过解析的方式得到其法线。

先来计算一下梯度向量(某个函数即这里的高度变化率最大的方向,因为这里的变量还都是水平方向的两个轴,因此这里的向量还是一个水平方向的向量),因为\tilde{h}不包含梯度计算相关的\vec{x}信息,因此可以直接移出来:
\nabla h(\vec{x},t) = \sum_{\vec{k}} \tilde{h}(\vec{k}, t) \nabla e^{i\vec{k}\vec{x}}

继续推导,我们有:
\nabla e^{i\vec{k}\vec{x}} = (\frac{\partial {e^{i(k_x x+k_z z)}}}{\partial x}, \frac{\partial {e^{i(k_x x+k_z z)}}}{\partial z}) \\ =e^{i(k_x x+k_z z)} * i(k_x, k_z) \\ = i\vec k e^{i \vec k \vec x}

代入前面的公式,我们就可以得到梯度向量的计算公式:
\nabla h(\vec{x},t) = \sum_{\vec{k}} i\vec k \tilde{h}(\vec{k}, t) e^{i \vec k \vec x}

针对垂直向上的向量、梯度向量与法线向量的关系[4]:

注意梯度向量本身是没有归一化的,我们可以有:\vec N = \vec up - \vec {grad},将前面的公式代入,则我们有:
\vec N = (0, 1, 0) - (\nabla h_x(\vec{x},t), 0, \nabla h_z(\vec{x},t)) \\ = ( - \nabla h_x(\vec{x},t), 1, - \nabla h_z(\vec{x},t))

page24

前面得到的是高度方向的偏移,但是我们实际上需要的是三个方向的偏移,因此还需要再额外处理一下另外两个方向的。

在Gerstner Wave的实现中,为了得到尖锐的波峰效果(choppy wave),需要基于如下公式[4]在XZ方向上做挤压:
P(x,y,t)= \begin{cases} x+\sum Q_i A_i \times D_i.x \times cos(w_iD_i(x, y)+\phi_i t) \\ y+\sum Q_i A_i \times D_i.y \times cos(w_iD_i(x, y)+\phi_i t) \\ \sum A_i sin(w_iD_i \cdot (x, y)+\phi_i t) \end{cases}

而同样的,FFT实现的海浪效果也需要做挤压,挤压公式为:
\vec D(\vec x, t) = \sum_{\vec k} - i \frac{\vec k}{k}\tilde h(\vec k, t)e^{i \vec k \vec x}

page31
page32

如下图[4]所示,当挤压力度过大的时候,就会出现水波的相互穿刺,即可以理解为水波碰撞在一起,此时会产生浮沫:

这个过程其实就是某个形状(比如上图中的近似四边形,通常称之为一个图元)发生了翻转,正面翻到了背面,用数学概念来描述,就是该图元有向面积变为负数,而这个在数学公式上来表达就是对应的雅可比矩阵计算结果为负数,具体可以参考[4]的推导。

page25

挤压强度假设为\lambda,我们就有:
\vec x^{'} = \vec x + \lambda *\vec D(\vec x, t)

而这个公式跟前面Gerstner Wave的挤压公式整体形式上其实是一样的,只是换了一种写法。

经过上面计算,我们就得到了空域中各个点的偏移向量\vec D(x, y, z),这个可以用一张3通道的贴图来存储,我们就有了上图所示的Displacement Map

page26

参考NVIDIA的解说图,我们回顾一下上面的计算逻辑:

  1. 我们基于频谱函数或者说频谱贴图P_h(\vec k)就能得到高度偏移D_y
  2. 同样基于频谱函数跟挤压公式,我们可以得到在水平方向上的偏移D_x, D_z
  3. 将上述三者合到一起,我们就有了displacement map
  4. 基于Displacement map,我们就有计算出法线贴图,同时基于雅可比矩阵计算结果,就能够计算出foam intensity map(也就是上图中的folding)
page28
page29
page30
page33

经过上述计算之后,我们就能得到上述相对真实的海面模拟效果

page39

这里采用的是Crysis Engine最开始实现海洋方案时的mesh方案,有点类似于Projected Grid,不同的是,这里选择的不是基于场景相机的视角做的投影,而是基于场景相机为中心,俯视角做的投影(那不就是挂在相机上的一个固定的mesh吗,只是不需要存储mesh的数据而已)。

这个方案实现简单,可以自动实现近密远疏效果,而且据说不会有瑕疵

从原理上来分析,Projected Grid那种随着相机移动或旋转而导致顶点跳变的问题应该还是会有,但这里又单独提了Projected grid的瑕疵,意思就是这个问题确实是被消灭了,不知道这里具体干了啥。。。

page40
page41

这是线框模式下的网格展示

page42

模拟逻辑用C++实现,着色则通过材质实现。

page43

水体着色这里主要关注反射跟折射效果。

而菲涅尔项则用于给出反射跟折射光强的比例,但是完整的菲涅尔计算逻辑太过复杂,这里采用的是Schlick Fresnel近似计算公式。

page44

也就是我们高光BRDF计算中常用的那个,如果避开复杂的指数计算(如何避开?),实测这个近似计算公式要比原始公式快30%。

page45

得到菲涅尔项之后,就可以通过lerp实现折射跟反射的混合。

除了这个计算逻辑之外,还借用了UE的SSR来添加带有遮挡信息的环境反射效果。

page47

[1]. 傅立叶变换简记
[2]. 离散傅里叶变换DFT详解及应用
[3]. FFT原理及实现(Radix-2)
[4]. fft海面模拟(一)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容