Skip to content

ADR-211: 水面功能开关体系 — 噪声原语消重与效果分组开关化

  • 状态: ✅ Part 1/2/3 完成(大波·焦散·水下效果开关全部落地;平面反射由 ADR-151 reflectionMode='none' 接管,豁免独立开关,详见 Part 3)(2026-07-30)
  • 日期: 2026-07-30
  • 相关: ADR-062(水面系统)、ADR-092(统一贴图工厂 / 平面反射引擎)、ADR-115(水面细节波与焦散)、ADR-137(EnvState 单一源 Schema)、ADR-212(命名 vs 功能审计——噪声原语已随其归位 @/core/math/hash-noise.ts
  • 源码锚点: core/math/hash-noise.ts(噪声原语,ADR-212 归位后落点)、scene/env/env-water.tsscene/env/env-caustics.tsscene/env/env-terrain.tscore/env-state-schema.tsmenus/env-water-levels.ts

背景

env-water.ts 达 1536 行,是 env/ 模块最大文件。审查发现水面并非"一块水",而是把所有"跟水沾边"的效果都往里倒:大波/小波(Gerstner)、LOD、平面镜像反射、交互涟漪、地面涟漪、焦散、水下雾、程序化法线纹理。两类问题突出:

  1. 算法重复:整数哈希 + 值噪声在三处各写一遍——env-water.ts_hash2d/_valueNoise(细节法线纹理)、env-caustics.ts 内联 hash2(Voronoi 焦散)、env-terrain.tshash2/valueNoise(地形 FBM)。三者本质是同一族 xorshift-mix 哈希,仅实现细节不同(| 0 vs Math.imul)。

  2. 效果不可独立开关:水面各子效果只能靠 slider 拖到 0 来"关",辨识度差。调试某类视觉问题(如高频波光干扰)时无法一键隔离。地面早已用 folder + headerToggle 模式支持分组开关,水面缺失。

需要澄清的一个认知陷阱:曾设想"让焦散纹理直接承担细节波生成"以消重,但焦散是 Voronoi 网状亮纹(emissiveTexture),细节波是 fBm 法线扰动(bumpTexture),是两种不可互换的图案。真正的重复只在底层噪声原语,纹理语义必须各自保留。

决策

分两步演进:先消除噪声原语重复(打地基),再以小波为试点建立效果分组开关体系(立范式),后续逐组推广。

Part 1 — 噪声原语消重(✅ 已完成)

新建纯叶子模块 scene/env/env-noise.ts,集中三个零依赖纯函数:

符号签名用途
hash2(ix, iy, seed=0) → [0,1]确定性整数哈希,Math.imul 实现避免 32 位溢出丢精度
hash2v(ix, iy, seed=0) → [number, number]二元组哈希,供 Voronoi 需两个独立随机偏移的场景
valueNoise(x, y, seed=0) → [0,1]四角哈希 + smoothstep 双线性插值
  • 哈希实现统一到 Math.imul(取 env-terrain 最健壮版本),seed 默认 0 兼容 water/caustics 的无 seed 调用。
  • 消费点改造:env-water.ts 删本地 _hash2d/_valueNoise 改 import valueNoiseenv-caustics.ts 删内联 hash2hash2venv-terrain.ts 删本地实现,import 后 re-export hash2/valueNoise(保护 env-ground.ts 与既有测试的外部引用)。
  • 代价(已知且接受):统一到 Math.imul 后,水面细节法线纹理与焦散纹理的具体像素图案改变(同质感的另一张噪声图,非画质退化)。terrain 侧本就是 Math.imul,地形图案零变化。

Part 2 — 小波效果开关试点(✅ 已完成)

以"小波/细节波"为试点,复用地面 folder + headerToggle 模式,建立可复制的效果开关范式:

  • Schemacore/env-state-schema.ts 新增 smallWaveEnabled: { type: 'boolean', default: true, group: 'water' }group: 'water' 使其自动进 _WATER_KEYSgetEnvKeys('water') 派生),改动即触发水面重同步,无需手工维护守卫数组(规避 _WATER_KEYS 历史坑)。
  • Menumenus/env-water-levels.tssmallWaveHeight slider 封装进独立 folder,folder 头挂 headerToggle: { bind: 'env.smallWaveEnabled' }
  • Shader 门控env-water.ts_syncWaterUniforms 改为 mat.setFloat('smallWaveHeight', (state.smallWaveEnabled ?? true) ? (state.smallWaveHeight ?? 1.0) : 0)。关闭时送 0 振幅,小波法线扰动归零,水面呈纯净反射面;字段缺失 ?? true 兜底为开启,旧存档行为不变。复用现有 smallWaveHeight uniform,不新增 shader 变量。
  • UI 行为约束:开关只控 shader 输出,不联动置灰/禁用 slider。跟随地面既有模式,slider 保持独立可见可调——不引入前端隐藏状态,避免额外 bug 面。
  • 字段保留smallWaveHeight 是 shader 实时消费的活字段,是本开关控制的目标振幅,保留不动。

Part 3 — 效果分组开关推广(✅ 已完成)

沿用 Part 2 的 folder + headerToggle + shader 门控 范式逐组落地,每组新增一个 xxxEnabled 字段(group: 'water',自动进 _WATER_KEYS):

效果组字段门控点关闭语义状态
大波(Gerstner)bigWaveEnabled_syncWaterUniformsbigWaveHeight 0 振幅水面趋于平静镜面
焦散causticEnabled_syncWaterUniformsuCausticIntensity 0 强度关闭水底光斑
水下效果underwaterEnabledupdateUnderwaterTransition_underwaterTarget 与门控相与相机潜入水下也不触发雾/色调/灯光衰减
平面反射⏭ 豁免
  • 大波 / 焦散:与小波同构,_syncWaterUniforms 内对目标 uniform 做 (state.xxxEnabled ?? true) ? 原值 : 0 门控;folder 头挂对应 headerToggle。复用现有 uniform,不新增 shader 变量。
  • 水下效果:门控点不在 _syncWaterUniforms(水下是相机穿越水面的后处理体验,非常驻材质 uniform),而在 updateUnderwaterTransition_underwaterTarget = (state.underwaterEnabled ?? true) && camY < waterLevel。关闭时视作非水下目标,复用现有过渡回退逻辑平滑退出(雾/色调/灯光归位),不硬切避免闪跳。underwater folder 头挂 headerToggle: { bind: 'env.underwaterEnabled' }
  • 平面反射豁免(不新增独立开关):Part 3 初版设想「提升 reflectionQuality 的 off 档为开关」,但反射已由 ADR-151 的 reflectionModenone/planar/ssr/probe/hybrid,跨 ground/water/reflection 三组的全局系统)统一管辖,reflectionMode='none' 即为关闭反射的权威开关。若在水面 folder 再塞独立 reflectionEnabled headerToggle,会与 reflectionMode 形成双源(违反「状态来源唯一」,见 AGENTS 反模式表)。故豁免,反射开关继续由 reflectionMode 承担。
  • UI 行为约束:所有开关只控效果输出,不联动置灰/禁用 slider,跟随地面既有模式。字段缺失 ?? true 兜底为开启,旧存档行为不变。

备选方案

  • 焦散承担次要波生成:已否决。Voronoi 亮纹与波面法线是不同图案,强行复用会使水面失去高频波光、观感变差。属"为消重牺牲功能"陷阱。
  • 保留两版哈希(|0Math.imul)以保图案不变:已否决。会在叶子模块留两个同类哈希,未彻底消重;统一到 Math.imul 的图案变化在同质感范围内可接受。
  • 关闭时置灰 slider:已否决。地面现有模式也不置灰,为调试引入前端隐藏联动机制易出 bug,收益低。

影响

  • 噪声算法从 3 份实现收敛为 1 份单一真相源(@/core/math/hash-noise.ts),env-terrain re-export 保证外部引用零破坏。
  • 水面建成完整的效果分组开关体系:小波(试点)+ 大波 + 焦散 + 水下效果共 4 组可独立开关;平面反射由 ADR-151 reflectionMode 统一承担(不双源)。
  • 新增 smallWaveEnabled / bigWaveEnabled / causticEnabled / underwaterEnabled 四个字段贯穿 schema → state 默认构造 → menu → shader/过渡门控,并同步契约/mock 测试。
  • 知识卡 docs/knowledge/env-water.md 更新:source_files 纳入 @/core/math/hash-noise.ts,不变量区补四组门控说明。

测试

  • 相关单测全过:scene/env-water.test.ts(含小波 3 项 + 大波/焦散 4 项 + 水下开关 2 项门控测试:underwaterEnabled 为 false→潜入也不激活 / 缺失→兜底开启)、env-state.test.tsbindings/app.contract.test.ts(共 68 项)。
  • 改动文件无新增类型错误(全量 tsc --noEmit 的遗留错误均与本改动无关,属在途其他工作基线)。
  • npm run check:docs 无 ERROR 级漂移,新增 underwaterEnabled 字段通过 group 校验(“所有字段均有 group 或已豁免”)。

备注

  • 本 ADR 与 ADR-210(iblIntensity/globalBrightness 重命名)无关,两者恰好在同一工作区并行,但属独立决策链。
  • 噪声原语已归位 @/core/math/hash-noise.ts(ADR-212 落实):Part 1 新建的 scene/env/env-noise.ts 已按 ADR-212-naming P1「纯数学工具不放 env/」搁置搬到 @/core/math/hash-noise.ts。核实现状:env-water.ts / env-caustics.ts / env-terrain.ts 三个消费方均已直引 @/core/math/hash-noise;过渡用的 scene/env/env-noise.ts re-export barrel 已确认零消费方并删除。
  • 命名迁移零漂移核实(接力 ADR-212):ADR-212 对本模块相关字段的重命名(planarReflectBlend→planarReflectionBlendcloudsEnabled→cloudEnabled 等)已全链路收口:前端 src 仅存新名,旧名限于 _bridge/env-bridge.ts_migrators 与 Go app.goLegacy* fallback(读旧盘兼容,照 ADR-210 范式)。已验证无消费残留、tsc 零错、相关单测 66 项全过。