Rock is normal-mapped triplanar in world space at two scales, sand carries an oriented ripple normal map on the xz plane, and a grain map drives detail occlusion, albedo mottling and roughness. The three maps are seamless, seeded, mipmapped textures built synchronously by SeabedDetailTextures, so no asset is needed and builds are deterministic. The mesh has no tangents: perturbed normals are computed in world space and written to NORMAL. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
112 lines
4.8 KiB
Plaintext
112 lines
4.8 KiB
Plaintext
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;
|
|
}
|