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. #include "res://game/world/sites/water_light.gdshaderinc" 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); // Encrusting growth on rock: turf algae on sunlit faces, coralline crusts in patches, // encrusting sponges in the shade. Patches come from the grain map read at large // scales; all of it thins out with depth as the light goes. uniform vec3 turf_color : source_color = vec3(0.3, 0.38, 0.2); uniform vec3 coralline_color : source_color = vec3(0.6, 0.4, 0.46); uniform vec3 sponge_color : source_color = vec3(0.82, 0.4, 0.18); uniform float growth_tile_m = 4.2; uniform float growth_strength = 1.0; uniform float growth_fade_start_m = 40.0; uniform float growth_fade_end_m = 75.0; 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); float depth_m = -world_position.y; // Encrusting growth on the rock. float growth_light = (1.0 - smoothstep(growth_fade_start_m, growth_fade_end_m, depth_m)) * growth_strength; float sunlit = clamp(n.y * 0.5 + 0.5, 0.0, 1.0); float patch_a = triplanar_grain(rock_grain_map, world_position / growth_tile_m + vec3(0.21, 0.73, 0.37), n); float patch_b = triplanar_grain(rock_grain_map, world_position / (growth_tile_m * 0.53) + vec3(0.61, 0.11, 0.9), n); float turf_mask = smoothstep(0.4, 0.58, patch_a) * sunlit * growth_light; float coralline_mask = smoothstep(0.6, 0.74, patch_b) * growth_light; float sponge_mask = smoothstep(0.62, 0.74, patch_a * 0.5 + patch_b * 0.5) * (1.0 - 0.6 * sunlit) * growth_light; float grain_mottle = 1.0 + (grain - 0.5) * 0.6; rock_albedo = mix(rock_albedo, turf_color * grain_mottle, turf_mask); rock_albedo = mix(rock_albedo, coralline_color * grain_mottle, coralline_mask); rock_albedo = mix(rock_albedo, sponge_color * grain_mottle, sponge_mask); float rock_roughness = 0.78 + 0.12 * grain; rock_roughness = mix(rock_roughness, 0.6, coralline_mask); rock_roughness = mix(rock_roughness, 0.9, sponge_mask); vec3 albedo = mix(sand_albedo, rock_albedo, rockness); // Water column: spectral transmittance relative to the reference depth. vec3 transmittance = water_transmittance(depth_m, water_absorption_per_m, reference_depth_m); daylight = water_daylight(transmittance, minimum_daylight); ALBEDO = albedo * water_chroma(transmittance, water_absorption_per_m, reference_depth_m); // 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, rock_roughness, 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 = water_sun_factor(LIGHT_IS_DIRECTIONAL, daylight); float lambert = clamp(dot(NORMAL, LIGHT), 0.0, 1.0); DIFFUSE_LIGHT += lambert * ATTENUATION * LIGHT_COLOR * ALBEDO * sun_factor / PI; }