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. // // Daylight is attenuated spectrally with depth (Beer-Lambert, per channel): red is // gone first, blue last, and the whole scene darkens by exp(-k * depth). The scalar // part scales the sun (light() function, directional only) and the sky ambient (AO); // the chroma part tints the albedo. Artificial lights such as the submarine's // projectors are not attenuated, so they stay useful on the drop-off. Everything is // normalised at reference_depth_m, where the look is the authored palette. uniform vec3 sand_color : source_color = vec3(0.56, 0.52, 0.42); uniform vec3 rock_color : source_color = vec3(0.22, 0.21, 0.19); uniform float sand_slope_cosine = 0.9397; // cos(20 deg) uniform float rock_slope_cosine = 0.7660; // cos(40 deg) // Per-channel absorption of clear tropical water, per metre, scaled for readability. uniform vec3 water_absorption_per_m = vec3(0.11, 0.04, 0.032); uniform float reference_depth_m = 24.0; // Floor on the daylight factor so the drop-off foot stays readable rather than black. uniform float minimum_daylight = 0.05; 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; // Second ripple scale (megaripples) and the slow domain warp that hide the tiling. uniform float sand_macro_tile_m = 11.3; uniform float sand_macro_strength = 0.45; uniform float sand_warp_tile_m = 37.0; uniform float sand_warp_m = 1.4; // Swell-built ripples fade out with depth as the wave energy at the bed dies. uniform float ripple_fade_start_m = 40.0; uniform float ripple_fade_end_m = 60.0; 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; varying float daylight; 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. The // plane is bent by a slow warp (two decorrelated reads of the seamless grain map) // and a second, larger ripple scale is blended in, so no period repeats across // the field. Ripples fade with depth. vec2 ripple_axis = normalize(ripple_direction); vec2 warp = vec2( texture(rock_grain_map, world_position.xz / sand_warp_tile_m).r, texture(rock_grain_map, world_position.xz / sand_warp_tile_m + vec2(0.37, 0.61)).r ) - 0.5; vec2 bent_xz = world_position.xz + warp * sand_warp_m; vec2 ripple_frame = vec2( dot(bent_xz, ripple_axis), dot(bent_xz, vec2(-ripple_axis.y, ripple_axis.x)) ); vec3 sand_tangent = unpack_normal(texture(sand_normal_map, ripple_frame / sand_tile_m)); vec3 macro_tangent = unpack_normal(texture(sand_normal_map, ripple_frame / sand_macro_tile_m + vec2(0.5, 0.25))); float depth_for_ripples = -world_position.y; float ripple_strength = sand_normal_strength * (1.0 - smoothstep(ripple_fade_start_m, ripple_fade_end_m, depth_for_ripples)); vec2 tangent_xy = sand_tangent.xy * ripple_strength + macro_tangent.xy * sand_macro_strength * ripple_strength; vec3 sand_perturbation = vec3(ripple_axis.x, 0.0, ripple_axis.y) * tangent_xy.x + vec3(-ripple_axis.y, 0.0, ripple_axis.x) * tangent_xy.y; 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. 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); // Water column: spectral transmittance relative to the reference depth. float depth_m = -world_position.y; vec3 transmittance = exp(-water_absorption_per_m * (depth_m - reference_depth_m)); float brightness = dot(transmittance, vec3(0.2126, 0.7152, 0.0722)); vec3 chroma = transmittance / max(brightness, 0.001); daylight = max(brightness, minimum_daylight); // Normalise so the authored palette holds at the reference depth once the // reference transmittance (red already filtered by 24 m of water) is applied. vec3 reference_chroma = exp(-water_absorption_per_m * reference_depth_m); reference_chroma /= dot(reference_chroma, vec3(0.2126, 0.7152, 0.0722)); ALBEDO = albedo * chroma * reference_chroma; // Detail shading: pits of the grain map and ripple troughs read as occlusion; // the daylight factor dims the sky ambient with depth. 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) * daylight; AO_LIGHT_AFFECT = 0.35; ROUGHNESS = mix(0.95, 0.78 + 0.12 * grain, rockness); METALLIC = 0.0; } // Lambert diffuse; the sun (directional) is dimmed by the water column, other lights // (submarine projectors) are not. Sand and rock are rough enough to skip specular. void light() { float sun_factor = LIGHT_IS_DIRECTIONAL ? daylight : 1.0; float lambert = clamp(dot(NORMAL, LIGHT), 0.0, 1.0); DIFFUSE_LIGHT += lambert * ATTENUATION * LIGHT_COLOR * ALBEDO * sun_factor / PI; }