shader_type spatial; render_mode depth_draw_opaque; // Sand / rock split by slope, matching the sonar material rule (sand under 20 deg, // rock over 40 deg). Rock is normal-mapped triplanar in world space so cliffs and // ledges keep their detail; sand carries an oriented ripple normal map on the xz plane. // The mesh has no tangents: perturbed normals are built in world space and written // to NORMAL in view space. Detail shading comes from the normal maps, a grain map // used as ambient occlusion in the pits, and albedo mottling. uniform vec3 sand_color : source_color = vec3(0.50, 0.46, 0.34); uniform vec3 rock_color : source_color = vec3(0.21, 0.20, 0.18); uniform vec3 deep_tint : source_color = vec3(0.04, 0.09, 0.15); uniform float sand_slope_cosine = 0.9397; // cos(20 deg) uniform float rock_slope_cosine = 0.7660; // cos(40 deg) uniform float depth_tint_start_m = 20.0; uniform float depth_tint_end_m = 90.0; uniform sampler2D sand_normal_map : hint_normal, repeat_enable, filter_linear_mipmap; uniform sampler2D rock_normal_map : hint_normal, repeat_enable, filter_linear_mipmap; uniform sampler2D rock_grain_map : hint_default_white, repeat_enable, filter_linear_mipmap; uniform float sand_tile_m = 3.2; uniform float rock_tile_m = 7.0; uniform float rock_grain_tile_m = 1.7; uniform float sand_normal_strength = 1.0; uniform float rock_normal_strength = 1.1; uniform float rock_grain_normal_strength = 0.45; uniform float grain_ao_strength = 0.5; uniform float grain_albedo_contrast = 0.45; uniform vec2 ripple_direction = vec2(0.8, 0.6); varying vec3 world_position; varying vec3 world_normal; void vertex() { world_position = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz; world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz); } vec3 unpack_normal(vec4 sample) { return sample.xyz * 2.0 - 1.0; } // Triplanar normal perturbation (UDN blend): each plane's tangent-space xy is added // onto the world normal along that plane's axes, weighted by how much the surface // faces the plane. vec3 triplanar_normal(sampler2D map, vec3 position, vec3 normal, float strength) { vec3 weights = pow(abs(normal), vec3(4.0)); weights /= weights.x + weights.y + weights.z; vec3 tx = unpack_normal(texture(map, position.zy)); vec3 ty = unpack_normal(texture(map, position.xz)); vec3 tz = unpack_normal(texture(map, position.xy)); vec3 perturbation = ( vec3(0.0, tx.y, tx.x) * weights.x + vec3(ty.x, 0.0, ty.y) * weights.y + vec3(tz.x, tz.y, 0.0) * weights.z ) * strength; return normalize(normal + perturbation); } float triplanar_grain(sampler2D map, vec3 position, vec3 normal) { vec3 weights = pow(abs(normal), vec3(4.0)); weights /= weights.x + weights.y + weights.z; return texture(map, position.zy).r * weights.x + texture(map, position.xz).r * weights.y + texture(map, position.xy).r * weights.z; } void fragment() { vec3 n = normalize(world_normal); float up = clamp(n.y, 0.0, 1.0); float rockness = 1.0 - smoothstep(rock_slope_cosine, sand_slope_cosine, up); // Sand: ripple normal map rotated to the swell direction, on the xz plane. vec2 ripple_axis = normalize(ripple_direction); vec2 sand_uv = vec2( dot(world_position.xz, ripple_axis), dot(world_position.xz, vec2(-ripple_axis.y, ripple_axis.x)) ) / sand_tile_m; vec3 sand_tangent = unpack_normal(texture(sand_normal_map, sand_uv)); vec3 sand_perturbation = ( vec3(ripple_axis.x, 0.0, ripple_axis.y) * sand_tangent.x + vec3(-ripple_axis.y, 0.0, ripple_axis.x) * sand_tangent.y ) * sand_normal_strength; vec3 sand_normal = normalize(n + sand_perturbation); // Rock: coarse triplanar normal map plus fine grain, both in world space. vec3 rock_normal = triplanar_normal(rock_normal_map, world_position / rock_tile_m, n, rock_normal_strength); rock_normal = triplanar_normal(rock_normal_map, world_position / rock_grain_tile_m, rock_normal, rock_grain_normal_strength); float grain = triplanar_grain(rock_grain_map, world_position / rock_grain_tile_m, n); vec3 shading_normal = normalize(mix(sand_normal, rock_normal, rockness)); NORMAL = normalize((VIEW_MATRIX * vec4(shading_normal, 0.0)).xyz); // Albedo: mottled rock, lightly grained sand, both tinted with depth. vec3 rock_albedo = rock_color * (1.0 + (grain - 0.5) * 2.0 * grain_albedo_contrast); vec3 sand_albedo = sand_color * (1.0 + (grain - 0.5) * 0.3); vec3 albedo = mix(sand_albedo, rock_albedo, rockness); float depth_m = -world_position.y; float tint = smoothstep(depth_tint_start_m, depth_tint_end_m, depth_m); albedo = mix(albedo, deep_tint, tint * 0.6); ALBEDO = albedo; // Detail shading: pits of the grain map and ripple troughs read as occlusion. float rock_ao = mix(1.0, 0.45 + 0.55 * grain, grain_ao_strength); float sand_ao = mix(1.0, 0.8 + 0.2 * clamp(sand_tangent.z, 0.0, 1.0), grain_ao_strength * 0.6); AO = mix(sand_ao, rock_ao, rockness); AO_LIGHT_AFFECT = 0.35; ROUGHNESS = mix(0.95, 0.78 + 0.12 * grain, rockness); METALLIC = 0.0; }