Build an ultra-realistic open ocean in Three.js with a deep-water Gerstner spectrum shaded per pixel from analytic derivatives, each wave faded at its own pixel
复制下面这句话,粘贴给 Claude Code、Codex、Cursor 等 AI 编程工具,它会读取安装说明并在你确认后完成安装。
请阅读 https://ai.atlankj.com/install/asset/gh-3d-ultra-realistic-water-881b32996df5 ,按照其中的说明把「3d-ultra-realistic-water」安装到你(当前 AI 工具)中。执行前先告诉我将运行的命令和写入的位置,等我确认。
查看 AI 将读取的安装说明正在读取 GitHub 原文…
内容来自 GitHub 原始文件,由原作者维护。在 GitHub 查看
Use this for an open sea seen by a moving camera, with a horizon and a sun. Reach for something else in these cases:
shaders-cursor-ripples.Source: extracted from Pirate Ship Sunset, a single-file Three.js r169 scene in which a three-masted ship sails a rolling sea at sunset under an auto-cycling camera. The water had to stay convincing from 2 m above the waves out to a 12 km horizon.
Shade the sea per pixel from the analytic derivatives of a deep-water Gerstner sum, and fade each wave out once its wavelength falls below the pixel footprint. The per-pixel derivatives keep the glitter and foam sharp on a mesh whose triangles are 30 m wide at the horizon. The footprint fade stops the far sea from boiling into moiré.
Copy assets/ocean.mjs. You pass in the project's own THREE, and the module does not import a second copy. Before you integrate it, read references/ocean.md, which covers the full constants, render order, buoyancy and post. The demo runs this same module inside the original scene. demo/build.mjs rebuilds it from the reference page.
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
| layer | what it does | constants |
|---|---|---|
| Spectrum | 8 Gerstner waves, deep-water dispersion ω = √(g·k) | L 78 → 3.1 m (×0.63 per step), A 0.8 → 0.02 m, headings 0, +23, −17, +38, −31, +12, −49, +64° off the wind, Q 0.17 → 0.03 (Σ 0.77) |
| Grid | exponential rings centred under the camera | r = 11.92·(e^(0.033·i) − 1), 210 rings × 400 segments = 84,001 verts, 167,600 tris, reaches ~12.2 km; spacing 0.033·(r + 12) |
| Footprint fade | per wave, in both shaders | fade = 1 − smoothstep(0.18·L, 0.5·L, footprint); vertex footprint 1.2 × ring spacing, pixel footprint 2 × (fwidth(x) + fwidth(z)) |
| Normal | per pixel, normalize(cross(dP/dz, dP/dx)) | Jacobian J = dPdx.x·dPdz.z − dPdx.z·dPdz.x drives crest foam |
| Micro detail | baked 512² tile: 48 sines with integer wave vectors, amplitude ∝ |k|^−1.35 | two octaves at 19 m and 7.3 m (the second rotated 90°), strength 0.4·e^(−d/500) + 0.08 |
| Reflection | Schlick with water F0 = 0.02; 256² sky cube re-rendered every 0.25 s; half-res planar pass for the ship and islands | planar UV nudged by N.xz · (0.06, 0.03) · clamp(30/d, 0.2, 1) |
| Body | deep colour plus sunlit subsurface through the wave crests | deep (0.006, 0.03, 0.048), scatter (0.035, 0.3, 0.27) linear; scatter × toward-sun² × crest height × (0.35 + 4·(1 − N.y)) |
| Glitter | HDR specular, left for the bloom to catch | pow(R·L, 90)·1.2 + pow(R·L, 12)·0.06 + pow(R·L, 1400)·40, sun colour (4.2, 2.3, 1.1); bloom threshold 1.6, knee 0.8 |
| Foam | thresholds a Worley lattice by a mask, so the foam stays lacy | crest smoothstep(0.6, 0.28, J); foam = smoothstep(1 − m, 1.2 − m, pattern·0.88 + m·0.22); gone by 1600 m |
| Wake | 512² half-float field over 380 m, trailing the ship | Kelvin arm offset 1.2 + 0.35·a (atan 0.35 ≈ 19.3°), arm decay 115 m, stern wash 170 m, calm slick 260 m |
| Haze | fogs into the sky's own horizon colour | fog = (1 − e^(−d·2.8e-4))^1.6 |
All of these were A/B tested in the demo scene with the sim frozen, so only the one change differed between frames.
setSeaState() lets amplitude keep rising but caps steepness at ΣQ = 0.95.R.y = abs(R.y) + 0.002. Some wave backs reflect downward. Without the clamp, those faces sample the flat lower half of the sky cube and read as pale grey smears with no glitter. At a grazing shot, 3–4% of the frame changed by as much as 154 levels./9 and /3.3 but offset it by /11 and /4.1. At every 1200 m rebase, the foam pattern jumped by about 24 tiles. The module fixes this.ω·t passes 16,000 rad for the 3 m wave within an hour. That makes the float32 sin() argument the part that degrades first. This rule is precision reasoning and was not observed in the demo.height(x, z) inverts the horizontal displacement with 4 fixed-point steps before it reads y. Reading disp(x, z).y directly was off by 0.15 m on average and by up to 1.17 m on steep crests, across 20,000 samples. After 4 steps, the worst horizontal miss is 0.18 m.renderer.toneMapping = NoToneMapping and applies ACES itself, at exposure 0.9 and bloom 0.35.shadowMap.autoUpdate = false).The ocean mesh is 167,600 triangles. Its fragment shader runs the 8-wave loop once per pixel, plus 6 texture taps, 1 cube tap and 1 planar tap. The planar pass re-renders the reflection layer at half resolution. Under heavy machine load, headless Chrome ran the whole Pirate Ship Sunset scene in 10–13 ms per frame at 1440×900. Hiding the ocean or skipping the reflection pass each changed that by less than the run-to-run noise (about ±2 ms), so neither is the bottleneck. The reference's lever is adaptive resolution: DPR is capped at 1.6, and a resolution scale of 0.6–1.0 steps down when the 60-frame average passes 26 ms and back up below 14.5 ms.
dt to 1/20 s. The reference also rebases the world every 1200 m, shifting the ship, wake, particles, camera and waves.offX/offZ together.__OD.hold() implements this.setAnimationLoop stops when the page is hidden, and the dt clamp absorbs the gap when it resumes.height() inverted), and the wake arms open at about 19°.