【學習筆記】Unity 基於GPU FFT海洋的實現-實踐篇
本文是繼【學習筆記】Unity 基於GPU FFT海洋的實現-理論篇 的實現。
這是最後得到的結果
https://www。zhihu。com/video/1188154326164643840
對於實現的話就比較簡單了,只需要照著公式抄一遍就可以了。在本次實現中我們將使用Compute Shader在計算頻譜和FFT。為了理清思路和便於debug,整個工程程式碼寫得比較野蠻==其中很多計算是可以放到一起的,也建立了很多的RenderTexture(實際並用不了那麼多),同時還使用了比較大的資料型別等。
首先我們需要先生成高斯隨機數
//計算高斯隨機變數
[
numthreads
(
8
,
8
,
1
)]
void
ComputeGaussianRandom
(
uint3
id
:
SV_DispatchThreadID
)
{
float2
g
=
gaussian
(
id
。
xy
);
GaussianRandomRT
[
id
。
xy
]
=
float4
(
g
,
0
,
0
);
}
//計算高斯隨機數
float2
gaussian
(
float2
id
)
{
//均勻分佈隨機數
rngState
=
wangHash
(
id
。
y
*
N
+
id
。
x
);
float
x1
=
rand
();
float
x2
=
rand
();
x1
=
max
(
1
e
-
6
f
,
x1
);
x2
=
max
(
1
e
-
6
f
,
x2
);
//計算兩個相互獨立的高斯隨機數
float
g1
=
sqrt
(
-
2。0
f
*
log
(
x1
))
*
cos
(
2。0
f
*
PI
*
x2
);
float
g2
=
sqrt
(
-
2。0
f
*
log
(
x1
))
*
sin
(
2。0
f
*
PI
*
x2
);
return
float2
(
g1
,
g2
);
}
//隨機種子
uint
wangHash
(
uint
seed
)
{
seed
=
(
seed
^
61
)
^
(
seed
>>
16
);
seed
*=
9
;
seed
=
seed
^
(
seed
>>
4
);
seed
*=
0x27d4eb2d
;
seed
=
seed
^
(
seed
>>
15
);
return
seed
;
}
//計算均勻分佈隨機數[0,1)
float
rand
()
{
// Xorshift演算法
rngState
^=
(
rngState
<<
13
);
rngState
^=
(
rngState
>>
17
);
rngState
^=
(
rngState
<<
5
);
return
rngState
/
4294967296。0
f
;;
}
我們將使用wangHash來生成隨機數種子,然後使用Xorshift演算法來生成均勻分佈的隨機數。可以參考這裡Quick And Easy GPU Random Numbers In D3D11 ,然後對得到均勻分佈的隨機數,透過Box-Muller轉換,將得到高斯隨機數
和
是兩個相互獨立的均勻分佈的隨機數,
和
是兩個相互獨立的高斯隨機數。可參考這裡GPU Gems 3 :Chapter 37 。
隨機數只需要計算一次就好了,然後我們在計算高度頻譜
//生成高度頻譜
[
numthreads
(
8
,
8
,
1
)]
void
CreateHeightSpectrum
(
uint3
id
:
SV_DispatchThreadID
)
{
float2
k
=
float2
(
2。0
f
*
PI
*
id
。
x
/
N
-
PI
,
2。0
f
*
PI
*
id
。
y
/
N
-
PI
);
float2
gaussian
=
GaussianRandomRT
[
id
。
xy
]。
xy
;
float2
hTilde0
=
gaussian
*
sqrt
(
abs
(
phillips
(
k
)
*
DonelanBannerDirectionalSpreading
(
k
))
/
2。0
f
);
float2
hTilde0Conj
=
gaussian
*
sqrt
(
abs
(
phillips
(
-
k
)
*
DonelanBannerDirectionalSpreading
(
-
k
))
/
2。0
f
);
hTilde0Conj
。
y
*=
-
1。0
f
;
float
omegat
=
dispersion
(
k
)
*
Time
;
float
c
=
cos
(
omegat
);
float
s
=
sin
(
omegat
);
float2
h1
=
complexMultiply
(
hTilde0
,
float2
(
c
,
s
));
float2
h2
=
complexMultiply
(
hTilde0Conj
,
float2
(
c
,
-
s
));
float2
HTilde
=
h1
+
h2
;
HeightSpectrumRT
[
id
。
xy
]
=
float4
(
HTilde
,
0
,
0
);
}
phillips譜
//計算phillips譜
float
phillips
(
float2
k
)
{
float
kLength
=
length
(
k
);
kLength
=
max
(
0。001
f
,
kLength
);
// kLength = 1;
float
kLength2
=
kLength
*
kLength
;
float
kLength4
=
kLength2
*
kLength2
;
float
windLength
=
length
(
WindAndSeed
。
xy
);
float
l
=
windLength
*
windLength
/
G
;
float
l2
=
l
*
l
;
float
damping
=
0。001
f
;
float
L2
=
l2
*
damping
*
damping
;
//phillips譜
return
A
*
exp
(
-
1。0
f
/
(
kLength2
*
l2
))
/
kLength4
*
exp
(
-
kLength2
*
L2
);
}
Donelan-Banner方向拓展
//Donelan-Banner方向拓展
float
DonelanBannerDirectionalSpreading
(
float2
k
)
{
float
betaS
;
float
omegap
=
0。855
f
*
G
/
length
(
WindAndSeed
。
xy
);
float
ratio
=
dispersion
(
k
)
/
omegap
;
if
(
ratio
<
0。95
f
)
{
betaS
=
2。61
f
*
pow
(
ratio
,
1。3
f
);
}
if
(
ratio
>=
0。95
f
&&
ratio
<
1。6
f
)
{
betaS
=
2。28
f
*
pow
(
ratio
,
-
1。3
f
);
}
if
(
ratio
>
1。6
f
)
{
float
epsilon
=
-
0。4
f
+
0。8393
f
*
exp
(
-
0。567
f
*
log
(
ratio
*
ratio
));
betaS
=
pow
(
10
,
epsilon
);
}
float
theta
=
atan2
(
k
。
y
,
k
。
x
)
-
atan2
(
WindAndSeed
。
y
,
WindAndSeed
。
x
);
return
betaS
/
max
(
1
e
-
7
f
,
2。0
f
*
tanh
(
betaS
*
PI
)
*
pow
(
cosh
(
betaS
*
theta
),
2
));
}
float
dispersion
(
float2
k
)
{
return
sqrt
(
G
*
length
(
k
));
}
這裡並沒有什麼好說的,基本就是照著公式抄==。Donelan-Banner方向拓展公式為
圖擷取自Empirical Directional Wave Spectra for Computer Graphics,
角頻率,
是波相對於風的角度,
是峰值頻率
,
是重力加速度,
是平均風速。
得到了高度頻譜,就可以使用他來計算我們的偏移頻譜
//生成偏移頻譜
[
numthreads
(
8
,
8
,
1
)]
void
CreateDisplaceSpectrum
(
uint3
id
:
SV_DispatchThreadID
)
{
float2
k
=
float2
(
2
*
PI
*
id
。
x
/
N
-
PI
,
2
*
PI
*
id
。
y
/
N
-
PI
);
k
/=
max
(
0。001
f
,
length
(
k
));
float2
HTilde
=
HeightSpectrumRT
[
id
。
xy
]。
xy
;
float2
KxHTilde
=
complexMultiply
(
float2
(
0
,
-
k
。
x
),
HTilde
);
float2
kzHTilde
=
complexMultiply
(
float2
(
0
,
-
k
。
y
),
HTilde
);
DisplaceXSpectrumRT
[
id
。
xy
]
=
float4
(
KxHTilde
,
0
,
0
);
DisplaceZSpectrumRT
[
id
。
xy
]
=
float4
(
kzHTilde
,
0
,
0
);
}
至此就得到了我們想要的所有頻譜,然後分別來對他們進行FFT就可以了
//橫向FFT計算,只針對第m-1階段,最後一階段需要特殊處理
[
numthreads
(
8
,
8
,
1
)]
void
FFTHorizontal
(
uint3
id
:
SV_DispatchThreadID
)
{
int2
idxs
=
id
。
xy
;
idxs
。
x
=
floor
(
id
。
x
/
(
Ns
*
2。0
f
))
*
Ns
+
id
。
x
%
Ns
;
float
angle
=
2。0
f
*
PI
*
(
id
。
x
/
(
Ns
*
2。0
f
));
float2
w
=
float2
(
cos
(
angle
),
sin
(
angle
));
float2
x0
=
InputRT
[
idxs
]。
xy
;
float2
x1
=
InputRT
[
int2
(
idxs
。
x
+
N
*
0。5
f
,
idxs
。
y
)]。
xy
;
float2
output
=
x0
+
float2
(
w
。
x
*
x1
。
x
-
w
。
y
*
x1
。
y
,
w
。
x
*
x1
。
y
+
w
。
y
*
x1
。
x
);
OutputRT
[
id
。
xy
]
=
float4
(
output
,
0
,
0
);
}
這裡只截取了一個,對於最後一個階段和縱向FFT,程式碼大同小異,其實也可以寫到一起。
當FFT計算完後,就可以生成我們的偏移紋理,這裡使用了幾個引數來控制他的偏移程度。
//生成偏移紋理
[
numthreads
(
8
,
8
,
1
)]
void
TextureGenerationDisplace
(
uint3
id
:
SV_DispatchThreadID
)
{
float
y
=
length
(
HeightSpectrumRT
[
id
。
xy
]。
xy
)
/
(
N
*
N
)
*
HeightScale
;
//高度
float
x
=
length
(
DisplaceXSpectrumRT
[
id
。
xy
]。
xy
)
/
(
N
*
N
)
*
Lambda
;
//x軸偏移
float
z
=
length
(
DisplaceZSpectrumRT
[
id
。
xy
]。
xy
)
/
(
N
*
N
)
*
Lambda
;
//z軸偏移
HeightSpectrumRT
[
id
。
xy
]
=
float4
(
y
,
y
,
y
,
0
);
DisplaceXSpectrumRT
[
id
。
xy
]
=
float4
(
x
,
x
,
x
,
0
);
DisplaceZSpectrumRT
[
id
。
xy
]
=
float4
(
z
,
z
,
z
,
0
);
DisplaceRT
[
id
。
xy
]
=
float4
(
x
,
y
,
z
,
0
);
}
最後根據偏移紋理,來計算法線和泡沫,計算方法就和我們上一節所講的那樣。
//生成法線和泡沫紋理
[
numthreads
(
8
,
8
,
1
)]
void
TextureGenerationNormalBubbles
(
uint3
id
:
SV_DispatchThreadID
)
{
//計算法線
float
uintLength
=
OceanLength
/
(
N
-
1。0
f
);
//兩點間單位長度
//獲取當前點,周圍4個點的uv座標
uint2
uvX1
=
uint2
((
id
。
x
-
1。0
f
+
N
)
%
N
,
id
。
y
);
uint2
uvX2
=
uint2
((
id
。
x
+
1。0
f
+
N
)
%
N
,
id
。
y
);
uint2
uvZ1
=
uint2
(
id
。
x
,
(
id
。
y
-
1。0
f
+
N
)
%
N
);
uint2
uvZ2
=
uint2
(
id
。
x
,
(
id
。
y
+
1。0
f
+
N
)
%
N
);
//以當前點為中心,獲取周圍4個點的偏移值
float3
x1D
=
DisplaceRT
[
uvX1
]。
xyz
;
//在x軸 第一個點的偏移值
float3
x2D
=
DisplaceRT
[
uvX2
]。
xyz
;
//在x軸 第二個點的偏移值
float3
z1D
=
DisplaceRT
[
uvZ1
]。
xyz
;
//在z軸 第一個點的偏移值
float3
z2D
=
DisplaceRT
[
uvZ2
]。
xyz
;
//在z軸 第二個點的偏移值
//以當前點為原點,構建周圍4個點的座標
float3
x1
=
float3
(
x1D
。
x
-
uintLength
,
x1D
。
yz
);
//在x軸 第一個點的座標
float3
x2
=
float3
(
x2D
。
x
+
uintLength
,
x2D
。
yz
);
//在x軸 第二個點的座標
float3
z1
=
float3
(
z1D
。
xy
,
z1D
。
z
-
uintLength
);
//在z軸 第一個點的座標
float3
z2
=
float3
(
z1D
。
xy
,
z1D
。
z
+
uintLength
);
//在z軸 第二個點的座標
//計算兩個切向量
float3
tangentX
=
x2
-
x1
;
float3
tangentZ
=
z2
-
z1
;
//計算法線
float3
normal
=
normalize
(
cross
(
tangentZ
,
tangentX
));
//計算泡沫
float3
ddx
=
x2D
-
x1D
;
float3
ddz
=
z2D
-
z1D
;
//雅可比行列式
float
jacobian
=
(
1。0
f
+
ddx
。
x
)
*
(
1。0
f
+
ddz
。
z
)
-
ddx
。
z
*
ddz
。
x
;
jacobian
=
saturate
(
max
(
0
,
BubblesThreshold
-
saturate
(
jacobian
))
*
BubblesScale
);
NormalRT
[
id
。
xy
]
=
float4
(
normal
,
0
);
BubblesRT
[
id
。
xy
]
=
float4
(
jacobian
,
jacobian
,
jacobian
,
0
);
}
這樣我們就有了所有的資料,接下來進行渲染就可以了。在頂點著色器根據偏移紋理 進行頂點偏移。片源著色器就進行了簡單的燈光計算,如果想要更真實的物理效果可以參考這篇論文Real-time Realistic Ocean Lighting using Seamless Transitions from Geometry to BRDF
v2f
vert
(
appdata
v
)
{
v2f
o
;
o
。
uv
=
TRANSFORM_TEX
(
v
。
uv
,
_Displace
);
float4
displcae
=
tex2Dlod
(
_Displace
,
float4
(
o
。
uv
,
0
,
0
));
v
。
vertex
+=
float4
(
displcae
。
xyz
,
0
);
o
。
pos
=
UnityObjectToClipPos
(
v
。
vertex
);
o
。
worldPos
=
mul
(
unity_ObjectToWorld
,
v
。
vertex
)。
xyz
;
return
o
;
}
fixed4
frag
(
v2f
i
)
:
SV_Target
{
fixed3
normal
=
UnityObjectToWorldNormal
(
tex2D
(
_Normal
,
i
。
uv
)。
rgb
);
fixed
bubbles
=
tex2D
(
_Bubbles
,
i
。
uv
)。
r
;
fixed3
lightDir
=
normalize
(
UnityWorldSpaceLightDir
(
i
。
worldPos
));
fixed3
viewDir
=
normalize
(
UnityWorldSpaceViewDir
(
i
。
worldPos
));
fixed3
reflectDir
=
reflect
(
-
viewDir
,
normal
);
// reflectDir *= sign(reflectDir。y);
//取樣反射探頭
half4
rgbm
=
UNITY_SAMPLE_TEXCUBE_LOD
(
unity_SpecCube0
,
reflectDir
,
0
);
half3
sky
=
DecodeHDR
(
rgbm
,
unity_SpecCube0_HDR
);
//菲涅爾
fixed
fresnel
=
saturate
(
_FresnelScale
+
(
1
-
_FresnelScale
)
*
pow
(
1
-
dot
(
normal
,
viewDir
),
5
));
half
facing
=
saturate
(
dot
(
viewDir
,
normal
));
fixed3
oceanColor
=
lerp
(
_OceanColorShallow
,
_OceanColorDeep
,
facing
);
fixed3
ambient
=
UNITY_LIGHTMODEL_AMBIENT
。
rgb
;
//泡沫顏色
fixed3
bubblesDiffuse
=
_BubblesColor
。
rbg
*
_LightColor0
。
rgb
*
saturate
(
dot
(
lightDir
,
normal
));
//海洋顏色
fixed3
oceanDiffuse
=
oceanColor
*
_LightColor0
。
rgb
*
saturate
(
dot
(
lightDir
,
normal
));
fixed3
halfDir
=
normalize
(
lightDir
+
viewDir
);
fixed3
specular
=
_LightColor0
。
rgb
*
_Specular
。
rgb
*
pow
(
max
(
0
,
dot
(
normal
,
halfDir
)),
_Gloss
);
fixed3
diffuse
=
lerp
(
oceanDiffuse
,
bubblesDiffuse
,
bubbles
);
fixed3
col
=
ambient
+
lerp
(
diffuse
,
sky
,
fresnel
)
+
specular
;
return
fixed4
(
col
,
1
);
}
邏輯程式碼並沒有粘,因為他實在是太簡單了。。。。。
原始碼已經上傳到了Github上,如果有什麼理解或錯誤的地方,望大佬告訴我。。。。。