目的:
根據前篇的水波Shader,優化成一個不透明的,適合廣闊海域的Shader,以解決前篇Shader的一些缺陷。
參考:
《GPU Gems》
引言:
對於我們之前的初步可用品Shader,可以從不同方面進行改造,適用於不同需求下的水面。接下來幾篇着重於造型上的改造,適用於不透明水面,如深邃廣闊的大海。
思路:
回想起前篇結尾我們談論到的當前Shader的不足之處:
1.只能模擬一個方向簡單的水波狀態,波間距一致,缺少自然感。
2.波形狀一致,單一,且與某些常見的海浪形狀不一致。
下面,針對這兩點分別改進。
1.通過疊加增加複雜感
沒錯,這個問題的解決方案很簡單,我們當前不就只有一個方向的波麼?多加兩個,調調參數就完事了。
記得高中、大學數學學過的導數法則:
所以,添加多個波之後的方程也極其簡單。
波面方程:
法線方程:
PS:
如果你知道傅里葉變換(Fourier transform),那麼就能意識到只要多加幾個波,什麼樣複雜的水面都可以模擬出來。
因爲幾乎任何函數都能被拆解成傅里葉級數(Fourier series),包括不連續的函數。
級數越多,“分辨率”越高。這個特性是不是像極了我們之前提到過的泰勒公式(Taylor's formula)?
在複分析(Complex Analysis)中,它們兩是"兩位一體"。(參考《複分析可視化方法》)
2.添加更有浪尖的Gerstner波
2.1Gerstner波方程
根據前文所說,我們的Sin函數不太像某些真實的浪,往往擁有鋒利的浪尖。
圖形學前輩們在我們目前的函數上進行改進,發明了Gerstner波。
形狀如下:
公式(顯然這是多個Gerstner波的疊加):
別忘了其中,只是爲了簡化寫法。
注意:我在此對《GPU Gem》上的Gerstner波公式改了寫法。一是爲了銜接我們之前推導的公式,改變了寫法。
二是原文的參數冗餘,不利於理解。但確爲完全一致的公式。
可以看出,Z方向上的函數與我們的函數一致,只是x,y方向加了一些東西。
在此我設,理解Gerstner波就在於理解。
下面是我的理解。
我們要幹什麼?我們要把波峯搞尖。
看到沒有?如果我把每個點往法線相反方向擠一擠,這個峯就尖了。
x軸的往法線x分量負方向擠一擠,y軸的往法線y分量負方向擠一擠。
當這個擠的量爲法線在x/y軸的分量,會導致法線朝上的被擠的少,法線水平得擠的多,就成了尖峯的樣子。
再加兩個參數控制一下擠的程度,就完事了。
這也就是爲什麼L(x),L(y)會這麼像我們原來的法線
那爲啥前面的係數是這樣呢?原來前輩早替不懂原理的美術們想好了,這樣設計,Q只要保證在[0,1]就行。取0時就和我們原來一樣,取1時浪尖最尖。Q大於1浪尖就不對了,浪尖出現環狀:
爲啥Q大於1就會這樣呢?
《GPU Gems》只簡單提了一下在頻域內可發現並解決這個問題。關於頻域我準備在以後必要的時候再回過頭來討論。
我現在給出一個簡單的幾何證明。波函數被簡化。
擠之前的點,
擠之後變成了點
直觀上來說,對於Sin函數,僅看處於的點x,x+L(x)還應該<=T/4。
有不等式
設u=wx,化簡得:
接下來得過程很像高三數學大題(江蘇)。
不論是畫出圖形,還是研究單調性。你會發現函數單調減。
照理來說,證明當u=pi/2時,G(u)=1,就行了。但cos(pi/2)=0,G(u)不存在。
只能求極限,u->pi/2,求lim G(u)。
直接上高數洛必達法則(L'Hopital's rule)
兩個趣點:
1.沒錯,咱江蘇數學就是有高數內容,咱就喜歡超綱,傻了8。可惜以後都是全國捲了...
2.別忘了洛必達法則是土豪洛必達買來的,沒錯,有錢就是可以爲所欲爲。
2.2Gerstner波的法線方程
記得之前博文關於法線推導的推導一嗎?我本人不贊成這種方法。但結果這兒發現了它的好處。
對於不存在z=Z(x,y)形式,而且全方程又很難列出來,但知道3分量各自解析式的情況,推導一就可用。
不再細述過程,結果:
這樣關於實現Gerstner波的內容就完整了。
步驟:
1.對材質進行修改。
運用上我們的公式,將結果給予WPOffset,Normal。
我在之前的shader的基礎上又加了2個Sin波,1個Gerstner波。
//wave normal
float3 re;
float pi=3.14f;
float w=2*pi/T;
float k=dot(float2(OriPos.x,OriPos.y),dir);
float phase=2*pi*frac(time/speedCycle)*phaseDir;
//wave2
float w2=2*pi/T2;
float k2=dot(float2(OriPos.x,OriPos.y),dir2);
float phase2=2*pi*frac(time/speedCycle2)*phaseDir2;
//wave3
float w3=2*pi/T3;
float k3=dot(float2(OriPos.x,OriPos.y),dir3);
float phase3=2*pi*frac(time/speedCycle3)*phaseDir3;
//add wave123
re.x=-A*cos(w*k+phase)*w*dir.x-A2*cos(w2*k2+phase2)*w2*dir2.x-A3*cos(w3*k3+phase3)*w3*dir3.x;
re.y=-A*cos(w*k+phase)*w*dir.y-A2*cos(w2*k2+phase2)*w2*dir2.y-A3*cos(w3*k3+phase3)*w3*dir3.y;
re.z=1;
//wave4
float3 wave4N;
float w4=2*pi/T4;
float k4=dot(float2(OriPos.x,OriPos.y),dir4);
float phase4=2*pi*frac(time/speedCycle4)*phaseDir4;
float S=sin(w4*k4+phase4);
float C=cos(w4*k4+phase4);
wave4N.x=-dir4.x*w4*A4*C;
wave4N.y=-dir4.y*w4*A4*C;
wave4N.z=1-Q*w4*A4*S;
re+=wave4N;
return re;
//WPOffset
float3 re;
float pi=3.14f;
re.x=0.0f;
re.y=0.0f;
float phase=2*pi*frac(time/speedCycle)*phaseDir;
float phase2=2*pi*frac(time/speedCycle2)*phaseDir2;
float phase3=2*pi*frac(time/speedCycle3)*phaseDir3;
float wave1=A*sin(2*pi/T*dot(float2(OriPos.x,OriPos.y),dir)+phase);
float wave2=A2*sin(2*pi/T2*dot(float2(OriPos.x,OriPos.y),dir2)+phase2);
float wave3=A3*sin(2*pi/T3*dot(float2(OriPos.x,OriPos.y),dir3)+phase3);
re.z=wave1+wave2+wave3;
//////////////////
//wave4,Gerstner
float3 wave4;
float w4=2*pi/T4;
float k4=dot(float2(OriPos.x,OriPos.y),dir4);
float phase4=2*pi*frac(time/speedCycle4)*phaseDir4;
wave4.x=Q*1/w4*dir4.x*cos(w4*k4+phase4);
wave4.y=Q*1/w4*dir4.y*cos(w4*k4+phase4);
wave4.z=A4*sin(w4*k4+phase4);
re+=wave4;
return re;
2.將平面擴大100倍,調整參數
平面擴大成爲海面,此時一定注意波長,小於最短波長會出現法線不對的情況。
(但有趣的是,失真有時會帶來比較複雜的波面,參考之前博文的失真情況。如果不需要用到法線,還是很用的)
結果:
我還將BaseColor簡單乘上一個貼圖,顯得有趣些。
3Sin不加Gerstner:
仔細看會發現還是有重複的模式(Pattern)出現,在高處俯視更加明顯,畢竟只有3個sin波。但是你去看看一些遊戲中的海面,說不定更加明顯。而且我們對其表面材質和光照還未關注,只有造型,遊戲中的海面通過其他東西增加了你的真實感。
增加Gerstner,模擬一陣一陣衝岸的小尖浪,或者災難片中的百尺巨浪,之後要解決固定波長的問題。
因爲這種浪速度較快要持續出現,固定波長的話就顯得十分人工。
這條斜面大,浪脊分明的波就是Gerstner波:
結語:
本篇對單一波進行疊加,並引入Gerstner波,完成了簡單的海面Shader。
下一篇會解決Gerstner波,波長固定的問題,並開始研究水波模擬的表面材質,光照部分。