"""Export the site meshes from the assembled site .blend, one FBX per object. blender -b --factory-startup Art/blender/site/site-2026-11-06.blend \ --python Art/blender/site/export_site_fbx.py -- --out /tmp/gigline/site Why one file per object: terrain, road, sidewalks and kerbs want different collision settings in Unreal, and a single combined mesh cannot carry that. Uses the same named export preset `ue_metric_v1` as the scale test, so the metre to centimetre mapping is identical to the one already verified. """ import argparse import os import sys import bpy sys.path.insert(0, os.path.join(os.path.join(os.path.dirname(__file__), "..", "..", ".."), "Tools")) import fbx_export_preset as fbx_preset # noqa: E402 MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", "Kerb_South", "Kerb_North", "Crossing_Markings") def main(): argv = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] ap = argparse.ArgumentParser() ap.add_argument("--out", default="/tmp/gigline/site") args = ap.parse_args(argv) os.makedirs(args.out, exist_ok=True) print(f"[site] scene objects: {len(bpy.data.objects)}") found = [] for name in MESHES: obj = bpy.data.objects.get(name) if obj is None or obj.type != "MESH": print(f"[site] MISSING {name}") continue found.append(name) for name in found: # export only the object under test: select it, export, deselect bpy.ops.object.select_all(action="DESELECT") obj = bpy.data.objects[name] obj.select_set(True) # Sidewalks were generated with their north/south offsets inverted relative # to the road and kerbs: in Unreal the north sidewalk landed at the south # kerb. Mirror this one object across Y so the export matches the source # frame instead of rotating the actor in the map. mirrored = False # the south sidewalk is correct in the .blend; see street_mesh if mirrored: obj.scale.y *= -1.0 bpy.context.view_layer.objects.active = obj bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) # Object mode has no mesh context; flip winding through the mesh data. me = obj.data for poly in me.polygons: verts = list(poly.vertices) verts.reverse() poly.vertices = verts me.update() _ = bpy.context.view_layer bpy.context.view_layer.objects.active = obj path = os.path.join(args.out, name + ".fbx") fbx_preset.export_static(path, selection=True) if mirrored: # The flip is an export correction, not a change to the source: undo it. obj.scale.y *= -1.0 bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) for poly in obj.data.polygons: verts = list(poly.vertices) verts.reverse() poly.vertices = verts obj.data.update() zs = [(obj.matrix_world @ v.co).z for v in obj.data.vertices] print(f"[site] {name:18} verts {len(obj.data.vertices):6} z {min(zs):+.2f}..{max(zs):+.2f} m " f"-> {path}") print(f"[site] exported {len(found)} of {len(MESHES)} meshes") main()