From f78c552d3048a97bbaa4f4ae72c4fce6a84bacb5 Mon Sep 17 00:00:00 2001 From: nathan Date: Sat, 16 May 2026 02:28:31 -0700 Subject: [PATCH] Add Ground Zero environment asset variation --- AGRARIAN_DEVELOPMENT_ROADMAP.md | 2 +- .../Maps/L_GroundZeroTerrain_Test.umap | 4 +- Docs/Terrain/GroundZeroFoliagePass.md | 4 + .../GroundZeroNaturalEnvironmentPass.md | 14 +- Scripts/setup_ground_zero_demo_map.py | 133 ++++++++++++++++++ ...fy_ground_zero_natural_environment_pass.py | 61 +++++++- 6 files changed, 209 insertions(+), 9 deletions(-) diff --git a/AGRARIAN_DEVELOPMENT_ROADMAP.md b/AGRARIAN_DEVELOPMENT_ROADMAP.md index b380dfa..21ff527 100644 --- a/AGRARIAN_DEVELOPMENT_ROADMAP.md +++ b/AGRARIAN_DEVELOPMENT_ROADMAP.md @@ -457,7 +457,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe - [x] Add biome-appropriate natural resources based on Ground Zero. - [x] Add water source. - [x] Replace grey-box environment presentation with an MVP natural environment pass: terrain material, grass, shrubs, bushes, trees, water-source visuals, and clearer Ground Zero biome dressing. Added repeatable Ground Zero environment material generation, applied terrain/foliage/resource/water materials in the map setup, regenerated the demo map, documented the pass, and added verification for material assets plus map assignments. -- [ ] Add first-pass environment asset variation so trees, bushes, grass, resource nodes, and water do not read as repeated placeholders. +- [x] Add first-pass environment asset variation so trees, bushes, grass, resource nodes, and water do not read as repeated placeholders. Added repeatable labeled variation actors for tree canopies/trunks, rounded bushes, grass mats, rock slabs, and a freshwater surface using multiple prototype mesh silhouettes, unique scales, rotations, and Ground Zero material families; extended the natural-environment verifier to require variation coverage. - [ ] Replace `LevelPrototyping` cube/cylinder mesh dependencies in Agrarian setup scripts and prototype Blueprints with Agrarian-native placeholder environment meshes. - [ ] Add weather exposure zones if needed. - [ ] Add landmark or ruin placeholder. diff --git a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap index 8bbbe58..c5f3669 100644 --- a/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap +++ b/Content/Agrarian/Maps/L_GroundZeroTerrain_Test.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6e99e4071b76ad17941a4988e6ec06201a31aa3efb9503ae28c26672ea4dc588 -size 7452338 +oid sha256:26752dbb323c8880ebb9d10037e5fdf972f5744f4141c6678f33daf3d87ae76e +size 7470505 diff --git a/Docs/Terrain/GroundZeroFoliagePass.md b/Docs/Terrain/GroundZeroFoliagePass.md index 889ec3a..5dd38b4 100644 --- a/Docs/Terrain/GroundZeroFoliagePass.md +++ b/Docs/Terrain/GroundZeroFoliagePass.md @@ -39,3 +39,7 @@ current prototype geometry now uses dedicated Ground Zero tree, shrub, and dry grass materials. The next biome/resource passes should replace prototype meshes with real coastal California scrub, grassland, woodland, and resource-specific assets, then tie spawn density to land-cover and hydrography data. + +The natural environment variation pass adds nearby labeled accent actors for +additional canopy, trunk, bush, grass mat, rock, and water silhouettes so the +demo no longer depends only on one repeated foliage mesh per family. diff --git a/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md b/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md index 2db3771..8acfe56 100644 --- a/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md +++ b/Docs/Terrain/GroundZeroNaturalEnvironmentPass.md @@ -11,6 +11,9 @@ space. tree, shrub, and dry grass materials. - Wood, fiber, stone, and freshwater actors receive distinct first-pass materials. +- First-pass asset variation actors add additional tree canopies/trunks, rounded + bushes, grass mats, rock slabs, and a visible freshwater surface with varied + meshes, scales, rotations, and material families. - The setup remains repeatable through `Scripts/setup_ground_zero_demo_map.py`. ## Material Assets @@ -29,11 +32,14 @@ space. `Scripts/verify_ground_zero_natural_environment_pass.py` checks that the materials exist, the landscape uses the terrain material, the foliage actor has the expected instance counts and material assignments, and resource/water actors -are visually dressed. +are visually dressed. It also checks the first-pass asset variation layer: +eleven labeled variation actors, at least four mesh silhouettes, unique scale +profiles, and coverage across tree, bush, grass, rock, and water visual +families. ## Follow-Up This pass deliberately keeps the current prototype geometry so it stays small -and stable. The next environment work should add first-pass asset variation so -trees, bushes, grass, resource nodes, and water no longer read as repeated -placeholder shapes. +and stable. Later environment work should replace these prototype silhouettes +with region-appropriate sourced or custom meshes, then drive density and shape +selection from land-cover, slope, hydrography, and biome data. diff --git a/Scripts/setup_ground_zero_demo_map.py b/Scripts/setup_ground_zero_demo_map.py index 062b091..dbe7d88 100644 --- a/Scripts/setup_ground_zero_demo_map.py +++ b/Scripts/setup_ground_zero_demo_map.py @@ -22,6 +22,13 @@ FOLIAGE_MESHES = { "shrub": "/Game/LevelPrototyping/Meshes/SM_Cube", "grass": "/Game/LevelPrototyping/Meshes/SM_Cylinder", } +VARIATION_MESHES = { + "cube": "/Game/LevelPrototyping/Meshes/SM_Cube", + "chamfer_cube": "/Game/LevelPrototyping/Meshes/SM_ChamferCube", + "cylinder": "/Game/LevelPrototyping/Meshes/SM_Cylinder", + "quarter_cylinder": "/Game/LevelPrototyping/Meshes/SM_QuarterCylinder", + "plane": "/Game/LevelPrototyping/Meshes/SM_Plane", +} MATERIAL_FOLDER = "/Game/Agrarian/Materials" ENVIRONMENT_MATERIALS = { "terrain": { @@ -239,6 +246,108 @@ WATER_SOURCE_ACTORS = [ }, ] +ENVIRONMENT_VARIATION_ACTORS = [ + { + "label": "AGR_GZ_EnvVar_Tree_Canopy_01", + "mesh_key": "chamfer_cube", + "material_key": "tree", + "location_xy": unreal.Vector(-27500.0, 6900.0, 0.0), + "z_offset": 390.0, + "scale": unreal.Vector(2.5, 2.0, 1.6), + "rotation": unreal.Rotator(0.0, 32.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Tree_Trunk_01", + "mesh_key": "cylinder", + "material_key": "wood_resource", + "location_xy": unreal.Vector(-27500.0, 6900.0, 0.0), + "z_offset": 170.0, + "scale": unreal.Vector(0.35, 0.35, 3.6), + "rotation": unreal.Rotator(0.0, 32.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Tree_Canopy_02", + "mesh_key": "chamfer_cube", + "material_key": "tree", + "location_xy": unreal.Vector(17600.0, 31800.0, 0.0), + "z_offset": 430.0, + "scale": unreal.Vector(2.0, 2.8, 1.45), + "rotation": unreal.Rotator(0.0, -18.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Tree_Trunk_02", + "mesh_key": "cylinder", + "material_key": "wood_resource", + "location_xy": unreal.Vector(17600.0, 31800.0, 0.0), + "z_offset": 190.0, + "scale": unreal.Vector(0.3, 0.3, 4.1), + "rotation": unreal.Rotator(0.0, -18.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Bush_Rounded_01", + "mesh_key": "chamfer_cube", + "material_key": "shrub", + "location_xy": unreal.Vector(-33400.0, -15200.0, 0.0), + "z_offset": 70.0, + "scale": unreal.Vector(1.9, 1.4, 0.7), + "rotation": unreal.Rotator(0.0, 11.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Bush_Rounded_02", + "mesh_key": "chamfer_cube", + "material_key": "shrub", + "location_xy": unreal.Vector(30400.0, -3900.0, 0.0), + "z_offset": 75.0, + "scale": unreal.Vector(1.45, 2.1, 0.85), + "rotation": unreal.Rotator(0.0, 73.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Grass_Mat_01", + "mesh_key": "plane", + "material_key": "grass", + "location_xy": unreal.Vector(8200.0, -17100.0, 0.0), + "z_offset": 14.0, + "scale": unreal.Vector(3.6, 2.2, 1.0), + "rotation": unreal.Rotator(0.0, -34.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Grass_Mat_02", + "mesh_key": "plane", + "material_key": "grass", + "location_xy": unreal.Vector(-6200.0, 8300.0, 0.0), + "z_offset": 14.0, + "scale": unreal.Vector(2.7, 4.0, 1.0), + "rotation": unreal.Rotator(0.0, 41.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Rock_Slab_01", + "mesh_key": "chamfer_cube", + "material_key": "stone_resource", + "location_xy": unreal.Vector(22900.0, 18300.0, 0.0), + "z_offset": 45.0, + "scale": unreal.Vector(1.8, 1.05, 0.45), + "rotation": unreal.Rotator(0.0, 24.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Rock_Slab_02", + "mesh_key": "quarter_cylinder", + "material_key": "stone_resource", + "location_xy": unreal.Vector(38100.0, 29300.0, 0.0), + "z_offset": 55.0, + "scale": unreal.Vector(1.2, 1.8, 0.75), + "rotation": unreal.Rotator(0.0, -47.0, 0.0), + }, + { + "label": "AGR_GZ_EnvVar_Water_Surface_01", + "mesh_key": "plane", + "material_key": "fresh_water", + "location_xy": unreal.Vector(-7200.0, 10400.0, 0.0), + "z_offset": 18.0, + "scale": unreal.Vector(2.6, 1.6, 1.0), + "rotation": unreal.Rotator(0.0, -15.0, 0.0), + }, +] + FOLIAGE_ZONES = { "trees": { @@ -535,6 +644,27 @@ def spawn_demo_actor(spec, height_values, materials): return actor +def spawn_environment_variation_actor(spec, height_values, materials): + location_xy = spec["location_xy"] + z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0) + actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world( + unreal.StaticMeshActor, + unreal.Vector(location_xy.x, location_xy.y, z), + spec["rotation"], + spec["label"], + ) + if not actor: + raise RuntimeError(f"Could not spawn {spec['label']}") + + set_actor_label(actor, spec["label"]) + actor.set_actor_scale3d(spec["scale"]) + mesh_component = actor.static_mesh_component + mesh_component.set_editor_property("static_mesh", load_required_asset(VARIATION_MESHES[spec["mesh_key"]])) + mesh_component.set_material(0, materials[spec["material_key"]]) + unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}") + return actor + + def main(): if not unreal.EditorLevelLibrary.load_level(MAP_PATH): raise RuntimeError(f"Could not load map: {MAP_PATH}") @@ -543,6 +673,7 @@ def main(): labels.update(LEGACY_DEMO_LIGHTING_LABELS) labels.update(spec["label"] for spec in BIOME_RESOURCE_ACTORS) labels.update(spec["label"] for spec in WATER_SOURCE_ACTORS) + labels.update(spec["label"] for spec in ENVIRONMENT_VARIATION_ACTORS) labels.add(FOLIAGE_LABEL) remove_existing_demo_actors(labels) @@ -554,6 +685,8 @@ def main(): spawn_demo_actor(spec, height_values, materials) for spec in WATER_SOURCE_ACTORS: spawn_demo_actor(spec, height_values, materials) + for spec in ENVIRONMENT_VARIATION_ACTORS: + spawn_environment_variation_actor(spec, height_values, materials) for spec in DEMO_ACTORS: spawn_demo_actor(spec, height_values, materials) diff --git a/Scripts/verify_ground_zero_natural_environment_pass.py b/Scripts/verify_ground_zero_natural_environment_pass.py index f4bc8fa..180d0b2 100644 --- a/Scripts/verify_ground_zero_natural_environment_pass.py +++ b/Scripts/verify_ground_zero_natural_environment_pass.py @@ -26,6 +26,16 @@ RESOURCE_MATERIALS = { "AGR_DemoFiberResource": "fiber_resource", "AGR_GZ_FreshWaterSource": "fresh_water", } +VARIATION_PREFIX = "AGR_GZ_EnvVar_" +EXPECTED_VARIATION_COUNT = 11 +EXPECTED_VARIATION_MATERIALS = { + "AGR_GZ_EnvVar_Tree_Canopy": "tree", + "AGR_GZ_EnvVar_Tree_Trunk": "wood_resource", + "AGR_GZ_EnvVar_Bush": "shrub", + "AGR_GZ_EnvVar_Grass": "grass", + "AGR_GZ_EnvVar_Rock": "stone_resource", + "AGR_GZ_EnvVar_Water": "fresh_water", +} def get_actor_label(actor): @@ -48,6 +58,13 @@ def material_key_for_label(label): return None +def variation_material_key_for_label(label): + for prefix, material_key in EXPECTED_VARIATION_MATERIALS.items(): + if label.startswith(prefix): + return material_key + return None + + def assert_asset(path): asset = unreal.EditorAssetLibrary.load_asset(path) if not asset: @@ -118,11 +135,50 @@ def main(): if checked_resource_actors < 10: failures.append(f"expected at least 10 dressed resource/water actors, checked {checked_resource_actors}") + variation_actors = [actor for actor in actors if get_actor_label(actor).startswith(VARIATION_PREFIX)] + if len(variation_actors) != EXPECTED_VARIATION_COUNT: + failures.append(f"expected {EXPECTED_VARIATION_COUNT} environment variation actors, found {len(variation_actors)}") + + variation_meshes = set() + variation_scales = set() + variation_material_keys = set() + for actor in variation_actors: + label = get_actor_label(actor) + expected_material_key = variation_material_key_for_label(label) + if not expected_material_key: + failures.append(f"{label} does not match a known variation material family") + continue + + mesh_components = actor.get_components_by_class(unreal.StaticMeshComponent) + if not mesh_components: + failures.append(f"{label} has no static mesh component") + continue + + component = mesh_components[0] + mesh = component.get_editor_property("static_mesh") + variation_meshes.add(mesh.get_path_name().split(".", 1)[0] if mesh else "") + scale = actor.get_actor_scale3d() + variation_scales.add((round(scale.x, 2), round(scale.y, 2), round(scale.z, 2))) + variation_material_keys.add(expected_material_key) + + assigned = material_path(component.get_material(0)) + expected = MATERIALS[expected_material_key] + if assigned != expected: + failures.append(f"{label} material expected {expected}, got {assigned}") + + if len(variation_meshes) < 4: + failures.append(f"expected at least 4 variation mesh silhouettes, got {len(variation_meshes)}") + if len(variation_scales) < EXPECTED_VARIATION_COUNT: + failures.append("environment variation actors should use unique scale profiles") + if not {"tree", "wood_resource", "shrub", "grass", "stone_resource", "fresh_water"}.issubset(variation_material_keys): + failures.append("environment variation actors do not cover tree, shrub, grass, resource, and water families") + docs = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "Docs/Terrain/GroundZeroNaturalEnvironmentPass.md" roadmap = unreal.Paths.convert_relative_path_to_full(unreal.Paths.project_dir()) + "AGRARIAN_DEVELOPMENT_ROADMAP.md" for path, snippet in [ - (docs, "MVP natural environment pass"), + (docs, "asset variation"), (roadmap, "[x] Replace grey-box environment presentation with an MVP natural environment pass"), + (roadmap, "[x] Add first-pass environment asset variation"), ]: with open(path, "r", encoding="utf-8") as handle: text = handle.read() @@ -134,7 +190,8 @@ def main(): unreal.log( "Ground Zero natural environment verification complete: " - f"{len(materials)} materials, {len(landscapes)} landscape(s), {checked_resource_actors} dressed resource/water actor(s)." + f"{len(materials)} materials, {len(landscapes)} landscape(s), " + f"{checked_resource_actors} dressed resource/water actor(s), {len(variation_actors)} variation actor(s)." )