Files
agame/world/ocean/water.gdshader
2026-09-23 17:11:13 +08:00

365 lines
19 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
shader_type spatial;
/* ===========================================================================
卡通海面 · 纯程序化,零贴图
---------------------------------------------------------------------------
渲染顺序: 水体底色 → 菲涅尔天空反射 → 太阳波光 → 泡沫 → 地平线雾霭
四层各管一件事,缺一层就露馅:
1 底色 水越深越暗越蓝(靠 DEPTH_TEXTURE 拿到真实水深)
2 菲涅尔 掠射角反射天空色、垂直看透出底色 —— 水面的「亮边」全靠它,最容易漏
3 波光 太阳的镜面反射,压成硬边色块
4 泡沫 岸线(水浅)+ 浪尖(波峰)两处,硬边
波形分工(这是本着色器最重要的一个决定):
· 位移只用 3 组低频正弦 —— 决定轮廓。幅度刻意压得很小,
大浪交给「明暗」而不是「几何」,否则顶点一密就是碾衣板
· 细纹 + 噪点只扰动法线、不给位移 —— 决定波光,且按距离淡出
**波形时间来自哪里(world_time 与 TIME 的关系):**
world_time >= 0 用传入值。运行时由 ocean_demo.gd 每帧推进(联机要同步的时钟)
world_time < 0 用引擎内置 TIME。**编辑器预览就是这种情况** ——
脚本在编辑器里不跑,所以没人推 world_time;
没这个兼底的话,编辑器里看到的海是**完全冻结**的,
只有按 F6 跑起来才动。
两者默认值就是 -1,所以场景一打开(未运行)海就在动,不用做任何配置。
**五个坑,都是实测踩出来的:**
① 纯正弦场只要两个方向接近平行,叠加后就会退化成一道规则条纹。
方向必须显式排开(夹角 90° 以上),别拿「看起来不一样」的坐标去拼。
② 只有正弦、没有噪声的法线,会让**太阳波光变成一张规则网格** ——
高光落在「法线刚好平分视线与阳光」的位置,周期性法线场的高光自然周期性。
③ 打散它的噪声**必须是梯度噪声,不能用值噪声**。值噪声的方形格子在
当『高度』用的时候看不出来,但拿来求导当法线,就是一块块方格。
④ TAU / PI / E 是着色器语言的内置常量,自己再 const 一遍会直接编译失败。
而**编译失败的外观是「没着色器的一团白」**,很容易当成「参数没调好」去瞎调。
⑤ 片元阶段的 VERTEX 是**视图空间**,不是世界空间(小岛着色器在那里把
MODEL_MATRIX 乘上去当海拔用,结果整座岛被染成一大块沙滩)。
两处刻意与 cartoon_sky.gdshader 对齐:
· 反射色 / 雾霭色直接取那条天空的色板,海天交界才能连成一条线
· 时间不用内置 TIME(运行时)—— 联机两端启动时刻差几秒,看到的就不是同一片海
关于「近处细平面 + 远处大平面」两层水面:
均匀网格做不了「近处要密、远处要远」这两件事。天空在地平线以下
是一片深色半球,有限大的水面边缘与真地平线之间会露出那一条深色带
(相机高 h、平面半宽 d 时,带宽 ≈ atan(h/d))。所以:
· 近平面小而密(2.5 m/格),跟随相机走 —— 波形是世界坐标锚定的,不会滑
· 远平面大而疏(约 500 m/格),把边缘推到 0.25° 以内,带宽只剩两三个像素
两者用同一个着色器,靠 swell_amount 区分(远平面 = 0,纯平)。
前提:本节点不能旋转(平移、缩放都可以)。波形与法线都在模型空间算。
调好的美术取值直接写在下面的 uniform 默认值里 —— 即使 .tres 里的
shader_parameter 被清掉,画面也不会跑偏。
=========================================================================== */
render_mode blend_mix, cull_disabled, unshaded, fog_disabled;
// --------------------------------------------------------------- 时间 -----
group_uniforms Time;
// -1 = 没人接管,用引擎 TIME(编辑器预览);>= 0 = 用传入值(运行时的同步时钟)
uniform float world_time : hint_range(-1.0, 3600.0) = -1.0;
// 风向。大浪与细纹都沿着它及其旋转方向铺开
uniform vec2 wind_direction = vec2(1.0, 0.25);
// ------------------------------------------------------- 大浪(真实位移) ---
group_uniforms Swell;
// 1 = 正常水面;0 = 纯平(远平面用,不参与几何)
uniform float swell_amount : hint_range(0.0, 1.0) = 1.0;
uniform float wave_height : hint_range(0.0, 3.0) = 0.38; // 米:最低频那组的振幅
uniform float wave_length : hint_range(2.0, 120.0) = 42.0; // 米:最低频那组的波长
uniform float wave_speed : hint_range(0.0, 4.0) = 1.00; // 传播速度倍率
uniform float choppiness : hint_range(0.0, 1.0) = 0.25; // 波峰尖锐度
// ------------------------------------------------- 细纹(只动法线,不动位移)---
group_uniforms Ripple;
uniform float ripple_scale : hint_range(0.05, 6.0) = 1.40; // 1/米:细纹密度
uniform float ripple_strength : hint_range(0.0, 1.5) = 0.16; // 坡度大小(不是高度)
uniform float ripple_speed : hint_range(0.0, 4.0) = 0.70;
// 米:细纹与噪点在这么远以后淡出。**不做这个远处会满屏闪烁** ——
// 空间频率一旦落到像素频率以下就是噪点,掠射角还会把它放大
uniform float ripple_fade_distance : hint_range(10.0, 600.0) = 110.0;
// --------------------------------- 噪点(打散正弦场的规则性,见文件头坑②③)---
group_uniforms Grain;
uniform float grain_scale : hint_range(0.02, 3.0) = 0.45; // 1/米:噪点密度
uniform float grain_strength : hint_range(0.0, 2.0) = 0.32; // 坡度大小
uniform float grain_speed : hint_range(0.0, 4.0) = 0.22;
// ----------------------------------------------------------------- 水体 -----
group_uniforms Water;
uniform vec3 shallow_color : source_color = vec3(0.240, 0.800, 0.760); // 浅滩
uniform vec3 deep_color : source_color = vec3(0.030, 0.200, 0.450); // 深海
uniform float depth_fade : hint_range(0.5, 40.0) = 7.0; // 米:多深算「全深色」
uniform float opacity_shallow : hint_range(0.0, 1.0) = 0.62;
uniform float opacity_deep : hint_range(0.0, 1.0) = 0.97;
// ------------------------------------------------------------- 天空反射 -----
group_uniforms Reflection;
uniform vec3 sky_color : source_color = vec3(0.380, 0.660, 0.980); // 抬头看到的天顶
uniform vec3 horizon_color : source_color = vec3(0.780, 0.880, 1.000); // 掠射看到的地平线
uniform float fresnel_power : hint_range(0.5, 12.0) = 3.2;
uniform float fresnel_amount : hint_range(0.0, 1.0) = 0.82;
// 0=完全平滑。段数越多越「卡通」,但也越容易把法线场切成条纹 ——
// 3 是实测下来既有色块感、又不会变成斑马线的值
uniform float band_count : hint_range(0.0, 12.0) = 3.0;
uniform float band_sharpness : hint_range(0.0, 1.0) = 0.70;
// --------------------------------------------------------------- 太阳 -----
group_uniforms Sun;
// 方向由脚本从 Sun 节点推过来,保证与场景光照一致;未接管时用默认值
uniform vec3 sun_direction = vec3(0.35, 0.55, 0.35);
uniform vec3 sun_color : source_color = vec3(1.000, 0.960, 0.850);
uniform float glitter_power : hint_range(10.0, 800.0) = 320.0; // 越大越碎
uniform float glitter_strength : hint_range(0.0, 4.0) = 0.85;
uniform float glitter_bands : hint_range(0.0, 8.0) = 3.0; // 波光分段
// --------------------------------------------------------------- 泡沫 -----
group_uniforms Foam;
uniform vec3 foam_color : source_color = vec3(0.960, 0.990, 1.000);
uniform float shore_depth : hint_range(0.0, 8.0) = 1.05; // 米:多浅开始出岸线泡沫
// 岸线泡沫很容易做过头 —— 调太满就是一圈白色描边,像贴纸不像浪。0.6 左右刚好
uniform float shore_amount : hint_range(0.0, 2.0) = 0.60;
uniform float crest_amount : hint_range(0.0, 2.0) = 0.18; // 浪尖泡沫
uniform float crest_start : hint_range(0.0, 1.5) = 0.92; // 浪尖阈值(越高越稀)
// 米:浪尖泡沫的可见距离。浪花是近景细节,让它铺到地平线只会糊成一片白网
uniform float crest_fade_distance : hint_range(20.0, 800.0) = 200.0;
uniform float foam_bands : hint_range(1.0, 5.0) = 3.0; // 硬边分段
// --------------------------------------------------------- 地平线雾霭 -----
group_uniforms Haze;
// 与 cartoon_sky.gdshader 的 Haze 组保持一致,海面才能在远处融进天空
uniform vec3 haze_color : source_color = vec3(0.800, 0.890, 1.000);
uniform float haze_strength : hint_range(0.0, 1.0) = 0.95;
uniform float haze_power : hint_range(1.0, 20.0) = 5.0; // 越大,雾霭只集中在最贴地平线的一条
uniform float haze_distance : hint_range(5.0, 400.0) = 90.0;
// ------------------------------------------- 场景深度(量「水有多深」)---
// 在 Forward+ 下可用
uniform sampler2D depth_texture : hint_depth_texture, repeat_disable, filter_nearest;
// ===========================================================================
// 工具
// ===========================================================================
// 波形时间:有人接管就用传入值,没人接管就用引擎 TIME。
// 这是为了让场景「一打开就在动」—— 否则编辑器里看到的是冻结的海。
float wave_time() {
return world_time >= 0.0 ? world_time : TIME;
}
// 二维旋转。写成显式形式而不是 mat2,避免行列顺序搞混
vec2 rot2(vec2 v, float a) {
float s = sin(a);
float c = cos(a);
return vec2(v.x * c - v.y * s, v.x * s + v.y * c);
}
// 卡通色阶:把 0..1 压成 n 段硬边色块(n<0.5 时原样返回 = 平滑)
float posterize(float t, float n, float sharp) {
if (n < 0.5) {
return t;
}
float s = t * n;
float base = floor(s);
float fr = s - base;
float e = mix(0.5, 0.015, clamp(sharp, 0.0, 1.0));
return (base + smoothstep(0.5 - e, 0.5 + e, fr)) / n;
}
// 伪随机梯度方向(每个分量 -1..1)。用 fract 混合而不是 sin,大坐标下也不掉精度。
vec2 hash22(vec2 p) {
vec3 p3 = fract(vec3(p.x, p.y, p.x) * vec3(0.1031, 0.1030, 0.0973));
p3 += dot(p3, vec3(p3.y, p3.z, p3.x) + 33.33);
return fract(vec2((p3.x + p3.y) * p3.z, (p3.x + p3.z) * p3.y)) * 2.0 - 1.0;
}
// 梯度噪声(Perlin)。比值噪声多几次乘法,但这个场合不能省:
// 值噪声的格点是「梯度为零」的平台,求导当法线时就是一块块方格。
float gradient_noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
// 五次插值:二阶导连续
vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0);
float a = dot(hash22(i), f);
float b = dot(hash22(i + vec2(1.0, 0.0)), f - vec2(1.0, 0.0));
float c = dot(hash22(i + vec2(0.0, 1.0)), f - vec2(0.0, 1.0));
float d = dot(hash22(i + vec2(1.0, 1.0)), f - vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// 两层。第二层旋转约 37° 并换频率 —— 两层同向的话网格会重新变得可辨。
// 再多一层在掠射角下只会变成噪点,没有收益。
float grain(vec2 p) {
return gradient_noise(p) * 0.65 + gradient_noise(rot2(p, 0.65) * 2.07 + 17.3) * 0.35;
}
// ===========================================================================
// 高度场 —— 顶点位移和法线用的是同一个函数
// ===========================================================================
// 三组正弦的振幅和。浪尖泡沫要拿它当分母,
// 拿 wave_height 当分母会永远溢出到 1(泡沫铺满整个海面)
float swell_amplitude_sum() {
return wave_height * 1.79; // 1 + 0.52 + 0.27
}
// 返回 (高度, dh/dx, dh/dz)
//
// 三组正弦的频率比取 1 : 1.73 : 2.61(≈ √3、√7),互不成整数倍,
// 叠加后不会每过一段就重复出同样的花样。方向两两相差至少 60°。
vec3 swell_field(vec2 p, float t) {
vec2 d0 = normalize(wind_direction); // 随风向
vec2 d1 = rot2(d0, 2.30); // +131.8°
vec2 d2 = rot2(d0, -1.15); // -65.9°
float k = TAU / max(wave_length, 0.001);
float k0 = k;
float k1 = k * 1.73;
float k2 = k * 2.61;
float a0 = wave_height;
float a1 = wave_height * 0.52;
float a2 = wave_height * 0.27;
float sp = t * wave_speed;
float p0 = dot(d0, p) * k0 + sp;
float p1 = dot(d1, p) * k1 + sp * 1.35;
float p2 = dot(d2, p) * k2 + sp * 1.80;
float h = a0 * sin(p0) + a1 * sin(p1) + a2 * sin(p2);
vec2 g = a0 * k0 * cos(p0) * d0
+ a1 * k1 * cos(p1) * d1
+ a2 * k2 * cos(p2) * d2;
return vec3(h, g.x, g.y);
}
// 细纹的坡度(只有坡度,没有高度)。直接返回「坡多陡」而不是「水多高」。
//
// 方向是**写死的固定角度**,只让第一组跟着风向走。
// 教训:曾经写成 normalize(vec2(1.0, wind_direction.y)) 与 normalize(vec2(-2.0, 9.0)),
// 而 wind_direction 是 (1, 0.25) —— normalize(0.25, -1) 与 normalize(-2, 9) 恰好反向平行,
// 两组余弦相消,整层退化成一道 1D 驻波。三组夹角依次约 96° / 90°,不会再退化。
vec2 ripple_grad(vec2 p, float t) {
vec2 d0 = normalize(vec2(wind_direction.y, -wind_direction.x)); // 与风向垂直
vec2 d1 = normalize(vec2(7.0, 3.0)); // 23.2°
vec2 d2 = normalize(vec2(-3.0, 7.0)); // 113.2°
float k = ripple_scale;
float sp = t * ripple_speed;
float p0 = dot(d0, p) * k + sp;
float p1 = dot(d1, p) * k * 1.73 + sp * 1.6;
float p2 = dot(d2, p) * k * 2.41 + sp * 0.9;
return ripple_strength * (cos(p0) * d0 + 0.7 * cos(p1) * d1 + 0.5 * cos(p2) * d2);
}
// 噪点的坡度:有限差分。
// **它就是「波光不规则」的全部来源**(见文件头坑②)—— 只有正弦的话高光会连成网格。
// 噪点本来就没有解析导数,而三次采样只贵一点点。
vec2 grain_grad(vec2 p, float t) {
float s = grain_scale;
vec2 q = p * s + vec2(t * grain_speed, -t * grain_speed * 0.7);
float e = 0.30;
float g0 = grain(q);
float gx = grain(q + vec2(e, 0.0));
float gz = grain(q + vec2(0.0, e));
return grain_strength * vec2(gx - g0, gz - g0) / e;
}
// ===========================================================================
// 顶点
// ===========================================================================
varying vec3 v_world_pos;
void vertex() {
// 波形用**世界坐标**锚定,不是模型坐标 —— 否则近平面跟着相机走的时候,
// 海浪会跟着一起滑,看上去像船在推着水跑
vec3 world = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
vec3 w = swell_field(world.xz, wave_time());
// 向波峰挤压:把波谷压平、波峰收尖,剪影才像「浪」而不是「正弦」
float h = w.x * (1.0 + choppiness * 0.35 * sign(w.x));
VERTEX.y += h * swell_amount;
NORMAL = normalize(vec3(-w.y * swell_amount, 1.0, -w.z * swell_amount));
v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
}
// ===========================================================================
// 片元
// ===========================================================================
void fragment() {
vec2 p = v_world_pos.xz;
float t = wave_time();
vec3 to_cam = CAMERA_POSITION_WORLD - v_world_pos;
float view_dist = length(to_cam);
vec3 view_dir = to_cam / max(view_dist, 0.001);
vec3 sun_dir = normalize(sun_direction);
// ------------------------------------------------------------ 法线 ----
vec3 swell = swell_field(p, t);
// 细纹与噪点按距离淡出
float ripple_fade = exp(-view_dist / max(ripple_fade_distance, 0.001));
vec2 grad = swell.yz * swell_amount
+ (ripple_grad(p, t) + grain_grad(p, t)) * ripple_fade;
vec3 n = normalize(vec3(-grad.x, 1.0, -grad.y));
// 反射方向:既决定反射到天空的哪一段,也决定地平线雾霭的强弱
vec3 refl = reflect(-view_dir, n);
// ------------------------------------------------ 场景深度 → 水深 ----
// 水面是半透明的(下面会写 ALPHA),所以走透明队列、在不透明物之后绘制,
// 此刻深度图里已经有岛和地形。没有这一步就没有浅滩渐变,也没有岸线泡沫。
float raw_depth = texture(depth_texture, SCREEN_UV).r;
vec3 ndc = vec3(SCREEN_UV * 2.0 - 1.0, raw_depth);
vec4 scene_view = INV_PROJECTION_MATRIX * vec4(ndc, 1.0);
scene_view.xyz /= scene_view.w;
float scene_dist = -scene_view.z;
float water_dist = -VERTEX.z; // 片元里的 VERTEX 是视图空间,-z 就是沿视线的距离
float depth = max(scene_dist - water_dist, 0.0);
// ------------------------------------------------------------ 1 底色 ---
float d = clamp(depth / max(depth_fade, 0.001), 0.0, 1.0);
vec3 body = mix(shallow_color, deep_color, d);
// -------------------------------------------------------- 2 菲涅尔 ---
float fres = pow(1.0 - clamp(dot(n, view_dir), 0.0, 1.0), fresnel_power);
fres = clamp(fres * fresnel_amount, 0.0, 1.0);
// 压成硬边色阶 —— 水体到天空反射之间是几段色块,不是一条渐变
fres = posterize(fres, band_count, band_sharpness);
vec3 sky = mix(horizon_color, sky_color, clamp(refl.y * 1.6, 0.0, 1.0));
vec3 col = mix(body, sky, fres);
// ---------------------------------------------------------- 3 波光 ---
vec3 half_v = normalize(view_dir + sun_dir);
float glitter = pow(max(dot(n, half_v), 0.0), glitter_power) * glitter_strength;
// 再压一次色阶:把连续的高光切成几块碎点,才是卡通水面的「闪光」
glitter = posterize(clamp(glitter, 0.0, 1.0), glitter_bands, 0.9);
col += sun_color * glitter;
// ---------------------------------------------------------- 4 泡沫 ---
// 岸线:水浅的地方
float shore = 1.0 - smoothstep(0.0, max(shore_depth, 0.001), depth);
// 浪尖:拿振幅之和当分母(见 swell_amplitude_sum 的注释),并按距离淡出
float crest = smoothstep(crest_start, 1.0, swell.x / max(swell_amplitude_sum(), 0.001));
crest *= exp(-view_dist / max(crest_fade_distance, 0.001));
float foam = clamp(shore * shore_amount + crest * crest_amount * swell_amount, 0.0, 1.0);
foam = posterize(foam, foam_bands, 0.95);
col = mix(col, foam_color, foam);
// ------------------------------------------------------ 地平线雾霭 ---
// 视线越平,反射到的天空越靠近地平线 —— 而那里本就是雾霭色。
// 于是海天交界不用画线就自然连上了(与 cartoon_sky 的 Haze 组同色)
float far = 1.0 - exp(-view_dist / max(haze_distance, 0.001));
float glare = pow(1.0 - clamp(refl.y, 0.0, 1.0), haze_power);
col = mix(col, haze_color, clamp(glare * haze_strength * (0.25 + 0.75 * far), 0.0, 1.0));
ALBEDO = col;
// 泡沫处不透明,否则浪花会透出底下的水,看着像一层贴纸
ALPHA = max(mix(opacity_shallow, opacity_deep, d), foam);
}