文章目录
这个案例在 Cesium 地形中放置一块有限矩形水面。海浪由 GPU FFT 频谱模拟产生,船体再从同一水面状态中读取高度和斜率,得到升沉、俯仰与横摇。

实现思路导图
flowchart TD
ROOT[Cesium FFT 矩形水面]
ROOT --> SPECTRUM[波谱模拟]
SPECTRUM --> H0[风与水深生成 h0]
SPECTRUM --> FFT[时变频谱与二维逆 FFT]
ROOT --> TEXTURE[位移导数与泡沫纹理]
ROOT --> WATER[Cesium 水面]
WATER --> ENU[ENU 矩形网格]
WATER --> SHADING[反射透射与浪尖]
ROOT --> BOAT[五点采样驱动船体]
ROOT --> QUALITY[级联 LOD 与资源管理]
截图中的矩形水面已经在持续计算,船体位于波场内部。状态面板显示的 FFT 尺寸、级联数量、计算 pass 和网格三角形数,分别对应频域分辨率、空间尺度数量、每帧 GPU 工作量和最终几何成本。
场景封装管理完整生命周期
CesiumFftWaterScene 负责 Viewer、水面 Primitive、船体、相机和销毁。页面面板只修改参数:
const scene = new CesiumFftWaterScene(viewer, {
center,
width,
height,
fftSize: 256,
});
水面位置或尺寸变化会影响模型矩阵和网格;浪高、浪尖、清澈度等参数只影响计算纹理或材质 uniform。
这个分层决定了交互能否保持流畅。中心点、宽高和网格等级改变顶点与包围体,需要重建 Primitive;颜色、透明度、Fresnel、浪高倍率只改 uniform;FFT 尺寸和级联范围改变 RenderTexture 尺寸,需要销毁并重建模拟器。
FFT 生成多尺度波浪
程序先根据风向和频谱生成初始复数振幅,再随时间更新频域相位,最后通过二维逆 FFT 得到位移和斜率纹理。
多个级联分别覆盖长浪、中浪和细碎波纹。每个级联使用不同世界尺度,最终在材质中合成。单一纹理很难同时表现公里级波形和近景细节。
初始频谱为每个波数 k 保存复数振幅 h₀(k)。运行时不重新生成随机海面,只推进色散相位:
h(k, t) = h₀(k) · exp(iωt) + conj(h₀(-k)) · exp(-iωt)
ω(k) = sqrt(g · |k| · tanh(|k| · depth))
第一项让波向前传播,第二项保证逆变换后的高度是实数。风向决定能量集中方向,水深进入色散关系,浪高只是最终位移的倍率。修改风场或频谱参数必须重建 h₀;修改时间速度只影响相位推进。
simulation.update(time);
const displacementMaps = simulation.getDisplacementMaps();
FFT 尺寸从 128 提升到 256 时,计算像素数按面积增长,二维变换的 pass 数也会增加。
二维逆 FFT 拆成横向和纵向蝶形计算,每一维需要 log₂N 个阶段。案例还要生成频谱、时变频谱、位移和导数,所以统计公式是每个级联约 4 × log₂N + 6 个计算 pass。256 不是 128 的两倍成本:纹理像素先变为四倍,每个方向又多一个蝶形阶段。
逆变换输出不只有高度。水平位移让浪尖前倾,空间导数重建法线,位移 Jacobian 的压缩区域用来生成泡沫。三者来自同一波谱,所以波峰、光照和白沫不会互相漂移。
Cesium Primitive 负责有限水面
矩形网格在局部 ENU 坐标中创建,再由 modelMatrix 放到指定经纬度和海拔。顶点 Shader 从位移纹理读取高度,片元 Shader使用法线、天空色和底色计算反射与透射。
LOD 网格让近处拥有更多三角形,远处减少细分。面板中的三角网开关用于检查网格密度,不属于最终水面材质。
矩形顶点先处于局部 ENU 平面。顶点 Shader 用局部 x/y 与各级联世界长度计算周期 UV,采样位移纹理后再由 modelMatrix 放回地球。有限矩形的包围球必须额外包含最大浪高,否则 Cesium 可能在波峰仍可见时提前把 Primitive 剔除。
片元阶段先由导数恢复动态法线,再计算视线与法线夹角。掠射角更偏天空反射,俯视角增加水体底色与透射,这就是 Fresnel 在水材质中的职责。“清澈度”改变反射与底色的混合,不会改变波浪几何。
船体跟随同一波场
船体中心、船首和船侧分别采样水面。中心高度决定升沉,前后高度差得到俯仰,左右高度差得到横摇。
const center = water.sampleWaterLocal(x, y, scratch.center);
const bow = water.sampleWaterLocal(bowX, bowY, scratch.bow);
const side = water.sampleWaterLocal(sideX, sideY, scratch.side);
采样必须与 GPU 海浪使用相同频谱、时间和尺度。另写一套正弦函数只能得到近似运动,船体会与可见波峰错位。
案例实际使用中心、船首、船尾、左舷和右舷五个位置。中心高度决定升沉;首尾高度差构成前向切线;左右高度差构成横向切线;两条切线叉乘得到船体朝上的法线。最终姿态会做时间平滑,避免 FFT 网格高频分量让船体快速抖动。
这里的“同源采样”是同一组频谱参数和时间函数,不代表从 GPU 纹理同步读回 CPU。频繁 readPixels 会阻塞渲染管线,因此船体采样器在 CPU 侧复现有限数量的频谱分量,用相同种子、尺度和相位近似可见波场。
一帧的执行顺序
- 根据当前时间更新各级联频域相位。
- 横向、纵向执行逆 FFT,组装位移和导数纹理。
- Cesium Primitive 在顶点阶段读取位移,在片元阶段读取导数。
- CPU 采样器计算船体五点高度,更新船体位置与姿态。
- 场景提交水面、船体和调试图层。
销毁时要同时释放 Primitive、级联纹理、Framebuffer、ShaderProgram、船体模型和事件监听。只从 scene.primitives 移除水面并不足以回收 FFT 的 GPU 资源。
适用边界
矩形水面适合湖泊、港池和局部海域。它不会自动贴合任意海岸线,多边形岸线仍需要遮罩、裁剪或专用网格。
主线是:频谱初始化 → GPU 逆 FFT → 位移与法线纹理 → Cesium Primitive 渲染 → 同源采样驱动船体姿态。
从零复现:Cesium FFT 矩形水面教程
案例源码位于 src/demos/cesiumdemos/fft-rectangle-water/。核心类分别负责频谱模拟、Cesium Primitive 和完整场景生命周期,学习时也按这个顺序实现。
第 1 步:先创建静态 ENU 矩形
根据中心经纬度建立 ENU 模型矩阵,在局部 XY 平面生成宽高可调的规则网格。初期使用纯色材质,不做任何位移。
const modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(center);
const primitive = new CesiumFftWaterPrimitive({
modelMatrix,
width: 2000,
height: 1200,
});
完成标志:矩形中心、宽高和朝向正确;移动相机后不会离开指定水域;包围球覆盖整个矩形。
第 2 步:生成初始频谱
为每个离散波数生成两个高斯随机数,通过目标波谱计算复数振幅 h₀(k)。风向、风速、重力、水深和级联世界长度都在这一阶段进入。
先固定随机种子。每次刷新得到同一个初始海面,才能比较参数和排查后端差异。改变风或频谱时重建 h₀,只改变时间速度则不需要。
第 3 步:推进时变频谱
每帧根据色散关系计算相位,组合 h₀(k) 和其共轭项,保证空间域高度为实数。同时在频域乘 ikx/iky 生成斜率和水平位移所需分量。
vec2 phase = vec2(cos(omega * time), sin(omega * time));
vec2 h = complexMultiply(h0, phase)
+ complexMultiply(conjugateH0MinusK, conjugate(phase));
临时显示频谱幅值,中心低频区应平滑,风向方向能量更集中。
第 4 步:执行二维逆 FFT
先做横向蝶形 pass,再做纵向蝶形 pass。每个阶段在两张浮点纹理之间 Ping-Pong,并根据阶段读取不同的 butterfly 索引或旋转因子。
128 需要每个方向 7 个阶段,256 需要 8 个阶段。所有级联完成后组装位移和导数纹理。最终若出现棋盘符号翻转,检查逆 FFT 的排列和 (-1)^(x+y) 修正。
第 5 步:把多级联位移接入 Primitive
顶点 Shader 根据局部世界坐标和每个级联长度计算周期 UV,分别采样长、中、短波位移并求和。最大水平位移和浪高要扩展 Primitive 包围体。
片元 Shader 读取导数重建法线,依次计算 Fresnel、太阳高光、深水颜色、透明度和泡沫。先关闭短波和泡沫,只看长波几何,再逐级打开。
第 6 步:加入网格 LOD
近水面需要足够顶点承载位移,远处则可以降低细分。质量档位分别保存 FFT 尺寸、级联数和网格行列数,不要用一个“高质量”布尔值隐式控制所有资源。
三角网调试开关用于确认近处细分是否足够。波谱纹理再精细,如果几何只有几十个三角形,也无法形成清晰轮廓。
第 7 步:驱动船体姿态
采样中心、船首、船尾、左舷和右舷五点。中心决定高度,首尾差和左右差构造两条切线,叉乘得到上方向,再生成四元数。
const forward = normalize(bow.position - stern.position);
const right = normalize(starboard.position - port.position);
const up = normalize(cross(forward, right));
对位置和四元数做与帧率无关的指数阻尼。短级联权重可以降低,防止船体追随每一条细碎波纹抖动。
第 8 步:管理重建与释放
中心、范围和网格改变时重建几何;FFT 尺寸与级联改变时重建模拟纹理;水色、清澈度和浪高倍率更新 uniform。页面卸载时销毁所有 Framebuffer、Texture、ShaderProgram、Primitive 和船体对象。
验收与排错
- 波浪只有上下移动:检查频域水平位移和顶点 XY 位移。
- 法线细节与波峰错位:导数纹理必须使用相同时间和级联 UV。
- 船体穿浪:检查 CPU 采样器的频谱参数、种子和时间。
- 提升 FFT 后帧率骤降:同时记录纹理像素、pass 数和级联数,不要只看 N。