feat(world): seabed flora and encrusting growth on rock
Quality Gate / Quality Gate (pull_request) Failing after 2m58s

SeabedFloraActor scatters seagrass tufts on flat sand in the lit band and
sea fans on rock between 15 and 60 m, deterministically from the site id,
one MultiMesh per tile and kind, with procedural alpha-cut meshes and
textures (SeabedFloraAssets), vertex sway and wrapped lighting, no
collision and no sonar presence. Placement reads the field arrays
directly and runs on the worker thread pool. The seabed shader grows
turf algae on sunlit rock, coralline crusts in patches and encrusting
sponges in the shade, fading out between 40 and 75 m. The water-column
optics move to a shared shader include.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
OlivierandClaude Fable 5.1 committed 2026-10-07 00:44:10 +02:00
1 parent 122f3f5800
commit 0f9918224a
16 files changed
+585 -14

No files matched your search

+34 -12
View File
@@ -15,8 +15,20 @@ render_mode depth_draw_opaque;
// 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.
@@ -123,19 +135,29 @@ void fragment() {
// 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.
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;
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.
@@ -143,14 +165,14 @@ void fragment() {
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);
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 = LIGHT_IS_DIRECTIONAL ? daylight : 1.0;
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;
}