365 lines
19 KiB
Plaintext
365 lines
19 KiB
Plaintext
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);
|
||
}
|