这一篇文章将作为海面渲染的最终篇。第四篇已经介绍了基于FFT的海面渲染的一般步骤,并介绍了基于Phillips频谱的海面高度偏移和水平偏移的过程,介绍了FFT蝶形变换的实际原理和实现它的方法,同时介绍了如何使用ComputeShader实现这一过程,最后介绍了海面网格的生成方法,其中包括基础的海面网格生成方法,Skirt-Mesh的生成过程。对于海面网格的渲染方法,只介绍了如何对海面网格的顶点进行偏移,更多的渲染内容没做介绍。
这一篇文章将会介绍基于Jonswap频谱的海面网格的实现方法,结合论文[1]中的公式和一份开源代码进行介绍。开源代码的地址会附在参考文献中。最后对五篇文章进行总结。关于海面网格的生成,FFT的内容就不再着墨,可以参考第四篇的介绍。
本篇文章不会对原论文内容进行过多地解读,有渠道的朋友可以自己下载论文阅读和研究。不过结合这篇文章和提供的开源代码,可以更好地理解论文中给出的公式。开源代码可以认为是对论文的复现,但是又略有差异。也有可能是我本人也没有把原论文完全精度导致理解上有偏差。
基于Jonswap频谱的海面渲染方法
在github上有一个Unity工程,链接附在后面。在该工程中,基本实现了论文[1]中的内容。在这里就结合这两份资料来进行介绍。
代码的目录结构如下,其中ComputeShaders里存着的是FFT和IFFT相关的代码(有一点问题,后面再说)。Resources里保存的是使用的噪声贴图。Scripts里保存的是所有相关的C#脚本,实现了网格生成,ComputeShader的调度和使用等逻辑。Shaders里只有一个渲染海面网格的Shader。
在Assets目录下,还有一个Waves Setting的配置,该配置的内容是用于生成初始频谱的参数。
初始频谱$h_0(\vec{k})$的生成
初始频谱的生成过程,涉及到的公式包括:JONSWAP频谱,哈塞尔曼方向扩展,短波过滤盒频率导数。
在代码中,该过程的实现在WavesCascade.cs
文件中的CalculateInitials方法内。
角度频率及其导数
角度频率$\omega$及其导数$d\omega_k$的计算公式如下:
$\vec{k}$的计算和第四篇中的一致,就不再说明。该公式是根据代码的实现来写的,看代码比较简洁,代码的实现如下:
在开始执行初始化频谱的过程时,会首先计算$\omega$和$d\omega_k$。$\omega$在后续的公式中用得比较多,$d\omega_k$在最后一步使用。
JONSWAP频谱公式
JONSWAP频谱公式如下:
该公式的参数非常多,没有办法一一讲解,如果本着纯抄公式去实现的初心去看的话,只需要按公式去计算即可。在具体的实现中,由外部传入的参数包括:
$\gamma,\omega_p,\alpha$。这在代码中有所体现,在下图中红框内的代码是设置这些参数的调用。
这一段代码里,计算了$\gamma,\omega_p,\alpha$。可以看到,$\gamma$是手动配置得到的,而$\omega_p,\alpha$还要经过计算,计算使用的参数是手动配置的。
$\alpha$的计算公式如下,其中g是重力加速度9.81。U在配置项中,被标记为Fetch,论文中解释为平均风速,F是风速。
$\omega_p$的计算公式如下,其中g是重力加速度,U和F同上。
根据论文的介绍,公式$S_{JONSWAP}(\omega)$仅仅表示了深水频谱,Kitaigorodskii和Huge等人提出了一种频率依赖函数,作为深水频谱的修正。该公式如下:
在代码中,使用的是它的近似公式:
将这两个公式的结果相乘作为第一部分的结果,称为非方向性频率函数(non-directional spectrum function)
代码中关于这部分的实现,可以看InitialSpectrum.compute
中,红框部分的函数:
哈塞尔曼方向扩展(Hasselmann Directional Spreading)
方向扩展函数的公式可以总结如下:
在上式中,$\xi$是由外部传入的一个极小值。$k$的计算,和上一篇文章介绍的Phillips的计算一致,这里$\theta$的计算与$k$有关。在代码中,还有一个插值的操作,为了便于理解,将代码简化如下,并把先关的函数都放在一起。其中,NormalisationFactor在论文中没有提及,也不太清楚这段代码的含义,大概就是在做归一化操作。
这部分代码,有一个地方与论文中的公式不一致,如下图,发现这里是写反了。在调试时,修正后发现影响不大,可能是由于$\xi^2$太小导致的。
$\theta$的计算如下图所示,直接代入k进行计算。
短波过滤(Short Waves Fade)
短波过滤是最后一项因子。其公式如下:
其中$f$是一个很小的值,代码中取值为0.01。当该值变大时,会发现网格的波动变得越来越平整。
最终计算
把输入参数代入上述公式计算之后,把结果相乘,并进行最终的计算,得到初始频谱$h_0(\vec{k})$。这里先看代码吧。
可以看到这里有一个参数没有写出来,即deltaK。变量spetrum保存的结果,就是上述所有公式计算结果的乘积,将最后开根号的结果与噪声值相乘,保存为$h_0(\vec{k})$。最终的公式如下:
有了$h_0(\vec{k})$之后,然后是计算$h_0^*(\vec{k})$。计算方法如下,这里特别注明的是,对H0的采样坐标有特殊的计算。
|$\vec{k}$|的范围限制
基于JONSWAP公式模拟的海面,需要限制|$\vec{k}$|的范围。只在一定范围内生成数据,在范围之外直接填零。同时,还要将$\vec{k},\omega$保存到贴图中,供后续计算瞬时频谱时使用。这里需要特别注意的是,在范围之外的点,将|$\vec{k}$|保存为1。
范围的计算与LengthScale相关,每一级LOD使用的LengthScale都不同。代码如下:
初始频谱效果
如果按示例代码生成的初始频谱,应该如下图所示,需要特别说明的是,这是放大后的结果,否则看不到像素颜色。
生成初始频谱的代码
上述所有过程,除了参数配置之外,都在ComputeShader中完成。在WavesCascade.cs
中,可以看到这里对ComputeShader进行了两次调用。第一次是计算$h_0(\vec{k})$,第二次是计算$h_0^*(\vec{k})$
瞬时频谱$h(\vec{k},t)$的生成
瞬时频谱的计算过程定义在TimeDependentSpectrum.compute
,该过程与上一篇介绍的海面模拟过程一致。不过这份代码计算了更多的内容,我直接标注在代码中,就不介绍了。如果不关心渲染结果,只做海面网格的顶点扰动,可以只看Dx_Dz及Dy_Dxz这两张贴图。
有了这些数据之后,对这些数据执行IFFT,可以得到最终的数据。
贴图合并
贴图合并的过程,定义在WavesTexturesMerger.compute
中。该过程主要将前面IFFT的结果,合并程三张贴图,标注如下。
第一张是顶点偏移贴图(Displacement),被用来对海面网格顶点进行扰动。第二张是Derivatives,被用来计算法线,在片元着色器中使用。第三个是Turbulence,用来模拟浪尖,也是在片元着色器中使用。
本章小节
贴图合并和瞬时频谱的计算是一起的,在WavesCascade.cs
中定义,如下图所示的方法中,先计算了瞬时频谱,然后再进行贴图合并。
整个海浪模拟的过程
整个海浪模拟的过程定义在WaveGenerator.cs
中。
在初始化时,创建了三个WavesCascade类,定义在WavesCascade.cs
中,然后计算初始频谱。
在每一帧更新时,计算瞬时频谱,并合并贴图。
纵观整个过程,这里模拟了三个不同LengthScale的波。在渲染时根据不同的LOD层级,叠加不同的波,得到看起来比较自然的效果。
渲染
关于海面网格的渲染,和上一篇一样,只展示对Displacement的采样。采样的UV坐标是与顶点的世界坐标有关。而后续具体的渲染方法说不上一二,就不过多介绍了。
最终生成的效果如下图所示:
总结
终于到了该系列文章的尾声。之所以想做这一系列内容,一个是因为还大学时欠下的债——FFT相关内容。二个是在学生时代,由于水面模拟没有做出来,受制于FFT,导致最后更换了研究课题,所以这部分内容一直是想尝试做。在朋友的指导下,对FFT的内容有了一个初步的理解,在网上找了很多资料学习,才克服了这个障碍,所以索性就做一个系列的内容。另外还有一个小动机是,网上的资料太散太杂,也算是为后来者铺平道路。
参考文献和资料
[1] Horvath C J .Empirical directional wave spectra for computer graphics[C]//Symposium.2015.DOI:10.1145/2791261.2791267.
[2] GitHub链接