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); }