Add Ground Zero natural environment pass
This commit is contained in:
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
@@ -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()
|
||||
Reference in New Issue
Block a user