""" FBX files of the captain's quarters' parts that were made in the ship (captains_suite.py), each with its box hulls. blender -b --factory-startup models/M_Ship_v2_interior_season2.blend --python build/captains_suite_fbx.py Saves nothing in the .blend. Writes models/vessel-interior/captains-suite/fbx/SM_CQS_*.fbx and fbx_report.json. Same convention as the model's own files (models/captains-quarters/build/export_fbx.py, which follows models/docking-arm): one file per mesh with its UCX hulls, 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 part's own space is the cabin's frame (origin on the cabin's floor at its middle), except the two tables, which are drawn at their own foot. Where each part stands in the ship is in vessel_interior_layout.json under "own" (mesh name -> matrix). The hulls are boxes (and slabs for the roof's slope, the canted panels and the walls under them), written down here from the same numbers the parts are drawn from: floors, roofs, walls, benches, sideboards, the landing, table tops and legs. Lettering, props and the light fixtures have none. """ import json import math import os import sys import bmesh import bpy from mathutils import Matrix, Vector HERE = os.path.dirname(os.path.abspath(__file__)) sys.path.insert(0, HERE) import captains_cabin as cc # noqa: E402 import captains_suite as cs # noqa: E402 OUT = os.path.join(os.path.dirname(HERE), "captains-suite", "fbx") os.makedirs(OUT, exist_ok=True) PRE = Matrix.Rotation(-math.pi / 2, 4, "Z") bq = cs.lib() HX, YD, YB, YK, H, T = bq.HX, bq.YD, bq.YB, bq.YO, bq.H, bq.T KH, HC = cs.KH, cs.HC sc = cs._scene_of(bpy.data.collections[cc.ROOM]) E = cs.entrance() def box(lo, hi): (x0, y0, z0), (x1, y1, z1) = lo, hi return [(x, y, z) for x in (x0, x1) for y in (y0, y1) for z in (z0, z1)] def hulls(): """mesh name -> list of convex point sets, in the part's own space.""" out = {} for key, b in cs.bays().items(): x0, x1 = b.x0, b.x0 + b.L u0, u1 = cs.BENCH[key] s0, s1 = cs.SIDEBOARD[key] out["%s%s_Floor" % (cs.PRE, key)] = [box((x0, YD, -0.05), (x1, YK, T))] out["%s%s_Ceiling" % (cs.PRE, key)] = [[tuple(b.FM.p(u, v, w)) for u in (0.0, b.L) for v in (0.0, b.SM) for w in (-0.05, T)]] # the roof's slope out["%s%s_Cant" % (cs.PRE, key)] = [[tuple(b.FS.p(u, v, w)) for u in (0.0, b.L) for v in (0.0, b.SL) for w in (-0.05, T)]] out["%s%s_Wall_Hull" % (cs.PRE, key)] = [ box((x0, YK - T, 0.0), (x1, YK + 0.05, KH)), # the knee wall box((x0 + u0, YK - 0.60, 0.0), (x0 + u1, YK - T, 0.46)), # the bench's seat box((x0 + u0, YK - 0.075, 0.46), (x0 + u1, YK - T, 1.09))] # its padded back side = [box((x0, YD - 0.05, 0.0), (x1, YD + T, HC)), # the corridor-side wall box((x1 - s1, YD + T, 0.0), (x1 - s0, YD + 0.44, 0.85))] # the sideboard if key == "Fore": side.append(box((x1 - s1, YD + T, 1.46), (x1 - s0, YD + 0.22, 1.495))) # the shelf over it out["%s%s_Wall_Corridor" % (cs.PRE, key)] = side # the two walls under the cant: full height at the cabin's depth, the knee wall's height at the far side gables = [] for xa, xb in ((x0 - 0.05, x0 + T), (x1 - T, x1 + 0.05)): gables.append([(x, y, z) for x in (xa, xb) for (y, z) in ((YB, 0.0), (YK, 0.0), (YK, KH), (YB, H))]) # the gable over the model's end wall, under the roof's slope (at the window for the forward bay, at the lockers aft) xa, xb = (x0 - 0.05, x0 + T) if key == "Fore" else (x1 - T, x1 + 0.05) gables.append([(x, y, z) for x in (xa, xb) for (y, z) in ((YD, H), (YB, H), (YD, HC))]) out["%s%s_Gables" % (cs.PRE, key)] = gables ax, half = E["x"], cs.OPEN_W / 2 head = E["z"] + cs.DOOR_TOP + cs.HEAD_OVER mid = cs.middle() out[cs.PRE + "Mid_Ceiling"] = [[tuple(mid.FM.p(u, v, w)) for u in (0.0, mid.L) for v in (0.0, mid.SM) for w in (-0.05, T)]] out[cs.PRE + "Wall_Door"] = [ box((-HX, YD - 0.05, 0.0), (ax - half, YD + T, HC)), box((ax + half, YD - 0.05, 0.0), (HX, YD + T, HC)), box((ax - half, YD - 0.05, head), (ax + half, YD + T, HC)), # over the doorway box((HX - 3.25, YD + T, 0.0), (HX - 0.85, YD + 0.60, 0.46)), # the model's bench: seat box((HX - 3.25, YD + T, 0.46), (HX - 0.85, YD + 0.075, 1.09))] # ... and its padded back out[cs.PRE + "Entry"] = [ box((ax - cs.LANDING[0] / 2, YD + 0.016, 0.0), (ax + cs.LANDING[0] / 2, YD + cs.LANDING[1], E["z"])), # the landing box((ax - half, E["y"], 0.0), (ax + half, YD + 0.016, E["z"]))] # ... through the wall for name, (ln, wd, ht), feet in (("Lounge_Table", cs.LOUNGE_TABLE, (-0.40, 0.40)), ("Mess_Table", cs.MESS_TABLE, (-0.52, 0.52))): hs = [box((-ln / 2, -wd / 2, ht - 0.036), (ln / 2, wd / 2, ht))] hs += [box((x - 0.05, -0.12, 0.0), (x + 0.05, 0.12, ht - 0.036)) for x in feet] out[cs.PRE + name] = hs return out 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) if bpy.context.window is not None: bpy.context.window.scene = sc room = [o for o in cs._room_meshes() if o.data.name.startswith(cs.PRE)] meshes = {} for o in room: meshes.setdefault(o.data.name, o) H_ = hulls() report = {"parts": {}, "in_the_ship": {}} ok = True made = [] for name in sorted(meshes): src = meshes[name] me = src.data.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(H_.get(name, [])): 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) want = {"tris": tris(ob), "dims": dims(ob), "mats": len(me.materials), "hulls": len(hs)} made += [ob] + hs # read it back and compare 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)} if len(got_m) == 1: g = got_m[0] r.update({"tris": tris(g), "dims": dims(g), "mats": len(g.data.materials), "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": src.name, "location_m": [round(v, 4) for v in src.matrix_world.translation], "rotation_z_deg": round(math.degrees(src.matrix_world.to_euler().z), 2)} 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))