HONEYGAME DEVKIT v1.0

正在运行 4000 行 shader,只为了让水面好看一点

--:--
NOW PLAYING · 2026.02.26 · 3 MIN

4000 行 shader,只为了让水面好看一点

我花了六周重写水面。这篇文章是给未来的自己看的:哪些尝试有用,哪些纯粹是在自我感动。

《HONEY GARDEN》里有一片湖。玩家会在湖边待很久——钓鱼、发呆、和祖母说话。

所以水面必须好看。这不是“想要”,这是“必须”。一个玩家盯着看二十分钟的平面,糊弄不过去。

我花了六周。4000 行 shader。这篇是账单。

v1:把教程拼起来

第一版是标准的“三件套”:

// 滚两层法线贴图 → 采样反射 → 加菲涅尔边缘
vec3 n = normalize(mix(
    texture(uNormalA, uv * 3.0 + uTime * 0.03).rgb,
    texture(uNormalB, uv * 2.0 - uTime * 0.021).rgb,
    0.5
));
vec3 r = reflect(-V, n);
vec3 col = texture(uReflect, r.xy).rgb;
col = mix(col, uDeepColor, fresnel);

跑起来了。看着像水。看着像任何一款 2015 年之后的游戏里的水。

问题在这:它是对的,但它不是我的。我盯着看了三天,越看越觉得这片湖和我的游戏没有关系。

v2:关掉反射

我做了一个所有人都会劝我别做的决定:把实时反射关了。

理由是——我的游戏里没有东西值得反射。

一座花园,几棵树,一片天。反射出来的东西,玩家已经看到了。我用掉了整个帧预算的 40%,去重复显示他们已经知道的信息。

关掉之后,多出来的预算我拿去做了三件事:

一、真实的水深

我用了一张手绘的水深图,让颜色真正取决于“这里有多深”,而不是一个统一的渐变。

float depth = texture(uDepthMap, worldXZ * 0.02).r;
// 浅处偏黄绿(能看到泥),深处偏蓝黑(看不到底)
vec3 waterCol = mix(uShallowCol, uDeepCol, smoothstep(0.0, 0.6, depth));
// 岸边一圈透光,是"浅"最直观的信号
float shore = 1.0 - smoothstep(0.0, 0.12, depth);
waterCol += uSunCol * shore * 0.35;

这一条改动带来的提升,比我前面所有法线贴图的调试加起来都大。玩家能看见湖底,于是他们会相信这片湖有三米深,而不是一张贴图。

二、风的记忆

我的游戏里有风。风吹过花田,也吹过水面。

但我不是实时算的——我用一个全局的 windPhase,让浪的传播方向和花田倒伏的方向一致。

// 浪从风向的来向传播,和花田、草的 shader 共用同一个相位
float wave = sin(dot(worldXZ, uWindDir) * 0.4 - uWindPhase);

这样做的副作用是:起风的时候,整个画面会同时动起来。 花、草、树、水,全部朝一个方向倒。第一次跑起来的时候我在椅子上坐直了。

这一条只值 20 行代码。它是整个水面里我最满意的部分。

三、不完美

真实的水面不平。有涟漪,有被风吹皱的地方,有鱼跳起来留下的一圈。

我加了一张手绘的“扰动图”,美术同事用 Aseprite 画的,4 个通道分别控制:涟漪强度、波纹方向偏移、高光锐度、透明度。

vec4 dist = texture(uDisturb, worldXZ * 0.01 - uTime * 0.005);
n.xy += (dist.rg - 0.5) * dist.b * 0.6;
specularPower = mix(80.0, 400.0, dist.g);

这张图只有 512×512,手绘的,一点都不物理。但它是唯一让水面看起来像“这个地方的水”的东西。

性能账

版本 帧时间(湖面) 观感
v1 三件套 + 实时反射 6.8 ms 像别人的游戏
v2 关反射 + 水深 2.1 ms 好一点,但平
v3 + 风的记忆 2.3 ms 整个场景活了
v4 + 扰动图 2.4 ms 终版

最后是 2.4 毫秒,比第一版快了将近三倍,而且好看得多。

关掉一个“正确”的技术,换回一个“属于这里”的技术。这是这六周里我学到的唯一一件事。

一些纯粹浪费掉的时间

诚实起见,也记一下没用的尝试:

  • 用 FFT 做海洋频谱。 花了一周,效果很好,但我的湖只有 60 米宽,风平浪静。它一直在算一些玩家永远看不到的浪。
  • 屏幕空间反射。 又是五天。最后还是关了,因为湖边的树反射在水里之后,画面变得非常乱。
  • 焦散(caustics)。 好看。但我的湖底没有东西给焦散照。空转。
  • 把水面细分成网格做真实位移。 做完发现,我的水面从来没有近距离特写。玩家永远在岸上。

这四项加起来大概三周。都是“因为能做到所以做了”,不是“因为需要所以做了”。

如果重来

我会先问一个问题:玩家会在这个画面上停留多久?

如果答案是“十分钟以上”,那就值得花六周。如果答案是“跑过去三秒”,那我应该去把岸边的石头画好——那个更划算。

我的湖值得。你的呢?

END OF CARTRIDGE