上一篇文章中,介绍了一个简单数值方法来模拟圆形扩散波的效果,但是这种方法对于自然中像海浪一样的波
就无能为力了。所以,这篇文章介绍用Gerstner波来模拟水面波纹效果。
一、Gerstner波介绍
Gerstner波是一种动态模拟海面幅度的方法,已有200多年的历史,后被用于计算机图形学。
首先,我们介绍一下几个常用的描述波函数的物理量,便于理解马上要介绍的Gerstner波方程。
A:amplitude 振幅,波相较于平衡位置的最大偏移量。
ω:角速度。
λ:波长
k:波数 (2π/λ)
K:波矢量(wavevector)其大小为波数,方向为波传播的方向,在3D波方程中,是一个2D向量
现在给出Gerstner波的方程,以参数方程的形式给出:
x=x0-(K/k)*Asin(K*x0-ωt)
y=Acos(K*x0-ωt)
肯定有朋友会问,明明是3D的波方程,为何只有两个变量xy。大家请注意,第一个式子中,加粗的部分,上面
已经介绍过,K是一个2D向量,方向表示在x-z平面波传播的方向,所以第一个加粗的部分(包括x,x0)都不是一个标量,而是一个二元量,表示的是在x-z平面上的具体位置,x=(x,z),x0=(x0,z0)。这样第一个式子得出的结果也自然是一个2D向量(K,x0点乘结果为一个标量)。
们模拟水面,经常是先在x-z平面上构建一个平面网格,然后修改每个顶点的y值,如下图:(x0,z0)又代表什么呢?这个坐标表示的是某一个顶点原本在x-z平面的位置。这里需要解释一下,总所周知,我
Gerstner波会根据固定的(x0,z0)位置,计算出新的在x-z平面的位置(也就是第一个式子的结果,x)。Gerstner波
的特征是 波峰较窄,波谷较宽,这符合自然中水波的特征。而从第二个公式我们可以看出,y是一个余弦函数,波峰波谷是一样的,所以波峰窄,波谷宽的特征便是通过对(x0,z0)进行一定量偏移造成的,如下图:
多个Gerstner波叠加
x=x0-∑(K/k)*Asin(K*x0-ωt)
y=∑Acos(K*x0-ωt)
多个波叠加也是非常好理解的,第一个公式叠加的是偏移量,所以x0不需要进行叠加。
注意
在选择Gerstner波的时候,是k,A是有一定限制的:
kA<1时,kA越大,波峰越窄。
kA>1时,会在波峰处产生环。如下图:
图片来自:《Simulating Ocean Water》 Jerry Tessendorf
二、Gerstner波的DirectX11实现
和上篇文章一样,我们同样适用DirectX11提供的Compute Shader进行并行计算。我们首先适用一个Buffer存放
网格数据,然后用一个RWStructuredBuffer存放计算后的数据。具体的HLSL代码片段如下:
计算法线的HLSL片段:StructuredBuffer
gOriVertices; RWStructuredBuffer gOutput; [numthreads(N, N, 1)] void csPos(int3 dispatchID:SV_DispatchThreadID) { int x = dispatchID.x; int z = dispatchID.y; float x0 = gOriVertices[z*gWidth + x].pos.x; float z0 = gOriVertices[z*gWidth + x].pos.z; float2 offset = float2(0.0f, 0.0f); float height = 0; float k = 0; float p; [unroll] for (uint i = 0; i < gWaveNum; ++i) { float2 waveVector = float2(gWaveVectors[i * 2 + 0], gWaveVectors[i * 2 + 1]); p = dot(waveVector, float2(x0, z0)) - gOmegas[i] * gTime; offset += normalize(waveVector)*gAmplitudes[i] * sin(p); height += gAmplitudes[i] * cos(p); } gOutput[z*gWidth + x].pos = float3(x0-offset.x, height, z0-offset.y ); }
[numthreads(N, N, 1)] void csVector(int3 dispatchID:SV_DispatchThreadID) { int x = dispatchID.x; int z = dispatchID.y; if (x == 0 || x == (gWidth - 1) || z == 0 || z == (gDepth - 1)) { return; } float3 left = gOutput[z*gWidth + x - 1].pos; float3 right = gOutput[z*gWidth + x + 1].pos; float3 top = gOutput[(z - 1)*gWidth + x].pos; float3 bottom = gOutput[(z + 1)*gWidth + x].pos; float3 tangent = normalize(right - left); float3 binormal = normalize(bottom - top); gOutput[z*gWidth + x].normal = normalize(cross(tangent, binormal)); gOutput[z*gWidth + x].tangent = tangent; }
DirectX11部分的详细代码就不放出了,没有什么难度,对DirectX11不是很熟悉的朋友,可以参考 此书。Dx11的
经典入门书籍,有DirectX龙书之称。
三、小结
多个适当的Gerstner波叠加,能够制造出较为真实的水面,但是如果需要电影级的水面波纹效果,Gerstner波可
能力不从心了。所以这个时候,就是FFT(快速傅里叶变化)发挥作用的,可能下篇文章会介绍利用FFT实现水面波纹的模拟(本人数学基础不是很好。需要补补课。。)
最后附几张截图:
简陋见解,不足之处,欢迎交流、拍砖。
参考资料:
《Simulating Ocean Water》 Jerry Tessendorf
JohnHany博客文:水面的简单渲染 – Gerstner波
================================The End==============================