# Imports the hero façade FBX into Unreal with the named preset `ue_metric_v1` # and places it at the measured site position. # # /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ # -script=/Tools/ue_hero_facade_import.py -nullrhi -unattended -nopause -stdout # # Placement follows Geo/manifests/local-origin.md and the blockout frame: # project X = UTM easting - 376515.389 (east +) # project Y = -(UTM northing - 3774224.083) in Unreal, because the FBX preset # maps Blender +Y (north) to Unreal -Y and Blender +X to Unreal +X. The building # line of the hero block sits at Unreal Y = -1450 cm (14.5 m south of the road # centreline, from the street profile), and the hero's east edge at # X = -12024.5 cm (projected control point in local-origin.md). import os import sys import unreal sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) import ue_common # noqa: E402 FBX = "/tmp/gigline/hero-facade.fbx" DEST = "/Game/Hollywood/ElCapitan" ACTOR_LABEL = "Hero_Facade" LEVEL = "/Game/Hollywood/DemoMap" # Unreal centimetres: south-east corner of the façade, on the building line. PLACEMENT_CM = (-12024.5, -1450.0, 0.0) RESULT = "/tmp/gigline/hero-facade-import.txt" lines = [] def log(msg): lines.append(str(msg)) def import_fbx(): task = unreal.AssetImportTask() task.filename = FBX task.destination_path = DEST task.destination_name = "SM_HeroFacade" task.automated = True task.save = True task.replace_existing = True ui = unreal.FbxImportUI() ui.import_mesh = True ui.import_materials = True ui.import_textures = False ui.import_as_skeletal = False smd = ui.static_mesh_import_data smd.import_uniform_scale = 1.0 smd.combine_meshes = True smd.convert_scene_unit = False smd.force_front_x_axis = False smd.normal_import_method = unreal.FBXNormalImportMethod.FBXNIM_IMPORT_NORMALS task.options = ui unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) def _make_map_writable(level: str) -> None: """Clear the read-only bit an LFS .umap checks out with. Without this `save_map()` returns False and the staged actor is lost on the next load — no exception, no non-zero exit (see MEMORY: 0.1.x trap). """ content = unreal.Paths.project_content_dir() rel = level[len("/Game/"):] if level.startswith("/Game/") else level umap = os.path.join(content, rel + ".umap") note = ue_common.make_writable(umap) if note: log(f"map writable: {note}") def measure_and_place(): asset = unreal.EditorAssetLibrary.load_asset(f"{DEST}/SM_HeroFacade") if asset is None: log(f"MISSING {DEST}/SM_HeroFacade") return # Collision: complex-as-simple, not auto-generated convex hulls. # The facade is 95 separate parts across 26 m; Unreal's auto hull generation # wraps them in a few coarse hulls that bulged into the street (the QA wall # probe went 0/35 -> 2/35 after trying it). Complex-as-simple makes the real # triangles the simple collision, so line traces and capsule sweeps block on # the actual wall — the same approach used for the terrain, for the same # reason. The flag lives on the asset, so save it explicitly. unreal.GigLineAssetTools.set_complex_collision_as_simple(asset, True) saved = unreal.EditorAssetLibrary.save_loaded_asset(asset) log(f"collision: {unreal.GigLineAssetTools.get_collision_complexity(asset)} (saved={saved})") box = asset.get_bounding_box() mn, mx = box.min, box.max log(f"SM_HeroFacade bounds cm: size=({mx.x - mn.x:.1f}, {mx.y - mn.y:.1f}, {mx.z - mn.z:.1f}) " f"min=({mn.x:.1f}, {mn.y:.1f}, {mn.z:.1f})") # StaticMaterial is a struct wrapper: the material is its material_interface. names = [] for sm in asset.get_editor_property("static_materials"): mat = sm.get_editor_property("material_interface") names.append(mat.get_name() if mat else "None") log(f"materials: {names}") # Place on the demo map if it exists; otherwise create it from the empty level. if unreal.EditorAssetLibrary.does_asset_exist(LEVEL): unreal.EditorLevelLibrary.load_level(LEVEL) else: if not unreal.EditorLevelLibrary.new_level(LEVEL): log(f"could not create {LEVEL}") return for actor in unreal.EditorLevelLibrary.get_all_level_actors(): if actor.get_actor_label() == ACTOR_LABEL: unreal.EditorLevelLibrary.destroy_actor(actor) actor = unreal.EditorLevelLibrary.spawn_actor_from_class( unreal.StaticMeshActor, unreal.Vector(*PLACEMENT_CM)) actor.set_actor_label(ACTOR_LABEL) comp = actor.static_mesh_component comp.set_static_mesh(asset) comp.set_mobility(unreal.ComponentMobility.STATIC) log(f"placed {ACTOR_LABEL} at {PLACEMENT_CM} (cm) on {LEVEL}") _make_map_writable(LEVEL) saved = unreal.EditorLoadingAndSavingUtils.save_map( unreal.EditorLevelLibrary.get_editor_world(), LEVEL) log(f"save_map {LEVEL}: {saved}") if not ue_common.require_saved(saved, LEVEL): log(f"FAILED to save {LEVEL} — staged content will not persist") try: import_fbx() measure_and_place() except Exception as exc: # noqa: BLE001 import traceback log(f"FAILED: {type(exc).__name__}: {exc}") log(traceback.format_exc()) with open(RESULT, "w") as handle: handle.write("\n".join(lines) + "\n")