# FBX export and re-import check. blender -b ../CaptainsQuarters.blend --python export_fbx.py (saves nothing) # One file per mesh with its UCX hulls, each at identity in its own space, plus one assembled file. # Same convention as models/docking-arm: data turned -90 degrees about Z, forward -Y, up Z, so that the project's # Unreal import settings ("convert scene" + "force front X axis") give ue = (bx, -by, bz) in centimetres. import bpy, os, json, math from mathutils import Matrix, Vector HERE = os.path.dirname(os.path.abspath(__file__)) FBX = os.path.join(os.path.dirname(HERE), 'fbx') os.makedirs(FBX, exist_ok=True) sc = bpy.context.scene PRE = Matrix.Rotation(-math.pi / 2, 4, 'Z') col = bpy.data.collections['CQ | Collision (UCX)'] col.hide_viewport = False bpy.context.view_layer.update() PARTS = [o for o in sc.objects if o.type == 'MESH' and o.name.startswith('SM_CQ_')] HULLS = [o for o in sc.objects if o.name.startswith('UCX_')] def tris(o): return sum(len(p.vertices) - 2 for p in o.data.polygons) def dims(o): cs = [v.co for v in o.data.vertices] return sorted(round(max(c[k] for c in cs) - min(c[k] for c in cs), 4) for k in range(3)) SRC = {o.name: {'tris': tris(o), 'verts': len(o.data.vertices), 'dims': dims(o), 'mats': len(o.data.materials)} for o in PARTS} def fbx(path, objs, types): for o in bpy.context.view_layer.objects: o.select_set(False) for o in objs: o.select_set(True) bpy.context.view_layer.objects.active = objs[0] bpy.ops.export_scene.fbx(filepath=path, use_selection=True, object_types=types, use_mesh_modifiers=False, mesh_smooth_type='OFF', add_leaf_bones=False, bake_anim=False, axis_forward='-Y', axis_up='Z', global_scale=1.0, apply_unit_scale=True, apply_scale_options='FBX_SCALE_NONE', use_space_transform=True, bake_space_transform=False, path_mode='STRIP', use_metadata=False) # ---- assembled: everything where it stands, under one root root = bpy.data.objects.new('CaptainsQuarters_Root', None) sc.collection.objects.link(root) tops = [o for o in PARTS if o.parent is None] for o in tops: o.parent = root root.rotation_euler = (0, 0, -math.pi / 2) bpy.context.view_layer.update() fbx(os.path.join(FBX, 'CaptainsQuarters_Assembled.fbx'), [root] + PARTS, {'MESH', 'EMPTY'}) root.rotation_euler = (0, 0, 0) for o in tops: o.parent = None bpy.data.objects.remove(root) # ---- parts: mesh at identity with its hulls bpy.context.view_layer.update() placement = {} for p in PARTS: # read every placement before anything is moved hulls = [h for h in HULLS if h.name.startswith('UCX_' + p.name + '_')] placement[p.name] = {'location_m': [round(v, 4) for v in p.matrix_world.translation], 'rotation_z_deg': round(math.degrees(p.matrix_world.to_euler().z), 2), 'parent': p.parent.name if p.parent else None, 'motion': p.get('motion', 'static'), 'hulls': len(hulls)} if p.parent: placement[p.name]['local_to_parent_m'] = [round(v, 4) for v in p.matrix_local.translation] for p in PARTS: hulls = [h for h in HULLS if h.name.startswith('UCX_' + p.name + '_')] p.parent = None p.matrix_world = Matrix.Identity(4) for h in hulls: h.parent = None h.matrix_world = Matrix.Identity(4) for o in [p] + hulls: o.data.transform(PRE) bpy.context.view_layer.update() fbx(os.path.join(FBX, p.name + '.fbx'), [p] + hulls, {'MESH'}) # ---- read every part file back and compare with the source report = {'parts': {}, 'placement': placement} ok = True for p in PARTS: before = set(bpy.data.objects) bpy.ops.import_scene.fbx(filepath=os.path.join(FBX, p.name + '.fbx')) new = [o for o in bpy.data.objects if o not in before] m = [o for o in new if o.type == 'MESH' and not o.name.startswith('UCX_')] h = [o for o in new if o.name.startswith('UCX_')] r = {'meshes': len(m), 'hulls': len(h)} if len(m) == 1: o = m[0] r.update({'tris': tris(o), 'dims': dims(o), 'mats': len(o.data.materials), 'uv': len(o.data.uv_layers), 'scale': [round(s, 4) for s in o.scale]}) s = SRC[p.name] r['match'] = (r['tris'] == s['tris'] and r['mats'] == s['mats'] and r['hulls'] == placement[p.name]['hulls'] and all(abs(a - b) < 1e-3 for a, b in zip(r['dims'], s['dims']))) else: r['match'] = False ok = ok and r['match'] report['parts'][p.name] = r for o in new: bpy.data.objects.remove(o) report['all_match'] = ok report['files'] = sorted(f for f in os.listdir(FBX) if f.endswith('.fbx')) json.dump(report, open(os.path.join(HERE, 'fbx_report.json'), 'w'), indent=1) json.dump(placement, open(os.path.join(os.path.dirname(HERE), 'quarters_layout.json'), 'w'), indent=1) print('FBX', len(report['files']), 'files, all match:', ok) for n, r in report['parts'].items(): if not r['match']: print('FBX MISMATCH', n, json.dumps(r), json.dumps(SRC[n]))