在前文《【UE】在虚幻引擎中参考<Gerstner Waves -GPU Gems> 从物理模型中实现有效的水体模拟》中,我们探讨并实现了基于Gerstner波而非FFT的水体模拟方案。
相较于 FFT,Gerstner 方案的工程优势显而易见:它无需依赖 Render Target 进行状态缓存,也完全摆脱了时间步长的累积限制。这种“无状态”的数学特性,赋予了系统极高的时间自由度——无论是时间的倒流、加速还是瞬间跳跃,都能完美支持。不仅如此,其纯解析式的计算方式对网络同步极其友好:只需统一时间参数,各客户端即可生成绝对一致的宏观海面;而给定任意二维坐标(Pos),也能瞬间精确求解出该位置的浪高,为物理交互提供了极大的便利。
在上一篇文章的末尾,我们初步展示了如何利用 RGBA 四个通道来封装单个波浪的参数,以及如何将多个波进行叠加合并:
构建多波浪叠加的宏观海面
现在,我们将在此基础上,实操构建一个由多组波浪叠加而成的复杂海面。(注:为了更符合完整的工程结构,本次材质函数的命名与第一篇略有调整,下文会进行具体说明。)
为了兼顾性能与视觉层次感,本次演示我们设置了8 个不同形态的波浪进行叠加,新建函数MF_GerstnerWaves。
放大材质节点网络查看细节:
图中的节点模块主要分为以下几部分:
- 黄色框(全局海洋变量):负责控制整片海洋的宏观环境参数,例如全局时间(Time)和基础风向(Wind Direction)。
- 蓝色框(独立波浪参数):定义了每个独立波浪的属性。我们将每个波浪的核心参数打包为一个
Vector4 (RGBA)。在实际工程中,非常建议将这些数据统一收拢到**材质参数集(Material Parameter Collection)**中进行管理(图中 Input 节点左侧的两个自定义函数,正是从参数集中读取的数据)。
- 绿色框(波浪求和
MF_GerstnerWave_∑):这是一个核心的累加函数,专门负责将波浪数据进行数学求和。
当所有波浪的位移数据合并完毕后,我们需要计算海面的法线。这里使用了一个名为MF_GerstnerWave_∑Transform的转换函数来推导最终的法线(Normal)。
提示:这个节点在核心数学逻辑上,对应了上一篇文章中最后提到的
MF_GerstnerWave_∑,此处我们做了功能拆分与重命名,使其职责更加清晰,具体实现参考上期。
经过上述封装,我们最终得到了一个高度内聚的水体计算函数。在后续的主材质连线中,我们实际只需要提取图中标注(黄框)的两个核心输出:顶点偏移(Position)与法线(Normal)即可。
技巧1: 波数(NumWaves)的自动统计与动态适配
细心的读者可能已经发现,在之前的节点图中,波浪函数向外输出了一根名为Num的引脚,并且它也参与了最终的求和。
这一步的玄机在于:我们在每一个独立的波浪函数MF_GerstnerWave内部,都硬编码输出了一个常量1。
通过这种方式,当所有的波浪节点经过∑函数累加后,Num通道自然就统计出了当前参与计算的波浪总数(NumWaves)。
随后,我们将这个求和得到的总数,反向连接回初始的输入端,喂给每个波,确保系统在计算每个独立波浪时,都能提前获知当前海面的“波浪总数”。
为什么要这么做?还记得我们在第一篇文章中提到的关于陡峭度计算的公式吗?
在实际应用中,我们可以提取一个总体的“陡峭度(Steepness)”参数Q QQ交给美术人员控制(取值范围0 00到1 11),并在内部使用公式Q i = Q w i A i × numWaves Q_i = \frac{Q}{w_i A_i \times \text{numWaves}}Qi=wiAi×numWavesQ来计算每个波的陡度。这样就能平滑地从完全平静的水面过渡到我们能生成的最大尖峰波浪。
利用这个自动统计的技巧,我们就彻底告别了手动输入波数。
未来无论是增加到 16 个波,还是减少到 4 个波,陡峭度公式都能自动适配计算。尤其是大后期,你需要使用LOD根据远近优化不同lod层级材质的波数,这是必要的。
海面材质设置
完成上述逻辑后,我们就得到了一个完整的海面计算函数。由于我们在函数的 Input 节点中设置了默认值,所以在主材质中直接调用时,甚至不需要连接任何外部参数就能直接看到波浪效果。
至此,我们的波浪网格体已经具备了动态起伏的能力。接下来是着色(Shading)部分。在这里,我使用了 UE5 的Substrate 材质系统。它的底层逻辑与旧版的单层水(Single Layer Water)一致,但默认参数的表现对美术极其友好。
关于SingleLayerWater新旧材质(Legacy 与 Substrate)的参数换算
顺带提一句,以防你还不知道:从 UE 5.7 开始,新旧材质系统(Legacy 与 Substrate)已经可以无痛并存了,不再需要转换整个项目。因此,本教程将优先按照更先进的 Substrate 新材质进行演示。
如果你坚持(或受限于项目)使用传统材质系统,你需要将主节点的【着色模型(Shading Model)】更改为【单层水(Single Layer Water)】,然后在图表中搜索并添加SingleLayerWaterMaterialOutput节点。默认连线和效果大致如下:
新旧版本核心的区别在于底层参数的逻辑:旧版使用的是纯粹的物理学参数Scattering(散射)和Absorption(吸收);而新版 Substrate 则将其重构为了WaterAlbedo(水体反照率)和WaterExtinction(消光系数)。
参数换算公式
如果你需要在新旧材质之间迁移资产,或者查阅了海洋学论文想套用真实的物理数据,可以使用以下公式进行换算:
从 旧版 (Legacy) 转换到 新版 Substrate:
WaterExtinction(消光)=Absorption+ScatteringWaterAlbedo(反照率)=Scattering/WaterExtinction
(注:反照率的物理意义是——在所有损失的光线中,有多大比例是因为“散射”造成的。因此它的值永远被安全地限制在 0 到 1 之间。)
从 新版 Substrate 转换回 旧版物理模型:
Scattering(散射)=WaterAlbedo*WaterExtinctionAbsorption(吸收)=WaterExtinction* (1.0 -WaterAlbedo)
为什么说 Substrate Single Layer Water 更好用?
目前网上关于 Substrate 单层水的深入说明还比较少,这里多啰嗦几句它的核心优势。
在以前的旧版材质中,如果你想调出一个“清澈的蓝水”,你必须去盲猜Absorption和Scattering的 RGB 值。这两个物理值是没有上限的,且互相牵制,导致美术极难精准调出想要的颜色。
Substrate 版的实现了美术友好化
- 颜色与浑浊度完美解耦:你现在可以独立控制“水的颜色”和“水的清澈度”,再也不用把颜色和浑浊度搅合在系数里互相折磨了。
- 直观的
WaterAlbedo:因为它的范围被严格限制在0 到 1,你完全可以把它当成“水的固有色 (BaseColor)”来用,甚至可以直接拿取色器去选你想要的海洋颜色 - 明确的
WaterExtinction:专门用来控制“水的能见度/浑浊度”。值越大,水越快变得不透明;值越小,水越清澈见底。 - TopMaterialOpacity:为遵循Substrate设计材质层准则。新增了
TopMaterialOpacity,等价于旧版主节点上的Opacity。
如果你只是做普通的水面,给个很小的值(比如0.01)来模拟极薄的表面反射即可;
如果你在水面上生成了白色的浪花(Foam),就把浪花的 Mask 连给TopMaterialOpacity,同时把浪花的颜色连给BaseColor。
同样是基于物理参数,Substrate 在经过重构后,开箱即用的体验简直完美。相比于传统单层水节点那浑浊的默认参数,Substrate 自带的默认值表现得极其通透,不用连任何参数,直接就是一杯清澈的纯净水!
铺垫了这么多理论,现在我们终于要正式动手连线了!(没错,现在才正式开始 😂)
将刚才写好的波浪函数接入新建的 Substrate 材质后,初步的起伏效果大概是这样的。
注:图中我刻意将波形参数调得比较宽,主要是因为演示用的测试面片(Plane)顶点精度有限,较宽的波形能避免低多边形带来的网格穿帮。
首先第一步,我们需要在材质节点的细节面板中,对两项关键属性进行修改:
- 关闭切线空间法线(Tangent Space Normal = False):必须取消勾选。因为我们在前文
MF_GerstnerWave_∑Transform函数中推导出的法线,本身就是基于世界空间(World Space)计算的绝对方向。 - 开启双面显示(Two Sided = True):勾选此项。因为在本次的海洋系统中,我们允许玩家的相机潜入水下,所以水面网格体必须是双面可见的。
既然开启了双面显示,我们就必须处理背面(水下视角)的法线朝向问题。水面上的凸起就是水面下的凹陷,所以水下表面法的线需要反转。
这里非常简单,只需将计算出的 World Normal 乘以一个TwoSidedSign节点即可。这样当相机进入水下观察时,能确保水下观察时的光照和反射计算依然正确。
单层水材质
在之前关于 Substrate Single Layer Water BSDF 的简介简单提及过。但为了构建出物理正确的真实海洋,有必要简单剖析一下单层水。
材质需要的参数
让我们先来看看单层水材质面板里的参数:
- WaterAlbedo(水体散射颜色)
- 直观来说,这就是你从水面上观察到的“水体基本色”。
- 它的物理含义是:光线穿透水面进入水体后,被水中的悬浮粒子散射,最终重新返回水面并进入你眼睛的颜色。
- 比如清澈的热带浅海偏青绿色
(0.04, 0.08, 0.06);浑浊的河水偏黄绿色(0.08, 0.06, 0.02)。
- WaterExtinction(水体消光/衰减系数)
- 决定了光线在水体中每前进一定距离后,被吸收和散射掉的总量。
- 值越大,光线的穿透距离越短,水看起来越浑浊;值越小,光线穿透越深,水越清澈。
- 这是一个 RGB 向量,因为水体对不同波长(颜色)的光吸收程度是不同的(例如红光在水中衰减得比蓝光快得多)。
- WaterPhaseG(散射相位函数)
- 用于控制光线在水体内部发生散射时的方向性偏好。取值范围为
-1到1。 0= 各向同性散射(光线均匀地向所有方向散射)。正值= 前向散射为主(光线倾向于继续沿着原方向前进)。负值= 后向散射为主(光线倾向于原路反射回来)。- 真实海水通常含有大量微粒,表现为极强的前向散射,值通常在
0.6 ~ 0.9之间。
- 用于控制光线在水体内部发生散射时的方向性偏好。取值范围为
- ColorScaleBehindWater(水下物体颜色缩放)
- 这是一个非物理的艺术化控制参数,相当于透过水面观察水下物体时的颜色乘数(Multiplier)。
- 通常设为
(1, 1, 1)即可。但它可以用来取巧:比如你想做水下散焦(Caustics)造成的色调变化,可以直接在这里叠加颜色,从而省去写复杂灯光函数的性能开销。
嗯,听上去就像一个多了一个水面界面的体积雾?
没错!因为水在本质上就是一种参与介质,也就是体积。要计算真实的水体表现,它底层真正需要的其实是体积渲染的要素:
- 吸收 (Absorption):纯水本身对不同波长光的吸收率。红光最容易被吸收,蓝光最难。这是纯水的固有物理属性,与浑浊度无关。这就是为什么深海即使纯净无暇,也呈现深邃的蓝色。
- 散射 (Scattering):水中悬浮粒子(泥沙、浮游生物、气泡)对光线的阻挡与散射。这才是决定“浑浊度”的真凶。粒子越多,散射越强。
- 散射相位 (Phase Function):粒子的大小决定了散射的方向。大颗粒(如泥沙)容易产生前向散射,小颗粒(如水分子)则产生均匀的瑞利散射。
- 色素来源 (Pigmentation):着色,水中杂质自带的吸光特性。如浮游植物的叶绿素吸收红蓝光反射绿光;溶解的有机物吸收蓝光让水泛黄。
以海洋光学为基础制作着色器模型
了解了参数,那么具体的数值该怎么填?
Shader显然不能纯靠主观臆想去写。让我们跨界到海洋学与计算机图形学的交叉领域,找寻一些坚实的理论支撑。
在 《Analysis of variations in ocean color》(Morel & Prieur, 1977) 这篇经典论文中,建立了海水光学性质(吸收、后向散射)与海色变化之间的定量联系。论文指出:蓝色海水的反射率由纯水吸收主导,而绿色海水的反射率则受叶绿素与颗粒物影响。
以“生物”和“悬浮物”这种表象物质作为切入点非常聪明,它可以让我们直接跳过那些隐性且难以量化的化学成分(比如氯离子含量)对海洋的影响。
不可避免搜到了它 Light and Water: Radiative Transfer in Natural Waters (Curtis D. Mobley, 1994)
这本书在图形学水体渲染领域被大量引用。但作为一整本书,它太长了。
我到一个网站可以通过AI来对书问答。借助 AI 文献阅读工具提炼后,我们会发现难怪被引用如此之多,这套理论几乎就是 UE 单层水模型底层的算法来源。
值得注意的是,书中除了“叶绿素”和“悬浮颗粒”外,还重点引入了一个概念:CDOM。
CDOM(有色溶解有机物),也被称为“黄色物质”。它主要吸收 350–500 纳米范围内的紫外线和短波蓝光。与悬浮颗粒不同,它是完全溶解在水里的,没有实体颗粒,因此理论上它只吸收光,完全不散射光。
有了上述理论模型,我们的 Shader 逻辑就非常清晰了:以纯水为基底,叠加叶绿素、泥沙和 CDOM。水体的最终视觉效果,就是这四个成分对光线“吸收”和“散射”的线性叠加。
1. 纯水
根据论文,纯水是所有水体的“基底”。即使没有任何杂质,水本身也会和光发生物理反应。
初中我们学过,水分子H 2 O H_2OH2O是一个氧原子和两个氢原子组成的V 型结构,像一个打击乐器。不同颜色的光,光子携带的能量不同。根据量子力学,它只吸收特定能量的光子,水的分子结构偏爱和红光这个范围共振,就把光能转化成了分子振动动能,转化为热量。
- 吸收:光线在纯水中前进几米,红光就会被吸收殆尽;绿光能穿透几十米;而蓝光能走得最远。
- 散射:纯水的散射非常微弱(类似于大气中的瑞利散射),且主要散射蓝光。
那么总的来说,随着深度的增加,水体会呈现深邃的蓝色,直至光线耗尽变成纯黑,类似下图中左侧部分。
2. 叶绿素/浮游植物
这就不需要过多解释了,这是水中以藻类为主的微小植物,其光学特性完全由光合作用决定。
- 吸收:为了进行光合作用,叶绿素会狠吃蓝光和红光,把吃剩下的绿光反射出去。
- 散射:藻类细胞是有体积的颗粒,所以会产生中等程度的散射。
那么根据理论模型,当纯水里加入叶绿素,原本的“纯蓝色”会被吃掉,绿光被保留并弹射进人的眼睛。
随着浓度增加(如富营养化导致的赤潮),水面会变成浓稠的“绿豆汤”,过渡类似上图中右侧部分。
3. 悬浮泥沙
被海浪卷起的沙子、泥土等微小颗粒。它们多为无机矿物质(如黏土、硅酸盐),并含有大量铁氧化物。就像铁锈是红色的一样,其分子结构会吸收高能量的短波光(蓝光),而将低能量的长波光(红/黄光)反射出去。
- 吸收:泥沙对各波段光的吸收相对均匀,但略微偏向吸收蓝光,因此自带黄褐色调。
- 散射:由于泥沙基本是不透明的固体颗粒,光线撞击后会发生极其强烈的米氏散射。
泥沙是决定水体透明度/浑浊度的绝对主力,极大地缩短光线的穿透距离(极大增加消光系数 WaterExtinction)。泥沙量会让水变得不透明、呈乳白色或黄褐色,光线无法穿透,水下能见度极低(比如渤海、黄河口,和暴雨后的湖泊)。
4. CDOM有色溶解有机物
动植物腐烂分解后溶解在水中的有机大分子(俗称“黄物质”)。这些充满了“碳环”和双键的有机大分子,极度偏爱吸收高能量的光子。生活中你可以常见到腐烂的植物,枯黄的树叶。最常见的例子就是泡红褐色的茶水,本质上就是茶叶向水中释放了高浓度的CDOM。
- 吸收:CDOM极其强烈地吸收蓝光和紫外线。它的吸收曲线呈指数级,波长越短吸收越狠。
- 散射:零散射。因为它完全溶解于水,没有实体颗粒,所以完全不散射光。
CDOM 就像给水加了一层茶色滤镜。它只会让水变暗、变黄,但不会让水变浑浊(除非伴随泥沙)。富含 CDOM 的水体会呈现琥珀色、红茶色或深褐色(例如热带雨林深处的红树林水系或泥炭沼泽),水面看起来像红茶,但凑近看其实依然是清澈透明的。
算法实现
理论很多,实际制作就比较简单了,首要原因是UE的单层水,已经实现了绝大部分算法。而所需我们填写的参数,基本依靠对真实水体的物理测量值。
等一下…接近一万字了,已经编辑不动了,浏览器太卡感觉随时会崩。
直接新开第三篇!