Files
agame/world/ocean/island.gdshader
T
2026-09-23 17:37:07 +08:00

150 lines
6.9 KiB
Plaintext

shader_type spatial;
/* ===========================================================================
卡通小岛 · 纯程序化,零贴图
---------------------------------------------------------------------------
一块平面按「径向剖面 + 噪声」抬起来,再按高度与坡度上色:
水下/湿沙 → 沙滩 → 草地 → 岩石
三个细节决定了它像不像岛:
· 海岸线加噪声扰动,否则是一个完美的圆,假得刺眼
· 岩石不只看高度,也看坡度 —— 陡的地方自然露岩,比只按海拔分层真实得多
· profile_power 越小越像「山」,越大越像「沙洲」。1.3 左右是个锥形岛;
调到 2 以上就变成一块平坦的沙滩,远处看着像浮在水面的饼
外面的裙边主动扎到水下(dip_depth),免得平面边缘悬空露馅。
法线用有限差分重算,因为几何被位移过了,原网格法线已经不对。
坑(实测):**片元阶段的 VERTEX 是视图空间,不是世界空间。**
一开始写成 (MODEL_MATRIX * vec4(VERTEX, 1.0)).y 当海拔用,算出来其实是
相机相对高度 —— 结果是整座岛被染成一大块沙滩,而且看上去「好像也对」。
海拔要嘛用 varying 从顶点传下来(现在这样),
要嘛用 (INV_VIEW_MATRIX * vec4(VERTEX, 1.0)).y,别拿 MODEL_MATRIX 去乘。
前提:节点摆在水面高度上,不旋转、不缩放。
=========================================================================== */
render_mode cull_disabled, diffuse_toon, specular_disabled;
// ------------------------------------------------------------- 形状 -----
group_uniforms Shape;
uniform float island_radius : hint_range(2.0, 60.0) = 15.0; // 米:陆地半径
uniform float island_height : hint_range(0.5, 30.0) = 9.0; // 米:最高点高出水面多少
// 平顶半径占 island_radius 的比例。0 = 锥形山(默认);0.7~0.85 = 台地岛。
// 主场景要能在上面走人,必须开平顶 —— 锥形岛的斜坡无法用一块平板碰撞近似
uniform float plateau : hint_range(0.0, 0.98) = 0.0;
// 越小越像山、越大越像沙洲。调到 2 以上就成了一块浮在水上的饼
uniform float profile_power : hint_range(0.5, 5.0) = 1.30;
uniform float dip_depth : hint_range(0.0, 30.0) = 12.0; // 米:裙边扎到水下多深
// --------------------------------------------------------- 海岸与地表 -----
group_uniforms Terrain;
uniform float coast_scale : hint_range(0.01, 0.50) = 0.055; // 海岸线噪声密度
uniform float coast_warp : hint_range(0.0, 0.60) = 0.22; // 海岸线扭曲幅度
uniform float detail_scale : hint_range(0.02, 2.0) = 0.22; // 地表起伏密度
uniform float detail_strength : hint_range(0.0, 8.0) = 2.20; // 地表起伏幅度(米)
// ------------------------------------------------------------- 上色 -----
group_uniforms Paint;
uniform vec3 sand_color : source_color = vec3(0.930, 0.870, 0.680);
uniform vec3 grass_color : source_color = vec3(0.440, 0.720, 0.360);
uniform vec3 rock_color : source_color = vec3(0.550, 0.560, 0.600);
uniform vec3 wet_color : source_color = vec3(0.620, 0.600, 0.440); // 水下 / 水线附近的湿沙
uniform float wet_line : hint_range(-2.0, 2.0) = 0.15; // 米:水线(y=0 就是水面)
uniform float sand_line : hint_range(0.0, 8.0) = 0.70; // 米:沙地到此结束
uniform float grass_line : hint_range(0.0, 20.0) = 3.40; // 米:草地到此结束,以上是岩
uniform float rock_slope : hint_range(0.0, 4.0) = 0.70; // 坡度大于此值直接露岩
uniform float band_soft : hint_range(0.02, 1.0) = 0.18; // 色阶过渡宽度(越小越硬)
// ===========================================================================
// 噪声
// ===========================================================================
float hash21(vec2 p) {
p = fract(p * vec2(123.34, 456.21));
p += dot(p, p + 45.32);
return fract(p.x * p.y);
}
float vnoise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
vec2 u = f * f * (3.0 - 2.0 * f);
float a = hash21(i);
float b = hash21(i + vec2(1.0, 0.0));
float c = hash21(i + vec2(0.0, 1.0));
float d = hash21(i + vec2(1.0, 1.0));
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
}
// 两层就够。这里值噪声可以用 —— 它只塑造「高度」,不拿它求导当法线
//(法线是另外用有限差分在「位移后」重算的,见 vertex)。水面着色器里不能这么偷懒。
float fbm2(vec2 p) {
return vnoise(p) * 0.65 + vnoise(p * 2.07 + 13.1) * 0.35;
}
// ===========================================================================
// 高度场
// ===========================================================================
// 返回高于水面的米数(水面 = 0)
float island_at(vec2 p) {
float r = length(p) / max(island_radius, 0.001);
// 海岸线:按噪声推拉半径,否则是一个完美的圆
r += (fbm2(p * coast_scale) - 0.5) * coast_warp;
// 径向剖面:plateau 以内是平顶,以外收到水线。
// 注意海岸线噪声已经加进 r 了,所以平顶边缘也是弯的,不会是一个正圆
float prof = 1.0 - smoothstep(plateau, 1.0, clamp(r, 0.0, 1.0));
prof = pow(prof, max(profile_power, 0.001));
float h = prof * island_height;
// 地表起伏:只作用在陆地上,海岸线附近淡出,否则海岸会碎成齿状。
// 主场景用平顶当可走地面时,这个值要压小 —— 碰撞是平的,起伏大了人会陷进坡里
h += (fbm2(p * detail_scale) - 0.5) * detail_strength * smoothstep(1.0, 0.35, r);
// 裙边:岛外主动扎到水下,免得平面边缘悬空
h -= dip_depth * smoothstep(0.85, 1.30, r);
return h;
}
// ===========================================================================
// 海拔从顶点传下来。**不要在片元里拿 MODEL_MATRIX 乘 VERTEX**(见文件头)
varying float v_height;
void vertex() {
vec2 p = VERTEX.xz;
float h = island_at(p);
VERTEX.y += h;
v_height = h;
// 几何被位移过了,原网格法线不再正确 —— 用有限差分重算。
// 四个额外采样换一个到处都对(包括噪声起伏)的法线,很划算。
const float e = 0.35;
float hx = island_at(p + vec2(e, 0.0)) - island_at(p - vec2(e, 0.0));
float hz = island_at(p + vec2(0.0, e)) - island_at(p - vec2(0.0, e));
NORMAL = normalize(vec3(-hx / (2.0 * e), 1.0, -hz / (2.0 * e)));
}
void fragment() {
float h = v_height;
// 陡峭度:1 - n.y。与坐标系无关,不用管 NORMAL 在哪个空间
float steep = 1.0 - clamp(NORMAL.y, 0.0, 1.0);
vec3 c = sand_color;
c = mix(c, grass_color, smoothstep(sand_line - band_soft, sand_line + band_soft, h));
c = mix(c, rock_color, max(
smoothstep(grass_line - band_soft, grass_line + band_soft, h),
smoothstep(rock_slope, rock_slope * 1.6, steep)
));
// 水下的部分压成湿沙色
c = mix(c, wet_color, smoothstep(wet_line + band_soft, wet_line - 0.30, h));
ALBEDO = c;
ROUGHNESS = 1.0;
METALLIC = 0.0;
}