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