Skip to content

ADR-115: 风格化水体竞品调研与波光粼粼增强方向

状态: P1+P2+P3+P4 已完成(2026-07-22);P5 海洋波澜专项已实施。 日期: 2026-07-22 背景: 评审了某竞品(风格化水体)的水系统截图,其设计思路与我方自研 Gerstner 物理水面(ADR-062 / frontend/src/scene/env/env-water.ts)完全不同:对方以「少参数、强预设、重法线」的美术水体路线实现"波光粼粼",而我方以「多参数、强物理、重波形」的可控路线实现。本文档记录参数映射、技术拆解、差异对比与可借鉴方向,供后续增强水面视觉效果时拍板。 范围: 仅水面视觉风格与交互复杂度对比;不涉及反射 RT 重构(ADR-062)、地面反射(ADR-114)等已立项工作。 外部依赖状态审核(2026-07-17): 已对体积云(ADR-113 / env-clouds.ts)与地面增强(ADR-114 / env-ground.ts)近期更新做集成兼容性评估。结论:两项更新均不实质影响本 ADR 实施前提;发现 1 项🟡P3 云-反射间隙(见§九),非回归,记录供 P1 实施前参考。


一、竞品水系统参数映射

截图 UI 仅 9 项,语义明确、预设驱动。逐条翻译为我方技术术语:

竞品参数技术含义我方对应项
类型:河流预设模板(河流/海洋/池塘),一键切换法线贴图 + 波长集WATER_PRESETS(平静/涟漪/海浪/风暴/热带,env-water.ts:794
高度 0.400水面 Y 高度waterLeveltypes.ts:436
波纹 3.000小尺度高频法线扰动强度(制造细碎反光)无直接对应;我方用 waterWaveHeight 统一控制 Gerstner 波高
大波 30.00大尺度低频波浪/位移强度waterWaveHeighttypes.ts:440),但对方量程更大、偏美术夸张
折射最大距离 0.350折射/透射有效距离(水面下景物扭曲范围)无直接对应
吸收距离 5.000水体颜色按 Beer-Lambert 吸收的距离接近 waterFogDensity / waterTransparency
焦散 0.500水下焦散强度我方程序化焦散,强度固定 uCausticIntensity=0.15env-water.ts:347),未暴露
吸收倍数 2.000颜色吸收系数(越深越偏水色)无直接对应
有效范围 0.100波浪/效果影响范围或地平线衰减范围接近 waterSizetypes.ts:441

关键观察

  • 竞品把"波纹/大波"拆成两层独立尺度(高频法线 + 低频波形),而非我方单一 waterWaveHeight
  • 截图 2(类型:海洋)显示水面与远处地平线融合,说明存在地平线淡出/无限水体处理。
  • 星空被反射在水面,说明反射源是cubemap 环境贴图(场景 environmentTexture),而非平面反射 RT。

二、"波光粼粼"技术拆解

竞品水面亮点并非来自复杂波形,而是以下四项组合:

  1. 法线贴图扰动(Normal Map)

    • 两张/多张法线贴图以不同速度滚动,制造高频表面起伏。
    • 法线变化使太阳光/环境光在微小 fragment 上产生快速变化的镜面高光 —— 这是"波光"主来源。
  2. Sun Glitter / 高光闪烁

    • fragment shader 中按 normal/lightDir/viewDir 计算窄域 specular highlight。
    • 叠加时间噪声或随机纹理,使亮点闪烁而非稳定一片。
  3. 反射 + Fresnel

    • 边缘用 Fresnel 反射天空/环境;中心看透射 + 焦散。
    • 平面反射 RT 可反射场景物体,形成动态高光。
  4. 焦散(Caustics)

    • 水下光斑滚动,强化"水在动"的感知。

2.5 地平线扩展与星空反射

新增截图(类型:海洋)显示两个关键特征:

  1. 水面延伸至地平线

    • 通常不是单张无限大平面,而是:
      • 远距离 LOD 降级:近处高细分,远处用低细分或 billboard 平面。
      • 地平线雾/颜色融合:远处水面与天空颜色通过雾统一,消除硬边。
      • 径向边缘淡出:类似我方 groundEdgeFade,但作用于水面。
    • 实现成本低,且能改善"方块水面"感。
  2. 反射星空贴图

    • 水面反射的是环境 cubemap(scene.environmentTexture),即天空盒/星空球。
    • 这是环境反射,我方当前在 WATER_FRAG_SRC 中已采样 envTexture(当 ENV_TEXTURE define 启用时),但受限于:
      • 需要环境贴图已设置(ADR-024 ReflectionProbe 提供)。
      • 我方水面默认可能未启用/不明显。
    • 对方效果强的原因:天空-水面-雾统一 color grading,反射率与 Fresnel 调得高。
  3. 水光联动

    • 天空颜色/时间变化自动驱动水面颜色与反射。
    • 我方可通过让 waterColor/waterFogColor 从天空色派生,或加强 envIntensity 调制来实现类似联动。

结论:波光粼粼 = 高频法线 + specular glitter + 动态反射 + 焦散; 地平线/星空感 = 无限水体 + 环境 cubemap 反射 + 天空-水面统一颜色分级


三、与我方系统的核心差异

维度竞品系统我方系统
设计哲学风格化、美术预设驱动物理程序化、参数可精细微调
波浪实现法线贴图/高度图叠加自研 Gerstner 顶点/片元着色器(WATER_VERT_SRC/WATER_FRAG_SRC
参数数量9 个,语义明确20+,覆盖颜色/泡沫/水下/反射/Fresnel 等
波光粼粼靠法线贴图 + specular 直接出靠 Gerstner 法线 + 涟漪 + 焦散,缺高频 glitter
焦散单一滑块暴露程序化生成,强度固定 0.15,未暴露 UI
反射默认开启的简单混合可选平面反射 RT(高/中/低/关,ADR-062)
地平线扩展无限水体 + 边缘雾融合waterSize 有限方形,远处硬边
环境反射/星空联动cubemap 环境反射强,天空-水色统一已采样 envTexture,但强度受环境贴图与 envIntensity 限制
泡沫UI 未暴露独立阈值/强度/透明度三参数
水下UI 未暴露完整雾 + 色调 + 色散后处理(updateUnderwaterTransition

四、可借鉴方向(代码级建议)

4.1 在 shader 加入高频法线扰动层(P1,待资源解锁)

4.1.1 程序化法线贴图生成方案(零资源依赖)

采用 createCanvasTexture 在运行时生成 512×512 双通道噪声法线图,避免引入外部 bundled 资源。生成算法:

typescript
// env-water.ts — ensureNormalTexture(scene, waterColor)
// 缓存为单例,随 waterColor 变化时重建
function generateNormalMap(size: number = 512): HTMLCanvasElement {
    const canvas = document.createElement('canvas');
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext('2d')!;
    const img = ctx.createImageData(size, size);

    // 多层 Simplex/Value noise 叠加(octaves),
    // 用中心差分从高度图推导法线,避免直接存 RGB 法线的色彩偏移问题
    const heights = new Float32Array(size * size);
    for (let y = 0; y < size; y++) {
        for (let x = 0; x < size; x++) {
            let h = 0, amp = 1, freq = 1;
            for (let oct = 0; oct < 4; oct++) {
                h += valueNoise(x * freq / size, y * freq / size) * amp;
                amp *= 0.5; freq *= 2;
            }
            heights[y * size + x] = h;
        }
    }
    // 中心差分求法线
    for (let y = 0; y < size; y++) {
        for (let x = 0; x < size; x++) {
            const xl = heights[y * size + ((x - 1 + size) % size)];
            const xr = heights[y * size + ((x + 1) % size)];
            const yl = heights[((y - 1 + size) % size) * size + x];
            const yr = heights[((y + 1) % size) * size + x];
            const nx = (xl - xr) * 0.5;  // 水平梯度 → 法线 X
            const ny = (yl - yr) * 0.5;  // 垂直梯度 → 法线 Z(Y是上)
            const nz = 1.0;
            const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
            const i = (y * size + x) * 4;
            img.data[i]     = Math.floor((nx / len * 0.5 + 0.5) * 255);  // R → X
            img.data[i + 1] = Math.floor((ny / len * 0.5 + 0.5) * 255);  // G → Z
            img.data[i + 2] = Math.floor((nz / len * 0.5 + 0.5) * 255);  // B → Y
            img.data[i + 3] = 255;
        }
    }
    ctx.putImageData(img, 0, 0);
    return canvas;
}

备选方案(画布质量不足时回退):直接在 shader 内用 snoise() 函数计算噪声法线,省去纹理采样但 ALU 开销更高。

4.1.2 Shader 集成与参数

WATER_FRAG_SRC 中叠加两张不同尺度、不同速度的法线采样(制造视差流动感):

glsl
uniform sampler2D uNormalTex;
uniform float uNormalStrength;    // 整体强度,默认 0.3
uniform float uNormalTiling1;     // 第一层平铺,默认 0.1
uniform float uNormalTiling2;     // 第二层平铺,默认 0.3
uniform float uNormalSpeed1;      // 第一层滚动速度,默认 0.05
uniform float uNormalSpeed2;      // 第二层滚动速度,默认 -0.08

// —— 在 normal 计算(Gerstner 之后,涟漪之前)插入 ——
vec2 nUV1 = vWorldPos.xz * uNormalTiling1 + time * uNormalSpeed1;
vec2 nUV2 = vWorldPos.xz * uNormalTiling2 + time * uNormalSpeed2;
vec3 n1 = texture2D(uNormalTex, nUV1).rgb * 2.0 - 1.0;
vec3 n2 = texture2D(uNormalTex, nUV2).rgb * 2.0 - 1.0;
vec3 detailNormal = normalize(n1 + n2 * 0.5);  // 第二层权重减半

// 与 Gerstner 法线混合(去重策略,见 4.1.3)
float gerstnerScale = 0.7;  // Gerstner 法线衰减系数,避免大尺度波峰过陡
normal = normalize(
    normal * gerstnerScale +
    vec3(detailNormal.x * uNormalStrength, 1.0, detailNormal.y * uNormalStrength)
);

需配套新增 EnvState 字段:

  • waterNormalStrength: number(默认 0.3,范围 0–1.5)
  • 其余 tiling/speed 参数暂不暴露 UI,在预设中固定,降低面板复杂度

4.1.3 Gerstner 去重策略

问题:我方 Gerstner 波法线已包含全频段贡献(包括高频段),再叠法线贴图会导致高频段 double-count,出现"波浪过密 / 高光发糊"。

分层方案(推荐):

  • Gerstner 负责大尺度波向与形状(保留 70% 法线强度,即 gerstnerScale = 0.7),衰减其高频贡献
  • 法线贴图仅接管高频细节uNormalStrength 控制,默认 0.3)
  • 实测校准:在 waveHeight = 1.0(海浪预设)下,将 Gerstner 衰减系数从 1.0 逐步降到 0.6–0.8,找到"波形感保留 + 波光细节充足"的平衡点

简化判定:若 uNormalStrength == 0,则 gerstnerScale 自动回到 1.0(关闭细节层时完全恢复 Gerstner 原貌),保证关闭新功能后行为零变化。

4.1.4 涟漪系统兼容性(莫尔纹防护)

当前 shaders/water.frag.glsl:125-137 涟漪法线在 Gerstner 之后、焦散之前写入 normal,新增法线层插入涟漪之前,顺序为:

Gerstner 法线 → 高频细节法线(新增) → 涟漪法线 → 计算反射/折射

加权融合策略:涟漪激活区域自动压低细节法线强度,避免两层高扰动叠加产生莫尔纹:

glsl
// 在涟漪计算之后,对细节法线强度做涟漪加权
float rippleInfluence = clamp(abs(rippleSum) * 2.0, 0.0, 1.0);
// rippleSum 大的地方,细节法线减弱,让涟漪主导高频
float detailBlend = mix(uNormalStrength, uNormalStrength * 0.2, rippleInfluence);

简化版(更省性能):直接按 rippleSum 衰减细节法线,涟漪中心区域细节层权重降至 20%,边缘区域保持 100%。实测若莫尔纹不明显,可直接跳过防护,两层独立叠加。

4.2 加入 Sun Glitter 项(P1,待资源解锁)

在水面漫反射计算之后、泡沫混合之前,叠加一层随视角和光照变化的闪烁高光。

4.2.1 实现方案

噪声策略:使用 shader 内 hash() 函数生成伪随机值,无需额外纹理采样(零资源依赖)。hash 函数基于屏幕空间坐标 + 时间,保证每帧闪烁但空间上连续不跳变。

glsl
// 简易 hash 函数,返回 0-1 随机值
float hash12(vec2 p) {
    vec3 p3  = fract(vec3(p.xyx) * 0.1031);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

// —— 插入位置:diffuse 之后、泡沫混合之前(shaders/water.frag.glsl 第 123 行之后)——
uniform float uGlintStrength;    // 整体强度,默认 0.4
uniform float uGlintPower;       // 高光锐利度,默认 96
uniform float uGlintScale;       // 噪声尺度,默认 80.0
uniform float uGlintSpeed;       // 闪烁速度,默认 2.0

// 以世界坐标 + 时间驱动噪声,模拟水面微闪
vec2 glitterUV = vWorldPos.xz * uGlintScale + time * uGlintSpeed;
float noiseVal = hash12(floor(glitterUV));
// 叠加两层不同尺度噪声,让闪烁既有大颗粒也有细闪
float noiseVal2 = hash12(floor(glitterUV * 3.7 + 13.0));
float glitterNoise = mix(noiseVal, noiseVal2, 0.3);

// 计算高光:用 reflectDir 与 lightDir 的夹角,窄域 specular
float spec = pow(max(dot(reflectDir, normalize(lightDir)), 0.0), uGlintPower);
// 乘以现有 diffuse 作为 mask:仅在迎光面闪烁
float glitterMask = diff * spec;
// 噪声调制:在高光基础上叠加随机衰减,制造"跳动"感
float glitter = glitterMask * (0.6 + 0.8 * glitterNoise) * uGlintStrength;

color += lightColor * glitter;

4.2.2 参数定义

uniform 变量EnvState 字段默认值范围说明
uGlintStrengthwaterGlintStrength0.40–2.0整体闪烁强度
uGlintPower—(预设固定)9632–256高光锐利度,值越大光斑越小越亮
uGlintScale—(预设固定)80.020–200噪声颗粒大小
uGlintSpeed—(预设固定)2.00.5–5.0闪烁动画速度

UI 仅暴露 waterGlintStrength 一个滑块(放在"基础参数"或"高级"中待定),其余参数在预设中固定,降低调节复杂度。

4.2.3 与法线细节层的关系

架构性观察:glitter 本质上是高频法线扰动在 specular 上的体现。如果 §4.1 的法线细节层质量足够好,reflectDir 本身已经包含了高频变化,自然会产生 specular 闪烁。

实施建议:先做 §4.1 法线层,观察现有 specular(diff 项 + 法线扰动)是否已经产生足够的"波光粼粼"感。如果自然闪烁已达标,glitter 项可:

  • 方案 A:省略不做,减少 shader 复杂度
  • 方案 B:保留但默认 uGlintStrength = 0,作为额外微调手段

本 ADR 暂保留 glitter 项作为 P1 备选,实测后决定是否启用。

光照数据可用性(已核实)shaders/water.frag.glsl:19uniform vec3 lightDir; 已存在,并由 mat.setVector3('lightDir', dirLight.direction)env-water.ts:336,无方向光时 fallback (-0.5,-1,-0.5):339)传入。Sun Glitter 可直接复用 lightDir无需新增光照数据通路——原「可能未传入方向光方向」担忧已化解。

4.3 焦散强度暴露给用户(P2,优先排期)

env-water.ts:347mat.setFloat('uCausticIntensity', 0.15) 改为读 state.causticIntensity,并在 EnvStatetypes.ts)与水面 UI(env-feature-levels.tsbuildWaterLevel)增加滑块。

4.3.1 参数规范

项目
EnvState 字段causticIntensity: number
默认值0.15(与当前硬编码一致,零行为变化)
范围0 – 0.5,步长 0.01
滑块位置env:water:basic folder 内,波高 之后,动画速度 之前
标签env.causticIntensity(焦散强度)
图标lucide:sun

4.3.2 预设同步

5 个 WATER_PRESETS 均补充 causticIntensity 字段,按预设风格差异化取值:

预设causticIntensity理由
平静 calm0.1水清澈但平静,焦散弱
涟漪 ripple0.15默认值,中等强度
海浪 ocean0.2波浪大,水下光斑更明显
风暴 storm0.25高反差,强焦散增强戏剧感
热带 tropical0.2热带水透亮,焦散鲜明

同时更新 buildWaterPresetEnvStateapplyWaterPresetToCurrent 同步写入该字段,确保预设切换后值持久化到 envState(参考现有 rippleNormalStrength 的预设同步模式,env-water.ts:928-932)。

4.3.3 状态链路变更清单(P2 焦散部分)

变更点文件说明
新增字段core/types.tscausticIntensity: number,默认 0.15
读取状态scene/env/env-water.ts _syncWaterUniformsmat.setFloat('uCausticIntensity', state.causticIntensity) 替换硬编码
绑定列表scene/env/env-bridge.ts确认 causticIntensity 在绑定列表中
预设定义scene/env/env-water.ts WATER_PRESETS5 个预设补充 causticIntensity 字段
预设映射scene/env/env-water.ts buildWaterPresetEnvState输出 causticIntensity
预设应用scene/env/env-water.ts applyWaterPresetToCurrent写入材质 uniform
UI 滑块menus/env-feature-levels.ts buildWaterLevel在 basic folder 内增加 slider
i18n语言文件补充 env.causticIntensity 键值
序列化兼容场景加载旧场景无此字段时取默认值 0.15

4.4 UI 预设化(P2,优先排期)

顶层以预设 chip 驱动,默认仅显示核心 6 项参数;其余高级参数收进折叠组,降低面板复杂度。复用现有 addSliderRow/headerToggle/folderWATER_PRESETS 体系,不引入新 UI 范式。

4.4.1 面板分层结构

buildWaterLevel() 重构后的层级(由上至下):

水面板
├── 预设 chip 行(5 个:平静 / 涟漪 / 海浪 / 风暴 / 热带)
├── 基础参数「展开」(默认展开,6 项核心)
│   ├── 水面高度(waterLevel)
│   ├── 水面范围(waterSize)
│   ├── 波高(waterWaveHeight)
│   ├── 动画速度(waterAnimSpeed)
│   ├── 焦散强度(causticIntensity) ← 新增
│   └── 水色(waterColor)
├── 颜色与雾「折叠」(默认折叠)
│   ├── 透明度(waterTransparency)
│   ├── 雾色(waterFogColor)
│   └── 雾密度(waterFogDensity)
├── 泡沫「折叠」(默认折叠)
│   ├── 泡沫阈值(foamThreshold)
│   ├── 泡沫强度(foamIntensity)
│   └── 泡沫透明度(foamOpacity)
├── 高级参数「折叠」(默认折叠,原 basic 中非核心项 + 所有杂项)
│   ├── Fresnel 偏移(fresnelBias)
│   ├── Fresnel 幂次(fresnelPower)
│   ├── Fresnel Alpha 影响(fresnelAlphaInfluence)
│   ├── 漫反射强度(diffuseStrength)
│   ├── 环境强度(ambientStrength)
│   ├── 泡沫过渡范围(foamTransitionRange)
│   ├── 涟漪法线强度(rippleNormalStrength)
│   ├── 涟漪光泽强度(rippleGlintStrength)
│   ├── 焦散颜色1(causticColor1)
│   ├── 焦散颜色2(causticColor2)
│   ├── 焦散滚动 X(causticScrollX)
│   ├── 焦散滚动 Y(causticScrollY)
│   ├── 雾不透明度影响(waterFogOpacityInfluence)
│   └── 水面翻转(waterFlip)
├── 反射「折叠」(默认折叠,已有)
└── 水下效果「折叠」(默认折叠,已有)

4.4.2 设计原则

  • 核心参数 ≤ 8 项:默认展开的"基础参数"控制在 6–8 项,一屏可见,减少滚动
  • 预设 chip 置顶:5 个预设按钮(preset-chip 样式,28px 高)放在面板最顶端,作为"快速入口"
  • 折叠组用 folder 组件:复用现有 folder kind(带箭头展开/收起),不新增 headerToggle
  • 功能分组清晰:颜色/泡沫/高级/反射/水下,每类 3–7 项,既不太碎也不太满
  • P1 新参数接入位置waterNormalStrengthwaterGlintStrength(P1 做了之后)放入"基础参数",放在"焦散强度"之后,保持核心参数区 8 项以内

4.4.3 现有 UI 迁移映射

当前位置迁移后位置说明
env:water:presets custom面板最顶端(独立行)预设 chip 从 folder 中移出,置顶
env:water:colorF folder颜色与雾 folder合并 waterColor 到基础参数,雾色/密度单独成组
env:water:basic folder基础参数 folder(默认展开)仅保留 6 项核心,其余迁出
高级散项高级参数 folderFresnel/扩散/环境/涟漪/焦散色等统一收纳
反射 folder反射 folder(不变)保持原状
水下 folder水下效果 folder(不变)保持原状

4.5 无限水面 + 地平线雾融合(P3,地平线视觉)

解决我方水面"方块硬边"问题,让水面延伸到视觉地平线。

4.5.1 两种实现方案对比

维度方案 A:独立径向淡出(推荐)方案 B:复用场景雾
原理Shader 内计算水面像素到相机的水平距离,按 horizonStart/horizonEnd 与天空色混合并淡出 alpha直接复用 scene.fogMode / scene.fogColor,让水面在远处自动融入全局雾
控制粒度水面独立可控(独立参数、独立颜色)与全局雾绑定,水面不能单独调
视觉一致性需手动匹配天空色,可能有断层与全局雾天然一致,零色差
实现复杂度中:新增 2–3 个 uniform + shader 计算低:几乎不用改 shader(Babylon 内置雾自动生效)
性能开销略高(每 fragment 一次距离计算 + smoothstep)零额外开销(Babylon 内置)
适用场景场景无全局雾 / 水面需特殊效果场景已开雾且水面雾效与全局一致
风险天空色匹配不准时出现硬边全局雾模式变化(如关闭雾)时水面硬边重现

推荐方案 A:独立径向淡出。理由:

  1. 我方当前场景默认不开启全局雾,方案 B 无依托
  2. 水面作为独立子系统,参数应独立可控,不与全局设置强耦合
  3. 天空色可从环境贴图或天空系统读取,匹配成本不高

4.5.2 方案 A 具体实现

几何扩展

  • 把现有 3 级 LOD(meshHigh/meshMid/meshLowenv-water.ts:580-584)的 meshLow 范围扩大,或新增第 4 级远景平面 meshFar(低细分、大尺寸)
  • 远景平面仅作颜色填充,不计算精细波形(顶点 shader 中降低波高贡献)

Shader 实现

glsl
uniform float uHorizonStart;     // 地平线淡出起始距离,默认 waterSize * 0.7
uniform float uHorizonEnd;       // 地平线淡出结束距离,默认 waterSize * 0.95
uniform vec3 uHorizonColor;      // 地平线融合色(取自天空底部或 fog 色)

// 在 fragment shader 末尾(gl_FragColor 之前)插入
float radialDist = length(vWorldPos.xz - cameraPosition.xz);
float horizonFade = 1.0 - smoothstep(uHorizonStart, uHorizonEnd, radialDist);
color = mix(uHorizonColor, color, horizonFade);
alpha *= horizonFade;  // 配合透明队列,远处渐隐

天空色来源

  • 优先读取 skyColorBot(天空系统底部色,若 ADR-026 环境系统已提供)
  • fallback:从 scene.environmentTexture 的最底部像素采样平均色
  • 再 fallback:用 waterFogColor 代替

4.5.3 参数字段

EnvState 字段默认值范围说明
waterHorizonFade0.00–1地平线淡出强度,0=关闭(硬边),1=完全淡出到地平线
horizonStart/End 不暴露 UI,按 waterSize 自动计算(70% 开始,95% 结束)

仅暴露一个强度滑块,放入"高级参数"或独立"地平线"折叠组,避免参数过多。

4.6 天空-水面颜色联动(P3,氛围统一)

让水面颜色随天空/时间变化自动调整,实现"天光水色"的氛围统一。

4.6.1 天空色来源

优先级来源获取方式说明
1(优先)天空系统底部色skyColorBot / envState.skyColorBottom若 ADR-026 环境系统已提供天空色状态字段,直接读取
2(备选)环境贴图平均色scene.environmentTexture 最底层 mip 采样取 cubemap 下半部分像素平均值,近似水平面反射色
3(兜底)雾色近似waterFogColor无环境贴图时用水体自身雾色代替

推荐实现方式:在 TS 端(_syncWaterUniforms 或独立的 _updateSkyBlendColor 函数)计算好天空基准色后,作为 uniform 传入 shader,避免 shader 内做复杂的 mip 采样。

4.6.2 混合公式

在 shader 中混合自定义水色与天空色:

glsl
uniform vec3 uSkyBlendColor;    // TS 端计算好的天空基准色
uniform float uSkyColorBlend;   // 混合比例,0=完全自定义,1=完全跟随天空

// 在 waterColor 使用前混合
vec3 finalWaterColor = mix(waterColor, uSkyBlendColor, uSkyColorBlend);
vec3 finalFogColor = mix(waterFogColor, uSkyBlendColor * 0.8, uSkyColorBlend);
// 用 finalWaterColor / finalFogColor 替换后续的 waterColor / waterFogColor

TS 端更新时机

  • 环境贴图变化时(scene.environmentTexture 变更)
  • 天空色状态变化时(envState.skyColor* 变更)
  • waterSkyColorBlend 参数变化时

更新频率控制:天空色变化缓慢(时间过渡),无需每帧更新,可在每 5 帧或变化超过阈值时更新一次,降低 CPU 开销。

4.6.3 参数字段

EnvState 字段默认值范围说明
waterSkyColorBlend0.00–1天空-水面颜色联动强度,0=不联动(保持用户自定义),1=完全跟随天空
  • UI 位置:"颜色与雾" folder 内,透明度滑块之后
  • 默认值 0:确保不破坏用户已调好的水色,用户须主动开启
  • 图标:lucide:sunrise

4.7 "波纹/大波"双层尺度拆分(P4,贴近竞品手感)

将单一 waterWaveHeight 拆为两个独立参数,让用户分别控制大尺度波浪与小尺度波纹,贴近竞品"波纹/大波"的双层操控手感。

4.7.1 现有 Gerstner 4 层结构分析

当前 vertex shader(water.vert.glsl:16-18)硬编码 4 层 Gerstner 波:

频率振幅速度语义
00.150.300.7大波(低频、高振幅)
10.200.250.9大波(中低频)
20.250.200.5小波(中高频)
30.300.151.2小波(高频、低振幅)

所有 4 层共享单一 waveHeight uniform 缩放振幅:a = WAVE_AMP[i] * waveHeight

4.7.2 拆分方案

核心思路:将 4 层按频率分为两组,各有独立的振幅缩放参数:

参数控制范围语义
bigWaveHeightGerstner 层 0,1(低频)大尺度波浪——涌浪、海浪主形态
smallWaveHeightGerstner 层 2,3(高频)小尺度波纹——表面细碎起伏
waterNormalStrengthP1 法线细节层(fragment shader)(P1 已实现,此处仅示意尺度层级,P4 不改动此字段)微观波光——最细粒度的法线扰动

⚠️ 范围界定waterNormalStrength 是 ADR-115 P1 已落地的字段(env-water.ts:479 uDetailNormalStrength),列于此仅为呈现「大→中→微」三级尺度体系的完整性。P4 新增的仅为 bigWaveHeightsmallWaveHeight 两个字段,不触碰 waterNormalStrength

三者形成大→中→微三级波浪尺度体系:

大波 (bigWaveHeight)     → Gerstner 低频 2 层 → 顶点位移 + 法线
小波 (smallWaveHeight)   → Gerstner 高频 2 层 → 顶点位移 + 法线
波光 (waterNormalStrength) → 程序化法线贴图     → 仅法线扰动(无位移)

4.7.3 Vertex Shader 改动

当前(water.vert.glsl:34-44):

glsl
for (int i = 0; i < WAVE_COUNT; i++) {
    float a = WAVE_AMP[i] * waveHeight;  // 单一缩放
    ...
}

改为(保留 waveHeight uniform 作为全局乘子,不删除,避免 _syncWaterUniforms:438 的旧 setFloat('waveHeight') 悬空写入不存在的 uniform):

glsl
uniform float waveHeight;       // 保留:全局振幅乘子(旧 uniform,向后兼容)
uniform float bigWaveHeight;    // 大波振幅缩放(默认 1.0)
uniform float smallWaveHeight;  // 小波振幅缩放(默认 1.0)

for (int i = 0; i < WAVE_COUNT; i++) {
    // 层 0,1 用 bigWaveHeight,层 2,3 用 smallWaveHeight,再乘全局 waveHeight
    float h = (i < 2) ? bigWaveHeight : smallWaveHeight;
    float a = WAVE_AMP[i] * h * waveHeight;
    ...
}

waveHeight 保留决策:审核采纳「保留」方案而非「删除」。理由:① 旧 uniform 不悬空,_syncWaterUniforms:438 现有 mat.setFloat('waveHeight', ...) 语义仍有效(作为全局乘子);② 三者语义正交清晰——waveHeight 全局缩放、bigWaveHeight/smallWaveHeight 分频段缩放;③ 旧场景 waterWaveHeight 可直接继续驱动 waveHeight,new 两字段默认 1.0,效果零回归。

零回归保障:当 bigWaveHeight == smallWaveHeight == 1.0 时,a = WAVE_AMP[i] * 1.0 * waveHeight,与当前 a = WAVE_AMP[i] * waveHeight 完全等价。旧场景仅存储 waterWaveHeight 时,两新字段取默认 1.0,视觉不变。

4.7.4 状态链路

变更点文件说明
#变更点文件
-----------------
1新增字段(必填core/types.ts EnvState
2默认值种入core/state.ts
3🔴 Uniform 每帧同步(关键写入点)env-water.ts _syncWaterUniforms(当前第 438 行 setFloat('waveHeight') 旁)
4绑定列表env-bridge.ts
5Go 后端internal/app/app.go
6🔴 重新生成 wails 绑定
7WaterPreset 类型扩展env-water.ts
8🔴 预设定义(防 NaN)env-water.ts WATER_PRESETS
9🔴 预设映射(带兜底)env-water.ts buildWaterPresetEnvState
10预设应用env-water.ts applyWaterPresetToCurrent

NaN 兜底三道防线小结state.ts 种默认值(源头)→ buildWaterPresetEnvState 双重 ?? 兜底(预设漏配)→ _syncWaterUniforms 写入前 ?? 1.0(终极守卫)。三道任缺一,都可能在特定路径下让水面消失。

4.7.5 旧字段兼容

waterWaveHeight 保留,语义升级为「全局振幅乘子」(对应 vert shader 中保留的 waveHeight uniform,见 §4.7.3),不再是"被忽略":

  • 新场景bigWaveHeight/smallWaveHeight 控分频段振幅,waterWaveHeight 继续作为全局乘子(a = WAVE_AMP[i] * h * waveHeight)。三者叠乘,语义正交。
  • 旧场景:仅有 waterWaveHeight 时,反序列化兜底:
    typescript
    // 反序列化侧(场景加载)兜底,确保 EnvState 必填字段有值
    state.bigWaveHeight   = raw.bigWaveHeight   ?? 1.0;
    state.smallWaveHeight = raw.smallWaveHeight ?? 1.0;
    // waterWaveHeight 缺失时另有既有默认,此处不涉
    两新字段默认 1.0 → a = WAVE_AMP[i] * 1.0 * waterWaveHeight,与旧版逐字等价,零回归
  • 序列化:新场景直接独立写入 bigWaveHeight + smallWaveHeight + waterWaveHeight 三字段,不再回写 (big+small)/2(原方案有损:如海浪预设 big=1.5,small=0.8 回读 1.15,与设计偏差明显)。旧版本打开新场景时仅读 waterWaveHeight(全局乘子),波形为「全局缩放后的旧四层」,属已知可接受降级——见 §5.2.5 验收补充。

4.7.6 预设差异化取值

预设bigWaveHeightsmallWaveHeight理由
平静 calm0.30.5大波弱、小波中等——平静水面有细碎涟漪但无涌浪
涟漪 ripple0.61.0中等大波、强小波——活跃的波纹表面
海浪 ocean1.50.8强大波、中小波——大浪翻涌,细碎波纹被大浪掩盖
风暴 storm2.00.5极强大波、弱小波——巨浪主导,高频细节被风压平
热带 tropical0.81.2中大波、强小波——温暖海域的活泼波光

4.7.7 UI 变更

基础参数 folder 中,将原来的"波高"滑块拆为两行。为遵守 §4.4.2「核心参数 ≤8 项」原则(解决原方案 9 项与该原则的冲突),将 P1 的 waterNormalStrength/waterGlintStrength 两项下沉至"高级参数"折叠组,基础区维持 8 项:

├── 基础参数「展开」(8 项,符合 §4.4.2 ≤8 上限)
│   ├── 水面高度(waterLevel)
│   ├── 水面范围(waterSize)
│   ├── 大波高度(bigWaveHeight)    ← 原"波高"拆分
│   ├── 小波纹(smallWaveHeight)    ← 原"波高"拆分
│   ├── 动画速度(waterAnimSpeed)
│   ├── 焦散强度(causticIntensity)
│   ├── 水色(waterColor)
│   └── (P1 参数下沉,见下方"高级参数")

├── 高级参数「折叠」
│   ├── 波纹细节(waterNormalStrength)  ← P4 从基础区下沉
│   ├── 波光闪烁(waterGlintStrength)   ← P4 从基础区下沉
│   └── …(原有高级散项)

冲突消解:原 §4.7.7 草案基础区达 9 项,与 §4.4.2「核心参数 ≤8 项」自相矛盾。审核决策采「方案②:P1 参数下沉高级组」而非放宽上限——理由:bigWaveHeight/smallWaveHeight 是高频调节项应置核心区,而 waterNormalStrength/waterGlintStrength 属"微调波光"低频操作,下沉高级组更符合"核心区精简"初衷。基础区最终 8 项,一屏可见,与 §4.4.2 一致。

📌 需同步修订 §4.4.1 面板结构图:将 waterNormalStrength/waterGlintStrength 从基础参数区移入高级参数区,保持两处文档一致(P4 实施 PR 内一并调整)。

4.7.8 i18n

5 语种各新增 2 个 key:

Keyzh-CNenjakozh-TW
env.bigWaveHeight大波高度Big Wave大波の高さ큰 파 높이大波高度
env.smallWaveHeight小波纹Small Wave小波の揺らぎ작은 파도小波紋

4.8 "海洋波澜"专项(P5,贴近竞品海洋风格)

4.8.1 根因(承接 §4.7 诊断)

P4 调的是 「几何振幅」轴(Gerstner 顶点位移的 bigWaveHeight/smallWaveHeight),而竞品的「海洋波澜」来自 「低频滚动法线 × 天光反射」轴。两轴不同源,故 P4 放大波高后 ocean 只是「平静水面放大版」,无连绵涌浪光带。

核心公式:波澜观感 = 大尺度滚动法线(吃天光)→ 粼光带。我方三处缺口:

#缺口证据
1缺低频法线层detail normal 仅两层:uDetailNormalTiling1=0.5/uDetailNormalTiling2=1.5(env-water.ts:705-706 实际生效值;shader 注释 0.1/0.3 已过时),全为细碎尺度(格 ≈2 / 0.67 单位)。中─大尺度法线带为空,仅有近平面细碎波光
2Gerstner 大波组波长偏短层 0/1 WAVE_FREQ=0.15/0.20(water.vert.glsl:18)→ 波长 ≈42/31 单位;waterSize 默认 50(env-state-schema.ts:174)→ 水面内约 1.2 个波,读成「单穹顶」而非连绵涌浪
3天光混合被压低ocean/tropical 预设 waterSkyColorBlend 仅 0.25/0.3(env-water.ts:1316,1352),滚动法线未被点亮

4.8.2 措施 A:新增低频滚动法线层(核心)

water.frag.glsl 现有两层 detail normal(n1/n2)之上新增第三层低频法线采样 n3,复用同一张 uDetailNormalTex(程序化生成,零资产依赖),极低 tiling 制造大尺度滚动光带:

glsl
// 现有(§4.1.2,细碎尺度)
vec2 nUV1 = vWorldPos.xz * uDetailNormalTiling1 + wind * wavePhase * uDetailNormalSpeed1; // tiling 0.5
vec2 nUV2 = vWorldPos.xz * uDetailNormalTiling2 - wind * wavePhase * uDetailNormalSpeed2; // tiling 1.5
vec3 n1 = texture2D(uDetailNormalTex, nUV1).rgb * 2.0 - 1.0;
vec3 n2 = texture2D(uDetailNormalTex, nUV2).rgb * 2.0 - 1.0;

// P5 新增:低频层(格 ≈ 12–25 单位,制造大尺度滚动光带)
vec2 nUV3 = vWorldPos.xz * uLowFreqNormalTiling + wind * wavePhase * uLowFreqNormalSpeed; // tiling ≈ 0.04
vec3 n3 = texture2D(uDetailNormalTex, nUV3).rgb * 2.0 - 1.0;

vec3 detailNormal = normalize(n1 + n2 * 0.5 + n3 * uLowFreqNormalStrength);
  • 新增 uniform:uLowFreqNormalTiling(默认 ≈0.04)、uLowFreqNormalStrength(默认 ≈0.15)、uLowFreqNormalSpeed(默认 ≈0.05)。
  • 接入 _syncWaterUniforms(env-water.ts setFloat 三处),并列入 uniform 列表(与 :878-879,892 同范式)。
  • 状态链路最小化uLowFreqNormalStrength 作为 预设驱动的可选字段 lowFreqNormalStrength?: number(沿用 §4.7 的 ?? 兜底范式),默认 0.15;_syncWaterUniforms 写入 ?? 0.15,预设映射 ?? 0.15(与 waterHorizonFade ?? 0 同型,env-water.ts:1186)。不新增必填字段,避免触发 P4 的 NaN 三道防线复杂度。
  • uLowFreqNormalTiling/uLowFreqNormalSpeed 为 shader 常量(不进 state),默认固化即可。

4.8.3 措施 B:调整 Gerstner 大波组频率配比

water.vert.glsl:18 当前层 0/1 WAVE_FREQ=0.15/0.20。P5 初值建议下调以拉长波长、降坡度:

glsl
const float WAVE_FREQ[4] = float[4](0.07, 0.11, 0.25, 0.3); // 层 0/1 拉长,层 2/3 不变

⚠️ 方向校准风险:纯降 freq 会减少水面波数(波长 42→90 单位,waterSize=50 内从 ~1.2 个波降至 ~0.55 个)。若视觉校准发现 ocean 仍偏「稀穹顶」而非「连绵涌浪」,应反向——保持或略提 freq(如 0.18/0.22)+ 适当增 WAVE_AMP[0/1],以在 waterSize 内形成 2–3 道涌浪。措施 B 终值以 ocean/tropical 预设视觉校准为准,初值仅作起点。

补充:坡度 = f·a,降 freq 即降坡度 → 更平缓,与「连绵」不冲突;「波数少」才是需监控的核心指标。

4.8.4 措施 C:ocean/tropical 预设提天光混合

当前 waterSkyColorBlend 预设值:ocean=0.25、tropical=0.3(env-water.ts:1316,1352)。P5 将这两档提到 0.5–0.7,让滚动法线吃进天空色 → 粼光浮现:

预设当前 waterSkyColorBlendP5 建议理由
ocean 海浪0.250.6强天光混合点亮低频滚动法线,出连绵粼光涌浪
tropical 热带0.30.55暖色海水 + 天光,活泼波光
calm / ripple / storm0.15 / 0.2 / 0.15不变非海洋 mood,保持冷静 / 风暴基调

4.8.5 预设差异化取值表(P5 增量)

仅列 P5 新增/改动字段,未列字段沿用 §4.7.6:

预设lowFreqNormalStrength备注
calm 平静0.0无大尺度波澜,保持冷静
ripple 涟漪0.08轻微滚动
ocean 海浪0.35强低频滚动光带
storm 风暴0.15风暴主靠几何巨浪,法线滚动次之
tropical 热带0.3暖海活泼滚动

uLowFreqNormalTiling 全局常量 ≈0.04,不随预设变)

4.8.6 UI / i18n

  • lowFreqNormalStrength 进「高级参数」折叠组(与 P1 的 waterNormalStrength 同组),滑块范围 0–0.5,默认 0.15。
  • i18n 新增键 env.lowFreqNormalStrength(大尺度波光 / Low-Freq Glint / 大波の光帯 / 큰 파 광택 / 大尺度波光)。
  • 措施 B(vert 常量)、措施 C(预设值)不新增 UI,仅在 shader / 预设层生效。

4.8.7 NaN 与回归防护

  • 零回归lowFreqNormalStrength 缺失时 ?? 0.15 兜底,且 calm 预设显式 0.0 → 该预设退化为纯 P4 效果,无意外波澜。
  • 不引入必填 state 字段:仅 uLowFreqNormalTiling/uLowFreqNormalSpeed 为 shader 常量,lowFreqNormalStrength 为可选字段 + ?? 兜底,不触发 P4 的 NaN 三道防线(无需 Go struct 改动 / 绑定重生成,因字段可选且前端 EnvState 已用可选链读取)。
  • 若后续决定暴露 uLowFreqNormalTiling 为可调字段:须走 §4.7.4 的 NaN 三道防线(state 种默认 + 预设双重 ?? + 同步 ?? 兜底)+ Go struct + npm run generate:bindings。本 ADR 暂将其固化为常量,规避该风险。

五、实施路径与排序(优化版)

按「成本-收益-可行性」重新排序:P2 成本最低、收益明确、零资源依赖 → 立即排期P1 视觉收益高但被法线资源门槛与 double-count 风险卡住 → 先解决资源与去重再实施;P3/P4 留作后续评估。

优先级阶段内容可行性成本收益状态
P2焦散强度 UI 暴露 + 水面 UI 预设化折叠✅ 已完成 (e54d67e)
P1shader 高频法线扰动层 + Sun Glitter 项✅ 已完成 (e54d67e)
P3无限水面 + 地平线雾融合 + 天空-水面颜色联动✅ 已完成 (df7db01)
P4"波纹/大波"双层尺度拆分✅ 已完成(2026-07-22 确认)
P5海洋波澜专项(低频法线层 + 波长配比 + 天光混合)✅ 已完成

P1 解锁前置条件(已全部解决,2026-07-17)

  1. 法线资源来源:程序化 512×512 Value noise 法线贴图(env-water.ts:regenerateDetailNormalTexture
  2. Gerstner 去重gerstnerScale = uDetailNormalStrength > 0 ? 0.7 : 1.0,分层清晰
  3. 涟漪系统兼容性:法线细节层在涟漪之前叠加,实测无莫尔纹

P4 实施前置条件

  1. Vertex shader 改动water.vert.glsl 新增 bigWaveHeight / smallWaveHeight uniform,保留 waveHeight 作全局乘子,改为 a = WAVE_AMP[i] * h * waveHeight(见 §4.7.3)
  2. NaN 三道防线state.ts 种默认 1.0 + buildWaterPresetEnvState 双重 ?? 兜底 + _syncWaterUniforms 写入前 ?? 1.0(见 §4.7.4,缺任一都可能致水面消失)
  3. 旧字段兼容:两新字段默认 1.0,旧场景 a = WAVE_AMP[i] * 1.0 * waterWaveHeight 与旧版逐字等价,零回归
  4. wails 绑定重生成:EnvState 新增 2 字段须同步 Go struct 并 npm run generate:bindings
  5. 预设重测:5 个预设的大波/小波差异化取值需视觉校准

5.1 P2 完整状态链路变更清单

P2(焦散强度 + UI 预设化)涉及的所有变更点,实施时逐项打勾:

5.1.1 状态与类型

#变更点文件说明
1新增 causticIntensity 字段core/types.ts EnvState类型 number,默认 0.15
2FNV method ID 更新若状态结构影响序列化 hash,需重新计算(通常自动生成)

5.1.2 水面子系统

#变更点文件说明
3读取 causticIntensityscene/env/env-water.ts _syncWaterUniformsmat.setFloat('uCausticIntensity', state.causticIntensity) 替换硬编码 0.15
4WaterPreset 类型扩展scene/env/env-water.ts新增可选字段 causticIntensity?: number
55 个预设补充值scene/env/env-water.ts WATER_PRESETS按 §4.3.2 表格取值
6预设映射函数scene/env/env-water.ts buildWaterPresetEnvState输出 causticIntensity
7预设应用函数scene/env/env-water.ts applyWaterPresetToCurrent写入材质 uniform
8绑定列表确认scene/env/env-bridge.ts确认 causticIntensityWATER_BINDINGS

5.1.3 UI 层

#变更点文件说明
9新增焦散强度滑块menus/env-feature-levels.ts buildWaterLevel放在 basic folder 内,波高之后
10UI 结构重构menus/env-feature-levels.ts buildWaterLevel按 §4.4.1 分层结构重组:预设置顶 + 基础参数展开 + 颜色与雾/泡沫/高级/反射/水下折叠
11i18n 新增键语言文件env.causticIntensity(焦散强度)
12预设 chip 样式确认menus/env-feature-levels.ts使用 preset-chip class,28px 高,6px gap

5.1.4 序列化与兼容

#变更点文件说明
13场景加载兼容场景反序列化逻辑旧场景无 causticIntensity 时取默认值 0.15
14场景保存场景序列化逻辑新字段自动纳入保存

5.2 验收标准

每项功能实施完成后,须满足以下标准方可视为通过:

5.2.1 P2 焦散强度

  • [ ] 滑块从 0 滑到 0.5,水下光斑亮度线性变化,无跳变
  • [ ] 默认值 0.15 与当前硬编码效果完全一致(零视觉回归)
  • [ ] 切换 5 个预设时,焦散强度同步变化为预设值
  • [ ] 保存场景后重新加载,焦散强度值保持不变
  • [ ] 旧场景文件加载后,焦散强度自动取默认值 0.15,无报错

5.2.2 P2 UI 预设化

  • [ ] 默认展开的"基础参数"≤8 项,一屏可见(1080p 分辨率下)
  • [ ] 预设 chip 置顶,点击切换预设后所有参数同步更新
  • [ ] 颜色与雾 / 泡沫 / 高级参数 / 反射 / 水下 5 个折叠组均可独立展开收起
  • [ ] 折叠状态不随预设切换而改变(用户展开的保持展开)
  • [ ] 移动端(窄屏)布局不溢出,可滚动

5.2.3 P1 法线扰动 + Glitter(待实施时验证)

  • [ ] 阳光入射角 30°–60° 时,水面出现明显闪烁高光点,随视角移动而流动
  • [ ] waterNormalStrength = 0 时,水面效果与当前版本完全一致(零回归)
  • [ ] 涟漪触发时,涟漪区域无莫尔纹(无规则横向/纵向条纹)
  • [ ] 切换预设时,法线强度与 glitter 强度同步变化
  • [ ] FPS 下降 ≤ 5%(1080p 中端 GPU 下,从 60fps 降至 ≥ 57fps)

5.2.4 P3 地平线淡出 + 天空联动(已实施,2026-07-17 验证通过)

  • [x] waterHorizonFade = 0 时,水面边缘为硬边(与当前一致)
  • [x] waterHorizonFade > 0 时,水面边缘平滑过渡到天空色,无明显断层
  • [x] 相机移动时,淡出区域跟随平滑移动,无闪烁
  • [x] waterSkyColorBlend = 0 时,水色与当前一致(零回归)
  • [x] waterSkyColorBlend = 1 时,水色与天空底部色一致

5.2.5 P4 波纹/大波双层拆分(已实施,2026-07-22 确认)

  • [ ] bigWaveHeight == smallWaveHeight == 1.0 时,水面效果与当前 waveHeight 完全一致(零回归)
  • [ ] bigWaveHeight = 0, smallWaveHeight = 1.0 时,仅高频小波纹可见,无大浪起伏
  • [ ] bigWaveHeight = 1.5, smallWaveHeight = 0 时,仅大浪翻涌,表面光滑无细碎波纹
  • [ ] 切换 5 个预设时,大波/小波高度同步变化为预设值
  • [ ] 旧场景加载后,bigWaveHeight / smallWaveHeight 自动兜底为 1.0,波形 = 全局 waterWaveHeight 缩放的旧四层(零回归)
  • [ ] 基础参数区 8 项一屏可见(1080p);P1 的 waterNormalStrength/waterGlintStrength 已下沉至高级参数组
  • [ ] 【NaN 防护回归】任一预设漏配 bigWaveHeight/smallWaveHeight 时,切到该预设水面仍正常渲染(退回 1.0 全局缩放),不出现水面消失
  • [ ] 【旧版本兼容】新场景(含 big/small 字段)用旧版本客户端打开,水面正常渲染(旧版仅读 waterWaveHeight 全局乘子),波形为已知可接受降级——非崩溃、非黑面
  • [ ] 修改 waterWaveHeight 全局乘子时,大波与小波振幅同步等比缩放(验证三者叠乘语义)

5.2.6 P5 海洋波澜专项(待实施时验证)

  • [ ] ocean 预设下水面出现中─大尺度滚动光带(连绵粼光),非单穹顶
  • [ ] lowFreqNormalStrength = 0(calm 预设)时水面退化为 P4 效果,无意外波澜(零回归)
  • [ ] 措施 B 校准后 ocean 水面在 waterSize 内呈 2–3 道涌浪;若初值(降 freq)致波过稀,已按 §4.8.3 反向校准并复核方向
  • [ ] ocean/tropical 预设 waterSkyColorBlend 提升至 0.6/0.55,天光粼光明显浮现
  • [ ] 切换 5 个预设时,低频法线强度同步变化为 §4.8.5 取值
  • [ ] FPS 影响 ≤ 3%(仅增 1 次 512² 纹理采样,属 P1 同类开销,由全局 qualityProfile 接管)

六、风险与取舍

等级风险说明缓解 / 决策
🟢 已化解法线与 Gerstner 叠加 double-countP1 实施时已解决:gerstnerScale = 0.7 衰减 Gerstner 高频贡献,法线贴图接管高频细节
🟢 已化解法线扰动与涟漪系统冲突(莫尔纹)P1 实施后实测无莫尔纹,法线细节层在涟漪之前叠加,涟漪激活时自然衰减
🟢 已化解Sun Glitter 光照方向依赖lightDir uniform 已存在并传入,可直接复用
🟢 P2焦散强度 UI 暴露后用户误调已设上限 max = 0.5,默认值 0.15
🟡 P3天空-水面颜色联动破坏自定义水色默认值 0(不联动),用户须主动上调
🟢 已化解性能额外法线贴图采样(单张 512² detail normal,1 次纹理采样)+ glitter(shader 内联 hash(),无贴图读取)增加少量 fragment ALU/采样开销性能由全局 qualityProfile 统一调控,无需水体专属开关getQualityenv-water.ts:145)将 qualityProfile 映射为平面反射 RT 分辨率(high 2048 / medium 1024 / low 512,ADR-151 反射引擎,resolutionMapenv-water.ts:144);低端档反射 RT 降为 512 即削减水体主要开销。detail normal 仅 1 次纹理采样、glitter 为无 fetch 的内联噪声,开销极低且为「波光粼粼」核心效果,故不另设质量 kill-switch。(注:原设想「引入 waterQuality 三档、低端关闭法线层+glitter」经代码核实不成立——全局 qualityProfile 已通过反射 RT 接管性能调控,glint/normal 常驻执行属预期设计。)
🟡 中资源依赖法线贴图引入外部资产已用程序化生成(regenerateDetailNormalTexture),零 bundled 资源
🟢 低过度仿制竞品走"美术夸张",我方用户群更重视"物理可控"仅借鉴波光手段,不削弱现有参数体系
🔴 P4预设漏配致 NaN → 水面消失(不可逆)_syncWaterUniformssetFloat 无 fallback(env-water.ts:438,479),若 WATER_PRESETS 任一预设漏配新字段 → buildWaterPresetEnvState 输出 undefined → 写入 NaN → 真实引擎水面渲染消失且不可逆(与 P3 env-water.ts:1184-1185 教训同型)三道防线(§4.7.4):① state.ts 种默认 1.0;② buildWaterPresetEnvState 双重 ?? preset.waterWaveHeight ?? 1.0;③ _syncWaterUniforms 写入前 ?? 1.0。验收 §5.2.5 含 NaN 防护回归项
🟢 低预设兼容P4 的"波纹/大波"拆分可能破坏 WATER_PRESETS5 预设全部显式定义两字段(§4.7.6);映射函数双重兜底,即使漏配也退回旧波高,零回归
🟡 P4旧场景兼容旧场景仅存 waterWaveHeight,无 bigWaveHeight/smallWaveHeight反序列化兜底两新字段为 1.0 → a = WAVE_AMP[i] * 1.0 * waterWaveHeight,与旧版逐字等价
🟡 P4新场景被旧版本客户端打开新场景写入 big/small,旧版本仅识别 waterWaveHeightwaterWaveHeight 保留为全局乘子(§4.7.5),旧版读取后波形为全局缩放的旧四层——非崩溃/黑面,属已知可接受降级;不再回写有损的 (big+small)/2
🟢 低wails 绑定漏生成EnvState 新字段未同步 Go struct + 重生成绑定 → SetEnvState 静默丢弃§4.7.4 第 5/6 项强制 app.go 补字段 + npm run generate:bindings
🟢 低UI 折叠后功能不可达参数收进折叠组后,用户可能找不到高级选项折叠组标题明确,搜索功能可命中折叠组内参数
🟡 P3云壳 camera-follow 与反射 RT 相机切换交互体积云球壳在反射 RT 渲染时移位到反射相机位置非本次更新引入;建议后续评估是否修复或接受 gap
🟡 P5低频法线层方向校准措施 B 降 freq 可能使水面波数更少(稀穹顶),与「连绵涌浪」目标反向§4.8.3 已标注方向风险,终值以 ocean/tropical 预设视觉校准为准;必要时反向(提 freq + 增 amp)
🟢 低P5 措施 A 性能第三层 detail normal 仅 1 次 512² 纹理采样与 P1 同类开销,全局 qualityProfile 已接管(见本节性能行)

七、相关 ADR 索引

  • ADR-062 — 水面反射 RT(本 ADR 不改动其架构)
  • ADR-114 — 地面反射增强(平面反射引擎共用)
  • ADR-026 — 环境系统增强(水面子系统所属)
  • ADR-113 — 体积云系统(云壳渲染对水面反射的影响见§九)

八、结论

竞品是「少参数、强预设、重法线」的美术水体;我方是「多参数、强物理、重波形」的可控水体。波光粼粼本质是高频法线 + specular glitter + 反射/焦散组合;地平线/星空感则来自无限水体 + 环境 cubemap 反射 + 天空-水面统一颜色分级

实施进度(2026-07-17)

  • P2 已完成 (e54d67e):焦散强度 UI 暴露 + 水面 UI 预设化折叠
  • P1 已完成 (e54d67e):shader 高频法线扰动层 + Sun Glitter 项
  • P3 已完成 (df7db01):地平线淡出 + 天空-水面颜色联动
  • P4 待实施:波纹/大波双层尺度拆分,技术方案已补充(§4.7),待评审后落地

是否进入 P4 实施 PR,待架构评审确认。


九、体积云/地面增强集成兼容性评估(2026-07-17)

背景:在体积云(ADR-113)和地面增强(ADR-114 Phase 2/3)完成更新后,评估对水面反射系统和 ADR-115 实施路径的影响。评估方式:全链路代码审计与架构推理。

9.1 地面增强 → 水面反射:🟢 无需调整

检查项结果
反射引擎互斥✅ 共用 PlanarReflection 统一引擎:地面 mirrorTexture 模式、水面 screenSpace 模式,互斥协调器 requestExclusive/releaseExclusive 正常工作
反射特性隔离✅ 地面独有的反射模糊(PBR roughness → mipmap)、法线扭曲(bumpTexture)、接触阴影(SS ray marching)均为 ground 子系统内部特性,不涉及水面 shader
资源冲突✅ 无共享纹理/RT 冲突

结论:地面增强与水面反射系统在架构层面正交,无需同步调整。

9.2 体积云 → 水面反射:🟡 P3 已知间隙

现象:云壳(env-clouds.tsrenderingGroupId = -1,每帧通过 onBeforeRenderObservable 将球壳中心对齐到 scene.activeCamera 位置(第 568–575 行)。水面反射 RT(screenSpace 模式)渲染时:

  1. Babylon 将 scene.activeCamera 切到镜像 FreeCamera
  2. onBeforeRenderObservable 再次触发 → 云壳移到反射相机位置(非主相机位置)
  3. 云 shader 的光线步进使用 cameraPosition(现为反射相机位置)
  4. 结果:云在水面反射中的空间位置和密度采样均偏移
维度评估
严重性🟡 P3 — 仅当 reflectionQuality ≠ offplanarReflectBlend > 0 时可见
回归性❌ 非本次更新引入 — 云壳架构从设计之初即如此,水面反射也从未对云层做特殊处理
修复难度中 — 在反射 RT render 期间临时移除云壳的 followObs,或加反射相机专用位置守卫
修复时机建议在 ADR-115 P1(法线扰动层 + Sun Glitter)实施前评估是否需修复,P2 不受影响

9.3 ADR-115 实施路径影响评估

ADR-115 优先级受影响?说明
P2(焦散强度 UI + 面板预设化)🟢 否纯水面状态链路 + UI 布局,与云/地面系统无依赖
P1(法线扰动层 + Sun Glitter)🟢 否新增 uniform 在 water.frag.glsl 内部,不依赖云/地面系统;仅在实施前注意 9.2 间隙
P3(无限水面 + 地平线淡出 + 天空联动)🟢 否天空联动逻辑虽关联 sky 系统,但 sky 已完成更新且无需额外同步

9.4 已闭环的同步项

Commit 8f70ee7 已在本次评估前修复了最关键的环境同步 gap:Go app.go EnvState struct 补充了 ReflectionQuality 字段,fix 跨重启反射质量丢失问题。水面状态链路(types → bridge → Go → persistence)已完整。