惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Jina AI
Jina AI
MyScale Blog
MyScale Blog
量子位
月光博客
月光博客
J
Java Code Geeks
A
About on SuperTechFans
H
Hackread – Cybersecurity News, Data Breaches, AI and More
U
Unit 42
WordPress大学
WordPress大学
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
腾讯CDC
G
Google Developers Blog
博客园 - 【当耐特】
Engineering at Meta
Engineering at Meta
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
宝玉的分享
宝玉的分享
IT之家
IT之家
N
Netflix TechBlog - Medium
Microsoft Security Blog
Microsoft Security Blog
博客园 - 叶小钗
B
Blog
Martin Fowler
Martin Fowler
P
Proofpoint News Feed
B
Blog RSS Feed

博客园 - zhang-yd

今日开源[第58期]NanoJev源码解读 今日开源[第56期]human-atlas源码解读 今日开源[第55期]model-x-studio源码解读 今日开源[第54期]Spark(@sparkjsdev/spark)源码解读 今日开源[第53期]PlayCanvas Engine(playcanvas/engine)源码解读 今日开源[第52期]SPEEDBALL GI WebGPU Showcase(speedball-gi)源码解读 今日开源[第51期]Pi Agent Harness(pi)源码解读 今日开源[第50期]World Monitor(worldmonitor)游戏项目解读 今日开源[第49期]竹知了(zhuzhiliao)小玩具项目解读 今日开源[第48期]Operation Ironhold 游戏项目解读 今日开源[第47期]Academic Research Skills for Claude Code(ARS)项目skill解读 今日开源[第46期]AI-Research-SKILLs项目skill解读 今日开源[第45期]nature-research-skills项目skill解读 今日开源[第44期]findskills项目skill解读 今日开源[第43期]last30days-skill 今日开源[第42期]Cangjie Skill 今日开源[第41期]MoneyPrinterTurbo 今日开源[第39期]img2threejs 今日开源[第39期]Kronos 今日开源[第38期]Open Code Review (OCR) 今日开源[第37期]Openship 今日开源[第36期]croc 今日开源[第35期] Code-Review-Graph 今日开源[第34期] 《深入理解 AI Agent:设计原理与工程实践》 今日开源[第33期] Home Assistant Core 今日开源[第32期] Vibe-Trading 今日开源[第31期]RuView 今日开源[第30期]Chrome DevTools MCP 今日开源[第29期]RomM (ROM Manager) 今日开源[第28期]Page Agent
今日开源[第57期]beautiful-water源码解读
zhang-yd · 2026-09-10 · via 博客园 - zhang-yd

源码解读 — beautiful-water

仓库https://github.com/VictorZakharov/beautiful-water
作者:Victor Zakharov
语言:JavaScript(ESM)
星标 / Fork:9 / 5(截至解读时)
许可证:MIT
主题标签:ocean、water、threejs、webgpu、webgl、shader、procedural-generation、playwright、github-pages
在线 Demohttps://victorzakharov.github.io/beautiful-water/
解读基准main 分支 commit 830c7f3(2026-08-23,Merge PR #15「fix/gpu-aware-adaptive-quality」)
依赖three@0.185.1;开发依赖 @playwright/test@^1.62.1vite@8.2.0;运行引擎要求 bun >= 1.3.11


一、项目简介与作用

1.1 这是什么

beautiful-water 是一个全屏电影级浅海(shallow ocean)程序化三维环境,使用 Three.js 在浏览器中实时渲染。它把一整套「海洋—天空—浮标—水下生态」整合进一个可交互场景:

  • 海面:方向性波谱(14 条宽窄不一的 Gerstner 式波 + 域扭曲 + 波包 + 波峰锐化 + 距离感知细节)+ 投影反射 / 折射(Reflector/Refractor 离屏 RT)+ Fresnel + GGX 太阳闪光 + 浅水透射(青→蓝随深度变化)+ 输送式泡沫(跟随高能波峰、不露平铺波纹、不整片移动)。
  • 天空与光照:可见太阳圆盘、程序化天空与云、自然反射路径、云影、水下焦散、体积感太阳光柱、海床植被与岩石。
  • 浮标:程序化导航浮标,会随浪上下浮沉、倾斜、反射、投影,并作为相机的固定环绕枢轴。
  • 水下生态:三群程序化鱼群(分离 / 对齐 / 聚合 / 避深 / 好奇心 / 习惯化 / 逃逸响应)+ 海床 + 岩石 + 水草 + 悬浮粒子 + 体积太阳光柱。
  • 跨介质:相机从水面俯冲穿过动态海面即可连续进入水下环境;含响应式加载屏、滚动 GPU 帧耗时、15 秒渲染 FPS 历史;点击性能面板可复制完整诊断报告。

与作者前两个 model-x-studio / human-atlas(我都解读过)不同,本项目不依赖任何外部模型 / 贴图素材:几何、行为、着色器、天空、噪声、栖息地细节全部由代码生成,以 MIT 发布。这一点在 README 与代码中反复强调("No downloaded scene assets")。

1.2 它最独特的定位:双后端 + 工程闭环 + 工作量标定样本

项目有三个相互叠加的「卖点」:

  1. 原生 WebGPU(TSL)与遗留 WebGL(GLSL)两条水体管线共享同一套波谱、场景、控制器、仿真时钟与质量控制器。后端专属代码按需动态加载,兼容选项不阻塞初始渲染。WebGPU 为默认,自动降级到 WebGL 2。
  2. 自动化视觉回归 + 性能自适应 + 4K/144Hz 吞吐证明。用 Playwright 冻结仿真时钟,在 20 个命名视角下抓回归图;对 WebGL/WebGPU 输出并排对比;并有原生 4K 持续吞吐门禁(本地、依赖硬件)。
  3. 「两天构建」的工作量估算案例研究。作者用自研的 EffortHours(离线 .NET CLI,静态分析「等效人力时 EHE」)估算该仓库代表 161.5 等效人力时(区间 78–306.75),而一份人工代码评审估 240 小时(182–302);两者与作者自述「两天 AI 辅助交付」对照,展示约 4–6 倍的产能放大。仓库把两份报告一并提交,作为校准该估算器的可复现样本。

1.3 技术栈一览

维度 选型
构建 / 开发服务器 Vite 8(bun run dev / bun run build
3D 引擎 Three.js 0.185.1(含 three/webgpuWebGPURenderer、TSL 节点材质、three/addonsReflector/Refractor/OrbitControls
WebGL 着色器 手写 GLSL(顶点 / 片元,含 #include <tonemapping_fragment> 等 Three 块)
WebGPU 着色器 Three.js TSL(Fn/If/uniform/reflect/refract/reflector 等节点式)
控制 OrbitControls,枢轴锁定浮标;禁用平移与光标缩放
UI 原生 DOM + 内联 SVG(性能面板)/ CSS(src/style.css,设计系统)
测试 / 回归 Playwright(@playwright/test)+ 自研 bun test 性能单测
部署 GitHub Pages(vite build --base=/beautiful-water/,官方 Actions,PR 预览)
包管理 / 运行 bun(要求 >=1.3.11

二、功能与各个模块分析

2.1 整体架构

index.html ──▶ src/main.js (编排器 / 状态机 / 动画循环)
                  │
                  ├─ core/                    性能与运行态
                  │   ├─ classifyGpu / inspectGpu / createAdaptiveQuality  (逐帧质量预算)
                  │   ├─ createGpuFrameTimer    (WebGPU/WebGL 时间戳查询 → 滚动 GPU 帧时分布)
                  │   ├─ createPresentationMonitor / formatPerformanceReport (FPS 历史 / 诊断报告)
                  │   ├─ createRenderPacer / readRenderCapPreference         (FPS 上限节流)
                  │   ├─ createRenderer / readRendererPreference             (WebGPU↔WebGL 选择+降级)
                  │   └─ describeRuntimeIdentity (浏览器/平台指纹,填入诊断报告)
                  │
                  ├─ scene/                   场景对象(双后端成对出现)
                  │   ├─ waves.js  (CPU 波采样,浮标/鱼共用;数据被 GPU 着色器复用)
                  │   ├─ pipeline.js(按 rendererMode 动态 import 对应后端模块)
                  │   ├─ ocean.js + ocean-webgpu.js
                  │   ├─ sky.js + sky-webgpu.js
                  │   ├─ underwater-rays.js + underwater-rays-webgpu.js
                  │   ├─ environment.js(海床/岩石/水草/粒子/光照/云影焦散)
                  │   ├─ buoy.js(浮标+尾迹+水面阴影)
                  │   ├─ fish.js(三群 Boids 鱼群 AI)
                  │   └─ noise-texture.js(确定性噪声 DataTexture)
                  │
                  ├─ shaders/
                  │   ├─ water/{vertex,fragment,functions}.js  (GLSL 水体)
                  │   ├─ sky.js(GLSL 天空)
                  │   └─ (WebGPU 对应物位于 scene/webgpu-*.js,用 TSL 改写同一套算法)
                  │
                  └─ ui/  loading.js / render-cap.js / renderer-toggle.js

scripts/     质量/部署脚本(性能剖析、Pages 校验、冒烟测试、线性历史检查)
tests/       visual/(Playwright 回归 + 性能 HUD/可持续 规格)
            performance/(bun 单测:自适应质量、GPU 计时、呈现监测…)
.github/      CI / 部署 Pages / PR 预览 三条工作流

渲染主循环(每帧)main.jsrenderFrame):

updateFrameState(t)       // 更新相机枢轴、海底/相机地板钳制、水面高程采样、underwaterMix 插值
  └─ 各 scene 对象的 update(t, underwaterMix, camera)
renderOceanCaptures()      // 若 WebGL:隐藏水面/太阳后离屏渲染反射/折射 RT(共享波形)
renderScene()              // renderer.render(scene, camera) + 可选叠加水下光柱
  └─ readRendererFrameDiagnostics()  // 记录 drawCalls / triangles

关键设计:共享波谱单源。 scene/waves.js 导出 OCEAN_WAVESOCEAN_DOMAIN_WARPOCEAN_ENERGY_WAVESOCEAN_DOMAIN_VARIANTS 等常量;sampleOceanSurface(x,z,time) 在 CPU 端计算结果,供浮标浮沉、鱼群避面、相机入水判定复用;同一份常量被 GLSL 顶点着色器(通过 functions.js 的模板字符串注入)与 TSL webgpu-wave-nodes.js 复用,保证两个后端波形完全一致(视觉回归「renderer-parity」门即验证此点)。

2.2 功能清单

功能 说明
方向性涌浪 14 条波宽窄不一、无公倍周期的波;域扭曲弯曲/分裂涌浪;波包让波峰弯曲/合并/消散;波峰锐化
反射 / 折射 投影式(Reflector/Refractor)离屏 RT + 屏幕空间扭曲;WebGPU 用 TSL reflector 节点 + viewportSharedTexture 屏采折射
浅水透射 光学深度随视角/深度变化;青→蓝随 opticalDepth 渐变;远距离吸收至地平色
太阳闪光 Fresnel + GGX distributionGGX + Smith 可见项 + 随风速平流的多尺度遮罩 → 风成闪片
输送式泡沫 foamLifecycle(出生—扩散—破碎)+ 卷曲位移(无散度场)+ 多孔丝带,跟随高能波峰且非整片移动
浮标 程序化船体/灯/系泊锚链;随浪浮沉倾斜;尾迹+水面阴影;相机环绕枢轴
鱼群 AI 分离/对齐/聚合 + 避顶/避底 + 好奇心环绕 + 习惯化(静止相机降低恐惧)+ 逃逸(移动相机逼近)
跨介质 俯冲穿过动态海面连续进入水下;underwaterMix 在 0/1 间插值混合天空/水下色
性能面板 实时 GPU PASS P50/P95、渲染 FPS 15 秒历史、1% 低帧;点击复制完整诊断
渲染上限 / 后端切换 30/60/120/144/240 FPS 上限(本地持久化);WebGPU↔WebGL 一键切换(URL/存储)
自适应质量 按 GPU 等级(software/integrated/unknown/discrete)定像素预算,帧率或 GPU 时间戳驱动上下调
视觉回归 Playwright 冻结时钟,20 命名视角抓图;WebGL/WebGPU 并排对比,像素误差+感知相似度限
4K 持续门 原生 4K 3840×2160 @ 目标刷新率,测量表面/水下各 30s,验证吞吐≥99.5% 目标

2.3 模块职责表(业务核心 vs 工程支撑)

本项目无 shadcn/自动生成脚手架(与 model-x-studio/human-atlas 不同),几乎全部手写。下面把文件分成「产品核心源码(逐行注释)」与「工程/测试支撑(模块级说明)」两档。

文件 角色 注释粒度
src/main.js 编排器:装配渲染器→场景→对象→动画循环→HUD→诊断 逐段(行号解析)
src/core/adaptive-quality.js GPU 分级 + 像素预算 + 帧率/GPU 时间戳驱动的质量自适应 逐行重点
src/core/gpu-frame-timer.js 滚动 GPU 帧时分布(WebGPU resolveTimestampsAsync / WebGL EXT_disjoint_timer_query_webgl2 逐行重点
src/core/presentation-monitor.js 渲染 FPS 历史、刷新率估算、CPU 帧时、诊断报告格式化 逐行重点
src/core/render-cap.js FPS 上限节流器 + 偏好读写(URL/localStorage) 逐行
src/core/renderer.js WebGPU↔WebGL 创建与降级 + 偏好读写 逐行
src/core/runtime-identity.js UA / userAgentData 指纹 → 浏览器/平台可读名 逐行
src/core/renderer-diagnostics.js renderer.info 抽取 drawCalls/triangles 逐行
src/scene/waves.js 波谱常量 + sampleOceanSurface(CPU 采样,多端复用) 逐行
src/scene/pipeline.js 按后端动态 import 场景模块 逐行
src/scene/ocean.js WebGL 水体:ShaderMaterial + Reflector/Refractor 离屏捕获 逐行
src/scene/ocean-webgpu.js WebGPU 水体:TSL 材质 + 节点反射器(无手动捕获) 逐行
src/scene/sky.js WebGL 天空:球壳着色器 + 太阳精灵 逐行
src/scene/sky-webgpu.js WebGPU 天空:烘焙云纹理 + TSL 色节点 + 镜像球壳兼容 WARP 逐行
src/scene/underwater-rays.js WebGL 体积太阳光柱(全屏 quad 叠加) 逐行
src/scene/underwater-rays-webgpu.js WebGPU 光柱:半分辨率 RT + QuadMesh 合成 逐行
src/scene/environment.js 海床几何/焦散着色、岩石/水草 InstancedMesh、粒子、光照、云影 逐行
src/scene/buoy.js 浮标几何、尾迹/水面阴影着色、随浪运动 逐行
src/scene/fish.js 三群 Boids 鱼群 AI + 尾摆 + 诊断 逐行
src/scene/noise-texture.js 确定性噪声 DataTexture(WebGL/WebGPU 共用) 逐行
src/scene/webgpu-water-functions.js TSL 工具:valueNoise/fbm/GGX/Fresnel/微梯度(与 GLSL 对应) 逐行
src/scene/webgpu-wave-nodes.js TSL 波位置节点(与 GLSL addWave 对应) 逐行
src/scene/webgpu-water-material.js TSL 水体材质(与 GLSL waterFragmentShader 对应,最长后端文件) 逐段
src/shaders/water/vertex.js GLSL 顶点着色器:域扭曲/能量/逐波位移,模板化注入波常量 逐行
src/shaders/water/functions.js GLSL 工具函数:噪声/GGX/泡沫生命周期/微梯度/天空反射 逐行
src/shaders/water/fragment.js GLSL 片元着色器:完整水体光照合成(~400 行) 逐段
src/shaders/sky.js GLSL 天空着色器:大气/云/太阳/水下光柱 逐行
src/shaders/water.js 重导出 waterVertexShader/waterFragmentShader 逐行
src/ui/loading.js / render-cap.js / renderer-toggle.js 加载屏、FPS 上限控件、后端切换按钮 逐行
index.html 入口 HTML:canvas、性能面板、渲染器切换、加载屏、启动脚本 逐行
package.json 脚本与依赖 逐行
playwright.config.js 视觉回归配置(视口、浏览器路径、webServer) 逐行
AGENTS.md 给编码 Agent 的协作备注 逐行
scripts/*.mjs / *.ps1 性能剖析、Pages 校验、冒烟、线性历史检查 模块级
tests/visual/*.spec.js Playwright 回归/性能/HUD/可持续规格 模块级 + 关键结构
tests/visual/{screenshot-metrics,wave-field}.js 像素差、波场相关性工具 逐行
tests/performance/*.test.js 自适应质量、GPU 计时、呈现监测等单测 模块级
src/style.css 设计系统(CSS 变量、布局、加载动画、面板) 模块级(非逻辑)
docs/effort-hours-*.md 工作量估算报告 不注释(数据文档)

2.4 双后端如何「共享同一波形」

waves.js 是单源真相:

export const OCEAN_WAVES = [ /* 14 条 wave(...) */ ];
export const OCEAN_DOMAIN_WARP = [ /* 3 条域扭曲 */ ];
export const OCEAN_ENERGY_WAVES = [ /* 3 条能量波 */ ];
export function sampleOceanSurface(x, z, time) { /* CPU 采样,返回 height + 法线 */ }
  • GLSL 路径water/vertex.js 在构建时把 OCEAN_WAVES 等以 toFixed(8) 硬编码进 addWave(...) 调用模板(见 waveCalls 数组),运行时在 GPU 顶点着色器逐顶点复现同一位移。
  • TSL 路径webgpu-wave-nodes.jsOCEAN_WAVES 等常量在 createWebGpuWavePositionNode 中生成 positionNode,算法与 GLSL addWave 逐字段对应(注释中明确「此 orientation 须与 GLSL mat2(0.80,-0.60,0.60,0.80) 一致」)。
  • CPU 复用buoy.jsfish.jsmain.js 的入水判定都直接调用 sampleOceanSurface,保证浮标浮在 GPU 画出的同一海面上。

pipeline.js 是切换点:

export async function loadScenePipeline(rendererMode) {
  if (rendererMode === 'webgpu') {
    const [sky, ocean, rays] = await Promise.all([
      import('./sky-webgpu.js'), import('./ocean-webgpu.js'), import('./underwater-rays-webgpu.js'),
    ]);
    return { createSky: sky.createWebGpuSky, createOcean: ocean.createWebGpuOcean,
             createUnderwaterRays: rays.createWebGpuUnderwaterRays };
  }
  // … 否则 import('./sky.js') 等 GLSL 版本
}

后端专属代码按需动态加载,因此 WebGL 兼容包不会拖慢 WebGPU 初始渲染。


三、核心源码逐行注释

3.1 入口与编排:index.html + src/main.js

index.html(入口骨架)

<!doctype html>
<html lang="en">
  <head>
    <!-- 主题色 #071b2b,description 点明 Three.js 海洋 -->
    <meta name="theme-color" content="#071b2b" />
    <link rel="icon" href="data:image/svg+xml,..." />        <!-- 内联 SVG favicon,零请求 -->
    <link rel="stylesheet" href="/src/style.css" />           <!-- 设计系统 -->
    <style> /* 内联兜底:满屏、#app 相对定位、canvas 初始透明、.loader 全屏遮罩 */ </style>
    <title>Open Water</title>
  </head>
  <body>
    <main id="app">
      <canvas id="ocean-canvas" aria-label="Animated open ocean with a floating red navigation buoy"></canvas>
      <div class="vignette" aria-hidden="true"></div>         <!-- 暗角 -->
      <!-- 性能面板:GPU PASS P50/P95、渲染 FPS 历史 SVG、CAP 状态 -->
      <button class="fps" data-performance-panel …> … </button>
      <!-- 渲染器切换 + 渲染上限控件(初始 hidden,JS 揭示) -->
      <div class="renderer-toggle" data-renderer-toggle hidden> … </div>
      <!-- 加载屏:太阳/地平线/波浪/浮标 动画 + 进度条 -->
      <div class="loader" …> … </div>
      <noscript>本场景需要 JavaScript 与 WebGPU 或 WebGL 2</noscript>
    </main>
    <script type="module">
      // 双 rAF 后在下一帧 import 主模块;失败则改写加载状态文案
      const start = () => import('/src/main.js').catch(() => { /* … */ });
      requestAnimationFrame(() => requestAnimationFrame(start));
    </script>
  </body>
</html>

要点:加载屏、性能面板、渲染器切换 UI 全部预置于 HTML,JS 仅在就绪后揭示(app.classList.add('is-ready'))或填充数据属性——避免「白屏 → 突然闪现」。

src/main.js(编排器,699 行)— 段级行号解析

main.js 是「状态机 + 装配 + 动画循环 + HUD」。按职责分段:

① 模块导入与 DOM 句柄(1–42)

  • 导入 Three、OrbitControls、core 各工厂、scene 各工厂、ui 各控件。
  • document.querySelector('[data-xxx]') 抓取性能面板、FPS/ GPU P50/P95 数值、HUD 历史 SVG 等。
  • const query = new URLSearchParams(...)runtimeIdentity = describeRuntimeIdentity(navigator) 解析运行环境指纹。

② 启动期模式判定(44–61)

  • harnessMode = query.has('harness'):测试/截图模式(冻结时钟、只显式渲染)。
  • nativeSustainProfile:仅当非 harness 且 ?sustain=native-4k 时启用原生 4K 持续证明模式(锁定像素比=1)。
  • preferredRenderCaprenderPacer = createRenderPacer(...)adaptiveTargetFrameRate = min(cap??60, 60)
  • requestedAntialias = shouldUseAntialias({...}):依据视口与 DPR 决定是否用 MSAA。
  • 114–118 行关键注释:WebGPU 的屏幕空间折射路径会采样主色/深度目标,保持该目标单采样可避免无效的多重采样深度绑定,并在高 DPI 集显上省去全分辨率 resolve。因此 antialias = preferredRenderer==='webgl' && requestedAntialias——WebGPU 路径主动关 MSAA。

③ 创建渲染器与质量基线(67–108)

  • createRenderer({ canvas, antialias, preferredMode })rendererInfo={renderer,pipeline,backend,adapterName,fallbackReason}
  • 设置 outputColorSpace=SRGBtoneMapping=ACESFilmicexposure=0.90shadowMap.enabledPCFShadowMap
  • WebGL 路径 shadowMap.autoUpdate=false(捕获与主渲染复用同一张图集,性能档可每两帧刷新)。
  • gpu = inspectGpu(renderer, {...}):拿到 GPU 名称/等级;harness 可用 ?gpuClass= 强制 software/integrated/unknown/discrete。
  • adaptiveQuality = createAdaptiveQuality({... gpuTimingEnabled: !harnessMode && gpuFrameTimer.supported, ...})

④ 场景与对象装配(109–183)

  • 加载水体管线 loadScenePipeline(rendererInfo.pipeline)
  • 创建 sceneFogExp2(0x063c48, 0.00001))、相机(PerspectiveCamera(51,1,0.08,520),初始位置 7.8,3.65,10.8)。
  • OrbitControls:枢轴 (0,0.54,0)、阻尼、禁用平移、min/max 距离 2.7–46、极角夹紧避免翻转;zoomToCursor=false
  • sunDirection = normalize(-0.58,0.10,-0.81)
  • surfaceSegments 按 GPU 等级与后端选择(discrete 300 / unknown 240 / webgpu 180 / 其他 210)。
  • 分别 createSky/createOcean/createEnvironment/createFishSchools/createBuoy/createUnderwaterRays
  • 收集 underwaterWorld = [...environment.underwaterObjects, ...fish, ...buoy](供反射捕获时临时隐藏)。
  • reducedMotion → 时间缩放 0.35(无障碍)。

⑤ 质量应用与生命周期(190–275)

  • applyRenderQuality():把 adaptiveQuality.getState() 映射到 renderer.setPixelRatio/setSizeocean.setCaptureResolutionenvironment.setShadowMapResolutionunderwaterRays.resize、相机投影。
  • disposeRenderer():页面 pagehide 时释放;WebGPU 4K 目标在 Chromium 拆文档时 dispose() 可能无限阻塞,故只对非 WebGPU 调 renderer.dispose()
  • resetPerformanceSampling():清空所有监测器与 HUD 文本。

⑥ 每帧状态更新 updateFrameState(elapsed)(304–339)

  • 非 harness 时把枢轴设为浮标位置 +0.62;controls.update()
  • seabedHeight/sampleOceanSurface 钳制相机与枢轴不穿海底。
  • cameraSurface = sampleOceanSurface(camX,camZ,elapsed)underwaterTarget = camY < surface.height ? 1 : 0cameraIsUnderwater
  • underwaterMix:harness 用固定值,否则 lerp(underwaterMix, underwaterTarget, 0.085) 平滑过渡(避免跨水面一帧闪烁)。
  • 依次 ocean/sky/environment/fishSchools/buoy/underwaterRays.update(...)
  • 雾密度、曝光随 underwaterMix 插值;app.classList.toggle('is-underwater', ...)

⑦ 渲染捕获与渲染(341–382)

  • renderOceanCaptures():WebGL 下先隐藏水面/太阳/隐藏物,再 reflector.onBeforeRender + 更新纹理矩阵;underwaterMix<0.65 才捕获(深水不反射)。harness 可用 harnessUnderwaterRaysEnabled 控制。
  • renderFrame(elapsed)renderer.info.reset()gpuFrameTimer.beginFrame()updateFrameState → renderOceanCaptures → renderScenefinally gpuFrameTimer.endFrame()renderedFrames%shadowFrameInterval==0 才刷阴影。

⑧ HUD 与诊断(384–528)

  • updateFps():每 ≥500ms 采样一次,平滑显示;填 GPU P50/P95、渲染 FPS 平均/1% 低、历史 SVG 路径(createHistoryPath)。
  • buildPerformanceReport():汇总呈现/FPS/渲染/GPU/渲染器/画布/质量/场景/三角面/页面状态/运行环境,调 formatPerformanceReport明确标注「渲染 FPS 非物理面板刷新率」「多屏时回调节奏可能跟随另一屏」——诚实披露测量局限。
  • createRenderCapControl(...):FPS 上限变更时写 URL(?renderCap=)与 localStorage,并重置采样。

⑨ 启动与动画循环 start()(550–693)

  • 预热:renderer.compileAsync + ocean.compileCaptures;依次渲染反射/折射捕获;renderScene()
  • harness 模式:挂 window.__WATER_HARNESS__(setView/advance/getDiagnostics/setUnderwaterRaysEnabled/samplePerformance),供 Playwright 确定性渲染与诊断。
  • 正常模式renderer.setAnimationLoop(t => { … renderFrame(timer.getElapsed()); updateFps(t); … })。每帧:presentationMonitor.recordFrameadaptiveQuality.sampleGpuTiming/observeFrameapplyAdaptiveQualityChangerenderPacer.shouldRender 节流、渲染、记录 CPU 帧时。

亮点:整个动画循环把「呈现帧率(浏览器回调)」与「渲染帧率(实际画了多少)」与「GPU 帧时(真实 GPU 耗时)」三条指标分开计量并都呈现给用户——这是该项目工程成熟的标志(见 §3.2/§3.3)。

3.2 性能中枢:src/core/adaptive-quality.js(422 行)

这是最值得读的「质量自适应」核心。要点:

  • GPU 像素预算分级(14–19):GPU_PIXEL_BUDGETS 对 software/integrated/unknown/discrete 各给 {minimum,initial,maximum}(单位百万像素)。例如 discrete 初始 6.0、上限 8.3;software 仅 1.45/1.85。
  • GPU 分类(41–52):正则匹配渲染器名(NVIDIA/GeForce/Apple M*d Max… → discrete;Intel/Iris/Adreno/Mali… → integrated;含 llvmpipe/swiftshader/warp → software);降级用 deviceMemory/hardwareConcurrency 推断。
  • 抗锯齿决策(76–82):shouldUseAntialias 在原生像素 ≤7MP 时才开 MSAA,否则让浏览器上采样平滑即可,避免带宽爆炸。
  • 质量档 / 捕获 / 阴影分辨率(84–103):qualityTier(high/balanced/performance 按 renderScale)、captureResolutionFor(软件 384、性能 512、平衡 640、高 768)、shadowResolutionFor(512/1024/1536/2048)。
  • state()(180–235):把当前 pixelBudget 换算成 pixelRatiorenderScaledrawingBufferWidth/HeightcaptureResolutionshadowMapResolutionshadowFrameInterval,以及 GPU 计时预算(gpuFrameBudgetMs=1000/targetgpuP95BudgetMs×0.90)。
  • applyFrameRate(fps)(237–284):平滑 FPS;<39 计慢窗、>56(且仅浏览器计时时)计快窗;严重变慢(<27)或连续 2 慢窗 → 预算 ×0.72/0.84(带 2 窗冷却);连续 4 快窗 → ×1.12 回升。改动超 1 像素才 revision++ 触发应用。
  • applyGpuTiming(timing)(286–363):若有 GPU 时间戳,用 p95 帧时 vs 预算判定 overloaded/severeOverload/headroom;过载时记下 learnedMaximumPixelBudget(安全上限),防止在昂贵的「捕获/阴影档位边界」反复横跳;有余量且 p95≤预算×0.70 且中值≤预算×0.60 才谨慎回升(×1.08)。这是「GPU 感知自适应」PR #15 的核心。
  • 对外接口getState/resize/observeFrame/sampleFrameRate/sampleGpuTiming/setTargetFrameRate/resetFrameSamplingobserveFrame 内部累积 ≥900ms 才出一次 FPS,避免抖动。

3.3 GPU 帧时:src/core/gpu-frame-timer.js(183 行)

把「GPU 查询缓冲解析」封装为滚动 10 秒 p50/p95 分布:

  • 采样节奏(4):每 60 动画帧解析一次时间戳,足够 10 秒分布又不扰动被测的浏览器回调节奏。
  • 支持探测(46–57):WebGPU 用 renderer.backend.trackTimestamp + resolveTimestampsAsync('render');WebGL 用扩展 EXT_disjoint_timer_query_webgl2(含 beginQuery/endQuery/getQueryParameter)。supported 决定后续是否启用 GPU 计时。
  • pollWebGlQueries()(88–100):逐个轮询挂起查询,跳过 GPU_DISJOINT_EXT 异常帧,结果入 record()
  • requestNodeTimestamp()(102–114):WebGPU 异步解析,用 generation 防竞态(requestedGeneration===generation 才记录)。
  • getState()(152–171):裁剪窗口、算 windowElapsedMs、用 summarizeFrameTimes 出 p50/p95;ready = 窗口满 && 样本≥2

3.4 呈现监测与诊断:src/core/presentation-monitor.js(381 行)

  • createPresentationMonitor(111–271):记录每帧时间戳 → 估算「渲染 FPS」、刷新率(estimateRefreshRate 取最快 25% 区间中位数再吸附到常见刷新率列表)、CPU 帧时 p50/p95/p99、缺失刷新率(missedRefreshes)、1% 低帧、最坏 1 秒。
  • createHistoryPath(series,{width,height,targetFps}):把 FPS 序列转成 SVG 路径 M/L 命令(性能面板的 15 秒历史曲线)。
  • formatPerformanceReport(308–380):拼出多行可读报告,含「Timing caveat: rendered FPS … are not physical panel measurements」——再次诚实声明测量边界。

3.5 帧率上限与后端选择:render-cap.js / renderer.js / runtime-identity.js

render-cap.js

  • RENDER_CAP_OPTIONS=[30,60,120,144,240]parseRenderCap 只接受这些值或 off/null
  • createRenderPacer(initialCap):核心 shouldRender(t)nextRenderAt 累加 1000/cap,带 0.5ms 容差与「跨槽补帧」逻辑,使非整除刷新率下仍稳定节流;cap=null 时每回调都渲染。

renderer.js

  • readRendererPreference(query):优先 ?renderer=,其次 localStorage,默认 webgpu
  • createRenderer({canvas,antialias,preferredMode}):若为 webgl 直接建 WebGLRenderer;否则 import('three/webgpu') 动态导入 WebGPURenderertrackTimestamp:trueoutputBufferType:UnsignedByteType 省 4K 带宽),await init();失败则 candidate?.dispose() 并降级 WebGL 2,记录 fallbackReason。返回 {pipeline,backend,adapterName,fallbackReason}

runtime-identity.js:从 userAgent / userAgentData.brands 解析浏览器名(Edge/Opera/Firefox/Chrome 兼容/Safari…),normalizePlatform 处理 Windows/Android/iOS/iPadOS/macOS/Linux,并区分 maxTouchPoints>1 的 iPadOS。用于诊断报告的「Runtime」行。

renderer-diagnostics.js:薄封装,从 renderer.info.renderdrawCalls/triangles(兼容 calls 旧字段)。

3.6 波谱单源:src/scene/waves.js(256 行)

  • wave(...)/domainWarp(...)/energyWave(...):构造助手,返回带方向/陡度/波长/速度/弯曲/波包/波峰锐化/LOD 区间的对象。
  • OCEAN_DOMAIN_WARP(3 条):超长非共周期波「弯曲坐标场」而非加可见正弦——几百米尺度弯曲/分裂涌浪系统而不改局部波峰尺度(注释 51–53)。
  • OCEAN_ENERGY_WAVES(3 条):宽风能量区,使某些波峰系统增强、另一些终止;频率刻意无公共平铺周期(64–65)。
  • OCEAN_WAVES(14 条):宽方向谱,避免从高处看时的完美平行 Gerstner 行;含 lodStart/lodEnd 距离 LOD(76–94)。
  • sampleOceanDomain(x,z,time,variant)(96–149):计算 warpedX/Z 及雅可比导数、energy 及其梯度。
  • sampleOceanSurface(x,z,time)(151–255):对每条波算 along/across、弯曲相位(双频)、波包包络、波数、锐化高度(sine - crestSharpness*cos(2θ))、用解析导数累积法线与梯度。返回 {height, normal}——CPU 与 GPU 共用同一数学,保证浮标/鱼/入水判定与渲染波形一致。

3.7 WebGL 水体:src/scene/ocean.js(204 行)

  • 创建 ShaderMaterial(双面、不透明、写深度),uniforms 含 uTime/uSunDirection/深浅色/地平色/uWaterDepth/uUnderwater/反射折射纹理矩阵/三张纹理
  • PlaneGeometry(420,420,surfaceSegments×surfaceSegments) 旋转到 XZ;renderOrder=5
  • 离屏捕获Reflector/Refractor(各 captureResolutionmultisample:0——因水着色器会立即过滤扭曲,reflector/refractorRT 单采样即可,主画布仍抗锯齿)。把它们的getRenderTarget().texture` 绑定到 uniforms。
  • updateReflection/RefractionTextureMatrix():用反射相机纹理矩阵 × 世界矩阵逆,把捕获投影到平均水面平面(注释 267–272:投影平均平面而非位移波峰,否则掠射角处齐次 w 跨零会暴露整片无效纹理)。
  • renderCaptures(hiddenObjects):先 hideCaptureScene(藏水面/太阳/隐藏物)→ 调 reflector/refractor.onBeforeRender → 还原。供 main.js 在每帧前调用。
  • getDiagnostics():回报 captureResolutioncaptureStrategy:'reflector-refractor'
  • update(t,underwaterMix):仅推进 uTime/uUnderwater

3.8 WebGPU 水体:src/scene/ocean-webgpu.js(86 行)

与 WebGL 版对比:

  • createWebGpuOcean:同样建平面几何,但 mesh.material = createWebGpuWaterMaterial({...}).material(TSL,见 §3.16)。
  • usesManualCaptures:false:WebGPU 路径用 TSL reflector 节点 + viewportSharedTexture 在着色器内直接采屏,无需 main.js 每帧手动离屏渲染(因此 compileCaptures/renderReflectionCapture/... 全是空操作)。
  • setCaptureResolution:改为调 reflectionSampler.reflector.resolutionScale = clamp(res/画布宽, 0.14, 0.42)——分辨率随画布缩放。
  • update:只推进 timeNode/underwaterNode

3.9 天空:sky.js / sky-webgpu.js

sky.js(76 行):球壳 BackSide 着色器(深度不写、无雾)+ 太阳 Sprite(加色混合、不色调映射)。update:天空跟随相机、太阳按 sunDirection*315 定位、水下时淡出太阳透明度。

sky-webgpu.js(299 行):

  • createSkyTexture/createSunTexture/createCloudTexture把云场烘焙进一张 768×384 纹理(多尺度 directionalFbm + 侵蚀 + 海拔淡入),避免每个天空片元与反射 pass 都跑程序化噪声(注释 141–143)。
  • colorNode = Fn(() => { ... }) 用 TSL:水下色作为底,underwaterNode<0.98 时按方向算云光、采样云纹理、与天空纹理混合;If 分支在节点图里实现介质切换(注释 287–289:连续淡入而非切换,避免一帧平色地平线)。
  • 253–257 行关键兼容:WARP(微软软件 WebGPU)对 BackSide 节点材质裁剪不一致,故把球壳 scale(-1,1,1) 翻转成普通正面渲染,使原生适配与软件 WebGPU 走同一可移植光栅路径。

3.10 水下光柱:underwater-rays.js / underwater-rays-webgpu.js

underwater-rays.js(141 行):全屏 quad 叠加(OrthographicCamera(-1,1,1,-1)),片段用 fbm 噪声造细/宽光束、rayDensity、径向包络、原点淡入、破碎;以折射太阳方向投影到屏幕 UV 定位光柱源。折射按 eta=1/1.333(水折射率)算 refractedSunrender(renderer)autoClear=false 后叠加,强度 <0.002 跳过。

underwater-rays-webgpu.js(191 行):半分辨率 RT(宽高 ×0.5)再 QuadMesh 合成——八次噪声读取在半分辨率跑可砍掉 75% 重 pass(注释 104–106)。用 TSL rayFbm/valueNoiseNode 复刻 GLSL 算法;render 先渲到 raysTargetcompositeQuad.render 合成。注释 132–135:QuadMesh 用渲染目标 UV 约定,普通 PlaneGeometry 会把 WebGPU RT 上下翻转、把光源从视口上方移到下方。

3.11 环境:src/scene/environment.js(428 行)

  • seabedHeight(x,z):确定性海床高度(正弦叠加,-3.55 基准)。
  • createSeabedTexture(WebGL 用):用 xorshift 伪随机 + 焦散 pow(1-|a-b|,12)DataTexture 烘焙沙地;repeat=420/20 保持 20 世界单位颗粒尺度跨全海域(避免掠射角露平背景)。
  • WebGL 海床着色器:fbm/valueNoise/voronoiEdge 造沙色与随时间的焦散(caustic),含雾与水下混合(#include <tonemapping_fragment>)。
  • WebGPU 海床:改用 MeshStandardMaterial + 烘焙 createSeabedTexture(带 emissive),性能路径提高水下 emissive 强度(注释 418–420)。
  • 岩石 / 水草InstancedMesh(34 岩 / 150 水草),用 xorshift 种子随机布点;水草聚成 7 簇。
  • 粒子:440 个 Points(确定性噪声纹理、加色、水下淡入)。注释 339–345:WebGPU 的 PointsMaterial 即便 pointUV 已提供也校验 UV 输入,故补一个中性 uv 属性避免主/反射 pass 警告
  • 光照HemisphereLight + DirectionalLight(投影);WebGPU 下 shadow.autoUpdate=false(其 ShadowNode 无 WebGL 式 autoUpdate,否则每帧重绘图集)。updateunderwaterMix 插值光强、粒子透明度。

3.12 浮标:src/scene/buoy.js(298 行)

  • createRadialTexture:尾迹环(青色描边)与水面阴影(深色径向)两版 Canvas 纹理。
  • createWake/createSurfaceShadow:各含 GLSL 着色器版本与 WebGPU MeshBasicMaterial 版本(rendererMode==='webgpu' 走后者)。尾迹用 hash21 噪声做向外涟漪;阴影用噪声边缘柔化。
  • createBuoy:用圆柱/圆锥/圆环/十二面体拼出船体、颈圈、灯(emissive 橙)、系泊锚(沉海床)、系泊绳(随浮标高度伸缩)。
  • update(t,underwaterMix):采样 sampleOceanSurface 得目标高度,浮标位置/四元数向波面法线 slerp;尾迹/阴影随波法线并令其偏移到太阳投影处;系泊绳随浮标上下伸缩;水下时隐藏尾迹/阴影。

3.13 鱼群 AI:src/scene/fish.js(407 行)

三群(中心 + 数量 + 色调)共 45 条鱼,身体 SphereGeometry + 尾巴 BufferGeometry 两个 InstancedMesh;逐鱼存位置/速度/加速度/规模/巡航速度/相位/好奇心。

update(t, underwaterMix, camera)(193–365):

  • Boids 三定律:邻居内 separation(反距离平方)、alignmentcohesion;加向所属「学校中心」(随时间缓慢漂移)的吸引。
  • 游荡:每鱼用相位噪声加微加速度,避免呆板。
  • 相机交互(仅水下):
    • 相机逼近半径 CAMERA_NOTICE_RADIUS + min(speed*0.45,3) 内产生 panic
    • 习惯化:相机静止越久(cameraStillTime),calmness/curiosityCalmness 越高,好奇鱼恐惧下降(注释 292–294:好奇鱼习惯化 0.97,普通鱼 0.72);
    • 恐惧 = max(personalSpace, panic*(1-habituation)),驱动远离相机;
    • 好奇心环绕(好奇鱼且 curiosityCalmness>0.1):在距相机 orbitRadius 处切向环绕 + 略上抬。
  • 避顶/避底:用 sampleOceanSurface/seabedHeight 算距顶/底,过近加相应加速度。
  • 速度裁剪clampVectorLength 限制加速度/速度;panic 时提速;下限保持巡航速度的 72%(鱼不停)。
  • updateMatrices:实体化位置/四元数(朝速度方向)、尾摆频率随速度、尾摆角随速度 smoothstep

getDiagnostics(camera):汇报附近/逃逸/好奇鱼数、径向速度、平均尾频、相机速度——供视觉回归的「calm/curious/startled」行为门。

3.14 共享噪声与管线:noise-texture.js / pipeline.js

noise-texture.js(44 行):xorshift 生成 512×512 RGBA 噪声 DataTextureNoColorSpace,供着色器当 valueNoise 晶格)。WebGL/WebGPU 双端共用(WebGPU 用 valueNoiseNode 采样 .r)。

pipeline.js(26 行):见 §2.4,按 rendererMode 动态 import 对应后端场景模块。

3.15 GLSL 水体着色器:water/vertex.js + water/functions.js + water/fragment.js

water/vertex.js(277 行):构建时把 OCEAN_WAVES 等以 toFixed(8) 注入 addDomainWarp/addEnergyWave/addWave 调用模板。main() 逐顶点复现 sampleOceanSurface 的位移与法线(用 out 参数累积导数),并把平均水面平面投影到反射/折射纹理空间(vReflectionCoord/vRefractionCoord)。

water/functions.js(315 行):GLSL 工具库——

  • hash21/hash22/valueNoise(采样噪声纹理,带 -0.65 LOD 偏置保留远处细节)、fbm(4 倍频)、directionalFbm(球面方向 5 倍频,无经度缝);
  • distributionGGX/visibilitySmithGGXCorrelated/fresnelSchlick(水 F0=0.02037)——物理朝向的太阳闪光
  • addRipple/microHeight/microGradient——多尺度毛细波(按距离/屏幕足迹淡出,避免远处条纹与近处闪烁);
  • skyReflection——程序化天空反射(含风转云);
  • cloudShadowMask——海面云影;
  • foamCurlDisplacement(无散度卷曲场,使泡沫随材料流动而非直线尾迹)+ foamLifecycle(出生—扩散—破碎,cell 噪声驱动)+ foamPacketMask(揭示/侵蚀用空间 AA 而非整片淡出)。

water/fragment.js(417 行):完整水体光照合成,是项目最复杂的单文件。主干:

  1. macroGradient(顶点法线)+ detailGradient(微梯度)合成 surfaceUp
  2. Fresnel 0.025+0.975*(1-viewFacing)^5;反射方向夹 y≥0.015;
  3. 光学深度 1-exp(-column*0.052) + 深度/清晰度变化 → 水body 青→蓝;
  4. 反射/折射捕获vReflectionCoord.xy/w 投影 UV + 梯度扭曲;projectiveValidity(w) 防掠射无效;reflectedScene 混合捕获与程序天空;refractedScene 透射染色;
  5. 距离色阶(近水偏青、远水偏蓝);
  6. 波峰透射(背光 + 掠射)→ 薄浪透青;
  7. GGX 太阳闪光microfacetAlpha = 粗糙度² + min(法线方差*0.32,0.055)(屏幕方差只加宽亚像素高光,防远处闪烁);broadAlpha 宽波瓣定位闪光场,advected 多尺度遮罩拆成风成闪片;
  8. 泡沫:风平流坐标 + 断裂区场(长跨浪前短跨浪行,避免棋盘白行)+ 卷曲位移 + 多孔丝带 + 生命周期遮罩 + 跨向家族(防单一朝向);
  9. 地平吸收;
  10. 水下侧:折射方向 refract(...,1.333) 算透射天空 + 水下雾;color = mix(surfaceColor, underwaterColor, uUnderwater)注释 46–51:两侧各用物理法线着色再混色,绝不在 50% 处插值/翻转对映法线——此前跨水面会整帧跳变)。

3.16 WebGPU 水体材质(TSL):webgpu-water-functions.js + webgpu-wave-nodes.js + webgpu-water-material.js

webgpu-water-functions.js(183 行):TSL 版 valueNoiseNode/fbmNode/distributionGGX/visibilitySmith/fresnelSchlick/microGradientNode。其中 microGradientNodedFdx/dFdy 从屏幕空间导数反解世界梯度,把 5 采样微高度只算一次而非四次中心差分,省 15 次纹理读取/片元(注释 160–162)。

webgpu-wave-nodes.js(181 行):createWebGpuWavePositionNode 用 TSL Fn 复刻 GLSL addWave——域扭曲、能量、逐波位移/梯度/LOD、锐化,并把 normal/surfacePosition/waveHeight/waveSlope 写入 varyingProperty 供片元用。注释强调矩阵朝向须与 GLSL mat2(0.80,-0.60,0.60,0.80) 一致。

webgpu-water-material.js(511 行,最长后端文件):MeshBasicNodeMaterialpositionNode = createWebGpuWavePositionNode(...)colorNode = Fn(() => {...})

  • reflector({bounces:false,samples:0}) 节点反射器(target.rotation.x=-π/2 暴露 +Y 平面法线);
  • 折射用 viewportSharedTexture(viewportSafeUV(screenUV - gradient*...)) 直接屏采,无需离屏 RT;
  • 片元逻辑与 GLSL waterFragmentShader 逐段对应:水body、云影、透射、反射混合、距离色阶、波峰透射、GGX 闪光(grazingResolutionCompensation 按相机高度补偿 WebGL MSAA 保留更多亚像素太阳面元)、泡沫、地平吸收;
  • If(underwaterNode<0.98)If(underwaterNode>0.02) 两支各自着色,最后 mix(surfaceOutput, underwaterOutput, mediumBlend)——与 GLSL 的「两侧各着色再混」一致,规避零向量归一化。注释 96–99/342–347/382–386 多处点明「与 WebGL 路径完全对齐」。

3.17 UI 控件:ui/loading.js / render-cap.js / renderer-toggle.js

  • loading.js:加载控制器,setStage 单调推进 --loader-progress CSS 变量与状态文案;paintrequestAnimationFrame 让出以刷新 UI;revealis-readyfail 标记错误态。
  • render-cap.js:绑定 <select data-render-cap-select>changepersistRenderCapPreference 并回调 onChange;揭示控件。
  • renderer-toggle.js:两个按钮绑定 webgpu/webgl,点击写 ?renderer=location.assign 重载(不改 URL 历史);状态文案区分 WebGPU 直连 / WebGPU 管线 WebGL2 降级 / WebGL2。

3.18 工程脚本与配置

package.jsontype:module;脚本 dev/build/build:pages/check:pages/quality:test/performance:profile/performance:sustain-4k/visual:test;依赖仅 three,开发依赖 playwright+viteengines.bun>=1.3.11

playwright.config.js:视觉测试目录 ./tests/visual;自动探测 Chrome/Edge 可执行路径;--use-angle=d3d11(Win)/ swiftshader(其他);CI 视口 960×540、本地 1600×900;webServer 启动 bun run dev --port 4173timeout 60sworkers:1fullyParallel:false(每用例独立浏览器进程,防 GPU 状态泄漏)。

scripts/profile-performance.mjs:对 1080p/1440p/4K × webgl/webgpu(可按环境变量筛选)各起全新浏览器跑「profile live renderer timings」,合并 10 秒时序报告到 visual-results/runtime-timings.json

scripts/profile-native-4k-sustain.mjs:原生 4K 持续门(PERFORMANCE_TARGET_FPS 默认 144)。对每个 view 起独立进程跑「sustains native 4K at target refresh」,轮询 native-4k-sustain.json 直到 complete;失败/卡死时用 taskkill /T /F(Win)或进程组 SIGTERM/SIGKILL 清理。判定:画布/质量修订不变、吞吐 ≥99.5% 目标、缺失回调 <0.5%、帧间隔尾/CPU/GPU 预算不超、无浏览器错误。

scripts/check-pages-build.mjs:校验 dist/index.html 不引用 /src/、所有资源路径以 /beautiful-water/ 为作用域且文件存在——保证 Pages 部署自包含。

scripts/smoke-pages.mjs:部署后最多 30 次、间隔 10s 探活 PAGES_URL,校验 <title>Open Water</title> 与 JS bundle 可 HEAD 访问。

scripts/check-linear-history.ps1:检查 Git 线性历史(Windows 沙箱 apply_patch 失败时 AGENTS.md 提及的兜底)。

3.19 测试支撑:tests/

视觉回归tests/visual/ocean.visual.spec.js,~1100 行结构):定义 20 个命名视角(views:sun-facing / 各种 grazing / foam-detail 及其 forming/transition/replaced / cross-sun / opposite-sun / top-down / wide / repetition / underwater / 鱼 calm/startled),冻结 time=11.75setView 渲染并截 20 张命名回归图;4 个相机/时间预设经 WebGL 与 WebGPU 各渲,写单帧 + 并排合成图,强制像素误差与感知相似度上限;CI 用 ci-scene 子集(7 视图)硬 2 分钟上限。screenshot-metrics.jsmeasureNormalizedImageDifference 用 Canvas 2D 算归一化绝对像素误差;wave-field.jsmeasureWaveFieldCorrelation 在 81×81 网格采样 sampleOceanSurface 并做互相关移位搜索,防止周期性平铺在三个仿真时刻悄悄回归(README 所述「displacement-field correlation」)。

性能/HUD/可持续规格performance-hud.spec.js(点性能面板复制 15s 报告内容)、runtime.performance.spec.jsruntime.sustain.spec.js(原生 4K 持续门,写 native-4k-sustain.json 含各判定 criteria)。

性能单测tests/performance/*.test.js,bun:adaptive-quality/gpu-frame-timer/presentation-monitor/render-cap/renderer-preference/runtime-diagnostics/workflow-budget):例如 renderer-preference.test.js 验证默认 WebGPU、接受 ?renderer=webgpu|webgl、忽略非法值回退 WebGPU。这些单测让「GPU 感知自适应」等核心逻辑可独立验证,不依赖浏览器。


四、优点、不足与潜在应用领域

4.1 优点(量化 + 工程)

  1. 双后端同源、可验证一致性。同一波谱常量驱动 GLSL/TSL/CPU 三处;renderer-parity 视觉门 + wave-field 相关性检测直接守住「WebGPU 与 WebGL 看起来一样、且波形不_periodic tiling_」这一最易回归的点。
  2. 性能工程极其成熟
    • 三级指标分离(呈现 FPS / 渲染 FPS / GPU 帧时),并诚实声明「渲染 FPS ≠ 物理面板刷新率」;
    • GPU 感知自适应(PR #15):用真实 GPU p95 帧时调像素预算,且记下 learnedMaximumPixelBudget 防在档位边界反复横跳
    • 反射/折射 RT multisample:0、WebGPU 折射单采样 + 半分辨率光柱(省 75% pass)、microGradientNode 用导数反解省 15 次纹理读、距离/屏幕足迹 LOD 淡出毛细波;
    • 原生 4K@144Hz 持续门(吞吐 ≥99.5%、缺失回调 <0.5%)——把「性能」做成可证伪的硬测试而非口头声称。
  3. 交互与视觉打磨细:浮标随浪浮沉倾斜并作枢轴;鱼群有分离/对齐/聚合 + 好奇心环绕 + 习惯化(静止相机降低恐惧)+ 逃逸;跨水面连续 underwaterMix 混合(特意规避 50% 处法线翻转跳变);泡沫为输送式多孔丝带而非整片白。
  4. 零素材、纯代码生成,MIT 授权,易 fork、易部署(无大资源下载)。
  5. 工程闭环完整:开发(Vite)→ 视觉回归(Playwright)→ 性能剖析 / 4K 门 → Pages 部署(含 PR 预览与冒烟)→ 工作量标定样本。CI 三条工作流分工清晰,CI 硬 2 分钟上限控制成本。
  6. 无障碍与健壮性prefers-reduced-motion 降速;WebGPU 失败自动降级 WebGL2 并记录原因;dispose 在 WebGPU 4K 下避免无限阻塞;多屏/隐藏页/页面卸载均有采样重置。

4.2 不足(诚实指出)

  1. 双后端维护成本翻倍。GLSL 与 TSL 两套水体/天空/光柱/微梯度/GGX 必须手写对齐,代码量显著上升(webgpu-water-material.js 511 行基本是 water/fragment.js 的翻版)。任何水体算法改动都需改两处并靠视觉回归兜底——一旦回归门松弛,两端就会悄悄分叉。
  2. 算法「调参即艺术」、难复现。波谱 14 条、breakingZone 多重 sin 组合、泡沫 lifecycle/curl/filaments、GLSL 内大量魔法常数(0.02037 F0、0.052 吸收、0.65 云光等)均为作者经验性手调,注释虽解释「为什么」但没有系统性的物理标定或可配置参数表。换一组海域/天气需大量试错。
  3. WebGPU 路径依赖 Three.js TSL 内部 APIreflectorviewportSharedTexturevaryingPropertyQuadMesh 等属 three/webgpu 较新的节点系统,版本敏感(three@0.185.1 锁定);升级 Three 风险高。
  4. 仅「浅海 + 单一晴天 + 固定太阳」场景。无昼夜循环、无坏天气、无多太阳角度 UI、无船只/人物等其他物体;sunDirection 写死。作为「作品」足够,作为「可配置海洋 SDK」尚早。
  5. 测试强依赖本地 Chromium/Chrome/Edge(非沙箱 SwiftShader 下 WebGPU 行为不同);native-4k-sustainperformance:profile 明确标注「hardware-dependent, local only」,CI 仅跑降分辨率视觉矩阵,4K 吞吐不进 CI——生产环境的 4K 性能未经持续集成保证
  6. 无组件级单元测试覆盖渲染;性能单测只覆盖 core/ 纯逻辑,GLSL/TSL 正确性完全靠视觉回归(阈值若调宽易漏回归)。
  7. 文档偏 README 级:缺少架构图、模块依赖、参数对照表;docs/ 是工作量报告而非开发文档。

4.3 潜在应用领域

领域 具体用途
游戏 / 实时渲染 作为开放世界浅海、港口、湖泊的水体与水下渲染参考实现;Boids 鱼群可直接用于生态点缀
图形教学 极佳的「GLSL ↔ TSL 双后端对照」教材;GGX 闪光、投影反射/折射、泡沫生命周期、体积光柱均有可运行范例
数字孪生 / 仿真 海岸、码头、海上风电/钻井平台周边水面可视化;浮标/系泊可作监测浮标原型
产品展示 / 官网 全屏电影感背景(汽车/旅游/地产落地页),零素材易嵌入
AI / Agent 可视化 window.__WATER_HARNESS__ 把场景暴露为确定性渲染接口,可被 AI 智能体驱动做「讲解/截图/对比」
性能工程样本 GPU 感知自适应、帧率上限、三级 FPS 计量、4K 持续门可作为 WebGL/WebGPU 应用的性能基准范式
工作量估算研究 与 EffortHours 配合,作为「AI 辅助 vs 传统人力」产能标定的可复现案例

五、总结

beautiful-water 是一个以极致工程纪律打磨的电影级程序化浅海 WebGL/WebGPU 演示:它在零外部素材的前提下,用一套共享波谱驱动 GLSL 与 TSL 双后端,覆盖海面反射折射、浅水透射、太阳闪光、输送式泡沫、浮标、三群具「好奇心/习惯化/逃逸」的鱼群,以及连续跨水面的水下世界;并围绕它建立了「视觉回归 + GPU 感知自适应 + 4K 持续门 + Pages 部署 + 工作量标定」的完整闭环。其最值得借鉴的是把性能做成可证伪测试、把测量局限写进产品文案的诚实工程文化;主要代价是双后端算法需手工对齐的高维护成本大量经验性魔法常数带来的可复现难度

解读基准main 分支 commit 830c7f3(2026-08-23,Merge PR #15「fix/gpu-aware-adaptive-quality」)。
范围声明:已逐行/逐段覆盖全部产品核心源码(src/main.jssrc/core/*src/scene/*src/shaders/*src/ui/*、入口与配置);工程测试与脚本(scripts/*tests/*src/style.cssdocs/*)按模块级说明,并对最关键的工具函数(screenshot-metrics.jswave-field.js)与结构(视觉回归视图矩阵、性能单测)做了引用级拆解。未运行项目(无 bun/Chromium 执行环境),所有分析基于静态源码与仓库元数据。