Add Ground Zero foliage pass
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user