Appearance
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.ts、scene/env/env-caustics.ts、scene/env/env-terrain.ts、core/env-state-schema.ts、menus/env-water-levels.ts
背景
env-water.ts 达 1536 行,是 env/ 模块最大文件。审查发现水面并非"一块水",而是把所有"跟水沾边"的效果都往里倒:大波/小波(Gerstner)、LOD、平面镜像反射、交互涟漪、地面涟漪、焦散、水下雾、程序化法线纹理。两类问题突出:
算法重复:整数哈希 + 值噪声在三处各写一遍——
env-water.ts的_hash2d/_valueNoise(细节法线纹理)、env-caustics.ts内联hash2(Voronoi 焦散)、env-terrain.ts的hash2/valueNoise(地形 FBM)。三者本质是同一族 xorshift-mix 哈希,仅实现细节不同(| 0vsMath.imul)。效果不可独立开关:水面各子效果只能靠 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改 importvalueNoise;env-caustics.ts删内联hash2改hash2v;env-terrain.ts删本地实现,import 后 re-exporthash2/valueNoise(保护env-ground.ts与既有测试的外部引用)。 - 代价(已知且接受):统一到
Math.imul后,水面细节法线纹理与焦散纹理的具体像素图案改变(同质感的另一张噪声图,非画质退化)。terrain 侧本就是Math.imul,地形图案零变化。
Part 2 — 小波效果开关试点(✅ 已完成)
以"小波/细节波"为试点,复用地面 folder + headerToggle 模式,建立可复制的效果开关范式:
- Schema:
core/env-state-schema.ts新增smallWaveEnabled: { type: 'boolean', default: true, group: 'water' }。group: 'water'使其自动进_WATER_KEYS(getEnvKeys('water')派生),改动即触发水面重同步,无需手工维护守卫数组(规避_WATER_KEYS历史坑)。 - Menu:
menus/env-water-levels.ts将smallWaveHeightslider 封装进独立 folder,folder 头挂headerToggle: { bind: 'env.smallWaveEnabled' }。 - Shader 门控:
env-water.ts的_syncWaterUniforms改为mat.setFloat('smallWaveHeight', (state.smallWaveEnabled ?? true) ? (state.smallWaveHeight ?? 1.0) : 0)。关闭时送 0 振幅,小波法线扰动归零,水面呈纯净反射面;字段缺失?? true兜底为开启,旧存档行为不变。复用现有smallWaveHeightuniform,不新增 shader 变量。 - UI 行为约束:开关只控 shader 输出,不联动置灰/禁用 slider。跟随地面既有模式,slider 保持独立可见可调——不引入前端隐藏状态,避免额外 bug 面。
- 字段保留:
smallWaveHeight是 shader 实时消费的活字段,是本开关控制的目标振幅,保留不动。
Part 3 — 效果分组开关推广(✅ 已完成)
沿用 Part 2 的 folder + headerToggle + shader 门控 范式逐组落地,每组新增一个 xxxEnabled 字段(group: 'water',自动进 _WATER_KEYS):
| 效果组 | 字段 | 门控点 | 关闭语义 | 状态 |
|---|---|---|---|---|
| 大波(Gerstner) | bigWaveEnabled | _syncWaterUniforms 送 bigWaveHeight 0 振幅 | 水面趋于平静镜面 | ✅ |
| 焦散 | causticEnabled | _syncWaterUniforms 送 uCausticIntensity 0 强度 | 关闭水底光斑 | ✅ |
| 水下效果 | underwaterEnabled | updateUnderwaterTransition 中 _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 的reflectionMode(none/planar/ssr/probe/hybrid,跨ground/water/reflection三组的全局系统)统一管辖,reflectionMode='none'即为关闭反射的权威开关。若在水面 folder 再塞独立reflectionEnabledheaderToggle,会与reflectionMode形成双源(违反「状态来源唯一」,见 AGENTS 反模式表)。故豁免,反射开关继续由reflectionMode承担。 - UI 行为约束:所有开关只控效果输出,不联动置灰/禁用 slider,跟随地面既有模式。字段缺失
?? true兜底为开启,旧存档行为不变。
备选方案
- 焦散承担次要波生成:已否决。Voronoi 亮纹与波面法线是不同图案,强行复用会使水面失去高频波光、观感变差。属"为消重牺牲功能"陷阱。
- 保留两版哈希(
|0与Math.imul)以保图案不变:已否决。会在叶子模块留两个同类哈希,未彻底消重;统一到Math.imul的图案变化在同质感范围内可接受。 - 关闭时置灰 slider:已否决。地面现有模式也不置灰,为调试引入前端隐藏联动机制易出 bug,收益低。
影响
- 噪声算法从 3 份实现收敛为 1 份单一真相源(
@/core/math/hash-noise.ts),env-terrainre-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.ts、bindings/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.tsre-export barrel 已确认零消费方并删除。 - 命名迁移零漂移核实(接力 ADR-212):ADR-212 对本模块相关字段的重命名(
planarReflectBlend→planarReflectionBlend、cloudsEnabled→cloudEnabled等)已全链路收口:前端 src 仅存新名,旧名限于_bridge/env-bridge.ts的_migrators与 Goapp.go的Legacy*fallback(读旧盘兼容,照 ADR-210 范式)。已验证无消费残留、tsc 零错、相关单测 66 项全过。