今天學習的是GDC 2015上分享的基于FFT的海洋渲染方案,其中通過參考知乎上的相關資料,對基于FFT的海洋模擬實現(xiàn)邏輯做了相對清晰的推導與總結(jié):
- 通過貼圖來給出某個2D的頻域信號,其中貼圖的uv代表兩個維度的頻率,數(shù)值代表該信號的振幅
- 基于上述頻域信號,可以通過逆變換得到隨時間變化的2D空域信號,這個變換過程需要在運行時完成(涉及到時間,當然也可以烘焙多張,保證首尾銜接的話即可)
- 頻域信號用的是菲利普頻譜,基于這個頻譜可以在運行時計算各個頻率的信號數(shù)值,也可以在離線完成計算。運行時計算的好處是:
- 可以根據(jù)當前環(huán)境的狀態(tài)比如風向等來調(diào)制得到更擬真的效果
- 可以運行時調(diào)整模擬參數(shù),實現(xiàn)海洋效果的動態(tài)變化
具體可以參考實現(xiàn)細節(jié):



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

Crysis的海洋效果

War Thunder的海洋效果

海洋奇緣

泰坦尼克號

這些游戲跟電影有如下的一些共同點
- 都是基于FFT來完成模擬的
- 模擬的輸入是一個頻譜,之后對這個頻譜做逆向的離散傅里葉變換就得到時域(空域)信號
- 時域信號是一個隨時間變化的高度displacement數(shù)據(jù)

為啥是FFT:DFT耗時太高了,時間復雜度是,而FFT的時間復雜度則是
在GPU中計算FFT的算法是Cooley-Tukey(蝴蝶)算法。

傅里葉時域(還是頻域?)貼圖的分辨率應該是多少呢?正常情況下128 ~ 512就夠了,泰坦尼克號跟Waterworld也就只用來2048,超過這個尺寸可能會導致浮點精度問題。
這里選用的分辨率是512。

傅里葉變換的公式(可以參考[4]了解一下具體的推導過程)是:
逆變換公式為:
在計算機中這個積分公式是沒有解析解的,只能轉(zhuǎn)換為離散的累加形式。

再來看離散形式的變換與逆變換,這里的N是說將分割成N份,N越大,我們得到的頻域信號就越豐富,信號就越逼真,模擬越接近,但是計算復雜度也就越高:

參考[1],DFT的計算其實就是一個矩陣的運算,而這個矩陣運算則可以通過排列得到如上圖所示的蝴蝶累加算法,從而可以很好的實現(xiàn)加速計算。


這里IFFT的實現(xiàn)是通過compute shader完成的,采用的是Radix-2算法(參考[3]),細節(jié)就不說了,網(wǎng)上有大把的開源實現(xiàn)。

上面介紹的是一個一維函數(shù)的FFT實現(xiàn),那么回到水體模擬上,這個公式是如何應用起來的呢?
我們知道,傅里葉變換其實是通過將信號拆解為正弦波的方式來完成對信號的表達的,每個正弦波都有一個頻率,一個信號的頻域表示,其實就是其拆解后得到的正弦波的振幅與頻率的表示,頻域信號在某個頻率上的取值就是當前對應的時(空)域信號在對應頻率上的振幅。
回到海洋這邊來,其實海浪可以看成是由空間上無窮個2D的正弦波信號疊加而成,同樣的,這個信號在頻域中的表示就是海浪在不同頻率的正弦波上的振幅。不同的是,這個頻率是2D的,有X&Z兩個方向的頻率,同樣的,我們的頻域信號也是2維的,即在X & Z上具有不同頻率的信號的振幅。
用公式來表達,先不考慮海洋波形隨著時間波動的變化,我們先看下某個時間點的海洋波形,這個波形剛說了,可以看成是無窮多個2D的正弦波組成的,在空域上,可以表示為h(x, z),即在坐標(x, z)處的水體高度。
這個水波對應的頻域信號,我們可以表示為,兩者可以通過傅里葉變換(空域->頻域)與逆變換(頻域->空域)實現(xiàn)轉(zhuǎn)換,在海洋的實現(xiàn)邏輯中,我們的輸入通常是頻域信號,且只能用IDFT的方式來完成計算,因此按照前面的說明,這個逆變換的公式給出如下:
其中是頻域上的2D向量,對應于前面的
,或者用k做前綴,將之改寫為
,根據(jù)我們FFT頻域模擬的尺寸,這里有
,n跟m分別是頻域貼圖(貼圖的uv代表著兩個方向的頻率,對應的數(shù)值對應的是這個頻率的幅度)的水平跟垂直的分辨率,也就是我們在頻譜上的水平跟垂直方向上以
為單位采樣的點數(shù),其中
又分別為海面X or Z方向上的patch尺寸,根據(jù)[4]中的解釋,我們的采樣點是分布在原點兩側(cè)的
在這樣的采點策略下,針對海洋波形,我們就總計拿到了MxN個頻域的信號(或者說正弦波),通過他們之間的疊加與逆變換得到海洋的波形,通常情況下,這兩個數(shù)值越大,波形精度越高,模擬越逼真。
則是空域上的2D信號,對應的是當前點的xz坐標,同樣的,在計算displacement的時候,我們也是針對離散點進行的,即
也是在海平面上以
為單位采樣NxM個點(雖然通常N=M),同樣的,在相同的覆蓋范圍下,這倆數(shù)值越大,頂點之間的密度就越大,水體精度就越高,當然計算消耗也同樣會增加。
另外需要注意的是,指數(shù)上的兩個向量的乘法是點乘,即有:
在這個公式下我們就有當某個維度固定住的時候,我們得到的就是一個一維的信號。
在這個基礎上,如果我們考慮水波隨著時間變化的過程,那么我們可以將前面的頻域信號改寫為時間的函數(shù),即有:

接下來對這個公式做一個拆解。

里面最大的問題是頻域信號怎么得到,這里就沒有介紹具體的背景,而是直接給出了結(jié)論,海洋波形的頻域信號通常使用的是菲利普頻譜,其公式為:
這個公式中跟
是共軛復數(shù)(兩個實部相等,虛部互為相反數(shù)的復數(shù)互為共軛復數(shù)),k則是
的模,
是一個函數(shù),其計算公式為:
這里的g是重力加速度。

再來看,有如下的計算公式:
是相互獨立的隨機數(shù),均服從均值為0,標準差為1的正態(tài)分布。

這個公式中的是歸一化后的風向(2D),L的計算公式為:
這里的g依然是重力加速度,V則是風速。
按照[4]的描述,這里的公式是基于統(tǒng)計給出的經(jīng)驗公式,并無過多的解析或物理含義在里面,可以不用過于糾結(jié)其來源。



這里對公式中各個變量的含義做了基本的介紹,這里補上前面遺漏的項,前面介紹過的這里就不再介紹:
- a是全局的海浪的振幅

基于高度偏移的公式,我們可以通過解析的方式得到其法線。
先來計算一下梯度向量(某個函數(shù)即這里的高度變化率最大的方向,因為這里的變量還都是水平方向的兩個軸,因此這里的向量還是一個水平方向的向量),因為不包含梯度計算相關的
信息,因此可以直接移出來:
繼續(xù)推導,我們有:
代入前面的公式,我們就可以得到梯度向量的計算公式:
針對垂直向上的向量、梯度向量與法線向量的關系[4]:

注意梯度向量本身是沒有歸一化的,我們可以有:,將前面的公式代入,則我們有:

前面得到的是高度方向的偏移,但是我們實際上需要的是三個方向的偏移,因此還需要再額外處理一下另外兩個方向的。
在Gerstner Wave的實現(xiàn)中,為了得到尖銳的波峰效果(choppy wave),需要基于如下公式[4]在XZ方向上做擠壓:
而同樣的,F(xiàn)FT實現(xiàn)的海浪效果也需要做擠壓,擠壓公式為:


如下圖[4]所示,當擠壓力度過大的時候,就會出現(xiàn)水波的相互穿刺,即可以理解為水波碰撞在一起,此時會產(chǎn)生浮沫:

這個過程其實就是某個形狀(比如上圖中的近似四邊形,通常稱之為一個圖元)發(fā)生了翻轉(zhuǎn),正面翻到了背面,用數(shù)學概念來描述,就是該圖元有向面積變?yōu)樨摂?shù),而這個在數(shù)學公式上來表達就是對應的雅可比矩陣計算結(jié)果為負數(shù),具體可以參考[4]的推導。

擠壓強度假設為,我們就有:
而這個公式跟前面Gerstner Wave的擠壓公式整體形式上其實是一樣的,只是換了一種寫法。
經(jīng)過上面計算,我們就得到了空域中各個點的偏移向量,這個可以用一張3通道的貼圖來存儲,我們就有了上圖所示的Displacement Map

參考NVIDIA的解說圖,我們回顧一下上面的計算邏輯:
- 我們基于頻譜函數(shù)或者說頻譜貼圖
就能得到高度偏移
- 同樣基于頻譜函數(shù)跟擠壓公式,我們可以得到在水平方向上的偏移
- 將上述三者合到一起,我們就有了displacement map
- 基于Displacement map,我們就有計算出法線貼圖,同時基于雅可比矩陣計算結(jié)果,就能夠計算出foam intensity map(也就是上圖中的folding)




經(jīng)過上述計算之后,我們就能得到上述相對真實的海面模擬效果

這里采用的是Crysis Engine最開始實現(xiàn)海洋方案時的mesh方案,有點類似于Projected Grid,不同的是,這里選擇的不是基于場景相機的視角做的投影,而是基于場景相機為中心,俯視角做的投影(那不就是掛在相機上的一個固定的mesh嗎,只是不需要存儲mesh的數(shù)據(jù)而已)。
這個方案實現(xiàn)簡單,可以自動實現(xiàn)近密遠疏效果,而且據(jù)說不會有瑕疵
從原理上來分析,Projected Grid那種隨著相機移動或旋轉(zhuǎn)而導致頂點跳變的問題應該還是會有,但這里又單獨提了Projected grid的瑕疵,意思就是這個問題確實是被消滅了,不知道這里具體干了啥。。。


這是線框模式下的網(wǎng)格展示

模擬邏輯用C++實現(xiàn),著色則通過材質(zhì)實現(xiàn)。

水體著色這里主要關注反射跟折射效果。
而菲涅爾項則用于給出反射跟折射光強的比例,但是完整的菲涅爾計算邏輯太過復雜,這里采用的是Schlick Fresnel近似計算公式。

也就是我們高光BRDF計算中常用的那個,如果避開復雜的指數(shù)計算(如何避開?),實測這個近似計算公式要比原始公式快30%。

得到菲涅爾項之后,就可以通過lerp實現(xiàn)折射跟反射的混合。
除了這個計算邏輯之外,還借用了UE的SSR來添加帶有遮擋信息的環(huán)境反射效果。

[1]. 傅立葉變換簡記
[2]. 離散傅里葉變換DFT詳解及應用
[3]. FFT原理及實現(xiàn)(Radix-2)
[4]. fft海面模擬(一)