外观
Water
约 3117 字大约 10 分钟
2025-05-16
一篇围绕 UE5
SingleLayerWater着色模型展开的笔记:先从光学的物理基础讲起,再看 UE 如何用一个高度简化的模型去近似这些现象,最后梳理桌面端渲染管线,并记录一次把它移植到移动端的实践。
一、光学的理论基础
光的波粒二象性
光既能表现出干涉、衍射等波动现象,也能看作携带量子能量的光子。在经典意义上,光是电磁波,其频率 ν 和波长 λ 满足 c=λν;从粒子角度看,单个光子的能量 E=hν。
- 波动特性:可见光是电磁波的一部分,波长不同表现为不同颜色。
- 粒子特性:光以光子的形式携带能量,可以参与粒子碰撞。
电磁波的行为由麦克斯韦方程组(微分形式)描述:
∇⋅D∇⋅B∇×E∇×H=ρ=0=−∂t∂B=J+∂t∂D
其中 D 为电感强度、E 为电场强度、B 为磁感强度、H 为磁场强度,ρ 是电荷体密度,J 是传导电流密度。后面讨论反射、折射、菲涅尔的边界条件,都从这组方程出发。
光与表面的相互作用
当光线到达两种介质的分界面时,能量被分成两部分:一部分反射回原介质,一部分折射进入新介质,而两者的比例由菲涅尔效应决定。
反射

光线射到表面时,会有一部分按入射角对称地反射回去(反射定律):入射角 θi 等于反射角 θr。这一法则适用于任意平面界面——无论是光滑表面的镜面反射,还是粗糙表面服从朗伯余弦定律的漫反射,"入射角等于反射角" 都是其几何基础。
折射

一部分光线穿过界面进入另一种介质时方向会改变,遵循斯涅尔定律(Snell's Law):
n1sinθ1=n2sinθ2
空气中 n≈1,水中 n≈1.33。折射使水下物体的位置看起来扭曲(鱼缸里的筷子像"折断"了)。当从高折射率介质向低折射率介质观察、入射角超过临界角时,会发生全反射。
菲涅尔效应
入射光的电矢量 E1 可以分解为垂直于入射面的分量(s 波)和平行于入射面的分量(p 波),分别讨论。依据麦克斯韦边界条件,两种偏振态的反射振幅比为:
rs=n1cosθi+n2cosθtn1cosθi−n2cosθt,rp=n2cosθi+n1cosθtn2cosθi−n1cosθt
在垂直入射时反射率退化为:
R=(n1+n2n1−n2)2
精确的菲涅尔计算开销很高,图形学中一般使用 Schlick 近似:
F(θ)=F0+(1−F0)(1−cosθ)5
其中 F0 是垂直入射时的反射率,cosθ 由视线方向与法线点乘得到。这也是水面边缘"越掠射越像镜子"的来源。
光在介质中传播的现象
进入介质之后,光在传播过程中还会经历吸收、散射与色散,这三者共同决定了水体最终呈现的颜色与通透感。
吸收
光在介质内传播时,介质中的束缚电子在光波电场作用下做受迫振动,因此光波要消耗能量来激发电子振动。其中一部分能量又以次波形式与入射波叠加成透射光射出介质;另一部分由于与周围原子分子相互作用,转化为分子热运动等其他形式的能量——这部分损耗就是介质对光的吸收。
在水中,长波长(红、橙)光的吸收系数远大于短波长(蓝、绿),因此水域整体呈现浅蓝色调。海水对红光吸收极其剧烈,深水中红色光几乎消失,所以红色的动物在深海里看起来反而是灰黑色的。
散射

光在均匀介质中有确定的传播方向。但如果介质里存在微小粒子、不均匀结构,或介质本身的密度涨落,传播方向就会随机改变。散射强度与溶液浓度(浑浊度)有关,最直观的反映就是液体的透明度。纯净液体同样有散射,这来自分子热运动导致的密度涨落,称为分子散射。本质上,散射光是电偶极子辐射的次波不能完全抵消的结果。


上图是一个简单的丁达尔效应实验:向清水中逐渐加入少量乳浊液,光束穿过时被散射得越来越明显,液体也随之变得乳白、不透明。

当粒子尺寸与波长相当时发生米氏散射(Mie Scattering):介质对各波长散射几乎均匀,可产生雾霾、乳白色光辉(如云雾)。水体中的细小悬浮物也会产生类似散射,使远处物体轮廓变得模糊。散射的方向分布由相位函数描述,常用 Henyey-Greenstein 模型:
PHG(θ)=4π1(1+g2−2gcosθ)3/21−g2
其中各向异性参数 g 控制前向/后向散射的偏好。对于水体或雾,g 一般取 0.7∼0.9,表示强烈的前向散射。
色散
介质的折射率随波长变化,称为色散。下图是氢在可见光区的色散曲线——在每条吸收线附近折射率会出现反常变化:

正因为折射率与波长相关,白光经过棱镜会被分解成连续光谱:

水面波纹可以看作一系列不断变化曲率的小透镜或棱镜,阳光入射后在每个波峰、波谷处发生折射与聚焦:
- 单一平坦界面:白光 → 彩虹色谱;
- 水波曲面的多重折射 + 动态聚焦 → 彩虹光斑;
- 池底不再是连续的光谱带,而是由红到蓝的微小色环、色斑组成的动态图案——这就是焦散(Caustics)。
色散程度决定色斑的宽度与色彩分离度;水波曲率和波面粗糙度则决定斑点的形状与对比度。
二、简化的着色模型——以 UE5 为例
UE 的 SingleLayerWater 着色是一个相当简化的模型:它没有体积计算和预积分步骤,而是依赖场景深度、水面高度、光照方向与视角方向来近似水体表现,更多依靠材质里的计算与渲染管线配合来模拟水面。
它向材质暴露的主要输入有:散射系数、吸收系数、相位函数、水下色度、不透明度。
散射系数
散射系数会影响最终的散射光强度与主光源的散射强度。核心计算(引擎源码)大致是:
const float3 ExtinctionCoeff = ScatteringCoeff + AbsorptionCoeff;
const float3 ExtinctionCoeffSafe = max(ExtinctionCoeff, 1e-5);
float3 SafeScatteringAmount = saturate(ScatteringCoeff * (1.0f - Transmittance) / ExtinctionCoeffSafe);
float3 ScatteredLuminance = IncomingLuminance * SafeScatteringAmount;可以看到,散射系数与吸收系数之和构成消光系数(Extinction),再用它去归一化散射比例。
吸收系数
在 UE 默认水材质中,吸收系数是一个"倒数"概念:值越大,颜色消失得越快。光穿过体积的距离用米的倒数表示,即 1/吸收距离。吸收系数本身只会与散射系数加起来,作为一个联合的消光因子去影响最终亮度。
相位函数(PhaseG)
PhaseG 控制光在水中散射的整体方向性,也就是散射光相对太阳方向的各向异性:
- g>0:增加向太阳方向(前向)散射的光量;
- g<0:增加背向太阳(后向)散射的光量。
根据当前视角和太阳在天空中的位置,这会让水看起来更亮或更暗。UE5 使用 Schlick 相位函数来近似 Henyey-Greenstein:
PSchlick(θ)=4π(1+kcosθ)21−k2,k≈1.55g−0.55g3
对应的引擎代码:
const float PhaseG = clamp(DFDemote(GetSingleLayerWaterMaterialOutput2(MaterialParameters).x), -1.0f, 1.0f);
DirLightPhaseValue = SchlickPhase(PhaseG, dot(-ResolvedView.DirectionalLightDirection.xyz, UnderWaterRayDir));
float3 SunScattLuminance = DirLightPhaseValue * SunIlluminance;下图直观对比了两种相位函数的散射叶瓣(上:Henyey-Greenstein,下:Schlick)以及它们的函数曲线——在常用区间内 Schlick 与 HG 几乎重合,却便宜得多:


不同 PhaseG 取值下,同一套水体的整体明暗与通透感差异:




水下色度
"水下色度" 输入会增加水下表面的亮度。这类效果可以用在材质里,去驱动焦散或阴影的明暗程度。
不透明度
用来控制光线传播的距离——某种意义上是个"心情参数",更多是凭观感去调,而不是严格的物理量。
水体折射
水体折射本质上是用 UV 扰动实现的,但需要同时采样深度来判断扰动后的采样点是否还在水面覆盖区域内——否则会把水面之外(更近)的像素也"吸"进折射里,产生穿帮。
三、水体的渲染管线
PC 管线概览
在桌面端,SingleLayerWater 的渲染穿插在 BasePass() 前后,大致分为两段:
SingleLayerWaterDepthPrepass
DepthBufferCopy:复制水体绘制前的场景深度;ParallelDraw:绘制水体,写入深度和模板;ClearBuffer:清理 Buffer,准备下一步;ClassifyTiles:识别出水体区域,用于后续的光追反射操作;- …
SingleLayerWater
CopySceneWithoutWater:复制水体绘制前的场景颜色;SingleLayerWaterParallel:绘制水体颜色;Reflection(Lumen 或 SSR):绘制水面反射;Composite:把水面反射纹理合成到水面上。
几个关键 Pass 的职责:
| Pass | 作用 |
|---|---|
SceneDepthCopy | 保存水面绘制前的深度,供折射时做覆盖判断 |
ParallelDrawDepth | 并行绘制水面,写入深度 / 模板 |
CopySceneWithoutWater | 保存"无水"的场景颜色,作为折射与散射吸收的输入 |
SingleLayerWaterParallel | 真正绘制水体颜色 |
Composite | 把反射结果合成回水面 |
整条管线里反复出现的模式是:先把水面之下/之前的深度与颜色拷一份,再在绘制水面时把它们当作"背景"去做折射、吸收、散射——这也是为什么折射要靠深度判断覆盖区域。
四、移动端管线移植
移动端有带宽、计算能力等限制,不会完全照搬 PC 管线。水体渲染也要尽可能一次计算完成,避免多次拷贝整屏的 RT 带来的带宽开销。改动主要落在以下几处:
MobileBasePassRendering.h / .cpp
在 FMobileBasePassUniformParameters 中增加水体渲染所需的参数,并增加独立的 Setup 函数:
CreateMobileSingleLayerWaterPassUniformBuffer:创建所需的 UniformBuffer;SetupMobileSingleLayerWaterPassUniformParameters:填充 Buffer 的参数。
RenderMobileSingleLayerWater()
在 MobileSceneRenderer 中增加一个 external 函数 RenderMobileSingleLayerWater(),包含两个功能:
AddCopySceneWithoutWaterPassMobile():拷贝水体渲染之前的 SceneDepth 和 SceneColor,用于折射、散射吸收等水体表现;RenderMobileSingleLayerWaterInner():实际的水体渲染函数,主要构造 Pass 参数并提交渲染命令。
SceneVisibility
在 TranslucencyPass 中排除使用 SingleLayerWater 的 Mesh,并单独增加 Water 的渲染 Pass——避免它被当作普通半透明物体走透明管线。
表现对比
最终在桌面端原始效果(PC)、移植后的移动端效果(Mobile New)与移植前的旧移动端效果(Mobile)之间做对比:在可接受的带宽与算力预算内,移动端新管线已经能还原出折射、散射吸收、相位散射等大部分水体特征。
