diff --git a/content/sites/terrain_showcase_terrain.res b/content/sites/terrain_showcase_terrain.res index acec241..b3ff08d 100644 Binary files a/content/sites/terrain_showcase_terrain.res and b/content/sites/terrain_showcase_terrain.res differ diff --git a/content/sites/zone1_vigilant.tres b/content/sites/zone1_vigilant.tres index dc5f216..e73392c 100644 --- a/content/sites/zone1_vigilant.tres +++ b/content/sites/zone1_vigilant.tres @@ -9,7 +9,7 @@ script = ExtResource("1_n3xpu") placement_id = &"prospection_vessel" kind = 0 -position_m = Vector3(-14.154642, -37.198338, 98.39586) +position_m = Vector3(-14.154642, -36.88306, 98.39586) yaw_radians = -1.380808047881126 footprint_m = Vector3(28, 8, 9) @@ -17,7 +17,7 @@ footprint_m = Vector3(28, 8, 9) script = ExtResource("1_n3xpu") placement_id = &"decoy_yacht" kind = 1 -position_m = Vector3(-30, -25.199738, -150) +position_m = Vector3(-30, -24.996498, -150) yaw_radians = 1.1 footprint_m = Vector3(12, 3, 4) @@ -25,7 +25,7 @@ footprint_m = Vector3(12, 3, 4) script = ExtResource("1_n3xpu") placement_id = &"decoy_coaster" kind = 1 -position_m = Vector3(140, -35.265244, 40) +position_m = Vector3(140, -35.34944, 40) yaw_radians = -0.6 footprint_m = Vector3(26, 6, 8) @@ -33,14 +33,14 @@ footprint_m = Vector3(26, 6, 8) script = ExtResource("1_n3xpu") placement_id = &"secondary_item" kind = 2 -position_m = Vector3(-6, -61.61916, 157) +position_m = Vector3(-6, -63.26594, 160) footprint_m = Vector3(1, 1, 1) [sub_resource type="Resource" id="Resource_aervf"] script = ExtResource("1_n3xpu") placement_id = &"debris_1" kind = 3 -position_m = Vector3(-12, -45.756382, 134) +position_m = Vector3(-12, -45.80824, 136) yaw_radians = 0.4 footprint_m = Vector3(3, 2, 2) @@ -48,7 +48,7 @@ footprint_m = Vector3(3, 2, 2) script = ExtResource("1_n3xpu") placement_id = &"debris_2" kind = 3 -position_m = Vector3(-10, -48.518223, 139) +position_m = Vector3(-10, -47.295185, 139) yaw_radians = 1.2 footprint_m = Vector3(6, 2.5, 2.5) @@ -56,14 +56,14 @@ footprint_m = Vector3(6, 2.5, 2.5) script = ExtResource("1_n3xpu") placement_id = &"debris_3" kind = 3 -position_m = Vector3(-9, -53.75445, 146) +position_m = Vector3(-9, -52.854576, 146) footprint_m = Vector3(2, 1.5, 2) [sub_resource type="Resource" id="Resource_hd5cl"] script = ExtResource("1_n3xpu") placement_id = &"debris_4" kind = 3 -position_m = Vector3(-7, -58.142864, 152) +position_m = Vector3(-7, -58.174175, 152) yaw_radians = 2.0 footprint_m = Vector3(4, 1.5, 1.5) @@ -91,7 +91,7 @@ position_m = Vector3(0, -25, -230) [sub_resource type="Resource" id="Resource_u8dqp"] script = ExtResource("1_n3xpu") placement_id = &"saignee_entrance" -position_m = Vector3(-56.978443, -35.474903, -124.287895) +position_m = Vector3(-56.978443, -35.54769, -124.287895) [sub_resource type="Resource" id="Resource_3uttw"] script = ExtResource("1_n3xpu") @@ -101,7 +101,7 @@ position_m = Vector3(40, -40, 125) [sub_resource type="Resource" id="Resource_xvesq"] script = ExtResource("2_36hd6") route_id = &"safe_ridge" -waypoints_m = PackedVector3Array(0, -14, -210, 70, -16, -200, 76, -16, -100, 78, -16, 0, 76, -18, 80, 40, -24, 105, 0, -26, 100, -14, -30, 98) +waypoints_m = PackedVector3Array(0, -14, -210, 70, -16, -200, 76, -16, -100, 78, -16, 0, 76, -18, 80, 40, -24, 105, 0, -24, 100, -14, -30, 98) maximum_depth_m = 32.0 [sub_resource type="Resource" id="Resource_lcu7s"] diff --git a/content/sites/zone1_vigilant_terrain.res b/content/sites/zone1_vigilant_terrain.res index 370bc29..92c9bcf 100644 Binary files a/content/sites/zone1_vigilant_terrain.res and b/content/sites/zone1_vigilant_terrain.res differ diff --git a/docs/adr/0012-authored-heightfield-dive-sites.md b/docs/adr/0012-authored-heightfield-dive-sites.md index 1df1eb4..0f4416c 100644 --- a/docs/adr/0012-authored-heightfield-dive-sites.md +++ b/docs/adr/0012-authored-heightfield-dive-sites.md @@ -74,8 +74,11 @@ pente, des matériaux imposés par primitive et des `MaterialPatch`. (butte ou fosse à sommet plat, contour bruité, matériau roche imposé), `BoulderField` (blocs semi-ellipsoïdaux tirés par graine, combinés par maximum, roche imposée). -- Modificateur : `Strata` quantifie les pentes raides en gradins horizontaux ; - les fonds et sommets plats ne bougent pas. +- Modificateurs : `Strata` quantifie les pentes raides en gradins horizontaux + dans un rectangle monde, les fonds et sommets plats ne bougent pas ; + `SlopeRoughness` ajoute un bruit à crêtes pondéré par la pente (roche + éraillée, sable lisse). `Roughness` accepte un nombre d'octaves et un type + fractal pour donner du relief de fond à plusieurs échelles. Tout aléa passe par une graine explicite (`FastNoiseLite`, `RandomNumberGenerator`) ; `tests/unit/run_site_baker_tests.gd` vérifie que diff --git a/docs/briefs/captures/terrain_showcase_boulders.png b/docs/briefs/captures/terrain_showcase_boulders.png index d409a32..91189a6 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 f4784b2..92f1af7 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 64c609c..3eacdeb 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_escarpment.png b/docs/briefs/captures/terrain_showcase_escarpment.png index da5b505..15d90ca 100644 Binary files a/docs/briefs/captures/terrain_showcase_escarpment.png and b/docs/briefs/captures/terrain_showcase_escarpment.png differ diff --git a/docs/briefs/captures/terrain_showcase_spurs.png b/docs/briefs/captures/terrain_showcase_spurs.png index 8198eed..7731aeb 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/terrain_showcase_terrain_preview.png b/docs/briefs/captures/terrain_showcase_terrain_preview.png index d856bd4..b0fd48b 100644 Binary files a/docs/briefs/captures/terrain_showcase_terrain_preview.png and b/docs/briefs/captures/terrain_showcase_terrain_preview.png differ diff --git a/docs/briefs/captures/zone1_vigilant_plateau.png b/docs/briefs/captures/zone1_vigilant_plateau.png new file mode 100644 index 0000000..397e7df Binary files /dev/null and b/docs/briefs/captures/zone1_vigilant_plateau.png differ diff --git a/docs/briefs/captures/zone1_vigilant_plateau.png.import b/docs/briefs/captures/zone1_vigilant_plateau.png.import new file mode 100644 index 0000000..95b6baa --- /dev/null +++ b/docs/briefs/captures/zone1_vigilant_plateau.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://d3qajwshxpplq" +path="res://.godot/imported/zone1_vigilant_plateau.png-6e6da03a831034d096e040e9808fd1b8.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/briefs/captures/zone1_vigilant_plateau.png" +dest_files=["res://.godot/imported/zone1_vigilant_plateau.png-6e6da03a831034d096e040e9808fd1b8.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/briefs/captures/zone1_vigilant_terrain_preview.png b/docs/briefs/captures/zone1_vigilant_terrain_preview.png index 7792a35..105bfaa 100644 Binary files a/docs/briefs/captures/zone1_vigilant_terrain_preview.png and b/docs/briefs/captures/zone1_vigilant_terrain_preview.png differ diff --git a/docs/briefs/captures/zone1_vigilant_tombant.png b/docs/briefs/captures/zone1_vigilant_tombant.png new file mode 100644 index 0000000..8513a9f Binary files /dev/null and b/docs/briefs/captures/zone1_vigilant_tombant.png differ diff --git a/docs/briefs/captures/zone1_vigilant_tombant.png.import b/docs/briefs/captures/zone1_vigilant_tombant.png.import new file mode 100644 index 0000000..515220c --- /dev/null +++ b/docs/briefs/captures/zone1_vigilant_tombant.png.import @@ -0,0 +1,40 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cm2nm65ktggp" +path="res://.godot/imported/zone1_vigilant_tombant.png-71b9b2e227c187a355cfe8ea0a7a15f7.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://docs/briefs/captures/zone1_vigilant_tombant.png" +dest_files=["res://.godot/imported/zone1_vigilant_tombant.png-71b9b2e227c187a355cfe8ea0a7a15f7.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/docs/briefs/terrain-site-zone1-gameplay.md b/docs/briefs/terrain-site-zone1-gameplay.md index cd9d4f8..774f10f 100644 --- a/docs/briefs/terrain-site-zone1-gameplay.md +++ b/docs/briefs/terrain-site-zone1-gameplay.md @@ -2,6 +2,14 @@ **Statut : validé par Olivier le 2026-10-06** (plan de masse, objet secondaire à 62 m, pipeline par script de cuisson, nom « Banc du Vigilant » conservé). Sert de spécification de référence pour l'implémentation. +**Recuisson du 2026-10-06 (demande d'Olivier)** : la rupture et le tombant +sont stratifiés en gradins de 4 m (`Strata`, bande z ≥ 118 m, hors Saignée et +amers) ; le plateau reçoit un relief de fond à trois échelles (250–15 m ± 2,2 m, +80–10 m ± 1,2 m, 20–5 m ± 0,6 m) et la roche un bruit de pente (`SlopeRoughness`). +L'objet secondaire est reculé de 3 m (63 m), le premier débris de 2 m, et le +point de la route sûre au-dessus de la lèvre de la Saignée remonté à 24 m pour +garder 3 m de dégagement. Voir `docs/briefs/terrain-varied-landforms.md` §4.3 bis. + Rédigée le 2026-10-06 à partir de `docs/GAME_DESIGN_PLAN.md`, `docs/VERTICAL_SLICE_TECHNICAL_PLAN.md`, `docs/POC_B_TECHNICAL_BASELINE.md`, des ADR 0004, 0006, 0008, 0010 et du code sous `game/world/`. Rien n'a été diff --git a/docs/briefs/terrain-varied-landforms.md b/docs/briefs/terrain-varied-landforms.md index d02a0fe..80f5de0 100644 --- a/docs/briefs/terrain-varied-landforms.md +++ b/docs/briefs/terrain-varied-landforms.md @@ -91,12 +91,23 @@ ou `smoothstep`) et **mono-segment**. Il manque : | Modificateur | Paramètres | Effet | |---|---|---| -| `Strata` | `layer_height_m`, `sharpness` (0–1), `minimum_slope_degrees`, `full_slope_degrees`, `centre`, `radius_m` (0 = partout) | quantifie l'élévation en gradins de `layer_height_m` là où la pente dépasse `minimum_slope_degrees` (plein effet à `full_slope_degrees`) ; `sharpness` 0 = aucun effet, 1 = marches franches ; le fond plat et le plateau ne bougent pas | +| `Strata` | `layer_height_m`, `sharpness` (0–1), `minimum_slope_degrees`, `full_slope_degrees`, `region_m: Rect2` (vide = partout) | quantifie l'élévation en gradins de `layer_height_m` là où la pente dépasse `minimum_slope_degrees` (plein effet à `full_slope_degrees`) ; `sharpness` 0 = aucun effet, 1 = marches franches ; le fond plat et le plateau ne bougent pas | Formule : `f = frac(e / h)`, `f' = clamp((f - 0.5) / (1 - s) + 0.5, 0, 1)`, `e' = lerp(e, floor(e / h) * h + h * f', w)` avec `w = smoothstep(tan(min), tan(full), |gradient|) * masque`. +### 4.3 bis Relief de fond (ajout du 2026-10-06, demande d'Olivier) + +Les plateaux cuits avec la seule `Roughness` à 50 m (± 0,6 m) restaient +plats. `Roughness` prend maintenant un nombre d'octaves et un type fractal +(`FRACTAL_FBM` roulant, `FRACTAL_RIDGED` à crêtes) : une fréquence de 0,004 +sur cinq octaves donne des mamelons et des cuvettes de 250 m à 15 m en une +primitive. Le modificateur `SlopeRoughness` (`seed`, `frequency`, +`amplitude_m`, `minimum_slope_degrees`, `full_slope_degrees`) ajoute un bruit +à crêtes pondéré par la pente : la roche s'éraille, le sable reste lisse. Les +deux sites l'utilisent (ondulation ± 2,2 m, roche ± 0,7 m). + ### 4.4 Ce qui ne change pas Le contrat de données (`SiteTerrainData`, ADR 0012 §2), l'acteur de monde, la @@ -170,7 +181,10 @@ Plan de masse (nord en haut) : (champ de hauteur vu de dessus, roche teintée) et cinq vues en jeu, graybox, `terrain_showcase_canyon.png` (gradins et fond ridé), `_crevasse.png`, `_boulders.png`, `_spurs.png`, `_escarpment.png` (face de la falaise vue du - pied). Le rendu (brouillard, rides en albédo) n'est pas l'objet de ce brief. + pied) ; pour le Banc du Vigilant recuit, `zone1_vigilant_tombant.png` + (gradins du tombant) et `zone1_vigilant_plateau.png` (plateau ondulé). Le + rendu (brouillard, rides en albédo, absence d'ombrage de détail) n'est pas + l'objet de ce brief : à ce stade, le relief se lit surtout au sonar. ## 8. Décisions laissées à Olivier @@ -179,9 +193,11 @@ Plan de masse (nord en haut) : couche : à 1 m par cellule, des couches de 4 m sur une paroi à 55° ; une paroi à 76° (première cuisson) ne montrait aucun gradin. Les sites de mission restent à 1,5 m sauf décision contraire ; une crevasse de 5 m à 1,5 m par cellule n'a que trois cellules de large. -2. **Strates partout ?** Le modificateur est appliqué au canyon seulement. - L'appliquer à la falaise sud du Banc du Vigilant est un changement de - recette, donc de `.res`, donc une re-cuisson à valider. +2. **Strates sur le Banc du Vigilant** : décidé par Olivier le 2026-10-06, + appliquées à la rupture et au tombant (bande z ≥ 118 m, couches de 4 m, + plein effet dès 40° ; la Saignée, l'Échine et les amers sont hors bande). + Le `.res` du site a été recuit ; l'objet secondaire, les débris et les + routes restent dans leurs contrats. 3. **Sites de mission suivants** (U-Boot sous falaise, océanographique en canyon) : les primitives sont prêtes ; le plan de masse de chaque site reste à proposer et valider séparément. diff --git a/tests/unit/run_site_baker_tests.gd b/tests/unit/run_site_baker_tests.gd index e0c3191..4665b84 100644 --- a/tests/unit/run_site_baker_tests.gd +++ b/tests/unit/run_site_baker_tests.gd @@ -8,7 +8,7 @@ const SiteTerrainData = preload("res://game/domain/sites/site_terrain_data.gd") const ZONE1_TERRAIN_PATH := "res://content/sites/zone1_vigilant_terrain.res" -const TEST_COUNT := 44 +const TEST_COUNT := 48 var _failures: Array[String] = [] @@ -23,6 +23,7 @@ func _init() -> void: _test_outcrop() _test_boulder_field() _test_strata() + _test_slope_roughness() if _failures.is_empty(): print("Site baker unit tests: %d passed, 0 failed." % TEST_COUNT) @@ -187,7 +188,7 @@ func _test_strata() -> void: recipe.origin_z_m = -10.0 recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 1.0, 0.0, 0.0, 0.0)) var plain := SiteTerrainBaker.bake(recipe) - recipe.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.9, 20.0, 35.0, Vector2.ZERO, 0.0)) + recipe.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.9, 20.0, 35.0, Rect2())) var terraced := SiteTerrainBaker.bake(recipe) var flat_cells := 0 var riser_cells := 0 @@ -202,17 +203,48 @@ func _test_strata() -> void: var flat := _small_recipe() flat.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.3, 0.0, 0.0, 0.0, 0.0)) - flat.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.9, 20.0, 35.0, Vector2.ZERO, 0.0)) + flat.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.9, 20.0, 35.0, Rect2())) var flat_data := SiteTerrainBaker.bake(flat) _expect(is_equal_approx(flat_data.elevations_m[flat_data.index_of(10, 10)], -30.3), "strata leave flat ground alone") var identity := _small_recipe() identity.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 1.0, 0.0, 0.0, 0.0)) var before := SiteTerrainBaker.bake(identity) - identity.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.0, 20.0, 35.0, Vector2.ZERO, 0.0)) + identity.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.0, 20.0, 35.0, Rect2())) var after := SiteTerrainBaker.bake(identity) _expect(before.elevations_m == after.elevations_m, "sharpness 0 is the identity") + # Depth 30.7 keeps the plane off the layer boundaries, where quantisation is the identity. + var banded := _small_recipe() + banded.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.7, 1.0, 0.0, 0.0, 0.0)) + var untouched := SiteTerrainBaker.bake(banded) + banded.modifiers.append(SiteTerrainBaker.Strata.new(2.0, 0.9, 20.0, 35.0, Rect2(-20.0, 0.0, 40.0, 20.0))) + var half := SiteTerrainBaker.bake(banded) + var north_same := half.elevations_m[half.index_of(13, 5)] == untouched.elevations_m[untouched.index_of(13, 5)] + var south_changed := half.elevations_m[half.index_of(13, 15)] != untouched.elevations_m[untouched.index_of(13, 15)] + _expect(north_same and south_changed, "strata stay inside their region") + + +func _test_slope_roughness() -> void: + var recipe := _small_recipe() + recipe.primitives.append(SiteTerrainBaker.SlopedPlane.new(30.0, 0.0, 0.0, 0.0, 0.0)) + recipe.primitives.append(SiteTerrainBaker.Blob.new(Vector2(10.0, 10.0), 5.0, 5.0, 12.0)) + var smooth := SiteTerrainBaker.bake(recipe) + recipe.modifiers.append(SiteTerrainBaker.SlopeRoughness.new(5, 0.2, 1.0, 20.0, 40.0)) + var rough := SiteTerrainBaker.bake(recipe) + _expect(rough.elevations_m[rough.index_of(0, 0)] == smooth.elevations_m[smooth.index_of(0, 0)], "slope roughness leaves flat sand untouched") + var flank_changed := false + for column in range(13, 18): + flank_changed = flank_changed or rough.elevations_m[rough.index_of(column, 15)] != smooth.elevations_m[smooth.index_of(column, 15)] + _expect(flank_changed, "slope roughness roughens the steep flank") + var octaves := _small_recipe() + octaves.primitives.append(SiteTerrainBaker.Roughness.new(7, 0.01, 2.0, 5, FastNoiseLite.FRACTAL_RIDGED)) + var ridged := SiteTerrainBaker.bake(octaves) + var within := true + for elevation: float in ridged.elevations_m: + within = within and absf(elevation) <= 2.0 + 0.001 + _expect(within, "roughness amplitude bounds the ridged multi-octave noise") + func _test_zone1_artefact_is_stable() -> void: var versioned := load(ZONE1_TERRAIN_PATH) as SiteTerrainData diff --git a/tools/site_terrain_baker.gd b/tools/site_terrain_baker.gd index 7281613..431b3de 100644 --- a/tools/site_terrain_baker.gd +++ b/tools/site_terrain_baker.gd @@ -410,18 +410,28 @@ class Ripples extends Primitive: return amplitude_m * sin(phase) * falloff +## Seeded fractal noise added everywhere. frequency is 1 / wavelength of the largest +## octave; each further octave halves the wavelength and the amplitude, so a low +## frequency with five octaves gives hummocks, swales and ripple-scale detail at once. +## FRACTAL_RIDGED makes sharp crests (rock), FRACTAL_FBM rolling ground (sediment). class Roughness extends Primitive: var amplitude_m: float var _noise: FastNoiseLite - func _init(p_seed: int, p_frequency: float, p_amplitude_m: float) -> void: + func _init( + p_seed: int, + p_frequency: float, + p_amplitude_m: float, + p_octaves: int = 3, + p_fractal_type: FastNoiseLite.FractalType = FastNoiseLite.FRACTAL_FBM + ) -> void: amplitude_m = p_amplitude_m _noise = FastNoiseLite.new() _noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH _noise.seed = p_seed _noise.frequency = p_frequency - _noise.fractal_type = FastNoiseLite.FRACTAL_FBM - _noise.fractal_octaves = 3 + _noise.fractal_type = p_fractal_type + _noise.fractal_octaves = p_octaves func elevation_delta(x_m: float, z_m: float) -> float: return _noise.get_noise_2d(x_m, z_m) * amplitude_m @@ -437,12 +447,13 @@ class Modifier extends RefCounted: ## Quantises steep ground into horizontal ledges layer_height_m apart: the stratified ## walls of a canyon or cliff. Flat floors and summits are below minimum_slope_degrees and ## stay untouched; the effect is full from full_slope_degrees. sharpness 0 leaves the -## field unchanged, 1 cuts hard steps. radius_m 0 applies everywhere. +## field unchanged, 1 cuts hard steps. region_m is a world-space (x, z) rectangle with a +## hard edge; an empty rectangle applies everywhere. Ledges need two to three cells of +## wall per layer to be resolved: at 1.5 m per cell, 4 m layers on a 50 degree wall. class Strata extends Modifier: var layer_height_m: float var sharpness: float - var centre: Vector2 - var radius_m: float + var region_m: Rect2 var _minimum_tangent: float var _full_tangent: float @@ -451,21 +462,19 @@ class Strata extends Modifier: p_sharpness: float, p_minimum_slope_degrees: float, p_full_slope_degrees: float, - p_centre: Vector2, - p_radius_m: float + p_region_m: Rect2 ) -> void: assert(p_layer_height_m > 0.0, "strata need a positive layer height") layer_height_m = p_layer_height_m sharpness = clampf(p_sharpness, 0.0, 0.999) - centre = p_centre - radius_m = p_radius_m + region_m = p_region_m _minimum_tangent = tan(deg_to_rad(p_minimum_slope_degrees)) _full_tangent = tan(deg_to_rad(p_full_slope_degrees)) func modify(x_m: float, z_m: float, elevation_m: float, gradient: Vector2) -> float: + if region_m.has_area() and not region_m.has_point(Vector2(x_m, z_m)): + return elevation_m var weight := smoothstep(_minimum_tangent, _full_tangent, gradient.length()) - if radius_m > 0.0: - weight *= 1.0 - smoothstep(radius_m * 0.8, radius_m, Vector2(x_m, z_m).distance_to(centre)) if weight <= 0.0: return elevation_m var layer := floorf(elevation_m / layer_height_m) @@ -474,6 +483,39 @@ class Strata extends Modifier: return lerpf(elevation_m, (layer + sharpened) * layer_height_m, weight) +## Seeded noise weighted by the local slope: rock faces and ridge flanks get crumbly +## while flat sand stays smooth. Zero below minimum_slope_degrees, full amplitude from +## full_slope_degrees. +class SlopeRoughness extends Modifier: + var amplitude_m: float + var _noise: FastNoiseLite + var _minimum_tangent: float + var _full_tangent: float + + func _init( + p_seed: int, + p_frequency: float, + p_amplitude_m: float, + p_minimum_slope_degrees: float, + p_full_slope_degrees: float + ) -> void: + amplitude_m = p_amplitude_m + _noise = FastNoiseLite.new() + _noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH + _noise.seed = p_seed + _noise.frequency = p_frequency + _noise.fractal_type = FastNoiseLite.FRACTAL_RIDGED + _noise.fractal_octaves = 3 + _minimum_tangent = tan(deg_to_rad(p_minimum_slope_degrees)) + _full_tangent = tan(deg_to_rad(p_full_slope_degrees)) + + func modify(x_m: float, z_m: float, elevation_m: float, gradient: Vector2) -> float: + var weight := smoothstep(_minimum_tangent, _full_tangent, gradient.length()) + if weight <= 0.0: + return elevation_m + return elevation_m + _noise.get_noise_2d(x_m, z_m) * amplitude_m * weight + + class MaterialPatch extends RefCounted: var centre: Vector2 var radius_m: float diff --git a/tools/sites/terrain_showcase_recipe.gd b/tools/sites/terrain_showcase_recipe.gd index 377165f..e22a602 100644 --- a/tools/sites/terrain_showcase_recipe.gd +++ b/tools/sites/terrain_showcase_recipe.gd @@ -73,10 +73,10 @@ static func recipe() -> SiteTerrainBaker.Recipe: SiteTerrainBaker.BoulderField.new(Vector2(-100.0, 130.0), 40.0, 302, 6, 6.0, 9.0, 0.6), SiteTerrainBaker.Ripples.new(Vector2(1.0, 0.3), 3.5, 0.3, BOULDER_FIELD_CENTRE, 110.0), - # SE — les Éperons: two pitted rock spurs 6-7 m high framing a 20 m sand channel + # SE — les Éperons: two pitted rock spurs 7-8 m high framing a 20 m sand channel # littered with small blocks. - SiteTerrainBaker.Outcrop.new(WEST_SPUR_CENTRE, 30.0, 60.0, 7.0, 5.0, 401, 7.0, true), - SiteTerrainBaker.Outcrop.new(EAST_SPUR_CENTRE, 30.0, 55.0, 6.0, 5.0, 402, 7.0, true), + SiteTerrainBaker.Outcrop.new(WEST_SPUR_CENTRE, 30.0, 60.0, 8.0, 5.0, 401, 7.0, true), + SiteTerrainBaker.Outcrop.new(EAST_SPUR_CENTRE, 30.0, 55.0, 7.0, 5.0, 402, 7.0, true), SiteTerrainBaker.BoulderField.new(CHANNEL_CENTRE, 50.0, 403, 12, 1.5, 3.0, 0.8), # S — la Corniche: sinuous 22 m escarpment, then a 42 degree slope to the 80 m floor, @@ -86,13 +86,19 @@ static func recipe() -> SiteTerrainBaker.Recipe: SiteTerrainBaker.Outcrop.new(Vector2(40.0, 236.0), 10.0, 9.0, 4.0, 9.0, 503, 2.0, true), SiteTerrainBaker.Outcrop.new(Vector2(130.0, 245.0), 14.0, 11.0, 6.0, 11.0, 504, 2.0, true), - # Roughness: broad undulation plus cell-scale pitting. + # Roughness: hummocks and swales from 250 m down to 15 m, the 50 m texture, + # and cell-scale pitting. + SiteTerrainBaker.Roughness.new(2026, 0.004, 2.2, 5), + SiteTerrainBaker.Roughness.new(2029, 0.012, 1.2, 4), + SiteTerrainBaker.Roughness.new(2030, 0.05, 0.6, 3), SiteTerrainBaker.Roughness.new(1933, 0.02, 0.6), SiteTerrainBaker.Roughness.new(2027, 0.15, 0.2), ] result.modifiers = [ - # Strata on the mesa flanks and canyon walls only: 4 m ledges. - SiteTerrainBaker.Strata.new(4.0, 0.85, 30.0, 50.0, MESA_CENTRE, 130.0), + # Crumbly rock faces, smooth sand. Before the strata so the ledges stay flat. + SiteTerrainBaker.SlopeRoughness.new(2028, 0.12, 0.7, 20.0, 40.0), + # Strata on the north-west quarter only (mesa flanks and canyon walls): 4 m ledges. + SiteTerrainBaker.Strata.new(4.0, 0.85, 30.0, 50.0, Rect2(-256.0, -256.0, 256.0, 256.0)), ] return result diff --git a/tools/sites/zone1_vigilant_recipe.gd b/tools/sites/zone1_vigilant_recipe.gd index 09dc484..168f17c 100644 --- a/tools/sites/zone1_vigilant_recipe.gd +++ b/tools/sites/zone1_vigilant_recipe.gd @@ -15,6 +15,9 @@ const GROOVE_END := Vector2(-10.0, 120.0) const RIDGE_START := Vector2(70.0, -150.0) const RIDGE_END := Vector2(80.0, 90.0) const BREAK_Z := 125.0 +# Strata apply from here south: past the groove's end cap (120 m) and still 3 m before +# the break's smoothstep has dropped half a metre. +const STRATA_START_Z := 118.0 # The target's centre sits this far up the groove from GROOVE_END, so its 28 m hull # stops one groove half-width short of the end cap and rests on the flat groove floor. const TARGET_SET_BACK_M := 22.0 @@ -57,8 +60,22 @@ static func recipe() -> SiteTerrainBaker.Recipe: SiteTerrainBaker.Blob.new(Vector2(25.0, 198.0), 6.3, 6.3, 2.0), # Les Rides: sand ripples near the vessel only. SiteTerrainBaker.Ripples.new(Vector2(1.0, 0.35), 9.0, 0.45, Vector2(0.0, -220.0), 90.0), - # Deterministic roughness everywhere. + # Deterministic roughness everywhere: the original 50 m texture, plus hummocks + # and swales from 250 m down to 15 m (five octaves) so the plateau is never flat. SiteTerrainBaker.Roughness.new(1933, 0.02, 0.6), + SiteTerrainBaker.Roughness.new(1934, 0.004, 2.2, 5), + SiteTerrainBaker.Roughness.new(1936, 0.012, 1.2, 4), + # Hummocks of 20 m down to 5 m, the scale the pilot sees through the fog. + SiteTerrainBaker.Roughness.new(1937, 0.05, 0.6, 3), + ] + result.modifiers = [ + # La rupture et le tombant are a carbonate drop-off: 4 m strata on the break + # (full effect) and fainter ledges on the 35 degree talus below. The band starts + # south of the target hull and of la Saignée's end cap, so neither changes. + # Crumbly rock on every slope steeper than the sand rule, smooth sand elsewhere. + # Before the strata so the ledges stay flat. + SiteTerrainBaker.SlopeRoughness.new(1935, 0.12, 0.7, 20.0, 40.0), + SiteTerrainBaker.Strata.new(4.0, 0.85, 25.0, 40.0, Rect2(-384.0, STRATA_START_Z, 768.0, 384.0 - STRATA_START_Z)), ] result.material_patches = [ SiteTerrainBaker.MaterialPatch.new(Vector2(140.0, 40.0), 28.0, SonarMaterial.Value.ROCK), @@ -90,9 +107,9 @@ static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition: _on_seabed(field, &"decoy_coaster", SitePlacement.Kind.WRECK_DECOY, Vector2(140.0, 40.0), -0.6, Vector3(26.0, 6.0, 8.0), 1.2), _on_seabed(field, &"secondary_item", SitePlacement.Kind.SECONDARY_ITEM, - Vector2(-6.0, 157.0), 0.0, Vector3(1.0, 1.0, 1.0), 0.0), + Vector2(-6.0, 160.0), 0.0, Vector3(1.0, 1.0, 1.0), 0.0), _on_seabed(field, &"debris_1", SitePlacement.Kind.DEBRIS, - Vector2(-12.0, 134.0), 0.4, Vector3(3.0, 2.0, 2.0), 0.3), + Vector2(-12.0, 136.0), 0.4, Vector3(3.0, 2.0, 2.0), 0.3), _on_seabed(field, &"debris_2", SitePlacement.Kind.DEBRIS, Vector2(-10.0, 139.0), 1.2, Vector3(6.0, 2.5, 2.5), 0.4), _on_seabed(field, &"debris_3", SitePlacement.Kind.DEBRIS, @@ -110,7 +127,7 @@ static func definition(terrain: SiteTerrainData) -> DiveSiteDefinition: _route(&"safe_ridge", 32.0, [ Vector3(0.0, -14.0, -210.0), Vector3(70.0, -16.0, -200.0), Vector3(76.0, -16.0, -100.0), Vector3(78.0, -16.0, 0.0), Vector3(76.0, -18.0, 80.0), Vector3(40.0, -24.0, 105.0), - Vector3(0.0, -26.0, 100.0), Vector3(-14.0, -30.0, 98.0), + Vector3(0.0, -24.0, 100.0), Vector3(-14.0, -30.0, 98.0), ]), _route(&"risky_groove", 48.0, [ Vector3(0.0, -14.0, -210.0), Vector3(-60.0, -20.0, -150.0), Vector3(-50.0, -31.0, -100.0),