【UE】在虚幻引擎中参考《Gerstner Waves -GPU Gems》 从物理模型中实现有效的水体模拟



这篇文章的重点在于结合《GPU Gems》一书中有关 Gerstner Waves(格斯特纳波)的数学公式,在虚幻引擎(Unreal Engine)中复现正确的 Gerstner Waves 及其表面法线。
文中的理论内容整理自原书,并附带了我的个人理解与在虚幻引擎中的具体实现步骤。建议大家在阅读本文时,可以参考原书对应章节一起看(原文在网上很容易找到,这里就不全文转载了)。
阅读提示:内容看上去有些乱?
为了区分理论与实践,与虚幻引擎无关的原书理论内容,将以普通文本的形式呈现;而需要强调的、与虚幻引擎实现相关的内容,我将紧挨着理论部分,以引用块的形式附上(就像这段话一样)。
这样排版是因为我希望读者能随着我一起“边看书,边实操”,书讲到哪,咱们的节点就连到哪。
书中前面还介绍了几种基础波形,这与本文无关,这里假设大家已经做过“课前预习”,本文我们将直奔主题,直接开始讲解 Gerstner Waves。
1.2.1 选择波形参数
我们需要一组参数来定义每一个波形。这些参数包括:
- Wavelength(波长 L L L)
- 世界空间中,从一个波峰到下一个波峰之间的距离。
- 波长 L L L 与角频率 w w w 之间的关系为: w = 2 π L w = \frac{2\pi}{L} w=L2π。
避坑指南 1:如果你看的是英伟达官方的网页版文献,他们在公式中弄丢了这个 π \pi π。这篇文章中的公式将严格采用原书中的正确公式。
-
Amplitude(振幅 A A A)
- 从静止水平面到波峰的高度。
-
Speed(速度 S S S)
- 波峰每秒向前推进的距离。在公式中,将速度表示为相位常数 φ \varphi φ 会更加方便进行计算。
- φ = S × 2 π L \varphi = S \times \frac{2\pi}{L} φ=S×L2π
-
Direction(方向 D D D)
- 一个垂直于波阵面的水平向量,波峰将沿着这个方向运动。
为了让场景中的水面动态更具变化,我们会在一定的约束条件下,生成这些波浪参数。
实践证明,这些参数往往是相互依赖的。我们必须仔细地为每个波浪生成一整套参数,并确保它们能以一种令人信服的方式组合在一起。
实操:MF_GerstnerWaves
新建一个名为MF_GerstnerWaves的材质函数,我们先把上方提到的变量在 Input 节点中预留出来:
以防新手有疑问,最后一个节点是“重路由(Reroute)”节点,它是用来整理连线、避免蓝图变成“意大利面”的帮手,没有实际的功能意义。
1.2.2 法线和切线
因为水面是一个显式函数,所以我们可以直接通过数学求导来计算表面上任何一点的方向,而不需要依赖有限差分技术。
副法线向量 B \mathbf{B} B(Binormal)和切线向量 T \mathbf{T} T(Tangent)分别是函数在 x x x 和 y y y 方向上的偏导数。对于 2D 水平面中的任何一点 ( x , y ) (x, y) (x,y),其表面上的三维位置 P \mathbf{P} P 的法线计算如下:
公式 6a (Equation 6a) - 叉乘法线 Normal N = B × T \mathbf{N}=\mathbf{B}\times\mathbf{T} N=B×T
N ( x , y ) = B ( x , y ) × T ( x , y ) \mathbf{N}(x,y)=\mathbf{B}(x,y)\times\mathbf{T}(x,y) N(x,y)=B(x,y)×T(x,y)
这个公式体现了法线 N \mathbf{N} N 与副法线 B \mathbf{B} B、切线 T \mathbf{T} T 之间的叉乘关系。但我们先跳过它,最后再来处理法线,这种做法将规避经典的法线混合错误问题。
1.2.3 格斯特纳波 (Gerstner Waves)
为了得到更逼真的模拟效果,我们需要控制波浪的陡峭程度。对于波涛汹涌的海面,我们需要形成更尖锐的波峰和更宽阔的波谷。因此需要 Gerstner Waves。
Gerstner Waves 早在计算机图形学普及之前就被开发出来,用于在物理基础上模拟海水。因此,它能提供一些水面微妙的运动细节,这些变化虽然不显眼,但非常真实可信(详细描述可参见 Tessendorf 2001 的论文)。
我们选择 Gerstner Waves,是因为它们有一个常被忽视的特性:它能将顶点向波峰处聚集,这正是我们希望水面网格表现出的特性,如图 1-5 所示。

以下是 Gerstner Waves 的位置函数:
公式 9 (Equation 9) - 位置 Position P \mathbf{P} P
P ( x , y , t ) = ( x + ∑ ( Q i A i × D i . x × cos ( w i D i ⋅ ( x , y ) + φ i t ) ) , y + ∑ ( Q i A i × D i . y × cos ( w i D i ⋅ ( x , y ) + φ i t ) ) , ∑ ( A i sin ( w i D i ⋅ ( x , y ) + φ i t ) ) ) \mathbf{P}\left(x, y, t\right) = \left(\begin{array}{l} \begin{alignedat}{3} &x&+&\sum \left(Q_{i}A_{i} \times \mathbf{D}_{i}.x \times \cos\left(w_{i}\mathbf{D}_{i} \cdot (x, y) + \varphi_{i}t\right)\right),\\ &y&+& \sum \left(Q_{i}A_{i} \times \mathbf{D}_{i}.y \times \cos\left(w_{i}\mathbf{D}_{i} \cdot (x, y) + \varphi_{i}t\right)\right),\\ & & &\sum \left(A_{i} \sin\left(w_{i}\mathbf{D}_{i} \cdot (x, y) + \varphi_{i}t\right)\right) \end{alignedat} \end{array}\right) P(x,y,t)= xy++∑(QiAi×Di.x×cos(wiDi⋅(x,y)+φit)),∑(QiAi×Di.y×cos(wiDi⋅(x,y)+φit)),∑(Aisin(wiDi⋅(x,y)+φit))
∑ \sum ∑ 为求和符号, ∑ ( ) \sum() ∑() 意味着将所有独立波浪的位移叠加在一起,然后再分别加上原始的 x x x 和 y y y 坐标。
在材质中,我们想要先实现单个波的计算,也就是 ∑ ( ) \sum() ∑() 内部的内容,那么公式可以简化为代码逻辑:Px = Q * A * Dx * cos(w * D·(x,y) + φ * t); Py = Q * A * Dy * cos(w * D·(x,y) + φ * t); Pz = A * sin(w * D·(x,y) + φ * t);为了实现这个公式,我们引入另外两个变量:位置 ( x , y ) (x,y) (x,y) 和时间 t t t。节点如下:
应当避免使用过大的 Q i Q_i Qi 值,因为这会导致波峰在顶部发生交叉,形成不自然的“自我穿插/卷曲(Loop)”。
在实际应用中,我们可以提取一个总体的“陡峭度(Steepness)”参数 Q Q Q 交给美术人员控制(取值范围 0 0 0 到 1 1 1),并在内部使用公式 Q i = Q w i A i × numWaves Q_i = \frac{Q}{w_i A_i \times \text{numWaves}} Qi=wiAi×numWavesQ 来计算每个波的陡度。这样就能平滑地从完全平静的水面过渡到我们能生成的最大尖峰波浪。
Q Q Q 值的计算节点:
其中numWaves为波的总数(例如水面由 3 个波组合,这里就是 3)。Steepness变量就是上文提到的“陡峭程度”参数。
我们还可以观察到,公式里的
(w * D·(x,y) + φ * t)是重复计算的。为了优化结构,我们先把它的 Sine 和 Cosine 结果算出来。
(这不会影响最终的着色器复杂度,只会让蓝图看起来更整洁,拒绝意大利面连线)。注:虚幻引擎材质中的
Sin和Cos节点的周期默认是 1 1 1,如果要对应数学公式中的 2 π 2\pi 2π 周期,需要乘以 2 π 2\pi 2π 或者调整节点属性。
现在我们可以把公式完整拼接出来了:
这就是单个波的顶点位移(WPO)结果,接下来我们计算法线。
值得注意的是,公式 3(普通正弦波,见原书)和公式 9(Gerstner 波)之间唯一的区别在于顶点的横向移动( x , y x, y x,y 轴的偏移),它们的高度( z z z 轴)计算是相同的。这意味着我们不再拥有一个严格意义上的高度场函数(即 P ( x , y , t ) . x ≠ x \mathbf{P}(x,y,t).x \neq x P(x,y,t).x=x)。然而,该函数仍然很容易求导,并且在此过程中有一些项可以方便地消去。
简化书写:
W A = w i × A i WA = w_{i}\times A_{i} WA=wi×Ai
S ( ) = sin ( w i × D i ⋅ P + φ i t ) S() = \sin\left(w_{i}\times\mathbf{D}_{i}\cdot\mathbf{P}+\varphi_{i}t\right) S()=sin(wi×Di⋅P+φit)
C ( ) = cos ( w i × D i ⋅ P + φ i t ) C() = \cos\left(w_{i}\times\mathbf{D}_{i}\cdot\mathbf{P}+\varphi_{i}t\right) C()=cos(wi×Di⋅P+φit)
避坑指南 2:这里的 P \mathbf{P} P 实际上指的是未偏移前的初始水平坐标 ( x , y ) (x, y) (x,y)。
在蓝图里,我将 S ( ) S() S() 和 C ( ) C() C() 直接对应为算好的Sin()和Cos()节点结果:
经过推导,我们得到的切线空间基础向量如下:
公式 10 (Equation 10) - 副法线 Binormal B \mathbf{B} B
B = ( 1 − ∑ ( Q i × D i . x 2 × W A × S ( ) ) , − ∑ ( Q i × D i . x × D i . y × W A × S ( ) ) , ∑ ( D i . x × W A × C ( ) ) ) \mathbf{B} = \begin{pmatrix} \begin{alignedat}{3} &1&-&\sum\left(Q_i \times \mathbf{D}_i.x^2 \times WA \times S()\right),\\ & &-&\sum\left(Q_i \times \mathbf{D}_i.x \times \mathbf{D}_i.y \times WA \times S()\right),\\ & & &\sum\left(\mathbf{D}_i.x \times WA \times C()\right) \end{alignedat} \end{pmatrix} B= 1−−∑(Qi×Di.x2×WA×S()),∑(Qi×Di.x×Di.y×WA×S()),∑(Di.x×WA×C())
Binormal 的蓝图实现:
(注意:这里依然是只计算单个波的部分,即 ∑ \sum ∑ 内部的内容,常数 1 1 1 会在最后总和时处理)Bx = Q * D.x^2 * WA * sin(); By = Q * D.x * D.y * WA * sin(); Bz = D.x * WA * cos();
公式 11 (Equation 11) - 切线 Tangent T \mathbf{T} T
T = ( − ∑ ( Q i × D i . x × D i . y × W A × S ( ) ) , 1 − ∑ ( Q i × D i . y 2 × W A × S ( ) ) , ∑ ( D i . y × W A × C ( ) ) ) \mathbf{T}= \begin{pmatrix} \begin{alignedat}{3} & &-&\sum\bigl(Q_{i}\times\mathbf{D}_{i}.x\times\mathbf{D}_{i}.y\times WA\times S()\bigr),\\ &1&-&\sum\bigl(Q_{i}\times\mathbf{D}_{i}.y^{2}\times WA\times S()\bigr),\\ & & &\sum\bigl(\mathbf{D}_{i}.y\times WA\times C()\bigr) \end{alignedat} \end{pmatrix} T= 1−−∑(Qi×Di.x×Di.y×WA×S()),∑(Qi×Di.y2×WA×S()),∑(Di.y×WA×C())
Tangent 的蓝图实现:
Tx = Q * D.x * D.y * WA * sin(); Ty = Q * D.y^2 * WA * sin(); Tz = D.y * WA * cos();
公式 12 (Equation 12) - 法线 Normal N \mathbf{N} N
N = ( − ∑ ( D i . x × W A × C ( ) ) , − ∑ ( D i . y × W A × C ( ) ) , 1 − ∑ ( Q i × W A × S ( ) ) ) \mathbf{N}=\begin{pmatrix} \begin{aligned} &{}- \sum\bigl(\mathbf{D}_i.x\times WA\times C()\bigr),\\ &{}- \sum\bigl(\mathbf{D}_i.y\times WA\times C()\bigr),\\ 1 &{}- \sum\bigl(Q_i\times WA\times S()\bigr) \end{aligned} \end{pmatrix} N= 1−∑(Di.x×WA×C()),−∑(Di.y×WA×C()),−∑(Qi×WA×S())
注意注意!不要直接使用公式 12 混合多波法线!
公式 12 是求解单个波或者直接在数学上求和的法线公式。但在游戏引擎中,当我们试图混合多个独立计算的波浪法线时,无论你使用哪种法线混合(Normal Blend)节点,直接混合法线的结果往往在视觉上是错误的。正确的做法是: 我们应该先分别累加所有波的 B i n o r m a l \mathbf{Binormal} Binormal 和 T a n g e n t \mathbf{Tangent} Tangent,最后再通过叉积(Cross Product)一次性求解出最终的 N o r m a l \mathbf{Normal} Normal(即回归公式 6a)。这能确保计算出数学意义上绝对正确的表面法线。
多波混合演示:
我们先做两个波来演示如何混合。
新建一个专门用于求和的函数MF_GerstnerWave_∑。
我们把前面算出的所有波的Position、Binormal、Tangent累加起来(对应公式 9、10、11 中的 ∑ \sum ∑ 部分),然后输入到这个函数中进行最终处理。
核心重构 MF_GerstnerWave_∑ 内部逻辑
1. Position (位置)
回顾公式 9,最终的 P \mathbf{P} P 需要在 ∑ P \sum P ∑P 的 x , y x, y x,y 通道上分别加上原始坐标 x , y x, y x,y。
但由于在虚幻引擎的材质中,顶点偏移(World Position Offset)的基准空间原点相对顶点本身就是 0 0 0,所以我们不需要做这个加上原始 ( x , y ) (x,y) (x,y) 的偏移。直接输出相加后的 Position 即可:// 不需要 + (x,y) Position.xyz = ∑P.xyz;P ( x , y , t ) = ( x + ∑ ( Q i A i × D i . x × cos ( w i D i ⋅ ( x , y ) + φ i t ) ) , y + ∑ ( Q i A i × D i . y × cos ( w i D i ⋅ ( x , y ) + φ i t ) ) , ∑ ( A i sin ( w i D i ⋅ ( x , y ) + φ i t ) ) ▲ 不加 ) \mathbf{P}\left(x, y, t\right) = \left(\begin{array}{l} \begin{alignedat}{3} &x&+&\sum \left(Q_{i}A_{i} \times \mathbf{D}_{i}.x \times \cos\left(w_{i}\mathbf{D}_{i} \cdot (x, y) + \varphi_{i}t\right)\right),\\ &y&+& \sum \left(Q_{i}A_{i} \times \mathbf{D}_{i}.y \times \cos\left(w_{i}\mathbf{D}_{i} \cdot (x, y) + \varphi_{i}t\right)\right),\\ & & &\sum \left(A_{i} \sin\left(w_{i}\mathbf{D}_{i} \cdot (x, y) + \varphi_{i}t\right)\right)\\ &\blacktriangle& &\\ &\mathbf{不加} \end{alignedat} \end{array}\right) P(x,y,t)= xy▲不加++∑(QiAi×Di.x×cos(wiDi⋅(x,y)+φit)),∑(QiAi×Di.y×cos(wiDi⋅(x,y)+φit)),∑(Aisin(wiDi⋅(x,y)+φit))
2. Binormal (副法线)
根据公式 10 补全常数:Binormal.x = 1 - ∑B.x; Binormal.y = 0 - ∑B.y; Binormal.z = ∑B.z;B = ( 1 − ∑ ( Q i × D i . x 2 × W A × S ( ) ) , 0 − ∑ ( Q i × D i . x × D i . y × W A × S ( ) ) , ∑ ( D i . x × W A × C ( ) ) ▲ 这里 ) \mathbf{B} = \begin{pmatrix} \begin{alignedat}{3} &1&-&\sum\left(Q_i \times \mathbf{D}_i.x^2 \times WA \times S()\right),\\ &\phantom0&-&\sum\left(Q_i \times \mathbf{D}_i.x \times \mathbf{D}_i.y \times WA \times S()\right),\\ & & &\sum\left(\mathbf{D}_i.x \times WA \times C()\right)\\ &\blacktriangle& &\\ &\mathbf{这里} \end{alignedat} \end{pmatrix} B= 10▲这里−−∑(Qi×Di.x2×WA×S()),∑(Qi×Di.x×Di.y×WA×S()),∑(Di.x×WA×C())
3. Tangent (切线)
根据公式 11 补全常数:Tangent.x = 0 - ∑T.x; Tangent.y = 1 - ∑T.y; Tangent.z = ∑T.z;T = ( 0 − ∑ ( Q i × D i . x × D i . y × W A × S ( ) ) , 1 − ∑ ( Q i × D i . y 2 × W A × S ( ) ) , ∑ ( D i . y × W A × C ( ) ) ▲ 这里 ) \mathbf{T}= \begin{pmatrix} \begin{alignedat}{3} &\phantom0&-&\sum\bigl(Q_{i}\times\mathbf{D}_{i}.x\times\mathbf{D}_{i}.y\times WA\times S()\bigr),\\ &1&-&\sum\bigl(Q_{i}\times\mathbf{D}_{i}.y^{2}\times WA\times S()\bigr),\\ & & &\sum\bigl(\mathbf{D}_{i}.y\times WA\times C()\bigr)\\ &\blacktriangle& &\\ &\mathbf{这里} \end{alignedat} \end{pmatrix} T= 01▲这里−−∑(Qi×Di.x×Di.y×WA×S()),∑(Qi×Di.y2×WA×S()),∑(Di.y×WA×C())
4. Normal (最终法线)
根据公式 6a: N = B × T \mathbf{N} = \mathbf{B} \times \mathbf{T} N=B×TNormal = Cross(Binormal, Tangent);N ( x , y ) = B ( x , y ) × T ( x , y ) \mathbf{N}(x,y)=\mathbf{B}(x,y)\times\mathbf{T}(x,y) N(x,y)=B(x,y)×T(x,y)
结合上述逻辑,
MF_GerstnerWave_∑函数的完整内部节点如下:
结果验证
为了验证我们算出的法线是否正确,我们可以使用
ddx和ddy节点来实现一个“差分几何法线”作为对照组。
这种方法利用屏幕空间相邻像素的偏导数来重建表面法线。虽然它受限于模型顶点精度,看起来会有 LowPoly(低多边形)的棱角感,不能直接用于最终渲染,但它是一个非常客观的参考标准,能告诉我们“物理上正确的法线大概长什么样”。下图中,左侧为差分几何法线,右侧为我们通过叉乘计算出的法线。可以看到整体光影走向完全一致,证明我们得到了精确且平滑的法线结果。
完成基础搭建后,我们会发现 Gerstner Waves 的一些参数其实是可以通过物理公式推导出来的。
接下来,我们将演示如何引入真实的物理常数来优化算法,减少需要手动调节的变量。
这些公式虽然不像原书的 4b、5b 和 6b 方程那样简洁明了,但它们在 GPU 上的计算效率非常高。
在探讨波峰“打卷(Loop)”现象时,仔细观察法线的 z z z 分量会发现一个有趣的现象。虽然 Tessendorf (2001) 是从流体动力学的纳维-斯托克斯方程(Navier-Stokes)和“李变换技术(Lie Transform Technique)”中推导出了他的“切碎效应(Choppiness)”,且最终结果是在频域中表达的 Gerstner 波变体;在频域中,我们可以避免并检测到波峰的自交叉,但在我们当前使用的空间域公式中,物理现象更加直观。
当 ∑ ( Q i × w i × A i ) \sum (Q_i \times w_i \times A_i) ∑(Qi×wi×Ai) 的值大于 1 1 1 时,我们计算出的法线 z z z 分量在波峰处可能会变为负值,这就是波浪在自身上方形成“卷曲/打结”的数学本质。
因此,只要我们合理限制 Q i Q_i Qi 的取值,确保这个总和始终小于或等于 1 1 1,我们就能得到极其尖锐的波峰,同时永远不会出现破图的卷曲现象。
1.2.4 参数的物理化解释
首先,我们将基础单位定于虚幻引擎标准单位1uu,也就是1cm
波长 (Wavelength) 与 速度 (Speed)
我们首先需要选择合适的波长。与其去追求完全真实的海洋波长分布,不如把有限的性能预算集中在少数几个能产生最大视觉效果的波浪上。叠加几个长度相近的波浪,能极大地凸显水面的动态感。
因此,我们通常选择一个“中值波长(Median Wavelength)”,并在该长度的 0.5 倍到 2 倍之间生成随机波长。这个中值波长由美术人员在材质中设定,并且它可以随时间动态变化(例如,暴风雨期间的中值波长显然比晴朗时要大得多)。
注意: 我们绝不能直接动态修改一个正在活动的波的波长。即使是缓慢地插值改变,波浪的波峰也会出现向原点收缩或扩张的视觉崩坏,显得极不自然。正确的做法是:改变全局的平均波长参数,当旧的波浪随着时间淡出(Fade out)后,再基于新的波长生成新的波浪淡入(Fade in)。方向参数的处理同理。
一旦确定了波长,我们就可以轻松计算出波浪在水面上的传播速度。Tessendorf (2001) 给出了忽略高次项的深水频散关系:
公式 13 (Equation 13) 角频率
w = g × 2 π L w=\sqrt{g\times{\frac{2\pi}{L}}} w=g×L2π
其中 w w w 是角频率(Angular Frequency), g g g 是重力常数(例如虚幻引擎默认单位下为 980 cm/s 2 980 \text{ cm/s}^2 980 cm/s2), L L L 是波长。
实操优化:引入物理常数
真实海面由多个波组成,每个波的参数越少,越易于我们进行宏观控制。
到目前为止,我们每个波的Speed_i都是独立的手动参数,如果做 10 个波就需要手动调 10 个速度,非常繁琐。
但根据公式 13,我们完全可以推导它。如果我们引入全局变量:风速(WindSpeed) 和 重力(Gravity,设为 980),就可以自动求解速度。
根据公式推导:
Speed = WindSpeed × Gravity × w i \text{Speed} = \text{WindSpeed} \times \sqrt{\text{Gravity} \times w_i} Speed=WindSpeed×Gravity×wiSpeed = WindSpeed * Sqrt(Gravity * w);
现在,所有波浪都能通过统一的全局“风向”和“风速”来自动计算出合理的运动速度了。
物理公式纠错补充(非常重要!)
在原书提供的公式推导中,这里发生了一个严重的物理量混淆,会导致不同波长的波移动速度错乱。
按照原书逻辑算出来的Speed (S) = Sqrt(980 * (2π/L)),这个值在物理学上其实是 角频率 ω \omega ω,而不是真正的线速度。
而在前面的公式 φ = S × 2 π L \varphi = S \times \frac{2\pi}{L} φ=S×L2π 中,原作者把算出来的 ω \omega ω 又乘以了一次 2 π L \frac{2\pi}{L} L2π(即波数 k k k)。
这导致最终用来乘时间的系数变成了 ω ⋅ k \omega \cdot k ω⋅k。这是一个物理学上不存在的错误单位,彻底打破了真实的波浪频散关系。修正方案:
如果我们希望Speed (S)代表真正的“相速度(Phase Speed, C C C)”,公式应该改为 C = g / k C = \sqrt{g / k} C=g/k。
但更简单的做法是:既然我们通过Sqrt(980 * (2π/L))算出来的本来就是角频率 ω \omega ω,我们直接把它作为最终与时间相乘的系数即可(跳过乘以波数 k k k 的步骤)。
振幅 (Amplitude)
如何设定振幅往往是一个见仁见智的美术问题。
虽然在物理上,振幅可以作为波长和当前风力条件的函数推导出来,但为了更好的美术可控性,我们通常倾向于使用一个手动指定的常数(或通过蓝图脚本控制的比例)。
具体来说,美术人员在指定“中值波长”的同时,也会指定一个“中值振幅”。对于生成的任意大小的波浪,系统会保证其“振幅/波长”的比率,始终等于设定的“中值振幅/中值波长”的比率。
方向 (Direction)
波浪行进的方向与波长、振幅在数学上是完全独立的,因此我们可以根据需要,为每个波浪赋予不同的方向。
通常的做法是:我们先设定一个代表主风向的全局基础向量。然后,允许美术人员设定一个“散射角度(Angle)”,系统在这个角度范围内随机为每个细分波浪生成偏移方向。这个角度范围可以在材质实例中静态指定,也可以在游戏运行时通过天气系统蓝图动态修改。
但仍要强调,不要实时改变整体方向
文章到这就结束了,接下来简单处理一下
优化数据结构
随着逻辑的完善,是时候整理一下 MF_GerstnerWaves 函数的输入变量了。首先,我们把时间(Time)也作为一个输入节点开放出来,以便后续在蓝图或全局参数中进行统一控制。

经过梳理,现在我们的函数包含了 4 个全局变量(下图红色部分,如风速、陡度等)和 4 个独立变量(下图黄色部分,即每个波独有的参数)。
既然每个波有 4 个独立变量,那我们是不是刚好可以用一个 Vector4(四维向量,即 RGBA 四个通道)来完整描述一个波呢?
遗憾的是还差一点:因为方向(Direction)是一个二维向量(Vector2,占用 X 和 Y 两个通道),所以这 4 个变量实际上占用了 5 个浮点数值(Float)。
在虚幻引擎中,要传递 5 个值,最少也需要两个向量参数(比如一个 Vector4 加一个 Scalar),这在数据管理上非常不优雅。
MF_AngleToVector2
在编写着色器(Shader)时,我们常说要“用空间换时间”(消耗内存来减少计算)。但在这里,我们要反其道而行之:稍微增加一点点计算量,以减少参数的输入数量。
—— “只送大脑!”
我们从方向(Direction)入手。它现在是一个 2 通道的值(Vector2),我们的目标是把它“反向”压缩为一个 1 通道的标量(Scalar)。
二维的 Direction 本质上就是一个 2D 平面上的指向。如果我们在世界空间中定义一个基准的“前方”,那么任何方向都可以用一个 角度(Angle) 来描述。
为此,我们新建一个逻辑(封装为 MF_AngleToVector2):
注意: 这里的 RotationAngle 并不是传统意义上的 0 ∘ ∼ 360 ∘ 0^\circ \sim 360^\circ 0∘∼360∘ 角度。为了方便美术填写和优化性能,我们将它的输入范围设定为 − 1 -1 −1 到 1 1 1。
这样不仅直观,还省略了多步角度转换的计算:将其乘以 π \pi π 后,直接分别连接 Sine 和 Cosine 节点,就能求出完美的 x x x 和 y y y 方向分量。
当默认输入为 0 0 0 时,输出的方向向量即为 ( 1 , 0 ) (1, 0) (1,0)。
整合为 Vector4
现在回到 MF_GerstnerWaves 函数,我们将原本的 Direction(Vector2)输入节点,替换为刚才做好的 DirectionAngle(Scalar)输入。
这样做还有一个意外惊喜:通过 Sine 和 Cosine 算出来的方向天生就是单位向量(长度为 1),所以我们顺手省下了一个 Normalize(归一化)节点的性能开销,绝对不亏!
完成这一步后,现在的参数结构具有了重大意义:我们真正实现了一个 Vector4 变量就能完美定义一个波浪!
这在工程数据结构上带来了极大的便利。例如,我们现在可以直接在**材质参数集(Material Parameter Collection, MPC)**中,极其整洁地定义和管理数十个波浪参数,或者通过蓝图数组轻松驱动它们:

效果
何尝不借此放一些美图呢








- 参考来自Gerstner Waves圣经—— Mark Finch - Cyan Worlds GPU Gems
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


















所有评论(0)