sync
This commit is contained in:
1 parent
dda27af15c
commit
2a4da34fe9
3 files changed
+201
-19
No files matched your search
@@ -9,10 +9,16 @@ Reads terrain_config.json (next to this script) and:
|
|||||||
|
|
||||||
Run import_environment_assets.py first so the mesh paths referenced in
|
Run import_environment_assets.py first so the mesh paths referenced in
|
||||||
terrain_config.json actually exist.
|
terrain_config.json actually exist.
|
||||||
|
|
||||||
|
Set the TERRAIN_CONFIG env var to point at a different config file, e.g. to
|
||||||
|
run terrain_config_variants_demo.json without touching the default config.
|
||||||
"""
|
"""
|
||||||
import os, json, math, random, unreal
|
import os, json, math, random, unreal
|
||||||
|
|
||||||
CONFIG_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "terrain_config.json")
|
CONFIG_PATH = os.environ.get(
|
||||||
|
"TERRAIN_CONFIG",
|
||||||
|
os.path.join(os.path.dirname(os.path.abspath(__file__)), "terrain_config.json"),
|
||||||
|
)
|
||||||
|
|
||||||
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
asset_tools = unreal.AssetToolsHelpers.get_asset_tools()
|
||||||
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem)
|
||||||
@@ -125,19 +131,33 @@ def build_terrain_mesh(cfg, field):
|
|||||||
|
|
||||||
dest_path = cfg["dest_path"]
|
dest_path = cfg["dest_path"]
|
||||||
asset_name = cfg["asset_name"]
|
asset_name = cfg["asset_name"]
|
||||||
if unreal.EditorAssetLibrary.does_asset_exist(f"{dest_path}/{asset_name}"):
|
asset_path = f"{dest_path}/{asset_name}"
|
||||||
unreal.EditorAssetLibrary.delete_asset(f"{dest_path}/{asset_name}")
|
if unreal.EditorAssetLibrary.does_asset_exist(asset_path):
|
||||||
static_mesh = asset_tools.create_asset(asset_name, dest_path, unreal.StaticMesh, unreal.StaticMeshFactoryNew())
|
unreal.EditorAssetLibrary.delete_asset(asset_path)
|
||||||
|
|
||||||
copy_options = unreal.GeometryScriptCopyMeshToAssetOptions()
|
create_options = unreal.GeometryScriptCreateNewStaticMeshAssetOptions()
|
||||||
target_lod = unreal.GeometryScriptMeshWriteLOD()
|
static_mesh, _outcome = unreal.GeometryScript_NewAssetUtils.create_new_static_mesh_asset_from_mesh(
|
||||||
unreal.GeometryScript_AssetUtils.copy_mesh_to_static_mesh(mesh, static_mesh, copy_options, target_lod)
|
mesh, asset_path, create_options,
|
||||||
|
)
|
||||||
unreal.EditorAssetLibrary.save_asset(static_mesh.get_path_name())
|
unreal.EditorAssetLibrary.save_asset(static_mesh.get_path_name())
|
||||||
|
|
||||||
bounds = (min_x, max_x, min_y, max_y)
|
bounds = (min_x, max_x, min_y, max_y)
|
||||||
return static_mesh, bounds
|
return static_mesh, bounds
|
||||||
|
|
||||||
|
|
||||||
|
_mesh_base_z_cache = {}
|
||||||
|
|
||||||
|
|
||||||
|
def mesh_base_z(mesh):
|
||||||
|
"""Local-space Z of the mesh's lowest point, so we can plant its base on the
|
||||||
|
ground instead of its (often bounding-box-centered) pivot."""
|
||||||
|
path = mesh.get_path_name()
|
||||||
|
if path not in _mesh_base_z_cache:
|
||||||
|
box = mesh.get_bounding_box()
|
||||||
|
_mesh_base_z_cache[path] = box.min.z
|
||||||
|
return _mesh_base_z_cache[path]
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Foliage scatter --------------------------------------------------------------
|
# Foliage scatter --------------------------------------------------------------
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -178,7 +198,11 @@ def scatter_layer(layer_cfg, field, bounds, origin, base_folder):
|
|||||||
placed_points.append((x, y))
|
placed_points.append((x, y))
|
||||||
|
|
||||||
mesh = rng.choice(meshes)
|
mesh = rng.choice(meshes)
|
||||||
location = unreal.Vector(origin.x + x, origin.y + y, origin.z + z)
|
scale = rng.uniform(layer_cfg["min_scale"], layer_cfg["max_scale"])
|
||||||
|
# Shift up by -min_z*scale so the mesh's lowest vertex sits on the ground,
|
||||||
|
# regardless of where the source asset's pivot was authored.
|
||||||
|
ground_offset = -mesh_base_z(mesh) * scale
|
||||||
|
location = unreal.Vector(origin.x + x, origin.y + y, origin.z + z + ground_offset)
|
||||||
yaw = rng.uniform(0.0, 360.0)
|
yaw = rng.uniform(0.0, 360.0)
|
||||||
if layer_cfg.get("align_to_normal", False):
|
if layer_cfg.get("align_to_normal", False):
|
||||||
# Approximate tilt from the surface normal; good enough for gentle slopes.
|
# Approximate tilt from the surface normal; good enough for gentle slopes.
|
||||||
@@ -189,7 +213,6 @@ def scatter_layer(layer_cfg, field, bounds, origin, base_folder):
|
|||||||
rotation = unreal.Rotator(roll=0.0, pitch=0.0, yaw=yaw)
|
rotation = unreal.Rotator(roll=0.0, pitch=0.0, yaw=yaw)
|
||||||
|
|
||||||
actor = actor_subsystem.spawn_actor_from_object(mesh, location, rotation)
|
actor = actor_subsystem.spawn_actor_from_object(mesh, location, rotation)
|
||||||
scale = rng.uniform(layer_cfg["min_scale"], layer_cfg["max_scale"])
|
|
||||||
actor.set_actor_scale3d(unreal.Vector(scale, scale, scale))
|
actor.set_actor_scale3d(unreal.Vector(scale, scale, scale))
|
||||||
actor.set_folder_path(f"{base_folder}/{layer_cfg['name']}")
|
actor.set_folder_path(f"{base_folder}/{layer_cfg['name']}")
|
||||||
actor.set_actor_label(f"{layer_cfg['name']}_{spawned:04d}")
|
actor.set_actor_label(f"{layer_cfg['name']}_{spawned:04d}")
|
||||||
@@ -199,28 +222,112 @@ def scatter_layer(layer_cfg, field, bounds, origin, base_folder):
|
|||||||
return spawned
|
return spawned
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
def place_explicit(entries, field, origin, base_folder):
|
||||||
def main():
|
"""Places an exact, hand-authored (or externally-generated) list of instances.
|
||||||
with open(CONFIG_PATH, "r") as f:
|
|
||||||
config = json.load(f)
|
|
||||||
|
|
||||||
terrain_cfg = config["terrain"]
|
Unlike scatter_layer's density/slope rules, this reproduces a literal
|
||||||
|
manifest byte-for-byte: the same JSON always yields the same named
|
||||||
|
landmarks at the same coordinates - useful for hero props, quest markers,
|
||||||
|
or a layout exported from an external design tool, none of which a
|
||||||
|
rule-driven scatter (ours or a graph-based tool like PCG) is meant to pin down.
|
||||||
|
"""
|
||||||
|
placed = 0
|
||||||
|
for entry in entries:
|
||||||
|
mesh = unreal.load_asset(entry["mesh"])
|
||||||
|
if mesh is None:
|
||||||
|
print(f" [explicit] missing mesh asset: {entry['mesh']}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
x, y, z_offset = entry.get("location", [0.0, 0.0, 0.0])
|
||||||
|
scale = entry.get("scale", 1.0)
|
||||||
|
if isinstance(scale, list):
|
||||||
|
scale_vec = unreal.Vector(*scale)
|
||||||
|
else:
|
||||||
|
scale_vec = unreal.Vector(scale, scale, scale)
|
||||||
|
|
||||||
|
z = z_offset
|
||||||
|
if entry.get("snap_to_terrain", True):
|
||||||
|
z += field.height(x, y) - mesh_base_z(mesh) * scale_vec.z
|
||||||
|
|
||||||
|
roll, pitch, yaw = entry.get("rotation", [0.0, 0.0, 0.0])
|
||||||
|
location = unreal.Vector(origin.x + x, origin.y + y, origin.z + z)
|
||||||
|
rotation = unreal.Rotator(roll=roll, pitch=pitch, yaw=yaw)
|
||||||
|
|
||||||
|
actor = actor_subsystem.spawn_actor_from_object(mesh, location, rotation)
|
||||||
|
actor.set_actor_scale3d(scale_vec)
|
||||||
|
actor.set_folder_path(f"{base_folder}/Explicit")
|
||||||
|
actor.set_actor_label(entry.get("name", f"Explicit_{placed:04d}"))
|
||||||
|
placed += 1
|
||||||
|
|
||||||
|
print(f" [explicit] placed {placed}/{len(entries)}")
|
||||||
|
return placed
|
||||||
|
|
||||||
|
|
||||||
|
def generate_tile(config, terrain_cfg, tile_label):
|
||||||
|
"""Builds one terrain + its scatter layers + its explicit placements, all
|
||||||
|
under a dedicated Outliner folder so multiple tiles can coexist in one level.
|
||||||
|
"""
|
||||||
field = FractalHeightField(terrain_cfg["noise"], terrain_cfg["height_scale"])
|
field = FractalHeightField(terrain_cfg["noise"], terrain_cfg["height_scale"])
|
||||||
|
base_folder = f"ProceduralTerrain/{tile_label}"
|
||||||
|
|
||||||
print("Building terrain mesh...")
|
print(f"[{tile_label}] building terrain mesh (seed={terrain_cfg['noise']['seed']})...")
|
||||||
static_mesh, bounds = build_terrain_mesh(terrain_cfg, field)
|
static_mesh, bounds = build_terrain_mesh(terrain_cfg, field)
|
||||||
|
|
||||||
loc = terrain_cfg["location"]
|
loc = terrain_cfg["location"]
|
||||||
origin = unreal.Vector(loc[0], loc[1], loc[2])
|
origin = unreal.Vector(loc[0], loc[1], loc[2])
|
||||||
terrain_actor = actor_subsystem.spawn_actor_from_object(static_mesh, origin, unreal.Rotator(0, 0, 0))
|
terrain_actor = actor_subsystem.spawn_actor_from_object(static_mesh, origin, unreal.Rotator(0, 0, 0))
|
||||||
terrain_actor.set_folder_path("ProceduralTerrain")
|
terrain_actor.set_folder_path(base_folder)
|
||||||
terrain_actor.set_actor_label("ProceduralTerrain")
|
terrain_actor.set_actor_label(f"Terrain_{tile_label}")
|
||||||
|
|
||||||
print("Scattering foliage layers...")
|
|
||||||
for layer_cfg in config.get("scatter_layers", []):
|
for layer_cfg in config.get("scatter_layers", []):
|
||||||
scatter_layer(layer_cfg, field, bounds, origin, "ProceduralTerrain")
|
scatter_layer(layer_cfg, field, bounds, origin, base_folder)
|
||||||
|
|
||||||
|
explicit_entries = config.get("explicit_placements", [])
|
||||||
|
if explicit_entries:
|
||||||
|
place_explicit(explicit_entries, field, origin, base_folder)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
with open(CONFIG_PATH, "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
|
||||||
|
base_terrain_cfg = config["terrain"]
|
||||||
|
variants = config.get("variants")
|
||||||
|
|
||||||
|
if not variants:
|
||||||
|
# Single-tile mode: unchanged behavior for existing configs.
|
||||||
|
generate_tile(config, base_terrain_cfg, "Main")
|
||||||
print("Done.")
|
print("Done.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Variant mode: one JSON list of seeds becomes N independent, deterministic
|
||||||
|
# tiles laid out side by side in a single run - each a full reproducible
|
||||||
|
# world driven by nothing but that seed. There's no "batch-produce N baked,
|
||||||
|
# comparable layouts from one data file" workflow in the Foliage tool or a
|
||||||
|
# PCG graph; you'd re-author or re-seed each one by hand, one at a time.
|
||||||
|
gap = base_terrain_cfg["size_x"] * 1.2
|
||||||
|
base_seed = base_terrain_cfg["noise"]["seed"]
|
||||||
|
base_layer_seeds = {layer["name"]: layer.get("seed", 0) for layer in config.get("scatter_layers", [])}
|
||||||
|
|
||||||
|
for i, seed_offset in enumerate(variants):
|
||||||
|
tile_label = f"Variant_{seed_offset}"
|
||||||
|
terrain_cfg = json.loads(json.dumps(base_terrain_cfg)) # cheap deep copy
|
||||||
|
terrain_cfg["noise"]["seed"] = base_seed + seed_offset
|
||||||
|
terrain_cfg["asset_name"] = f"{base_terrain_cfg['asset_name']}_{tile_label}"
|
||||||
|
terrain_cfg["location"] = [
|
||||||
|
base_terrain_cfg["location"][0] + i * gap,
|
||||||
|
base_terrain_cfg["location"][1],
|
||||||
|
base_terrain_cfg["location"][2],
|
||||||
|
]
|
||||||
|
|
||||||
|
tile_config = json.loads(json.dumps(config))
|
||||||
|
tile_config["terrain"] = terrain_cfg
|
||||||
|
for layer_cfg in tile_config.get("scatter_layers", []):
|
||||||
|
layer_cfg["seed"] = base_layer_seeds.get(layer_cfg["name"], 0) + seed_offset
|
||||||
|
|
||||||
|
generate_tile(tile_config, terrain_cfg, tile_label)
|
||||||
|
|
||||||
|
print(f"Done: {len(variants)} variant(s) generated side by side.")
|
||||||
|
|
||||||
|
|
||||||
main()
|
main()
|
||||||
@@ -51,5 +51,23 @@
|
|||||||
"min_spacing": 150.0,
|
"min_spacing": 150.0,
|
||||||
"seed": 42
|
"seed": 42
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"explicit_placements": [
|
||||||
|
{
|
||||||
|
"name": "LandmarkTree_Overlook",
|
||||||
|
"mesh": "/Game/ImportedAssets/Trees/SM_tree01",
|
||||||
|
"location": [1200.0, 800.0, 0.0],
|
||||||
|
"snap_to_terrain": true,
|
||||||
|
"rotation": [0.0, 0.0, 30.0],
|
||||||
|
"scale": 2.5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "RockCairn_Waypoint",
|
||||||
|
"mesh": "/Game/ImportedAssets/Rocks/SM_rock2",
|
||||||
|
"location": [-1500.0, -600.0, 0.0],
|
||||||
|
"snap_to_terrain": true,
|
||||||
|
"rotation": [0.0, 0.0, 0.0],
|
||||||
|
"scale": [1.0, 1.0, 1.8]
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
{
|
||||||
|
"_comment": "Presentation demo: run with TERRAIN_CONFIG env var pointed at this file. One 'variants' list turns this single config into 4 independent, deterministic worlds laid out side by side in one run - no hand re-authoring or re-painting per tile.",
|
||||||
|
"terrain": {
|
||||||
|
"location": [0.0, 0.0, 0.0],
|
||||||
|
"size_x": 3000.0,
|
||||||
|
"size_y": 3000.0,
|
||||||
|
"resolution": 48,
|
||||||
|
"height_scale": 500.0,
|
||||||
|
"dest_path": "/Game/ImportedAssets/Terrain",
|
||||||
|
"asset_name": "SM_TerrainDemo",
|
||||||
|
"noise": {
|
||||||
|
"seed": 1000,
|
||||||
|
"octaves": 5,
|
||||||
|
"frequency": 0.0006,
|
||||||
|
"lacunarity": 2.0,
|
||||||
|
"gain": 0.5
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"scatter_layers": [
|
||||||
|
{
|
||||||
|
"name": "Trees",
|
||||||
|
"meshes": [
|
||||||
|
"/Game/ImportedAssets/Trees/SM_tree01",
|
||||||
|
"/Game/ImportedAssets/Trees/SM_tree02",
|
||||||
|
"/Game/ImportedAssets/Trees/SM_tree03",
|
||||||
|
"/Game/ImportedAssets/Trees/SM_tree04"
|
||||||
|
],
|
||||||
|
"count": 60,
|
||||||
|
"min_height": -50.0,
|
||||||
|
"max_height": 300.0,
|
||||||
|
"max_slope_degrees": 25.0,
|
||||||
|
"min_scale": 0.85,
|
||||||
|
"max_scale": 1.3,
|
||||||
|
"align_to_normal": true,
|
||||||
|
"min_spacing": 200.0,
|
||||||
|
"seed": 7
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Rocks",
|
||||||
|
"meshes": [
|
||||||
|
"/Game/ImportedAssets/Rocks/SM_rock1",
|
||||||
|
"/Game/ImportedAssets/Rocks/SM_rock2",
|
||||||
|
"/Game/ImportedAssets/Rocks/SM_rock3"
|
||||||
|
],
|
||||||
|
"count": 30,
|
||||||
|
"min_height": -100.0,
|
||||||
|
"max_height": 500.0,
|
||||||
|
"max_slope_degrees": 45.0,
|
||||||
|
"min_scale": 0.5,
|
||||||
|
"max_scale": 2.0,
|
||||||
|
"align_to_normal": true,
|
||||||
|
"min_spacing": 150.0,
|
||||||
|
"seed": 42
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variants": [0, 1, 2, 3]
|
||||||
|
}
|
||||||
Reference in new issue
Block a user