"""Builds /Game/Maps/L_World: a world-partitioned level with a ~200 km2 landscape from the heightmap PNGs in RawContent/World/Heightmaps/, the Elite_RockyMeadows landscape material, daylight and a player start. Rebuilds from scratch every run; the level is a product of this script and the PNGs, never hand-edited. UnrealEditor-Cmd.exe Salty.uproject -run=pythonscript -script=Scripts/Authoring/create_world.py Swapping the terrain later: replace L_World_Height.png (16-bit greyscale, any resolution the landscape supports) and the three L_World_.png weightmaps, or rerun generate_heightmap.py with another seed, then rerun this. If the PNGs are missing this script generates them first. Size: 4033 vertices a side at 350 cm a quad is 14.11 km a side, 199 km2. Z scale 500 spans -1280 m to +1280 m, so 65535 in the PNG is 1280 m above the landscape origin and sea level (0.18 of the range) is -820 m. """ import os import shutil import sys import unreal HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) sys.path.insert(0, os.path.join(HERE, ".pylib")) LEVEL_PATH = "/Game/Maps/L_World" HEIGHTMAP_DIR = os.path.normpath(os.path.join(HERE, "..", "..", "RawContent", "World", "Heightmaps")) HEIGHTMAP = os.path.join(HEIGHTMAP_DIR, "L_World_Height.png") PACK = "/Game/Elite_RockyMeadows" LANDSCAPE_MATERIAL = f"{PACK}/Materials/M_Landscape_Main_Inst_RockyMeadows02" LAYER_INFOS = { # layer info asset -> weightmap file f"{PACK}/Materials/Material_Layers/Base_Layer_LayerInfo": "L_World_Base_Layer.png", f"{PACK}/Materials/Material_Layers/Layer_02_LayerInfo": "L_World_Layer_02.png", f"{PACK}/Materials/Material_Layers/Layer_03_LayerInfo": "L_World_Layer_03.png", } QUAD_SCALE_CM = 350.0 Z_SCALE = 500.0 GRID_SIZE_COMPONENTS = 4 # streaming proxies of 4x4 components (4 x 126 quads x 3.5 m = 1.76 km a side) level_subsystem = unreal.get_editor_subsystem(unreal.LevelEditorSubsystem) actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) editor_subsystem = unreal.get_editor_subsystem(unreal.UnrealEditorSubsystem) asset_lib = unreal.EditorAssetLibrary def ensure_heightmaps(): missing = [f for f in [HEIGHTMAP] + [os.path.join(HEIGHTMAP_DIR, f) for f in LAYER_INFOS.values()] if not os.path.isfile(f)] if not missing: return unreal.log(f"generating heightmaps, missing {missing}") import generate_heightmap sys.argv = ["generate_heightmap.py"] generate_heightmap.main() def spawn(actor_class, label, location=unreal.Vector(0, 0, 0), rotation=unreal.Rotator(0, 0, 0)): actor = actor_subsystem.spawn_actor_from_class(actor_class, location, rotation) actor.set_actor_label(label) return actor def recreate_level(): if asset_lib.does_asset_exist(LEVEL_PATH): unreal.log(f"{LEVEL_PATH} exists; deleting it and its external actors") level_subsystem.new_level("/Temp/Untitled_Scratch", False) # do not delete the level we stand in asset_lib.delete_asset(LEVEL_PATH) # The external actor packages of streaming proxies cannot be loaded on their own, so the asset library # refuses to delete the folder; remove what is left on disk instead. content_dir = unreal.Paths.project_content_dir() for folder in ("__ExternalActors__/Maps/L_World", "__ExternalObjects__/Maps/L_World"): path = os.path.join(content_dir, folder) if os.path.isdir(path): shutil.rmtree(path, ignore_errors=True) level_subsystem.new_level(LEVEL_PATH, True) # world-partitioned: 200 km2 must stream def create_landscape(): material = unreal.load_asset(LANDSCAPE_MATERIAL) if not material: raise RuntimeError(f"landscape material {LANDSCAPE_MATERIAL} not found; is the pack in Content/?") weightmaps = [] for asset_path, file_name in LAYER_INFOS.items(): layer_info = unreal.load_asset(asset_path) if not layer_info: raise RuntimeError(f"layer info {asset_path} not found") entry = unreal.LandscapeAuthoringWeightmap() entry.set_editor_property("layer_info", layer_info) entry.set_editor_property("file", os.path.join(HEIGHTMAP_DIR, file_name)) weightmaps.append(entry) world = editor_subsystem.get_editor_world() landscape = unreal.LandscapeAuthoringLibrary.create_landscape_from_heightmap( world, HEIGHTMAP, weightmaps, material, unreal.Vector(0, 0, 0), unreal.Vector(QUAD_SCALE_CM, QUAD_SCALE_CM, Z_SCALE), GRID_SIZE_COMPONENTS) if not landscape: raise RuntimeError("landscape creation failed; see LogSaltyEditor") return landscape def ground_height_at(x, y, fallback): world = editor_subsystem.get_editor_world() start, end = unreal.Vector(x, y, 200000.0), unreal.Vector(x, y, -200000.0) hit = unreal.SystemLibrary.line_trace_single(world, start, end, unreal.TraceTypeQuery.ECC_VISIBILITY, False, [], unreal.DrawDebugTrace.NONE, True) if hit and hit.to_tuple()[0]: return hit.to_tuple()[4].z unreal.log_warning("no landscape hit under the spawn; using the fallback height") return fallback def ensure_daylight(): # Keyword arguments on purpose: positional unreal.Rotator is (roll, pitch, yaw), and a sun pitched upward is night. sun = spawn(unreal.DirectionalLight, "World_Sun", unreal.Vector(0, 0, 50000), unreal.Rotator(roll=0.0, pitch=-38.0, yaw=25.0)) sun.light_component.set_editor_property("intensity", 10.0) sun.light_component.set_editor_property("atmosphere_sun_light", True) sun.light_component.set_editor_property("dynamic_shadow_distance_movable_light", 60000.0) spawn(unreal.SkyAtmosphere, "World_Sky") sky_light = spawn(unreal.SkyLight, "World_SkyLight", unreal.Vector(0, 0, 50000)) sky_light.light_component.set_editor_property("real_time_capture", True) spawn(unreal.VolumetricCloud, "World_Clouds") fog = spawn(unreal.ExponentialHeightFog, "World_Fog") fog.component.set_editor_property("fog_density", 0.005) fog.component.set_editor_property("fog_height_falloff", 0.05) def ensure_player_starts(): # The heightmap has a flat pad at its centre; two starts so two PIE clients spawn without a warning. fallback = (0.24 * 65535.0 - 32768.0) / 128.0 * Z_SCALE z = ground_height_at(0.0, 0.0, fallback) + 120.0 for index, y in enumerate((-200.0, 200.0)): spawn(unreal.PlayerStart, f"World_PlayerStart_{index}", unreal.Vector(0.0, y, z)) def main(): ensure_heightmaps() recreate_level() landscape = create_landscape() ensure_daylight() ensure_player_starts() level_subsystem.save_current_level() unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) unreal.log(f"{LEVEL_PATH} saved with landscape {landscape.get_actor_label()}") main()