""" FBX files of the reactor room's own parts (reactor_room.py), each with its box hull. blender -b --factory-startup models/M_Ship_v2_interior_season2.blend --python build/reactor_room_fbx.py Saves nothing in the .blend. Writes models/vessel-interior/reactor-room/fbx/SM_RR_*.fbx and fbx_report.json. The room is the pack's pieces (they are in vessel_interior_layout.json as instances) and eleven meshes of its own: cut copies of pack pieces (the wall pieces without the base that stood below the floor, the door-side brace, a pilaster, a wire, the glass floor tile's top) and the few parts drawn here (dais, base course, sill plate, platform pad). These travel with the ship; where each stands is in vessel_interior_layout.json under "own" (mesh name -> matrix). They use the pack's own materials: fbx_report.json lists each part's slots with the Unreal material path. Same convention as the captain's quarters' files (captains_suite_fbx.py): one file per mesh with its hull, each at identity in its own space, the 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. A cut copy keeps the pack piece's own origin (a wall piece's is 0.9 m below the floor at its back lower corner). Hulls: one box per part, its bounds (the floor module's up to its rim, not to the clips on it). The wire has none. """ 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 reactor_room as rr # noqa: E402 OUT = os.path.join(os.path.dirname(HERE), "reactor-room", "fbx") os.makedirs(OUT, exist_ok=True) PRE = Matrix.Rotation(-math.pi / 2, 4, "Z") NO_HULL = (rr.PRE + "Wire_Wall_B",) rec = rr.record() if rec is None: raise RuntimeError("no reactor room 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 hull_points(name, me): if name in NO_HULL: return [] lo, hi = bounds(me) if name == rr.PRE + "Floor_Frame": hi[2] = lo[2] + rr.FLOOR_KEEP # to the rim's top if hi[2] - lo[2] < 0.02: lo[2] = hi[2] - 0.05 # a plate gets a body 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_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): 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))