GPU Gems 1 · Chapter 1. Effective Water Simulation from Physical Models
以物理有意义的波参数,在 GPU 上叠加几何波面与动态法线贴图,构成可控的实时水面系统。
学习目标
- 能用波长、振幅、速度、方向和相位写出可采样的水面高度函数,并解释每个参数的视觉意义
- 能从高度场的两个偏导数构造切线、双切线和法线,说明为什么几何波与法线贴图要分层
- 能区分正弦波、Gerstner 波、方向波与圆形波的适用场景,并用水深和网格边长过滤参数
- 能把模型落到 GPU 流水线:顶点阶段更新几何、像素阶段生成动态法线并完成反射合成
为什么水面需要两层模型
如果把所有细节都交给顶点几何,网格必须密到能够表达最短波,远处会浪费大量顶点;如果只保留法线贴图,水面轮廓和遮挡永远是平的。两层模型把低频形状交给几何,把高频方向交给纹理,正好对应 GPU 在顶点和像素阶段的不同采样能力。
目标:用少量参数让水面持续有生命
GPU Gems 第1章研究的是大到海湾、小到池塘的实时水面。系统把水面拆成两类互补信号:顶点阶段更新低频的几何起伏,像素阶段在 render target 中生成高频的动态法线贴图。两者都来自同一族周期函数,但服务于不同的采样尺度。
这不是严格求解 Navier–Stokes 方程的流体模拟,而是一个有物理依据、能在 GPU 上稳定运行的近似。正因为参数对应波长、振幅、速度和风向,艺术家可以脚本化地改变天气;正因为所有波都能独立推进,水面不会像平铺的一张静态贴图。
1. 用正弦波表示高度
先把水平面上的位置记为 X=(x,y),时间记为 t。一条方向波可以写成:
其中 A_i 是振幅,D_i 是水平传播方向,S_i 是速度,φ_i 是初相位,ω_i 控制空间频率。用波长 L_i 时,常见的归一化约定会令空间频率与 1/L_i 成正比;实现中只要前后一致,关键是波峰间距和相位推进保持可控。
总高度是多条波的叠加:
这条函数同时给出每个水平位置的高度和表面朝向。几何网格只在顶点位置采样 H;比网格更细的变化则在纹理空间重新采样同类函数,生成动态法线。
参数不是装饰:它们决定观感
↡相邻两个波峰之间的世界空间距离 决定尺度:长波负责海面的缓慢起伏,短波负责细碎波纹。 ↡从静水面到波峰的高度 决定高度范围;振幅与波长的比例过大,波峰会显得不可信。 ↡波峰沿传播方向每秒前进的距离决定相位随时间如何移动; ↡波峰在水平面中前进的单位方向把波与风向或水流联系起来。
为了让系统持续变化,可以让一条波逐渐淡出,再用新的一组参数淡入。每次重新生成都要同时约束波长、振幅和速度,否则独立随机会产生相互矛盾的视觉节奏。
2. 从高度场得到表面方向
令三维表面为:
对 x 与 y 求偏导,得到两个切向基向量:
两者的叉积给出未归一化的表面朝向:
这里的↡高度函数对水平坐标求导后得到的变化率不是额外的近似采样,而是由显式高度函数直接计算。因为正弦函数的导数仍是正弦/余弦,H_x 与 H_y 仍然可以按波逐条累加。
从法线还能构造切线空间的基。对小波纹来说,逐像素更新法线比继续细分几何更划算;这就是为什么同一个高度模型会同时出现在顶点 shader 和像素 shader 中。
3. 几何波与纹理波分工
几何层只保留少量低频波:它改变轮廓、遮挡和水面与岸边的关系。纹理层叠加更多频率和方向的波,在 256×256 左右的 render target 中生成↡编码局部表面朝向、供光照采样使用的纹理。水面看起来丰富,主要依赖这层高频细节,而不是把主网格无限细分。
正弦波与 Gerstner 波
直接叠加正弦波的波峰比较圆,适合平静池塘。需要更尖的波峰和更宽的波谷时,可以使用↡通过水平顶点位移控制波峰陡峭度的物理启发波函数。它不只改变高度,还把顶点向波峰方向移动,因此能把采样点集中到高频特征附近。
Gerstner 波的陡峭度参数通常记为 Q。Q 太大时,波峰可能形成回环;生产工具应把它限制在安全范围,并让 Q 与波数、振幅和波的数量共同约束,而不是把它当作无上限的“尖锐度滑杆”。
方向波适合风驱动的大水面,方向向量在波的生命周期中保持稳定;圆形波适合瀑布落点或局部扰动,方向由波心指向顶点的向量实时归一化得到。两者共享同一套采样流程,差别在于方向来自常量还是来自空间位置。
4. 水深与网格边长进入 authoring
水面网格的顶点除了水平位置,还可以携带水底高度,从水面高度减去水底高度得到↡水面与水底之间的局部垂直距离。水深可以同时控制透明度、反射强度和几何波幅:浅水区域更透明,靠岸处波幅逐渐衰减,顶点不会穿过岸线。
边长滤波与纹理坐标
局部网格边长决定几何层能表达的最短波长。若波长约为边长的四倍,通常可以保留完整响应;当波长接近边长的两倍时,应把响应衰减到零。这种↡根据局部网格边长衰减无法被几何采样的短波可以避免短波混叠,同时允许重要区域使用更密的三角形、远处区域使用更粗的网格。
普通池塘可以从顶点位置推导纹理坐标;蜿蜒河流则需要显式纹理坐标和切线空间变换,把纹理中的法线旋转到弯曲河道的世界空间。对每个顶点统一采用同一处理流程,差异只来自 authoring 数据和过滤参数。
5. GPU 运行时流水线
顶点阶段先计算几何波,按水深和边长对波幅衰减,再输出表面位置、切线空间基和与环境贴图相关的向量。像素阶段从动态法线贴图取样,把纹理空间法线变换到水面空间,用它查询环境反射,再和水本身的颜色、透明度合成。
一个高层伪代码如下:
vertex:
depth = waterHeight - bottomHeight
H, dHdx, dHdy = sumGeometricWaves(position, time, filteredParameters)
normalBasis = basisFromPartials(dHdx, dHdy)
emit displacedPosition, normalBasis, depth
pixel:
detailNormal = sampleDynamicNormalMap(uv, time)
worldNormal = transform(detailNormal, normalBasis)
reflection = sampleEnvironment(worldNormal, correctedEyeVector)
output = blend(waterColor(depth, viewAngle), reflection)其中 correctedEyeVector 解决小水池环境贴图中心与观察点不一致的问题;对无限远天空,简单环境向量已经足够,对能看到对岸物体的池塘,则需要按水体范围修正反射方向。
Water Wave Lab
先调一条波,再理解参数耦合
6. 一次可验证的调参顺序
- 先预测:只改变波长、振幅和速度时,你预期波峰间距、垂直高度和移动节奏分别如何变化;再固定四条几何波的波长和风向,确认轮廓移动稳定。
- 再给每条波分配振幅和速度,检查振幅/波长比例与波峰形状。
- 添加纹理波,先覆盖不同频率,再调整方向以避免重复图案。
- 用水深衰减靠岸波幅,用边长滤波关闭网格无法表达的短波。
- 最后调透明度、反射和 Fresnel 近似,避免把几何问题误判为材质问题。
本章回顾:一套有物理依据的近似
- 水面可以用多条周期波的高度和来近似;参数对应波长、振幅、速度、方向和相位。
- 高度场的两个偏导数直接给出切线空间基,从而得到法线和动态光照所需的方向。
- 少量几何波负责轮廓,更多纹理波负责法线细节;两层不应互相替代。
- Gerstner 波通过顶点水平位移提高波峰陡峭度,但陡峭度必须避免形成回环。
- 水深控制靠岸衰减,边长滤波控制几何采样能力,二者都是 authoring 数据的一部分。
- 顶点 shader 更新几何,像素 shader 生成法线并合成反射;GPU 承担计算,CPU 保留给游戏逻辑和 AI。
练习:从公式走到 shader
练习
问题 1 单条波取 A=0.2、波长 L=4、速度 S=1,说明增大 A、L、S 各自会怎样改变水面观感,并指出不能只独立随机它们的原因。
问题 2 已知高度场 H(x,y)=0.2sin(x)+0.1sin(2y),写出 H_x、H_y,再写出未归一化法线的方向。
问题 3 为什么几何波只保留少量,而纹理波可以使用更多频率?请把“轮廓”和“局部光照”分别对应到两层。
问题 4 一个靠岸顶点的水深接近零,且局部网格边长很大。应如何处理波幅?这同时解决了什么视觉问题?
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 波长
相邻波峰之间的世界空间距离,决定波的尺度。
- 振幅
从静水面到波峰的高度,决定波的垂直起伏范围。
- 偏导数
高度场沿水平坐标某个方向的变化率,用于构造表面方向。
- 法线贴图
记录局部表面朝向的纹理,用于在不增加几何的情况下丰富光照细节。
- 水深
水面高度与水底高度之间的局部距离,可控制透明度、反射和波幅。
- 边长滤波
根据网格局部边长衰减无法被几何采样的短波,避免混叠。
- 速度
波峰沿传播方向每秒前进的距离,决定相位随时间推进的节奏。
- 方向
波峰在水平面中前进的单位向量,通常与风向或水流有关。
- Gerstner 波
通过水平顶点位移控制波峰陡峭度的物理启发波函数。