Appearance
水面苏醒
背景:水面有 Gerstner 波浪但缺少波光细节——像塑料薄膜,没有光斑闪烁、没有远处自然消失、大小波无法独立控制。
过程:四阶段增强——法线扰动+Sun Glitter、焦散强度暴露+UI 折叠、无限水面+天空联动、双尺度波高拆分。
外交官站在湖边,看着水面。
Gerstner 波浪在跑——四层叠加,风向联动,波形正确。但水面看起来像一块半透明的塑料薄膜铺在地面上。有波,但没有光。
"你看到了什么?"桌面壳问。
"一片死水,"外交官说,"它会动,但它不亮。"
外交官蹲下来,盯着水面看了一会儿。
"你知道'波光粼粼'是什么意思吗?"
"水面反射阳光,产生闪烁的光斑。"
"对。但你看看你的水面——有反射吗?有。有波浪吗?有。有光斑吗?"
桌面壳沉默了。
"没有。"
四个缺失
外交官翻开水面着色器的代码,用手指划过四个空白区域。
"波光粼粼不是一种效果,"外交官说,"它是四种效果的叠加。你现在只有第一种。"
第一,法线细节。 "你的 Gerstner 波产生了一个法线——但只有低频的大波浪法线。真实水面上有细密的微涟漪,它们产生高频法线扰动,让反射光碎裂成无数小光斑。你没有这一层。"
第二,光斑闪烁。 "阳光打在波浪尖端的镜面反射——随机分布的亮闪点。你的 shader 里没有这一项。"
第三,远处消失。 "你的水面是一块 60×60 的有限平面。远处是硬边缘——水面到边界突然结束,后面是天空。真实水面的远处应该和天空融为一体。"
第四,波浪层次。 "你有一个 waveHeight 滑块——所有波浪统一缩放。但真实水面的大浪和涟漪是不同频率的,它们需要独立控制。"
桌面壳看着这四个缺口。
"所以'波光粼粼'……"
"是四种东西的乘积,"外交官说,"缺任何一个,它都不亮。"
第一刀:法线与闪烁
外交官打开 env-texture.ts,开始写一个函数。
"先做法线细节层。"
他用 createCanvasTexture 生成了一张 512×512 的纹理——4 个 octave 的 Value noise,用中心差分推导法线。双通道采样,两个通道用不同的速度和尺度滚动,避免重复感。
"512×512,"外交官说,"零外部资源依赖。全部在 Canvas 里算。"
法线层贴到 fragment shader 里之后,外交官处理了一个冲突——新法线层和 Gerstner 自带的法线会叠加出莫尔纹。他用了一个分层策略:uDetailNormalStrength 大于 0 时,Gerstner 法线衰减到 70%,细节层接管高频;等于 0 时完全恢复原貌。
glsl
float gerstnerScale = uDetailNormalStrength > 0.0 ? 0.7 : 1.0;
normal = normalize(
normal * gerstnerScale +
vec3(detailNormal.x * uDetailNormalStrength, 1.0, detailNormal.y * uDetailNormalStrength)
);"等于 0 时呢?"桌面壳问。
"gerstnerScale 回到 1.0,细节项归零。零回归。"
然后是 Sun Glitter——外交官在 shader 里加了一个 hash12 伪随机噪声函数。零纹理依赖,纯算术。
glsl
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);
}用这个噪声调制镜面高光,在迎光面撒下随机闪烁的光斑:
glsl
if (uGlintStrength > 0.0) {
vec2 glitterUV = vWorldPos.xz * uGlintScale + time * uGlintSpeed;
float glitterNoise = mix(hash12(floor(glitterUV)), hash12(floor(glitterUV * 3.7 + 13.0)), 0.3);
float spec = pow(max(dot(reflectDir, normalize(lightDir)), 0.0), uGlintPower);
float glitter = diff * spec * (0.6 + 0.8 * glitterNoise) * uGlintStrength;
color += lightColor * glitter;
}"0.6 加 0.8 乘噪声——基线亮度加随机跳动。diff 做 mask,只在迎光面闪烁。"
桌面壳看着 uGlintStrength 的默认值。
"0?"
"先关着,"外交官说,"法线层做好了,光斑会从法线的 specular 中自然产生。glitter 是锦上添花——先看法线够不够好,不够再开。"
第二刀:焦散与折叠
水面底下,焦散光斑的强度是硬编码的。
外交官在 _syncWaterUniforms 里找到那行代码——0.15,写死的。用户拖滑块没有任何反应。
他把硬编码替换成 state.causticIntensity,在 EnvState 里加了字段,范围 0 到 0.5,步长 0.01。然后他重组了 UI 面板——之前水面参数是一长条平铺的,二十多个滑块。外交官把它们折叠成 6 组:基础参数默认展开(高度、范围、波高、速度、焦散强度、水色),其余五组折叠收起。
"核心参数 ≤8 项一屏可见,"外交官说,"剩下的藏起来。用户需要的时候再展开。"
五个预设各配了焦散值——平静 0.1,风暴 0.25。
"为什么不统一?"
"平静水面的焦散柔和、缓慢——0.1 就够了。风暴天水面剧烈翻涌,焦散又亮又急——0.25 才有存在感。"
第三刀:地平线的消失
外交官站在湖边远眺。水面在远处戛然而止——60 单位的平面,边缘是一条硬线。
"真实水面没有边缘,"外交官说,"它在远处和天空融为一体。"
他在 fragment shader 里加了一段径向淡出——计算像素到相机的水平距离,在 uHorizonStart 和 uHorizonEnd 之间用 smoothstep 混合到天空色,同时把 alpha 推向 1(远处不透明,融入天空)。
glsl
float radialDist = length(vWorldPos.xz - cameraPosition.xz);
float horizonFactor = 1.0 - smoothstep(uHorizonStart, uHorizonEnd, radialDist);
float horizonMix = (1.0 - horizonFactor) * uHorizonFade;
color = mix(color, uHorizonColor, horizonMix);
alpha = mix(alpha, 1.0, horizonMix);"uHorizonFade 等于 0 时呢?"
"horizonMix 全程为 0,完全不混合。零回归。"
"那天空色从哪来?"桌面壳问。
外交官列了三级来源:天空系统的 skyColorBot → 环境贴图 mipmap 最顶层平均色 → 雾色兜底。混合比例由 uSkyColorBlend 控制,默认 0——不联动时水面不受影响。
glsl
vec3 finalWaterColor = mix(waterColor, uSkyBlendColor, uSkyColorBlend);
vec3 finalFogColor = mix(waterFogColor, uSkyBlendColor * 0.8, uSkyColorBlend);"每 5 帧更新一次天空色,"外交官说,"不需要每帧——天空色变化慢。"
第四刀:大浪与涟漪的分离
最后一刀。外交官回到 vertex shader,看着 Gerstner 循环。
glsl
for (int i = 0; i < WAVE_COUNT; i++) {
float a = WAVE_AMP[i] * waveHeight;
...
}"四个波层,全乘同一个 waveHeight,"外交官说,"你想让大浪更高但涟漪不变——做不到。"
他把四层拆成两组:层 0、1 为大波组,乘 bigWaveHeight;层 2、3 为小波组,乘 smallWaveHeight。两个新 uniform 各自独立缩放,再乘全局 waveHeight 保持向后兼容。
glsl
float h = (i < 2) ? bigWaveHeight : smallWaveHeight;
float a = WAVE_AMP[i] * h * waveHeight;五个预设各配了差异化值——平静 bigWaveHeight=0.3, smallWaveHeight=0.5,风暴 bigWaveHeight=2.0, smallWaveHeight=0.5。
"风暴预设大浪高、涟漪低?"
"对——风暴天水面以大浪为主,微涟漪被风压碎了。"
UI 基础参数区里,原来的"波高"滑块拆成了"大浪高度"和"涟漪高度"两个。waterNormalStrength 和 waterGlintStrength 移到高级参数折叠区——基础区保持 7 项,不超载。
活过来了
外交官把四个阶段全部接上,走到控制台前,选了"海浪"预设。
大浪从远处推来——层 0、1 的低频波以 1.5 倍振幅推进,层 2、3 的高频涟漪以 0.8 倍振幅叠在上面。法线细节层在水面上撒了一层细密的微涟漪,反射光碎裂成无数小光斑。阳光入射角 45 度时,水面出现流动的闪烁高光点。
远处的水面不再戛然而止——它在 uHorizonStart 到 uHorizonEnd 之间缓缓淡出,和天空色融为一体。焦散光斑在水底摇曳——0.2 的强度,不抢眼,但能感觉到光在穿过水面。
"它活了,"桌面壳说。
外交官没有说话。他看着水面,看着那些光斑随着视角移动而流动,看着远处的水平线在天空色中消失。
"波光粼粼不是一种效果,"外交官最后说,"它是四种效果在乘法里相遇。少一种,它就是塑料薄膜。"
他转过身。
"走吧。水面醒了。"
教训:波光粼粼不是一种效果,是四种效果的乘积——法线细节、光斑闪烁、远处融合、波浪层次。缺任何一项,水面都是死的。