关于 3d-ultra-realistic-water
这项 Three.js 水面渲染技能面向有移动镜头、地平线和太阳的开阔海面场景,提供可复用的海洋模块及波浪、反射、泡沫、尾流和性能实现指南。
主要功能
- 01解析 Gerstner 波着色
- 02按距离淡化波浪细节
- 03天空与平面反射
- 04泡沫和船舶尾流效果
- 05与波谱一致的高度查询
- 06减少动态效果支持
适用场景
- 01渲染延伸至地平线的开阔海面
- 02为 Three.js 场景添加波浪与反射
- 03稳定远处海面的细节表现
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ mengto/ skills --skill 3d-ultra-realistic-water安装后在 Claude Code 中输入/3d-ultra-realistic-water确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlassets/ocean.mjsdemo/build.mjsPROMPT.mddemo/src/ui.cssui.htmlui.jsreferences/ocean.md详细介绍
3D 超写实海水
适用于移动摄像机视角下、带有地平线和太阳的开阔海面。以下情况请考虑使用其他方案:
- **在水线处看到的镜面般平静水面,且没有网格:**全屏解析水面更省资源。
- **随光标在 UI 表面上移动的涟漪:**使用
shaders-cursor-ripples。 - **触碰后会产生反应的小型水池:**使用乒乓高度场。
来源:摘自 Pirate Ship Sunset。这是一个由单文件构成的 Three.js r169 场景,船上有三根桅杆,在日落时航行于起伏的海面,摄像机则自动循环移动。海面必须从高出海浪 2 m 的视角一直到 12 km 外的地平线都保持逼真。
实现机制
**根据深水 Gerstner 波叠加的解析导数逐像素为海面着色,并在波长小于像素覆盖范围时淡出该波。**逐像素导数让闪光和泡沫在网格上保持清晰,即使地平线处的网格三角形宽达 30 m。覆盖范围淡出可防止远处海面因混叠而剧烈闪动。
复用已实现的效果
复制 assets/ocean.mjs。传入项目自己的 THREE,该模块不会再导入另一份副本。集成前,请阅读 references/ocean.md,其中介绍了完整的常量、渲染顺序、浮力和后期处理。演示会在原始场景中运行同一个模块。demo/build.mjs 会根据参考页面重新构建它。
import { createWaves, bakeOceanDetail, createOcean, createWake, createPlanarReflection } from './ocean.mjs';
const waves = createWaves({ direction: 1.22 }); // wind heading, radians
const ocean = createOcean(THREE, {
waves,
detailTexture: bakeOceanDetail(THREE, renderer), // baked once, 512²
skyCube, // cube rendered from YOUR sky shader
hazeGLSL: SKY_GLSL + 'vec3 oceanHaze(vec3 d){ return hazeColor(d); }',
hazeUniforms: skyUniforms, // may include uSunDir
wake: createWake(THREE, renderer, { shipLength: 34 }), // optional
hull: { halfWidths, zMin: -18, zMax: 18, bowZ }, // optional, ship-local metres
});
scene.add(ocean.mesh);
const reflection = createPlanarReflection(THREE, renderer, { layer: 1 });
ocean.setReflection(reflection);
// every frame, in this order
waves.update(dt); // CPU phases, before any height() query
ship.y = waves.height(ship.x, ship.z); // buoyancy uses the same spectrum
ocean.update({ time, camera, ship: { x, z, fx, fz } });
wake.update(x, z, fx, fz, speed * dt);
reflection.render(scene, camera); // before the main pass
renderer.render(scene, camera); // into a HalfFloat target, then bloom + ACES
技术栈及实际使用的数值
| 层 | 作用 | 常量 |
|---|---|---|
| 波谱 | 8 个 Gerstner 波,深水色散关系 ω = √(g·k) | L 78 → 3.1 m(每步 ×0.63),A 0.8 → 0.02 m,波向相对风向为 0、+23、−17、+38、−31、+12、−49、+64°,Q 0.17 → 0.03(Σ 0.77) |
| 网格 | 以摄像机为中心的指数环 | r = 11.92·(e^(0.033·i) − 1),210 个环 × 400 个线段 = 84,001 个顶点、167,600 个三角形;延伸约 12.2 km;间距为 0.033·(r + 12) |
| 覆盖范围淡出 | 在两个着色器中逐波计算 | fade = 1 − smoothstep(0.18·L, 0.5·L, footprint);顶点覆盖范围为 1.2 × ring spacing,像素覆盖范围为 2 × (fwidth(x) + fwidth(z)) |
| 法线 | 逐像素计算,normalize(cross(dP/dz, dP/dx)) | 雅可比行列式 J = dPdx.x·dPdz.z − dPdx.z·dPdz.x 用于驱动浪尖泡沫 |
| 微观细节 | 烘焙的 512² 贴图:48 个具有整数波向量的正弦波,振幅 ∝ |k|^−1.35 | 两个八度,波长为 19 m 和 7.3 m(第二个旋转 90°),强度为 0.4·e^(−d/500) + 0.08 |
| 反射 | 水面 F0 = 0.02 的 Schlick 反射;每 0.25 s 重新渲染 256² 天空立方体贴图;以半分辨率平面渲染船只和岛屿 | 平面 UV 偏移量为 N.xz · (0.06, 0.03) · clamp(30/d, 0.2, 1) |
| 水体 | 深色水体加上透过浪尖的阳光次表面散射 | 深色部分为 (0.006, 0.03, 0.048),散射部分为 (0.035, 0.3, 0.27),均为线性空间;散射部分 × 朝向太阳程度² × 浪尖高度 × (0.35 + 4·(1 − N.y)) |
| 闪光 | HDR 镜面反射,交由泛光处理捕捉 | pow(R·L, 90)·1.2 + pow(R·L, 12)·0.06 + pow(R·L, 1400)·40,太阳颜色为 (4.2, 2.3, 1.1);泛光阈值 1.6,过渡宽度 0.8 |
| 泡沫 | 用遮罩对 Worley 晶格进行阈值处理,使泡沫保持蕾丝般的纹理 | 浪尖 smoothstep(0.6, 0.28, J);foam = smoothstep(1 − m, 1.2 − m, pattern·0.88 + m·0.22);到 1600 m 处消失 |
| 尾流 | 覆盖 380 m 的 512² 半浮点场,跟随船只移动 | Kelvin 波臂偏移量 1.2 + 0.35·a(atan 0.35 ≈ 19.3°),波臂衰减距离 115 m,船尾扰流 170 m,平静水迹 260 m |
| 薄雾 | 雾化效果融入天空自身的地平线颜色 | fog = (1 − e^(−d·2.8e-4))^1.6 |
每条规则及其可防止的问题
这些规则都在演示场景中、模拟暂停时进行了 A/B 测试,因此每组帧之间只有一项改动。
- 在顶点着色器和片元着色器中,都在每个波浪自身的覆盖范围内进行淡出。 不做淡出时,3–8 m 的波浪采样频率会低于其波长。远处的海面会变成密集颗粒,一直延伸到生硬的地平线,远处水面上的日光亮泽也会消失。相邻帧之间,远景波段的闪烁平均从 2.3 个灰阶上升到 5.8 个灰阶,95 百分位从 5 上升到 23。运动画面中,这看起来就像沸腾一样。
- 根据解析导数在片元着色器中计算法线。 如果使用插值后的顶点法线进行着色,近景以外的细节就会消失。远处海面会变成长而平滑的条纹,沿着网格环分布,闪光也会变得模糊,而不是散成闪烁的光点。
- 确保 ΣQ 小于 1。 陡峭度会缩放侧向位移。总和超过约 1 后,雅可比行列式就会变成负值:表面会自我折叠,泡沫遮罩会在波峰上饱和成平坦的白色板块。
setSeaState()允许振幅继续增大,但会将陡峭度上限设为 ΣQ = 0.95。 - 将反射向量限制在地平线以上:
R.y = abs(R.y) + 0.002。 有些波浪背面会向下反射。不加限制时,这些面会采样天空立方体下方的平坦半区,呈现为没有闪光的浅灰色模糊区域。在掠射视角下,画面中有 3–4% 的区域变化幅度最高达 154 个灰阶。 - 使用与天空相同的雾霾函数进行雾化。 单一的雾色只有在太阳一侧才能与天空匹配。其他方向都会在水天交界处留下明显的硬边。
- 使用与采样比例相同的除数,根据浮动原点偏移滚动细节纹理和泡沫纹理。 参考实现以
/9和/3.3对泡沫进行采样,却以/11和/4.1进行偏移。每次以 1200 m 为间隔重定位时,泡沫图案都会跳动约 24 个图块。本模块修复了这个问题。 - 在 CPU 上以 float64 将波浪相位(包括浮动原点项)限制到 [0, 2π)。 如果着色器接收原始时间,3 m 波浪的
ω·t会在一小时内超过 16,000 rad。这会导致 float32 的sin()参数精度首先下降。这条规则基于精度方面的推理,在演示中未观察到相关问题。 - 使用与渲染相同的频谱查询高度。 Gerstner 波会使点在这里侧向移动最多 4.5 m,因此
height(x, z)会先通过 4 次定点迭代反解水平位移,再读取 y。在 20,000 次采样中,直接读取disp(x, z).y的平均误差为 0.15 m,在陡峭波峰上最大误差达 1.17 m。经过 4 次迭代后,最差的水平偏差为 0.18 m。
颜色与渲染管线
- 所有 uniform 颜色都是线性 HDR。将场景渲染到 HalfFloat 目标,并在合成阶段只进行一次色调映射:先用 ACES,再转为 sRGB。参考实现将
renderer.toneMapping = NoToneMapping,并自行应用 ACES,曝光度为 0.9,泛光强度为 0.35。 - 天空立方体使用页面自身的天空着色器渲染,并省略微小的太阳圆盘。解析高光项会在水面上绘出太阳的路径。
- 平面反射只渲染一个包含水中物体的图层。它会在 y > −0.05 处裁剪,并复用当前帧的阴影贴图(
shadowMap.autoUpdate = false)。
性能开销
海洋网格包含 167,600 个三角形。其片元着色器每像素运行一次 8 波循环,另加 6 次纹理采样、1 次立方体采样和 1 次平面采样。平面反射通道会以半分辨率重新渲染反射图层。在机器负载较高时,无头 Chrome 以 1440×900 分辨率运行整个《Pirate Ship Sunset》场景,每帧耗时 10–13 ms。隐藏海洋或跳过反射通道对这一耗时的影响都小于不同运行之间的噪声(约 ±2 ms),因此两者都不是瓶颈。参考实现采用的调节手段是自适应分辨率:DPR 上限为 1.6;当 60 帧平均耗时超过 26 ms 时,分辨率缩放系数会在 0.6–1.0 的范围内逐步降低,低于 14.5 ms 时则逐步升高。
生命周期和无障碍
- 将
dt限制为 1/20 s。参考实现还会每隔 1200 m 对世界坐标重新定基,将船只、尾流、粒子、相机和waves.offX/offZ一起平移。 - 在减少动态效果模式下,停止模拟时间,并继续渲染,直到片头淡出结束,然后定格画面。仅在控件发生变化时重新渲染。演示中的
__OD.hold()实现了这一功能。 - 使用选项卡暂停:页面隐藏时,
setAnimationLoop会停止;页面恢复时,dt限制会吸收这段时间间隔。
验证
- 相机位于水面上方 2 m 并朝向太阳时,远处的海面应呈现雾霭和光泽,而非颗粒感。关闭足迹渐隐后,远处的区域会在不同帧之间闪烁。
- 顶点法线应明显丢失远处的微观细节。如果没有,说明法线仍取自顶点。
- 将海况提高到 1.6:浪尖仍应轻盈细碎,不会出现平坦的白色大片。
- 以掠射角观察时,波浪背面不应出现浅灰色拖痕。
- 背向太阳的地平线上,水面应与天空融为一体,不出现分界线。
- 漂浮物应随浪尖起伏(
height()反转),尾流两臂的张开角约为 19°。 - 减少动态效果模式下,画面应定格为构图完整的静帧;控件变化后仍会重新渲染;在 1440×900 和 390×844 分辨率下,控制台无报错。