""" FBX files of the lower deck's own meshes (lower_deck.py), each with its box hull. blender -b --factory-startup models/M_Ship_v2_interior_season2.blend --python build/lower_deck_fbx.py Saves nothing in the .blend. Writes models/vessel-interior/lower-deck/fbx/SM_LD_*.fbx and fbx_report.json. The pack's wall pieces cut at the floor (SM_LD_Wall_HB_A, A2, A3) and the plate under the cut end walls (SM_LD_Sill_Strip). The floor modules are the reactor room's SM_RR_Floor_Frame (its FBX is in reactor-room/fbx), the rest is the pack's pieces (instances in vessel_interior_layout.json). Same convention as reactor_room_fbx.py: one file per mesh, each at identity in its own space, the data turned -90 degrees about Z, forward -Y, up Z; a cut copy keeps the pack piece's origin. Hulls: one box per mesh, its bounds. """ import json import math import os import sys import bmesh import bpy from mathutils import Matrix sys.dont_write_bytecode = True HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import lower_deck as ld # noqa: E402 OUT = os.path.join(os.path.dirname(HERE), "lower-deck", "fbx") os.makedirs(OUT, exist_ok=True) PRE = Matrix.Rotation(-math.pi / 2, 4, "Z") rec = ld.record() if rec is None: raise RuntimeError("no lower deck in this file") sc = bpy.context.scene def bounds(me): xs = [v.co.x for v in me.vertices] ys = [v.co.y for v in me.vertices] zs = [v.co.z for v in me.vertices] return [min(xs), min(ys), min(zs)], [max(xs), max(ys), max(zs)] def box(lo, hi): return [(x, y, z) for x in (lo[0], hi[0]) for y in (lo[1], hi[1]) for z in (lo[2], hi[2])] def hull_points(name, me): lo, hi = bounds(me) if hi[2] - lo[2] < 0.02: lo[2] = hi[2] - 0.05 return [box(lo, hi)] def hull_object(name, pts): bm = bmesh.new() for p in pts: bm.verts.new(p) bmesh.ops.convex_hull(bm, input=bm.verts) bm.normal_update() me = bpy.data.meshes.new(name) bm.to_mesh(me) bm.free() ob = bpy.data.objects.new(name, me) sc.collection.objects.link(ob) return ob 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)) def fbx(path, objs): for o in list(bpy.context.selected_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={"MESH"}, 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) report = {"parts": {}, "in_the_ship": {}} ok = True made = [] for name in sorted(n for n in rec["meshes"] if n in bpy.data.meshes and n.startswith(ld.PRE)): src = bpy.data.meshes[name] users = [o for o in bpy.data.objects if o.type == "MESH" and o.data is src] me = src.copy() me.name = name + "__export" me.transform(PRE) ob = bpy.data.objects.new(name, me) if ob.name != name: # the name must be the mesh's own in the file raise RuntimeError("an object named %s is in the way" % name) sc.collection.objects.link(ob) hs = [] for i, pts in enumerate(hull_points(name, src)): h = hull_object("UCX_%s_%02d" % (name, i + 1), pts) h.data.transform(PRE) hs.append(h) bpy.context.view_layer.update() path = os.path.join(OUT, name + ".fbx") fbx(path, [ob] + hs) # (the pack's wire has one material in two slots; an FBX names each material once) want = {"tris": tris(ob), "dims": dims(ob), "mats": len({m.name for m in me.materials if m is not None}), "hulls": len(hs)} made += [ob] + hs before = set(bpy.data.objects) bpy.ops.import_scene.fbx(filepath=path) new = [x for x in bpy.data.objects if x not in before] got_m = [x for x in new if x.type == "MESH" and not x.name.startswith("UCX_")] got_h = [x for x in new if x.name.startswith("UCX_")] r = {"meshes": len(got_m), "hulls": len(got_h), "file_kb": round(os.path.getsize(path) / 1024.0, 1), "instances_in_the_ship": len(users), "cut_from": src.get("cut_from", ""), "materials": [{"slot": m.name, "unreal": m.get("ue_material", "")} for m in src.materials if m is not None]} if len(got_m) == 1: g = got_m[0] r.update({"tris": tris(g), "dims": dims(g), "mats": len({m.name for m in g.data.materials if m is not None}), "uv": len(g.data.uv_layers), "scale": [round(s, 4) for s in g.scale]}) r["match"] = (r["tris"] == want["tris"] and r["mats"] == want["mats"] and r["hulls"] == want["hulls"] and all(abs(a - b) < 1e-3 for a, b in zip(r["dims"], want["dims"]))) else: r["match"] = False ok = ok and r["match"] report["parts"][name] = r report["in_the_ship"][name] = [{"object": o.name, "location_m": [round(v, 4) for v in o.matrix_world.translation], "rotation_z_deg": round(math.degrees(o.matrix_world.to_euler().z), 2)} for o in sorted(users, key=lambda o: o.name)] for x in new: bpy.data.objects.remove(x) for x in made: bpy.data.objects.remove(x) report["all_match"] = ok report["files"] = sorted(f for f in os.listdir(OUT) if f.endswith(".fbx")) report["hulls"] = sum(r["hulls"] for r in report["parts"].values()) report["triangles"] = sum(r.get("tris", 0) for r in report["parts"].values()) with open(os.path.join(os.path.dirname(OUT), "fbx_report.json"), "w", encoding="utf-8") as fh: json.dump(report, fh, indent=1) print("FBX", len(report["files"]), "files,", report["hulls"], "hulls, all match:", ok) for n, r in report["parts"].items(): if not r["match"]: print("FBX MISMATCH", n, json.dumps(r))