The bpy.ops.export_scene.fbx argument block was duplicated in six scripts (hero, block frontage, north buildings, site, scale test, MPFB character) and all six carried mesh_smooth_type="OFF", which drops smoothing groups and made every Unreal import warn and every round part faceted. Tools/fbx_export_preset.py now owns the presets: - preset_kwargs(preset, selection, **overrides) is bpy-free and unit-tested; - export_static()/export_character() are thin wrappers that import bpy lazily; - ue_metric_v1 and ue_character_v1 keep the recorded scale contract (FBX_SCALE_NONE, apply_unit_scale, -Y forward / Z up) and use FACE smoothing. Callers switched: hero_facade_build, export_block_frontage, export_north_buildings, scale_test_export, mpfb_character_export, Art/blender/site/export_site_fbx. Verified by re-exporting every asset through the preset and re-importing all of them: 0 smoothing warnings on site, frontage and north buildings; packaging and -qaselftest all PASS (walk, transect 0 holes, wall probe 0/35, facade probe 0/9). 49 tool tests pass (9 new for the preset contract).
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""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()
|