Add Ground Zero natural environment pass

This commit is contained in:
2026-05-16 02:18:46 -07:00
parent a7292bbae1
commit 25ffbfc564
14 changed files with 355 additions and 12 deletions
+1 -1
View File
@@ -456,7 +456,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
- [~] Add resource nodes.
- [x] Add biome-appropriate natural resources based on Ground Zero.
- [x] Add water source.
- [ ] 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.
- [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.
- [ ] 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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+4 -3
View File
@@ -35,6 +35,7 @@ and wildlife spawn area so the initial investor path remains readable.
## Follow-Up
This is a visual and performance-safe placeholder pass, not final ecology. 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.
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.
@@ -0,0 +1,39 @@
# Ground Zero MVP Natural Environment Pass
The Ground Zero map now has a first MVP natural environment pass so the investor
demo reads as coastal California scrub/woodland instead of a plain grey-box test
space.
## Scope
- Terrain receives a warm coastal scrub ground material.
- Foliage patch instances keep the current prototype meshes but use distinct
tree, shrub, and dry grass materials.
- Wood, fiber, stone, and freshwater actors receive distinct first-pass
materials.
- The setup remains repeatable through `Scripts/setup_ground_zero_demo_map.py`.
## Material Assets
- `/Game/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub`
- `/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak`
- `/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush`
- `/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal`
- `/Game/Agrarian/Materials/M_AGR_GZ_Wood_Resource`
- `/Game/Agrarian/Materials/M_AGR_GZ_Fiber_Resource`
- `/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone`
- `/Game/Agrarian/Materials/M_AGR_GZ_FreshWater`
## Validation
`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.
## 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.
+144 -6
View File
@@ -22,6 +22,57 @@ FOLIAGE_MESHES = {
"shrub": "/Game/LevelPrototyping/Meshes/SM_Cube",
"grass": "/Game/LevelPrototyping/Meshes/SM_Cylinder",
}
MATERIAL_FOLDER = "/Game/Agrarian/Materials"
ENVIRONMENT_MATERIALS = {
"terrain": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Terrain_CoastalScrub",
"color": unreal.LinearColor(0.28, 0.24, 0.16, 1.0),
"roughness": 0.92,
},
"tree": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Tree_CoastalOak",
"color": unreal.LinearColor(0.18, 0.31, 0.16, 1.0),
"roughness": 0.88,
},
"shrub": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Shrub_CoyoteBrush",
"color": unreal.LinearColor(0.31, 0.39, 0.20, 1.0),
"roughness": 0.9,
},
"grass": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Grass_DryCoastal",
"color": unreal.LinearColor(0.47, 0.42, 0.23, 1.0),
"roughness": 0.95,
},
"wood_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Wood_Resource",
"color": unreal.LinearColor(0.26, 0.16, 0.08, 1.0),
"roughness": 0.86,
},
"fiber_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Fiber_Resource",
"color": unreal.LinearColor(0.55, 0.49, 0.28, 1.0),
"roughness": 0.93,
},
"stone_resource": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_Stone_Sandstone",
"color": unreal.LinearColor(0.43, 0.40, 0.35, 1.0),
"roughness": 0.97,
},
"fresh_water": {
"path": f"{MATERIAL_FOLDER}/M_AGR_GZ_FreshWater",
"color": unreal.LinearColor(0.08, 0.28, 0.38, 1.0),
"roughness": 0.35,
},
}
RESOURCE_MATERIAL_BY_LABEL_PREFIX = {
"AGR_GZ_Wood": "wood_resource",
"AGR_GZ_Fiber": "fiber_resource",
"AGR_GZ_Stone": "stone_resource",
"AGR_DemoWoodResource": "wood_resource",
"AGR_DemoFiberResource": "fiber_resource",
"AGR_GZ_FreshWaterSource": "fresh_water",
}
DEMO_ACTORS = [
@@ -245,6 +296,75 @@ def load_required_asset(path):
return asset
def ensure_environment_materials():
if not unreal.EditorAssetLibrary.does_directory_exist(MATERIAL_FOLDER):
unreal.EditorAssetLibrary.make_directory(MATERIAL_FOLDER)
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
created_or_loaded = {}
for key, spec in ENVIRONMENT_MATERIALS.items():
material = unreal.EditorAssetLibrary.load_asset(spec["path"])
if not material:
asset_name = spec["path"].rsplit("/", 1)[-1]
material = asset_tools.create_asset(asset_name, MATERIAL_FOLDER, unreal.Material, unreal.MaterialFactoryNew())
if not material:
raise RuntimeError(f"Could not create material: {spec['path']}")
base_color = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant3Vector, -420, -120
)
base_color.set_editor_property("constant", spec["color"])
unreal.MaterialEditingLibrary.connect_material_property(
base_color, "", unreal.MaterialProperty.MP_BASE_COLOR
)
roughness = unreal.MaterialEditingLibrary.create_material_expression(
material, unreal.MaterialExpressionConstant, -420, 80
)
roughness.set_editor_property("r", spec["roughness"])
unreal.MaterialEditingLibrary.connect_material_property(
roughness, "", unreal.MaterialProperty.MP_ROUGHNESS
)
unreal.MaterialEditingLibrary.recompile_material(material)
unreal.EditorAssetLibrary.save_asset(spec["path"])
unreal.log(f"Created Ground Zero environment material: {spec['path']}")
created_or_loaded[key] = material
return created_or_loaded
def apply_material_to_actor_meshes(actor, material):
applied_count = 0
for component in actor.get_components_by_class(unreal.StaticMeshComponent):
slot_count = component.get_num_materials()
if slot_count == 0:
component.set_material(0, material)
applied_count += 1
continue
for slot_index in range(slot_count):
component.set_material(slot_index, material)
applied_count += 1
return applied_count
def material_key_for_actor_label(label):
for prefix, material_key in RESOURCE_MATERIAL_BY_LABEL_PREFIX.items():
if label.startswith(prefix):
return material_key
return None
def apply_landscape_material(material):
applied = 0
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
if isinstance(actor, unreal.Landscape):
actor.set_editor_property("landscape_material", material)
applied += 1
unreal.log(f"Applied Ground Zero terrain material to {applied} landscape actor(s).")
return applied
def load_heightmap():
raw = HEIGHTMAP_PATH.read_bytes()
expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2
@@ -295,6 +415,18 @@ def configure_foliage_meshes(foliage_actor):
component.set_editor_property("static_mesh", mesh)
def apply_foliage_materials(foliage_actor, materials):
component_materials = {
"tree_instances": materials["tree"],
"shrub_instances": materials["shrub"],
"grass_instances": materials["grass"],
}
for property_name, material in component_materials.items():
component = foliage_actor.get_editor_property(property_name)
component.set_material(0, material)
def choose_foliage_points(height_values, zone, reserved_points, existing_points):
rng = random.Random(FOLIAGE_RANDOM_SEED + len(existing_points) + int(zone["count"]))
chosen = []
@@ -325,7 +457,7 @@ def choose_foliage_points(height_values, zone, reserved_points, existing_points)
return chosen
def spawn_foliage_actor(height_values):
def spawn_foliage_actor(height_values, materials):
reserved_points = [
spec["location_xy"]
for spec in DEMO_ACTORS
@@ -343,6 +475,7 @@ def spawn_foliage_actor(height_values):
set_actor_label(foliage_actor, FOLIAGE_LABEL)
configure_foliage_meshes(foliage_actor)
apply_foliage_materials(foliage_actor, materials)
foliage_actor.clear_foliage()
existing_points = []
@@ -375,7 +508,7 @@ def spawn_foliage_actor(height_values):
return foliage_actor
def spawn_demo_actor(spec, height_values):
def spawn_demo_actor(spec, height_values, materials):
location_xy = spec["location_xy"]
z = spec.get("fixed_z")
if z is None:
@@ -395,6 +528,9 @@ def spawn_demo_actor(spec, height_values):
raise RuntimeError(f"Could not spawn {spec['label']}")
set_actor_label(actor, spec["label"])
material_key = material_key_for_actor_label(spec["label"])
if material_key:
apply_material_to_actor_meshes(actor, materials[material_key])
unreal.log(f"Placed {spec['label']} at {actor.get_actor_location()}")
return actor
@@ -410,14 +546,16 @@ def main():
labels.add(FOLIAGE_LABEL)
remove_existing_demo_actors(labels)
materials = ensure_environment_materials()
apply_landscape_material(materials["terrain"])
height_values = load_heightmap()
spawn_foliage_actor(height_values)
spawn_foliage_actor(height_values, materials)
for spec in BIOME_RESOURCE_ACTORS:
spawn_demo_actor(spec, height_values)
spawn_demo_actor(spec, height_values, materials)
for spec in WATER_SOURCE_ACTORS:
spawn_demo_actor(spec, height_values)
spawn_demo_actor(spec, height_values, materials)
for spec in DEMO_ACTORS:
spawn_demo_actor(spec, height_values)
spawn_demo_actor(spec, height_values, materials)
unreal.EditorLevelLibrary.save_current_level()
unreal.log("Ground Zero demo map setup complete.")
@@ -0,0 +1,141 @@
import unreal
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
MATERIALS = {
"terrain": "/Game/Agrarian/Materials/M_AGR_GZ_Terrain_CoastalScrub",
"tree": "/Game/Agrarian/Materials/M_AGR_GZ_Tree_CoastalOak",
"shrub": "/Game/Agrarian/Materials/M_AGR_GZ_Shrub_CoyoteBrush",
"grass": "/Game/Agrarian/Materials/M_AGR_GZ_Grass_DryCoastal",
"wood_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Wood_Resource",
"fiber_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Fiber_Resource",
"stone_resource": "/Game/Agrarian/Materials/M_AGR_GZ_Stone_Sandstone",
"fresh_water": "/Game/Agrarian/Materials/M_AGR_GZ_FreshWater",
}
EXPECTED_FOLIAGE_COUNTS = {
"trees": 42,
"shrubs": 96,
"grass": 180,
}
RESOURCE_MATERIALS = {
"AGR_GZ_Wood": "wood_resource",
"AGR_GZ_Fiber": "fiber_resource",
"AGR_GZ_Stone": "stone_resource",
"AGR_DemoWoodResource": "wood_resource",
"AGR_DemoFiberResource": "fiber_resource",
"AGR_GZ_FreshWaterSource": "fresh_water",
}
def get_actor_label(actor):
try:
return actor.get_actor_label()
except Exception:
return actor.get_name()
def material_path(material):
if not material:
return ""
return material.get_path_name().split(".", 1)[0]
def material_key_for_label(label):
for prefix, material_key in RESOURCE_MATERIALS.items():
if label.startswith(prefix):
return material_key
return None
def assert_asset(path):
asset = unreal.EditorAssetLibrary.load_asset(path)
if not asset:
raise RuntimeError(f"Missing required environment material: {path}")
return asset
def main():
if not unreal.EditorLevelLibrary.load_level(MAP_PATH):
raise RuntimeError(f"Could not load map: {MAP_PATH}")
materials = {key: assert_asset(path) for key, path in MATERIALS.items()}
actors = unreal.EditorLevelLibrary.get_all_level_actors()
failures = []
landscapes = [actor for actor in actors if isinstance(actor, unreal.Landscape)]
if not landscapes:
failures.append("no landscape actor found")
for landscape in landscapes:
assigned = material_path(landscape.get_editor_property("landscape_material"))
expected = MATERIALS["terrain"]
if assigned != expected:
failures.append(f"landscape material expected {expected}, got {assigned}")
foliage_actors = [actor for actor in actors if get_actor_label(actor) == FOLIAGE_LABEL]
if len(foliage_actors) != 1:
failures.append(f"expected one {FOLIAGE_LABEL}, found {len(foliage_actors)}")
else:
foliage = foliage_actors[0]
counts = {
"trees": foliage.get_tree_instance_count(),
"shrubs": foliage.get_shrub_instance_count(),
"grass": foliage.get_grass_instance_count(),
}
for key, expected_count in EXPECTED_FOLIAGE_COUNTS.items():
if counts[key] != expected_count:
failures.append(f"{key} expected {expected_count}, got {counts[key]}")
component_expectations = {
"tree_instances": "tree",
"shrub_instances": "shrub",
"grass_instances": "grass",
}
for property_name, material_key in component_expectations.items():
component = foliage.get_editor_property(property_name)
assigned = material_path(component.get_material(0))
expected = MATERIALS[material_key]
if assigned != expected:
failures.append(f"{property_name} material expected {expected}, got {assigned}")
checked_resource_actors = 0
for actor in actors:
label = get_actor_label(actor)
material_key = material_key_for_label(label)
if not material_key:
continue
checked_resource_actors += 1
expected = MATERIALS[material_key]
mesh_components = actor.get_components_by_class(unreal.StaticMeshComponent)
if not mesh_components:
failures.append(f"{label} has no static mesh component for material assignment")
continue
assigned_any = any(material_path(component.get_material(0)) == expected for component in mesh_components)
if not assigned_any:
failures.append(f"{label} did not use expected material {expected}")
if checked_resource_actors < 10:
failures.append(f"expected at least 10 dressed resource/water actors, checked {checked_resource_actors}")
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"),
(roadmap, "[x] Replace grey-box environment presentation with an MVP natural environment pass"),
]:
with open(path, "r", encoding="utf-8") as handle:
text = handle.read()
if snippet not in text:
failures.append(f"{path} missing {snippet}")
if failures:
raise RuntimeError("Ground Zero natural environment verification failed: " + "; ".join(failures))
unreal.log(
"Ground Zero natural environment verification complete: "
f"{len(materials)} materials, {len(landscapes)} landscape(s), {checked_resource_actors} dressed resource/water actor(s)."
)
main()