站内搜索

输入关键词开始搜索

Three Tile FFT 海面:地图瓦片与频谱海浪的组合渲染

文章目录

Three Tile FFT 海面把地图瓦片框架和频谱海浪放进同一个 Three.js 场景。它既保留地图浏览视角,也能切换到贴近水面的观察方式。

three-tile 地图上的矩形 FFT 海面、视角切换和运行状态

实现思路导图

flowchart TD
  ROOT[three-tile FFT 海面]
  ROOT --> MAP[地图空间]
  MAP --> PROJECT[投影水域中心]
  MAP --> ORIGIN[浮动原点同步]
  ROOT --> OCEAN[WebGL 多级联 FFT]
  OCEAN --> WATER[矩形海面位移]
  OCEAN --> MRT[MRT 与回退]
  ROOT --> COMPOSE[组合渲染]
  COMPOSE --> DEPTH[深度与顺序]
  COMPOSE --> CAMERA[统一相机控制]
  ROOT --> STATS[瓦片与海面联合统计]

截图中的地图瓦片与矩形海面已经同时渲染。黄色边界标出水域作用范围,状态面板显示 WebGL、128 FFT、三个级联和当前 MRT 状态;它说明水面使用了哪套运行参数,而不只是展示一张远景地图。

页面组件保持轻量

页面只维护质量和 MRT 状态,场景组件返回运行 API 与统计:

<ThreeTileOceanScene
  :quality="quality"
  :use-mrt="useMrt"
  @ready="handleReady"
  @stats="handleStats"
/>

查询参数可以覆盖初始质量和 MRT,便于在同一设备上建立可复现的对比链接。

场景运行时拥有 renderer、three-tile 地图、FFT 海面、天空、相机控制器和统计采样器。页面只保存可序列化参数。质量或 MRT 改变需要 restart 时,旧运行时先统一释放 GPU 与地图监听,再按查询参数重建,避免同一 Canvas 叠着两套动画循环。

两套空间基准需要统一

地图瓦片使用 three-tile 的世界坐标和相机控制,FFT 海面最初是在局部平面中生成。运行时必须把海面中心、尺度和相机近远裁面放到地图当前坐标基准下。

如果地图启用了浮动原点,海面也要同步接收原点偏移。只移动瓦片不移动海面,会在相机远离初始位置后产生漂移。

经纬度中心先由 three-tile 投影到地图世界坐标,水面宽高仍使用米。海面几何以中心为局部原点,顶点值保持在较小范围;地图世界矩阵负责把它放到瓦片旁边。相机移动较远时,运行时同步更新地图、海面、边界辅助线和模型的参考偏移,减少浮点精度损失。

这里不能把经纬度差直接当米。相同经度差在不同纬度对应的东西向距离不同,矩形尺寸应在投影或局部切平面中计算。否则控制面板写 1000 米,实际范围会随纬度变化。

WebGL FFT 管线

案例固定使用 WebGL 后端。频谱状态存入浮点纹理,通过水平、垂直多轮 pass 完成逆 FFT。MRT 开启时,一次 pass 可以输出多个波浪分量:

const runtime = createThreeTileOceanRuntime({
  renderer,
  map,
  quality,
  useMRT,
});

运行面板显示 FFT 尺寸、级联、pass 数、网格、三角面和采样分量。截图中的中等质量使用 128 FFT 和三个级联。

初始化时根据风速、风向、水深和随机种子生成 h₀(k)。每帧更新复数相位,随后在浮点 RenderTarget 上执行横向和纵向蝶形 pass。128 尺寸每个方向需要 7 个阶段;三个级联分别计算自己的波数范围,再组装成位移和导数纹理。

两张 RenderTarget 轮流承担输入和输出,避免在一次 pass 中同时读写同一纹理。MRT 开启后可把多个结果附件一次写出;运行面板同时显示“请求 MRT”和“实际 MRT”,因为浏览器或显卡能力不足时会自动回退。

水面顶点用世界位置换算级联 UV,读取水平/垂直位移。片元阶段根据导数重建法线,再组合 Fresnel、太阳高光、深水色、散射和浪尖泡沫。频谱、几何和材质共用同一个时间值,避免波形推进与光照细节不同步。

地图视角与海面视角

地图视角沿用瓦片浏览控制,适合确认海面范围与地理位置。海面视角改变相机高度、俯仰和控制器约束,强调波形细节。切换时需要平滑交接相机状态,不能同时让两个控制器响应输入。

实现使用同一个 TileMapControls,切换模式时保存目标点并重新设置相机位置、近远裁面和控制目标,再调用一次同步更新。动画循环在海面视角暂停不必要的地图相机更新,但控制器仍只更新一次。若同时挂 OrbitControls 和 TileMapControls,滚轮和拖拽事件会被处理两遍,相机会出现跳变。

海面范围与瓦片关系

当前实现是一块可调范围的海面,不会自动从地图水体数据生成岸线。用于真实海岸时,需要水域掩膜、海岸多边形裁剪或专门的海面网格。

地图层与海面层还要明确深度和渲染顺序。海面透明或带折射时,应避免读取包含自身的颜色缓冲,否则会形成反馈。

当前案例把海面材质设为不写深度,并通过明确的 render order 放在地图之后,目的是让有限水域稳定覆盖瓦片而不发生 Z-fighting。这是一种展示策略,不等同于真实海岸遮挡。若要让建筑、岸线和船体正确穿插,需要恢复深度测试,给水域生成与岸线一致的掩膜,并把透明排序纳入设计。

黄色边界只是调试辅助线。它验证中心、尺寸和高度偏移是否正确,也能在水材质参数过暗时确认水面仍处于预期位置。发布场景可以关闭它,但截图保留边界是为了让案例结果可解释。

质量策略

质量档位同时控制 FFT、网格和级联。地图瓦片本身也有 LOD 与网络成本,因此不能只按海面帧率选档。运行统计应和瓦片请求、显存占用一起观察。

质量切换涉及三种不同成本:FFT 尺寸控制每个计算纹理的像素数,级联数控制整套模拟重复次数,网格分辨率控制最终绘制三角形。three-tile 还会根据相机距离请求和解析瓦片。定位卡顿时,应先固定相机和瓦片缓存,再单独改变海面质量;否则网络请求峰值会被误判为 FFT 性能问题。

一帧的组合顺序

  1. 地图视角下更新 three-tile 的瓦片选择与世界矩阵。
  2. 推进海浪时间,执行各级联 WebGL FFT。
  3. 更新水面 uniform、边界和随波模型。
  4. 更新唯一的相机控制器。
  5. 先绘制地图与实体,再按设定顺序绘制透明海面和天空。
  6. 汇总 FPS、FFT、pass、MRT 与三角形统计。

销毁顺序与所有权同样重要:先停止 requestAnimationFrame,再解除 resize 和输入监听,销毁 GUI、地图瓦片、模型、FFT RenderTarget、材质、几何体,最后释放 renderer。这样重新进入案例时不会出现重复请求、重复控制器或持续增长的显存。

这个案例的主线是:three-tile 提供地图空间,WebGL FFT 提供动态波场,运行时负责统一坐标、相机、深度和生命周期。

从零复现:three-tile 与 FFT 海面组合教程

案例源码位于 src/demos/threedemos/three-tile-ocean/。它不是单独的海浪教程,重点是把已有 WebGL FFT 运行时接入地图瓦片坐标、相机和渲染顺序。

第 1 步:先跑通 three-tile 地图

创建 renderer、camera、TileMap 与 TileMapControls,选择固定经纬度飞到目标区域。等待瓦片加载后记录地图中心的世界坐标与米制比例。

完成标志:缩放和旋转地图时瓦片 LOD 正常,页面只存在一个动画循环和一个控制器。

第 2 步:创建独立 WebGL FFT 海面

先在原点生成单级联水面,验证频谱、FFT、位移和材质。运行时接口至少提供 update(time, dt)、updateUniforms()、sampleWater() 和 dispose()。

three-tile 接入层只调用这些接口,不应进入海洋内部直接修改 RenderTarget。这样海洋可以在独立页面单测,也便于以后替换实现。

第 3 步:把经纬中心放入地图世界

使用地图提供的投影或经纬转换函数得到中心世界坐标。水域宽高仍以米定义,在中心局部平面创建矩形,并同步更新黄色调试边界。

const centerWorld = map.geo2world(region.lon, region.lat, region.altitude);
ocean.mesh.position.copy(centerWorld);

不要直接把经纬度值写入 Three.js position.x/z。完成标志:黄色边界覆盖预期水域,修改中心经纬度时海面与边界一起移动。

第 4 步:同步浮动原点

地图为保持精度移动世界原点时,海面、调试边界、船体、天空参考点和控制器目标都应用相同偏移。集中由运行时处理,不让每个对象自行读取相机位置。

测试方法是连续平移到远离初始位置,再回到水域。海面中心不应相对瓦片漂移,也不应出现顶点抖动突然增加。

第 5 步:明确深度与渲染顺序

先用不透明纯色水面开启深度测试,验证它与地图高程的空间关系。再引入透明度、反射和泡沫,明确 depthTest、depthWrite 和 renderOrder。

当前演示采用不写深度并在地图后绘制的展示策略。若业务需要建筑和岸线穿插,必须生成水域掩膜并恢复正确深度测试,不能依赖固定 renderOrder 假装遮挡正确。

第 6 步:共用一个相机控制器

地图视角和海面视角保存不同的相机位置、目标点、FOV 和近远面,但都通过同一个 TileMapControls 更新。切换时停止旧过渡,插值相机,再同步控制器目标。

海面视角可以暂停不必要的瓦片相机更新,但不能再创建 OrbitControls。两个控制器监听同一个 Canvas 会导致滚轮和拖拽重复响应。

第 7 步:接入多级联与 MRT

按质量档位创建 128/256 FFT、级联数量和网格分辨率。请求 MRT 后检查实际 framebuffer 能力,将“用户请求”和“实际启用”分别显示在面板。

MRT 回退不能改变纹理通道语义。多 pass 路径输出的位移、导数和泡沫必须与 MRT 路径一致,材质层不应关心它们来自哪条管线。

第 8 步:联合统计和释放

固定相机并等待瓦片缓存稳定后,再测海面质量。面板同时报告瓦片更新、FFT pass、级联、三角形和 FPS,避免把网络峰值误判为 FFT 计算瓶颈。

销毁时先停止动画循环,再释放控制器、地图、模型、海洋 RenderTarget、材质、几何和 renderer。重新进入页面后检查网络请求数量与 GPU 内存不应持续翻倍。

验收与排错

  • 海面与黄色边界分离:两者必须使用同一地图转换与原点偏移。
  • 相机切换后拖拽跳动:同步控制器 target 和内部状态。
  • 海面永远覆盖建筑:当前展示式 renderOrder 不适合真实遮挡,需要水域掩膜与深度方案。
  • 首次进入卡顿、之后正常:分别记录瓦片网络加载和 FFT 稳态成本。

相关文章

Three.js FFT 真实海面:WebGPU、WebGL 与多级联波谱

拆解 Poseidon 海面的频谱初始化、GPU FFT、多级联合成、水面材质、天空光照和浮船采样