""" FBX files of the parts drawn for the raised fore main deck (fore_deck.py), each with its hulls. blender -b --factory-startup models/M_Ship_v2_interior_season2.blend --python build/fore_deck_fbx.py Saves nothing in the .blend. Writes models/vessel-interior/fore-deck/fbx/SM_FD_*.fbx and fbx_report.json. Three meshes: the sensor room's pier-beam-pier (SM_FD_Beam), the floor strip under it (SM_FD_Floor_Strip) and the landing with its two flights before the fore passage's raised door in the sub bay (SM_FD_Door_Podium). Everything else of the raised deck is the pack's pieces (instances in vessel_interior_layout.json) or plain boxes of the sub bay's lining. Where each stands is in vessel_interior_layout.json under "own". Same convention as reactor_room_fbx.py. Hulls: the two piers and the beam as three boxes; the strip as a slab; the landing as a box and each flight as a wedge from the landing's edge down to the lowest riser (a ramp to walk on, no bumping on the steps). """ 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 fore_deck as fd # noqa: E402 OUT = os.path.join(os.path.dirname(HERE), "fore-deck", "fbx") os.makedirs(OUT, exist_ok=True) PRE = Matrix.Rotation(-math.pi / 2, 4, "Z") rec = fd.record() if rec is None: raise RuntimeError("the fore deck is not raised 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 name == fd.PRE + "Beam": zb = hi[2] - 0.30 - fd.BEAM_D return [box((lo[0], lo[1], lo[2]), (lo[0] + fd.PIER_D, hi[1], zb)), box((hi[0] - fd.PIER_D, lo[1], lo[2]), (hi[0], hi[1], zb)), box((lo[0], -0.20, zb), (hi[0], 0.20, hi[2]))] if name == fd.PRE + "Door_Podium": H = rec["raised_by"] r = H / fd.RISERS out = [box((-fd.LAND_D, -fd.LAND_W, 0.0), (fd.NICHE, fd.LAND_W, H))] for s in (1.0, -1.0): ya, yb = s * fd.LAND_W, s * (fd.LAND_W + (fd.RISERS - 1) * fd.TREAD) out.append([(x, y, z) for x in (-fd.LAND_D, 0.0) for y, z in ((ya, 0.0), (ya, H), (yb, r), (yb, 0.0))]) return out 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): 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))