feat(world): depth-attenuated daylight and cold-water palette for the seabed

The seabed shader applies per-channel Beer-Lambert transmittance relative
to a 24 m reference depth: the scalar part dims the directional light in a
custom light() function and the sky ambient through AO, the chroma part
tints the albedo, and artificial lights are left untouched for the
submarine's projectors. Sky, fog and sun are cooled and the sun softened
so the plateau reads green-blue, the canyon floor cold blue and the
drop-off foot near-indigo.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Fable 5.1 committed 2026-10-06 23:21:14 +02:00
1 parent 26a7dfc4f7
commit 34b39a8dbc
9 files changed
+58 -23

No files matched your search

+11 -11
View File
@@ -4,12 +4,12 @@
[ext_resource type="Script" path="res://game/world/sites/bathymetric_terrain_actor.gd" id="2_terrain"]
[sub_resource type="ProceduralSkyMaterial" id="SkyUnderwater"]
sky_top_color = Color(0.3, 0.62, 0.64, 1)
sky_horizon_color = Color(0.13, 0.36, 0.41, 1)
sky_top_color = Color(0.22, 0.52, 0.66, 1)
sky_horizon_color = Color(0.09, 0.28, 0.4, 1)
sky_curve = 0.2
sky_energy_multiplier = 1.0
ground_horizon_color = Color(0.11, 0.3, 0.35, 1)
ground_bottom_color = Color(0.015, 0.05, 0.07, 1)
ground_horizon_color = Color(0.05, 0.17, 0.27, 1)
ground_bottom_color = Color(0.01, 0.03, 0.07, 1)
ground_curve = 0.1
ground_energy_multiplier = 0.6
sun_angle_max = 40.0
@@ -26,7 +26,7 @@ background_mode = 2
sky = SubResource("SkyResource")
ambient_light_source = 3
ambient_light_sky_contribution = 1.0
ambient_light_energy = 1.0
ambient_light_energy = 1.3
reflected_light_source = 1
tonemap_mode = 2
tonemap_exposure = 1.15
@@ -48,8 +48,8 @@ glow_intensity = 0.8
glow_blend_mode = 0
glow_hdr_threshold = 1.2
fog_enabled = true
fog_light_color = Color(0.1, 0.3, 0.35, 1)
fog_light_energy = 0.52
fog_light_color = Color(0.05, 0.18, 0.29, 1)
fog_light_energy = 0.38
fog_sun_scatter = 0.15
fog_density = 0.006
fog_aerial_perspective = 0.5
@@ -57,7 +57,7 @@ fog_height = -4.0
fog_height_density = 0.025
volumetric_fog_enabled = true
volumetric_fog_density = 0.01
volumetric_fog_albedo = Color(0.6, 0.85, 0.9, 1)
volumetric_fog_albedo = Color(0.5, 0.78, 0.92, 1)
volumetric_fog_anisotropy = 0.45
volumetric_fog_length = 72.0
volumetric_fog_ambient_inject = 0.5
@@ -73,11 +73,11 @@ environment = SubResource("EnvironmentUnderwater")
[node name="AmbientDirection" type="DirectionalLight3D" parent="."]
rotation_degrees = Vector3(-62, -28, 0)
light_color = Color(0.56, 0.8, 0.82, 1)
light_energy = 1.35
light_color = Color(0.5, 0.78, 0.88, 1)
light_energy = 0.9
light_angular_distance = 3.0
shadow_enabled = true
shadow_opacity = 0.7
shadow_opacity = 0.45
shadow_blur = 2.0
directional_shadow_max_distance = 160.0
directional_shadow_fade_start = 0.75
+39 -12
View File
@@ -7,14 +7,23 @@ render_mode depth_draw_opaque;
// 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.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 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)
uniform float depth_tint_start_m = 20.0;
uniform float depth_tint_end_m = 90.0;
// 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;
@@ -31,6 +40,7 @@ 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;
@@ -92,20 +102,37 @@ void fragment() {
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.
// 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);
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.
// 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);
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;
}