420 lines
14 KiB
Python
420 lines
14 KiB
Python
import math
|
|
import random
|
|
import struct
|
|
from pathlib import Path
|
|
|
|
import unreal
|
|
|
|
|
|
MAP_PATH = "/Game/Agrarian/Maps/L_GroundZeroTerrain_Test"
|
|
PROJECT_ROOT = Path(r"Z:\AgrarianGameBulid")
|
|
TILE_ID = "gz_us_ca_pacifica_utm10n_e544_n4160"
|
|
HEIGHTMAP_PATH = PROJECT_ROOT / "Data" / "Terrain" / "Unreal" / TILE_ID / f"{TILE_ID}_unreal_1009.r16"
|
|
LANDSCAPE_SIZE = 1009
|
|
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 = [
|
|
{
|
|
"label": "AGR_DemoPlayerStart",
|
|
"class": unreal.PlayerStart,
|
|
"location_xy": unreal.Vector(-18500.0, -6400.0, 0.0),
|
|
"z_offset": 220.0,
|
|
"rotation": unreal.Rotator(0.0, 72.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_DemoWoodResource_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
|
|
"location_xy": unreal.Vector(-16800.0, -5400.0, 0.0),
|
|
"z_offset": 80.0,
|
|
"rotation": unreal.Rotator(0.0, 25.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_DemoFiberResource_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
|
|
"location_xy": unreal.Vector(-15600.0, -7850.0, 0.0),
|
|
"z_offset": 80.0,
|
|
"rotation": unreal.Rotator(0.0, -15.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_DemoCampfire_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Structures/BP_Campfire",
|
|
"location_xy": unreal.Vector(-13200.0, -6350.0, 0.0),
|
|
"z_offset": 90.0,
|
|
"rotation": unreal.Rotator(0.0, 0.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_DemoPrimitiveShelter_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Structures/BP_PrimitiveShelter",
|
|
"location_xy": unreal.Vector(-11500.0, -7750.0, 0.0),
|
|
"z_offset": 120.0,
|
|
"rotation": unreal.Rotator(0.0, -30.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_DemoRabbitWildlife_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Wildlife/BP_RabbitWildlife",
|
|
"location_xy": unreal.Vector(-19800.0, -8550.0, 0.0),
|
|
"z_offset": 120.0,
|
|
"rotation": unreal.Rotator(0.0, 135.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_DemoSkyLightingController",
|
|
"class": unreal.AgrarianSkyLightingController,
|
|
"location_xy": unreal.Vector(-18000.0, -7000.0, 0.0),
|
|
"fixed_z": 12000.0,
|
|
"rotation": unreal.Rotator(-42.0, -35.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_DemoNoticeActor",
|
|
"class": unreal.AgrarianDemoNoticeActor,
|
|
"location_xy": unreal.Vector(-18500.0, -6400.0, 0.0),
|
|
"fixed_z": 1600.0,
|
|
"rotation": unreal.Rotator(0.0, 0.0, 0.0),
|
|
},
|
|
]
|
|
|
|
LEGACY_DEMO_LIGHTING_LABELS = {
|
|
"AGR_DemoSun",
|
|
"AGR_DemoSkyLight",
|
|
"AGR_DemoFog",
|
|
}
|
|
|
|
|
|
BIOME_RESOURCE_ACTORS = [
|
|
{
|
|
"label": "AGR_GZ_Wood_CoastalScrub_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
|
|
"location_xy": unreal.Vector(-28600.0, 7400.0, 0.0),
|
|
"z_offset": 70.0,
|
|
"rotation": unreal.Rotator(0.0, 18.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Wood_CoastalScrub_02",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
|
|
"location_xy": unreal.Vector(-5400.0, 21400.0, 0.0),
|
|
"z_offset": 70.0,
|
|
"rotation": unreal.Rotator(0.0, -42.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Wood_Hillside_03",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_WoodResourceNode",
|
|
"location_xy": unreal.Vector(18400.0, 32200.0, 0.0),
|
|
"z_offset": 70.0,
|
|
"rotation": unreal.Rotator(0.0, 86.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Fiber_Grassland_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
|
|
"location_xy": unreal.Vector(-33400.0, -16200.0, 0.0),
|
|
"z_offset": 65.0,
|
|
"rotation": unreal.Rotator(0.0, 6.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Fiber_Grassland_02",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
|
|
"location_xy": unreal.Vector(8200.0, -18200.0, 0.0),
|
|
"z_offset": 65.0,
|
|
"rotation": unreal.Rotator(0.0, -28.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Fiber_Scrub_03",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
|
|
"location_xy": unreal.Vector(29200.0, -4200.0, 0.0),
|
|
"z_offset": 65.0,
|
|
"rotation": unreal.Rotator(0.0, 54.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Fiber_DrainageCandidate_04",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_FiberResourceNode",
|
|
"location_xy": unreal.Vector(-6200.0, 9200.0, 0.0),
|
|
"z_offset": 65.0,
|
|
"rotation": unreal.Rotator(0.0, 114.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Stone_Slope_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
|
|
"location_xy": unreal.Vector(22600.0, 17600.0, 0.0),
|
|
"z_offset": 45.0,
|
|
"rotation": unreal.Rotator(0.0, 12.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Stone_Slope_02",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
|
|
"location_xy": unreal.Vector(37600.0, 28600.0, 0.0),
|
|
"z_offset": 45.0,
|
|
"rotation": unreal.Rotator(0.0, -36.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Stone_ExposedTerrain_03",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
|
|
"location_xy": unreal.Vector(11800.0, 38200.0, 0.0),
|
|
"z_offset": 45.0,
|
|
"rotation": unreal.Rotator(0.0, 71.0, 0.0),
|
|
},
|
|
{
|
|
"label": "AGR_GZ_Stone_ValleyEdge_04",
|
|
"class_path": "/Game/Agrarian/Blueprints/Resources/BP_StoneResourceNode",
|
|
"location_xy": unreal.Vector(-24800.0, 28600.0, 0.0),
|
|
"z_offset": 45.0,
|
|
"rotation": unreal.Rotator(0.0, 133.0, 0.0),
|
|
},
|
|
]
|
|
|
|
|
|
WATER_SOURCE_ACTORS = [
|
|
{
|
|
"label": "AGR_GZ_FreshWaterSource_01",
|
|
"class_path": "/Game/Agrarian/Blueprints/World/BP_FreshWaterSource",
|
|
"location_xy": unreal.Vector(-7200.0, 10400.0, 0.0),
|
|
"z_offset": 36.0,
|
|
"rotation": unreal.Rotator(0.0, 0.0, 0.0),
|
|
},
|
|
]
|
|
|
|
|
|
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()
|
|
except Exception:
|
|
return actor.get_name()
|
|
|
|
|
|
def set_actor_label(actor, label):
|
|
try:
|
|
actor.set_actor_label(label, mark_dirty=True)
|
|
except TypeError:
|
|
actor.set_actor_label(label)
|
|
|
|
|
|
def load_blueprint_class(path):
|
|
generated_class = unreal.EditorAssetLibrary.load_blueprint_class(path)
|
|
if not generated_class:
|
|
raise RuntimeError(f"Could not 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
|
|
if len(raw) != expected_bytes:
|
|
raise RuntimeError(f"Unexpected heightmap size: {len(raw)} != {expected_bytes}")
|
|
return struct.unpack(f"<{LANDSCAPE_SIZE * LANDSCAPE_SIZE}H", raw)
|
|
|
|
|
|
def terrain_z_cm(height_values, x_cm, y_cm):
|
|
sample_x = max(0, min(LANDSCAPE_SIZE - 1, round((x_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
|
|
sample_y = max(0, min(LANDSCAPE_SIZE - 1, round((y_cm - LANDSCAPE_MIN_XY) / XY_SCALE_CM)))
|
|
height_value = height_values[int(sample_y) * LANDSCAPE_SIZE + int(sample_x)]
|
|
elevation_m = (float(height_value) - 32768.0) * Z_SCALE_CM / (100.0 * 128.0)
|
|
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_DemoSkyLightingController", "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")
|
|
if z is None:
|
|
z = terrain_z_cm(height_values, location_xy.x, location_xy.y) + spec.get("z_offset", 0.0)
|
|
|
|
actor_class = spec.get("class")
|
|
if actor_class is None:
|
|
actor_class = load_blueprint_class(spec["class_path"])
|
|
|
|
actor = unreal.AgrarianEditorAutomationLibrary.spawn_actor_in_editor_world(
|
|
actor_class,
|
|
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"])
|
|
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}")
|
|
|
|
labels = {spec["label"] for spec in DEMO_ACTORS}
|
|
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.add(FOLIAGE_LABEL)
|
|
remove_existing_demo_actors(labels)
|
|
|
|
height_values = load_heightmap()
|
|
spawn_foliage_actor(height_values)
|
|
for spec in BIOME_RESOURCE_ACTORS:
|
|
spawn_demo_actor(spec, height_values)
|
|
for spec in WATER_SOURCE_ACTORS:
|
|
spawn_demo_actor(spec, height_values)
|
|
for spec in DEMO_ACTORS:
|
|
spawn_demo_actor(spec, height_values)
|
|
|
|
unreal.EditorLevelLibrary.save_current_level()
|
|
unreal.log("Ground Zero demo map setup complete.")
|
|
|
|
|
|
main()
|