← ALL ARTICLES
VFX与模拟10 MIN READ

水面与海洋渲染

全库目录221
本文目录18

水面渲染由三个相对独立的子问题组成:网格如何组织、波形如何生成、表面如何着色。三者可以自由搭配——同一套波形可以跑在不同的网格方案上,也可以配不同的着色模型。分开理解比整体记忆某个方案更有用。

水面网格

海面理论上无限大,而顶点预算有限。核心要求是近处密、远处疏,且随相机移动时密度分布保持稳定。

投影网格

把一个规则网格铺在屏幕空间,再投影到水平面上求交点。

它的特性由构造方式直接决定:顶点在屏幕上均匀分布,因此投影到世界空间后天然近密远疏,且顶点总数恒定不随视角变化——预算完全可控。

代价是顶点的世界位置每帧都在变。依赖固定世界采样点的逐顶点缓存需要重投影或重建,所有信息必须能够从世界位置实时求出。相机接近水平时,投影范围趋于无穷,需要额外的边界处理。

分层网格

以相机为中心铺设若干层同心网格,每向外一层网格间距翻倍。相机移动时整体跟随,并按网格间距对齐吸附。

网格吸附稳定的是采样点:连续跟随相机时,顶点在波形上的采样位置不断变化,容易产生采样闪动。波形相位始终用世界坐标求值,即使网格移动,波本身也应固定在世界中。按间距取整吸附可以进一步稳定离散采样。

层与层之间的接缝需要处理。相邻层顶点密度差一倍,边界上稀疏侧的顶点必须与密集侧对齐,否则出现裂缝。常见做法是在过渡带插入专门的接缝三角形,或让密集侧的边界顶点插值到稀疏侧的位置。

四叉树细分

按相机距离递归细分平面,与地形 LOD 的思路一致。灵活性最高,可以按海岸线、重要区域局部加密,代价是需要维护树结构与相邻节点的层级差约束。

硬件曲面细分

提交一张粗网格,由细分阶段按屏幕投影尺寸动态加密。实现简单,但相邻补丁的边细分因子必须一致,否则出现裂缝;且细分因子需用小数模式避免密度跳变。

这些方案可以组合:用分层网格确定粗略结构,再用细分阶段做最后一级加密。

波形生成

Gerstner 波

Gerstner 波在正弦波的基础上增加了水平位移,让波峰变尖、波谷变平,比纯正弦更接近真实水面:

ϕi=kiDi⋅P0,xz−ωit+ϕ0,i,P=P0+∑i(QiAiDi,xcos⁡ϕiAisin⁡ϕiQiAiDi,zcos⁡ϕi)\phi_i=k_i\mathbf D_i\cdot\mathbf P_{0,xz}-\omega_i t+\phi_{0,i},\qquad \mathbf P=\mathbf P_0+\sum_i\begin{pmatrix} Q_i A_i D_{i,x}\cos\phi_i\\ A_i\sin\phi_i\\ Q_i A_i D_{i,z}\cos\phi_i \end{pmatrix}

Di\mathbf{D}_i 是 XZ 平面上的单位传播方向,AiA_i 是长度单位的振幅,ki=2π/λik_i=2\pi/\lambda_i 是波数,ωi\omega_i 是弧度每秒的角速度。ϕ0,i\phi_{0,i} 是初相位,QiQ_i 控制水平位移比例。QQ 过大时顶点会互相穿越,水面自交出现翻折。

法线可以解析求导得到,不必依赖差分——这是 Gerstner 相对高度图方案的一个优势。

叠加数个波即可得到可用效果,成本低,适合移动端与中小水体。

顶点评估

输入是平面网格的顶点位置与一组波参数,输出是偏移后的位置与解析法线。下面计算一个波。多波合成时,位置从原始平面位置开始,每个波只累加位移;两条导数分别从 (1,0,0)、(0,0,1) 开始,每个波只累加相对于这些单位基底的导数增量。若直接相加下方完整导数,会把单位项重复加入,法线随波数错误变平:

hlsl
// 每个波携带:方向 dir(单位向量)、波数 k、振幅 A、角速度 w、陡峭度 Q
float3 pos = basePos;
float phase = k * dot(dir, basePos.xz) - w * time;
float s = sin(phase), c = cos(phase);

pos.xz += dir * (Q * A * c);   // 水平位移,Q 控制波峰尖锐度
pos.y  += A * s;               // 竖直位移

// 解析法线:对 Gerstner 位移求偏导,不必用差分
float wa = k * A;
float3 tangent  = float3(1 - Q * wa * s * dir.x * dir.x,
                         wa * c * dir.x,
                        -Q * wa * s * dir.x * dir.y);
float3 binormal = float3(-Q * wa * s * dir.x * dir.y,
                         wa * c * dir.y,
                         1 - Q * wa * s * dir.y * dir.y);
float3 normalWS = normalize(cross(binormal, tangent));

验证方法是把 QQ 调大:波峰应变尖、波谷变宽;QQ 过大导致顶点互相穿越时,能直观看到水面翻折。法线部分可把 AA 设为 0 验证,此时法线应全部竖直向上。

FFT 海面

FFT 海面把大量规则波叠加到一张周期高度场中。输入是边长 LL 米、每边 NN 个采样点的水平正方形;采样间距为 L/NL/N,波数间距为 2π/L2\pi/L。整数索引 (n,m)(n,m) 对应 k=2π(n,m)/L\mathbf k=2\pi(n,m)/L,空间采样点是 (jL/N,lL/N)(jL/N,lL/N)。增加 LL 能容纳更长的波,固定 NN 时也会降低可表示的最高频率;增加 NN 才能在相同范围内保留更短的波。

波谱与随机种子

Phillips 谱提供各波数的相对强度。令 k=∥k∥k=\|\mathbf k\|、单位风向为 w^\hat{\mathbf w}、风速为 VV,取 Lw=V2/gL_w=V^2/g:

P(k)=Aexp⁡[−1/(kLw)2]k4(k^⋅w^)2exp⁡(−k2ℓ2),h0(k)=P(k)/2(ξr+iξi).P(\mathbf k)=A\frac{\exp[-1/(kL_w)^2]}{k^4} (\hat{\mathbf k}\cdot\hat{\mathbf w})^2\exp(-k^2\ell^2),\qquad h_0(\mathbf k)=\sqrt{P(\mathbf k)/2}(\xi_r+i\xi_i).

ξr,ξi\xi_r,\xi_i 是独立标准正态数,初始化后固定;ℓ\ell 抑制短于所需尺度的细波。k=0k=0 的种子直接置零,避免除零并固定平均水位。这里把 PP 定义为离散种子的方差,AA 的单位和数值随这一约定标定;若输入是连续波谱密度,还须乘波数单元面积再转换为离散方差。改变网格尺寸后直接沿用未经标定的连续谱数值,会改变总波高。

风向垂直的波在该谱下权重为零。平方点积同时允许顺风和逆风分量,它本身没有把能量全部限制到顺风半平面。每帧重新抽随机数会让波形跳动;稳定动画来自固定种子的相位演化。

时间演化与实数约束

深水重力波取 ω(k)=gk\omega(\mathbf k)=\sqrt{gk},每帧构造:

h^(k,t)=h0(k)eiωt+h0∗(−k)e−iωt,h(x,t)=∑kh^(k,t)eik⋅x.\hat h(\mathbf k,t)=h_0(\mathbf k)e^{i\omega t} +h_0^*(-\mathbf k)e^{-i\omega t},\qquad h(\mathbf x,t)=\sum_{\mathbf k}\hat h(\mathbf k,t)e^{i\mathbf k\cdot\mathbf x}.

负波数必须以取模索引读取,例如数组索引 (a,b)(a,b) 的配对位置是 ((−a) mod N,(−b) mod N)((-a)\bmod N,(-b)\bmod N)。两项共同保证 h^(−k)=h^(k)∗\hat h(-\mathbf k)=\hat h(\mathbf k)^*,于是空间结果为实数。只旋转独立随机复数而不构造配对,会产生明显的虚部;直接丢弃虚部会掩盖输入不满足约束的错误。

上述空间和没有归一化因子。若库的二维 IFFT 自带 1/N21/N^2,输出需乘 N2N^2;若使用已经按该归一化标定的频谱,则保持库的约定。频谱以数组零号存零频时,可直接使用标准 IFFT;以中心存零频时先做索引移位,避免空间结果出现交错符号。偶数网格的 Nyquist 行、列在离散负索引下映射到自身,奇数阶导数需要特殊处理;便于审查的实现将这些种子置零,使高度和导数场共享同一实数约束。

二维 IFFT 可以先变换每行,再变换每列。每条长度 N=2pN=2^p 的一维逆变换有 pp 级蝶形:长度 MM 的子问题把偶序列变换 EjE_j 和奇序列变换 OjO_j 合成 Ej+e2πij/MOjE_j+e^{2\pi ij/M}O_j 与 Ej−e2πij/MOjE_j-e^{2\pi ij/M}O_j。每级输出到另一张缓冲后交换读写,行阶段完成后列阶段读取其结果;GPU 实现须保证级间写入可见。高度、位移与导数字段可以打包变换,但打包和解包仍要遵守各字段的共轭关系。

位移、导数与法线

高度的两个斜率在频域直接乘 ikxik_x、ikzik_z 再逆变换。没有水平位移时,法线为 normalize⁡(−hx,1,−hz)\operatorname{normalize}(-h_x,1,-h_z)。在接近采样极限的短波处,中心差分的频率响应是 isin⁡(kΔx)/Δxi\sin(k\Delta x)/\Delta x,它会低估真实的 ikik,因此谱导数和低分辨率高度差分并不等价。

尖峰效果使用 D^=−ik^h^\hat{\mathbf D}=-i\hat{\mathbf k}\hat h,零频取零;逆变换得到 Dx,DzD_x,D_z,表面位置为 (x+λDx,h,z+λDz)(x+\lambda D_x,h,z+\lambda D_z)。每个 DD 的空间偏导同样在频域乘对应 ikik。两条切向量因此为:

Px=(1+λDx,x,hx,λDz,x),Pz=(λDx,z,hz,1+λDz,z).\mathbf P_x=(1+\lambda D_{x,x},h_x,\lambda D_{z,x}),\qquad \mathbf P_z=(\lambda D_{x,z},h_z,1+\lambda D_{z,z}).

法线取 normalize⁡(Pz×Px)\operatorname{normalize}(\mathbf P_z\times\mathbf P_x)。只用高度斜率求法线,会遗漏水平压缩后的表面朝向。水平雅可比为 (1+λDx,x)(1+λDz,z)−λ2Dx,zDz,x(1+\lambda D_{x,x})(1+\lambda D_{z,z})-\lambda^2D_{x,z}D_{z,x},接近零说明局部压缩,变为负值说明翻折。

取 L=4L=4 米,只保留 kx=±π/2k_x=\pm\pi/2 两项且振幅各为0.5米,某帧四个采样高度是 [1,0,−1,0][1,0,-1,0] 米,谱导数是 [0,−π/2,0,π/2][0,-\pi/2,0,\pi/2]。水平位移为 sin⁡(πx/2)\sin(\pi x/2) 米;取 λ=−0.2\lambda=-0.2,波峰处雅可比为 1−0.2π/2≈0.6861-0.2\pi/2\approx0.686,取 λ=−1\lambda=-1 则为 −0.571-0.571,表面翻折。这个例子同时检验归一化、导数符号和尖峰方向。

FFT 生成的是给定统计谱的周期表面。浅水色散可用 ω2=gktanh⁡(kd)\omega^2=gk\tanh(kd) 表示恒定水深 dd 的传播;变化岸线、船体尾流和破碎飞沫需要额外动力学。贴图周期性则由有限波数网格直接决定。

平铺重复

无论是少量 Gerstner 波还是 FFT 生成的贴图,远景俯视时都会暴露明显的重复图案。人眼对规则重复极其敏感,这是水面观感的主要破绽之一。

几种应对,通常叠加使用:

多尺度叠加。用两到三套不同周期、不同旋转角度的波形叠加。不同尺度与角度能让重复特征更难辨认。只有周期能写成同一基础单位的整数倍、方向也兼容时,才能按最小公倍数讨论精确重复周期;旋转后的二维图案通常不能用这条一维结论概括。

波数与方向的选取。Gerstner 方案中,避免各波的频率成简单整数比、方向过于集中。方向分布在一个主风向附近的扇形内比完全随机更真实,也更不容易形成规则干涉图案。

低频扰动。用一张大尺度的低频噪声调制波的振幅或 UV,打破规律性。成本低,效果明显。

距离衰减。远处本就看不清细节,随距离降低高频波的振幅,同时提高法线贴图的 Mip级别。这既减轻了重复感,也缓解了远处法线的高频闪烁。

最后一点值得单独强调:远处水面的法线闪烁是普遍问题。大量亚像素级的高频法线在时域上剧烈跳变,即使有时域抗锯齿也难以稳定。让远处的法线收敛到平均朝向(接近水平)是必要的处理。

表面着色

反射与折射

水面的反射率随视角变化剧烈:俯视时主要看到水下,掠射时接近镜面。这由 Fresnel 项描述,是水面观感最重要的单一因素。

环境贴图查询成本较低,但内容更新受捕获策略限制。平面反射增加一个镜像视图,成本取决于分辨率、剔除和所启用的通道;屏幕空间反射复用已有画面,却缺少屏幕外和遮挡后的内容。三者没有固定的成本排序。实践中常以屏幕空间反射为主,失败处回退到环境贴图。

折射通常用扰动后的 UV 采样当前场景颜色实现。两个必须处理的细节:采样位置要按水下深度与折射强度偏移,且必须防止采样到水面之前的物体——否则岸边会出现水下颜色渗到岸上的错误。做法是比较采样点的深度,若该处物体在水面之上则退回未扰动的 UV。

水体颜色与透光

水色由选择性吸收、体散射和入射环境共同决定。清水在可见光范围常对长波红光吸收较强,传播越远,底色的光谱变化越明显;悬浮物与藻类还会改变吸收和散射,水体颜色不能仅由深度决定。

简化模型是按水面到水底的距离做指数衰减,对 RGB 使用不同的衰减系数:

C=Cbottom⋅e−σd+Cwater(1−e−σd)C = C_{bottom}\cdot e^{-\boldsymbol{\sigma} d} + C_{water}\left(1-e^{-\boldsymbol{\sigma} d}\right)

dd 是光在水中的路径长度;屏幕实现常用同一线性观察空间的水面与水底深度差近似,但斜视和折射时它不等于真实路径,σ\boldsymbol{\sigma} 是三个通道各自的衰减系数。这比直接用透明度混合更符合观察——浅滩自然透出底色,较深时收敛到本模型的等效散射颜色。这里的Cwater表示均匀源项下的沿途散射近似;只有吸收、没有入射散射源时应取零,吸收本身不会生成非零的水色。

泡沫

泡沫出现在两类位置,判据不同。

波峰泡沫。判据不是高度,而是表面的形变程度。Gerstner 波中可以用雅可比行列式判断水平压缩程度,压缩剧烈处即波峰破碎位置;更简化的做法是用法线的偏导数或曲率,变化剧烈处生成泡沫。

用高度做判据是常见的错误,它会让所有波峰不分强弱地一律泛白,看起来像一层白纱而非破碎的浪花。

岸边泡沫。由水面到岸边地形的距离决定,通常从深度缓冲比较得到。距离越近泡沫越强,并叠加一张随时间流动的噪声表现拍岸的动态。

两类泡沫可以共用一张泡沫纹理,但强度遮罩的来源完全不同。

对于单个波,水平映射的雅可比行列式为 J=1−QAksin⁡ϕJ=1-QAk\sin\phi;QAk<1QAk<1 能保证这份单波水平映射不翻折。多波叠加时要对合成导数求行列式,不能逐波通过就宣称整体安全。泡沫可在较小的 J 区域增加,但这只是破碎倾向的艺术判据,不是空气卷入量的物理解。

岸边过渡

浅水区需要同时处理几件事:水体颜色变淡透出底色、泡沫增强、以及波高衰减——浅水波会发生折射、浅化和破碎,不能把深水波直接延伸到岸上。

波高衰减容易被忽略,缺失时会看到大浪直接穿过沙滩的穿帮。一种便于美术控制的近似是按水深衰减振幅,使岸线处回到静止水面。它用于避免穿岸,完整浅水动力学还要描述浅化和破碎。

验证方法

  • 相机贴近水面平移,检查波形是否钉在世界空间中,出现整体滑动时先检查波形是否误用相机局部坐标,再检查采样吸附。
  • 拉到高空俯视,观察是否出现规则重复图案。
  • 沿网格分层边界检查是否有裂缝或密度突变。
  • 在浅滩处检查水体颜色是否平滑过渡、波高是否随水深衰减。
  • 关闭折射扰动对比,检查岸边是否有水下颜色渗到岸上。
  • 远景静止观察法线闪烁,确认距离衰减生效。
  • 单独输出泡沫遮罩,确认波峰泡沫只出现在形变剧烈处而非所有波峰。

本机机制检查:30 组固定种子的 CPU 输入中,解析切向量与中心差分吻合;这只验证单波公式,没有执行水面着色器。检查代码见 reference.py,运行 python 知识库/examples/mechanisms/reference.py。引擎画面、GPU捕获与性能另外验证。FFT 的共轭演化、四点空间和与谱导数另由 独立参考模型 检查,运行 python 知识库/examples/mechanism_depth/water_atmosphere/test_reference.py;该模型直接求有限傅里叶和,尚未执行 GPU 蝶形变换。

相关主题

参考资料