Add Ground Zero foliage pass

This commit is contained in:
2026-05-14 06:41:23 -07:00
parent 7a05e324a3
commit 7ffe3ec978
7 changed files with 397 additions and 4 deletions
+155
View File
@@ -1,3 +1,5 @@
import math
import random
import struct
from pathlib import Path
@@ -13,6 +15,14 @@ XY_SCALE_CM = 100000.0 / (LANDSCAPE_SIZE - 1)
Z_SCALE_CM = 100.0
LANDSCAPE_MIN_XY = -50000.0
FOLIAGE_LABEL = "AGR_GroundZeroFoliage_FirstPass"
FOLIAGE_RANDOM_SEED = 4160544
FOLIAGE_MESHES = {
"tree": "/Game/LevelPrototyping/Meshes/SM_Cylinder",
"shrub": "/Game/LevelPrototyping/Meshes/SM_Cube",
"grass": "/Game/LevelPrototyping/Meshes/SM_Cylinder",
}
DEMO_ACTORS = [
{
@@ -88,6 +98,34 @@ DEMO_ACTORS = [
]
FOLIAGE_ZONES = {
"trees": {
"count": 42,
"x_range": (-25000.0, 42000.0),
"y_range": (-12000.0, 42000.0),
"min_elevation_m": 18.0,
"avoid_radius_cm": 3600.0,
"scale_range": (0.75, 1.35),
},
"shrubs": {
"count": 96,
"x_range": (-42000.0, 45000.0),
"y_range": (-38000.0, 45000.0),
"min_elevation_m": 7.0,
"avoid_radius_cm": 2600.0,
"scale_range": (0.45, 1.05),
},
"grass": {
"count": 180,
"x_range": (-46000.0, 46000.0),
"y_range": (-46000.0, 46000.0),
"min_elevation_m": 4.0,
"avoid_radius_cm": 1800.0,
"scale_range": (0.35, 0.9),
},
}
def get_actor_label(actor):
try:
return actor.get_actor_label()
@@ -109,6 +147,13 @@ def load_blueprint_class(path):
return generated_class
def load_required_asset(path):
asset = unreal.EditorAssetLibrary.load_asset(path)
if not asset:
raise RuntimeError(f"Required asset not found: {path}")
return asset
def load_heightmap():
raw = HEIGHTMAP_PATH.read_bytes()
expected_bytes = LANDSCAPE_SIZE * LANDSCAPE_SIZE * 2
@@ -125,12 +170,120 @@ def terrain_z_cm(height_values, x_cm, y_cm):
return elevation_m * 100.0
def terrain_elevation_m(height_values, x_cm, y_cm):
return terrain_z_cm(height_values, x_cm, y_cm) / 100.0
def remove_existing_demo_actors(labels):
for actor in unreal.EditorLevelLibrary.get_all_level_actors():
if get_actor_label(actor) in labels:
unreal.EditorLevelLibrary.destroy_actor(actor)
def distance_2d(a, b):
return math.hypot(a.x - b.x, a.y - b.y)
def make_foliage_transform(height_values, x, y, yaw, z_offset, scale_xy, scale_z):
return unreal.Transform(
location=unreal.Vector(x, y, terrain_z_cm(height_values, x, y) + z_offset),
rotation=unreal.Rotator(0.0, yaw, 0.0),
scale=unreal.Vector(scale_xy, scale_xy, scale_z),
)
def configure_foliage_meshes(foliage_actor):
component_meshes = {
"tree_instances": load_required_asset(FOLIAGE_MESHES["tree"]),
"shrub_instances": load_required_asset(FOLIAGE_MESHES["shrub"]),
"grass_instances": load_required_asset(FOLIAGE_MESHES["grass"]),
}
for property_name, mesh in component_meshes.items():
component = foliage_actor.get_editor_property(property_name)
component.set_editor_property("static_mesh", mesh)
def choose_foliage_points(height_values, zone, reserved_points, existing_points):
rng = random.Random(FOLIAGE_RANDOM_SEED + len(existing_points) + int(zone["count"]))
chosen = []
attempts = 0
max_attempts = zone["count"] * 80
while len(chosen) < zone["count"] and attempts < max_attempts:
attempts += 1
x = rng.uniform(zone["x_range"][0], zone["x_range"][1])
y = rng.uniform(zone["y_range"][0], zone["y_range"][1])
point = unreal.Vector(x, y, 0.0)
if terrain_elevation_m(height_values, x, y) < zone["min_elevation_m"]:
continue
if any(distance_2d(point, reserved) < zone["avoid_radius_cm"] for reserved in reserved_points):
continue
if any(distance_2d(point, existing) < zone["avoid_radius_cm"] * 0.55 for existing in existing_points):
continue
chosen.append(point)
existing_points.append(point)
if len(chosen) != zone["count"]:
unreal.log_warning(f"Placed {len(chosen)} of {zone['count']} requested foliage instances for zone.")
return chosen
def spawn_foliage_actor(height_values):
reserved_points = [
spec["location_xy"]
for spec in DEMO_ACTORS
if spec["label"] not in {"AGR_DemoSun", "AGR_DemoSkyLight", "AGR_DemoFog", "AGR_DemoNoticeActor"}
]
foliage_actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
unreal.AgrarianFoliagePatch,
unreal.Vector(0.0, 0.0, 0.0),
unreal.Rotator(0.0, 0.0, 0.0),
FOLIAGE_LABEL,
)
if not foliage_actor:
raise RuntimeError("Could not spawn first-pass foliage actor.")
set_actor_label(foliage_actor, FOLIAGE_LABEL)
configure_foliage_meshes(foliage_actor)
foliage_actor.clear_foliage()
existing_points = []
rng = random.Random(FOLIAGE_RANDOM_SEED)
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["trees"], reserved_points, existing_points):
scale = rng.uniform(*FOLIAGE_ZONES["trees"]["scale_range"])
foliage_actor.add_tree_instance(
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 5.0, scale, scale * 5.5)
)
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["shrubs"], reserved_points, existing_points):
scale = rng.uniform(*FOLIAGE_ZONES["shrubs"]["scale_range"])
foliage_actor.add_shrub_instance(
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 4.0, scale * 2.2, scale * 0.85)
)
for point in choose_foliage_points(height_values, FOLIAGE_ZONES["grass"], reserved_points, existing_points):
scale = rng.uniform(*FOLIAGE_ZONES["grass"]["scale_range"])
foliage_actor.add_grass_instance(
make_foliage_transform(height_values, point.x, point.y, rng.uniform(0.0, 360.0), 2.0, scale * 0.28, scale * 1.6)
)
unreal.log(
"Placed first-pass Ground Zero foliage: "
f"{foliage_actor.get_tree_instance_count()} trees, "
f"{foliage_actor.get_shrub_instance_count()} shrubs, "
f"{foliage_actor.get_grass_instance_count()} grass clumps."
)
return foliage_actor
def spawn_demo_actor(spec, height_values):
location_xy = spec["location_xy"]
z = spec.get("fixed_z")
@@ -160,9 +313,11 @@ def main():
raise RuntimeError(f"Could not load map: {MAP_PATH}")
labels = {spec["label"] for spec in DEMO_ACTORS}
labels.add(FOLIAGE_LABEL)
remove_existing_demo_actors(labels)
height_values = load_heightmap()
spawn_foliage_actor(height_values)
for spec in DEMO_ACTORS:
spawn_demo_actor(spec, height_values)