diff --git a/docs/briefs/captures/terrain_showcase_boulders.png b/docs/briefs/captures/terrain_showcase_boulders.png index 4ffb25c..45a6962 100644 Binary files a/docs/briefs/captures/terrain_showcase_boulders.png and b/docs/briefs/captures/terrain_showcase_boulders.png differ diff --git a/docs/briefs/captures/terrain_showcase_canyon.png b/docs/briefs/captures/terrain_showcase_canyon.png index 2692e79..c2296c1 100644 Binary files a/docs/briefs/captures/terrain_showcase_canyon.png and b/docs/briefs/captures/terrain_showcase_canyon.png differ diff --git a/docs/briefs/captures/terrain_showcase_crevasse.png b/docs/briefs/captures/terrain_showcase_crevasse.png index 837d41e..87095f8 100644 Binary files a/docs/briefs/captures/terrain_showcase_crevasse.png and b/docs/briefs/captures/terrain_showcase_crevasse.png differ diff --git a/docs/briefs/captures/terrain_showcase_spurs.png b/docs/briefs/captures/terrain_showcase_spurs.png index 2d7f21b..b792a26 100644 Binary files a/docs/briefs/captures/terrain_showcase_spurs.png and b/docs/briefs/captures/terrain_showcase_spurs.png differ diff --git a/docs/briefs/captures/zone1_vigilant_plateau.png b/docs/briefs/captures/zone1_vigilant_plateau.png index 0dbf5e6..1c9e600 100644 Binary files a/docs/briefs/captures/zone1_vigilant_plateau.png and b/docs/briefs/captures/zone1_vigilant_plateau.png differ diff --git a/docs/briefs/captures/zone1_vigilant_tombant.png b/docs/briefs/captures/zone1_vigilant_tombant.png index 51e020c..b40a037 100644 Binary files a/docs/briefs/captures/zone1_vigilant_tombant.png and b/docs/briefs/captures/zone1_vigilant_tombant.png differ diff --git a/docs/briefs/terrain-site-zone1-gameplay.md b/docs/briefs/terrain-site-zone1-gameplay.md index 6f8d9e2..cde9bfa 100644 --- a/docs/briefs/terrain-site-zone1-gameplay.md +++ b/docs/briefs/terrain-site-zone1-gameplay.md @@ -265,6 +265,14 @@ de contacts existant les sépare du fond. atténués. Plancher à 5 % pour que le pied du tombant reste lisible. Palette GDP §18.2 obtenue de fait : vert-bleu à 24 m, bleu froid à 45 m, indigo au pied du tombant ; ciel, brouillard et soleil (0,9, ombres 45 %) refroidis. +- Brouillard par profondeur : un `FogVolume` (`WaterColumn`, 1,8 km × 260 m) + porte un shader `fog` (`water_column_fog.gdshader`) dont l'albédo suit la même + transmittance spectrale que le fond et dont la densité croît lentement sous + 24 m ; la densité volumétrique globale passe à 0, la portée volumétrique à + 128 m, le brouillard de distance (couleur fixe, 0,0045) ne couvre que + l'au-delà. Tuilage du sable cassé par une déformation de domaine lente + (37 m), une seconde échelle de rides (11,3 m) et une extinction des rides + entre 40 et 60 m de fond. **État du 2026-10-06 (demande d'Olivier)** : le shader mélange sable et roche par la pente à partir de 20° (sable sous 20°, roche au-dessus de 40°, fondu diff --git a/game/world/sites/dive_site.tscn b/game/world/sites/dive_site.tscn index 6c1f647..736222d 100644 --- a/game/world/sites/dive_site.tscn +++ b/game/world/sites/dive_site.tscn @@ -1,7 +1,11 @@ -[gd_scene load_steps=6 format=3] +[gd_scene load_steps=9 format=3] [ext_resource type="Script" path="res://game/world/sites/dive_site.gd" id="1_site"] [ext_resource type="Script" path="res://game/world/sites/bathymetric_terrain_actor.gd" id="2_terrain"] +[ext_resource type="Shader" path="res://game/world/sites/water_column_fog.gdshader" id="3_water_column"] + +[sub_resource type="ShaderMaterial" id="WaterColumnMaterial"] +shader = ExtResource("3_water_column") [sub_resource type="ProceduralSkyMaterial" id="SkyUnderwater"] sky_top_color = Color(0.22, 0.52, 0.66, 1) @@ -51,15 +55,16 @@ fog_enabled = true 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_density = 0.0045 fog_aerial_perspective = 0.5 fog_height = -4.0 fog_height_density = 0.025 volumetric_fog_enabled = true -volumetric_fog_density = 0.01 +volumetric_fog_density = 0.0 volumetric_fog_albedo = Color(0.5, 0.78, 0.92, 1) volumetric_fog_anisotropy = 0.45 -volumetric_fog_length = 72.0 +volumetric_fog_length = 128.0 +volumetric_fog_detail_spread = 1.6 volumetric_fog_ambient_inject = 0.5 adjustment_enabled = true adjustment_contrast = 1.06 @@ -82,6 +87,11 @@ shadow_blur = 2.0 directional_shadow_max_distance = 160.0 directional_shadow_fade_start = 0.75 +[node name="WaterColumn" type="FogVolume" parent="."] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -110, 0) +size = Vector3(1800, 260, 1800) +material = SubResource("WaterColumnMaterial") + [node name="Terrain" type="StaticBody3D" parent="."] unique_name_in_owner = true script = ExtResource("2_terrain") diff --git a/game/world/sites/seabed_terrain.gdshader b/game/world/sites/seabed_terrain.gdshader index ff2206b..69ddf31 100644 --- a/game/world/sites/seabed_terrain.gdshader +++ b/game/world/sites/seabed_terrain.gdshader @@ -29,6 +29,14 @@ uniform sampler2D sand_normal_map : hint_normal, repeat_enable, filter_linear_mi 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; @@ -81,17 +89,27 @@ void fragment() { 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. + // 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 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; + 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. diff --git a/game/world/sites/water_column_fog.gdshader b/game/world/sites/water_column_fog.gdshader new file mode 100644 index 0000000..99f3f66 --- /dev/null +++ b/game/world/sites/water_column_fog.gdshader @@ -0,0 +1,25 @@ +shader_type fog; + +// Water column for the volumetric fog: the scattering albedo follows the same +// per-channel Beer-Lambert transmittance as the seabed shader, so the haze turns +// from green-blue near the plateau to indigo at the drop-off foot, and the density +// grows slowly with depth (more suspended sediment, less light). It covers every +// object in the volume, not only the terrain. + +uniform vec3 shallow_albedo : source_color = vec3(0.5, 0.78, 0.92); +uniform vec3 water_absorption_per_m = vec3(0.11, 0.04, 0.032); +uniform float reference_depth_m = 24.0; +uniform float minimum_albedo = 0.03; +uniform float base_density = 0.012; +uniform float density_per_m = 0.00012; +uniform float emission_strength = 0.0; + +void fog() { + // Above the reference depth the haze keeps its shallow colour: brightening it + // further would wash the upper water column towards white. + float excess_depth_m = max(-WORLD_POSITION.y - reference_depth_m, 0.0); + vec3 transmittance = exp(-water_absorption_per_m * excess_depth_m); + ALBEDO = clamp(shallow_albedo * transmittance, vec3(minimum_albedo), vec3(1.0)); + DENSITY = base_density + density_per_m * excess_depth_m; + EMISSION = vec3(emission_strength); +} diff --git a/tests/integration/run_site_terrain_integration.gd b/tests/integration/run_site_terrain_integration.gd index 266923c..6f167e8 100644 --- a/tests/integration/run_site_terrain_integration.gd +++ b/tests/integration/run_site_terrain_integration.gd @@ -19,7 +19,7 @@ const DIVE_SITE_SCENE = preload("res://game/world/sites/dive_site.tscn") const ZONE1_SITE_PATH := "res://content/sites/zone1_vigilant.tres" const SHOWCASE_SITE_PATH := "res://content/sites/terrain_showcase.tres" const GROOVE_PROBE_OFFSET_M := 25.0 -const TEST_COUNT := 40 +const TEST_COUNT := 41 var _failures: Array[String] = [] @@ -179,6 +179,12 @@ func _test_dive_site_scene() -> void: await physics_frame await physics_frame _expect(site.terrain().tile_count() > 0, "site builds its terrain tiles") + var water_column := site.get_node_or_null("WaterColumn") as FogVolume + _expect( + water_column != null and water_column.material is ShaderMaterial + and (water_column.material as ShaderMaterial).shader != null, + "site carries a water-column fog volume with its depth shader" + ) _expect(site.placeholder_count() == 2, "calibration target and beacon are placed") var target := site.get_node_or_null("%Placements/calibration_target") as StaticBody3D _expect(target != null and int(target.get_meta("sonar_material")) == SonarMaterial.Value.METAL, "wreck placeholder is a metal sonar body")