"""Exports the sliding pressure door for Unreal: one FBX per mesh at its own pivot with its UCX hulls, an assembled FBX for a quick look, and PressureDoorSliding_layout.json (pivots, motion, timing, materials). The project's import pair (FBX forward -Y, up Z; Unreal "Convert Scene" + "Force Front X Axis") gives ue = (-by, -bx, bz); the data is turned -90 deg about Z first, as for the docking arm and the hinged door, so in Unreal ue_cm = (x, -y, z) * 100 (Blender x right, y into the wall, z up) Everything is exported from temporary copies: the scene is left as it was. UCX hulls live in the collection "PDS | collision" (kept, excluded from the view layer) so they can be edited. The numbers below follow sd_build.py's dimensions (second version: opening 1.50 x 2.10, plate +-1.75, travel 0.90). """ import bpy, bmesh, math, os, json from mathutils import Vector, Matrix FOLDER = r"D:\HADAL - The Descent\models\pressure-door-sliding" FBX = os.path.join(FOLDER, "fbx") PRE = Matrix.Rotation(-math.pi / 2, 4, 'Z') ROOT = "PressureDoorSliding_Root" PARTS = ["SM_PDSliding_Frame", "SM_PDSliding_Wall", "SM_PDSliding_LeafL", "SM_PDSliding_LeafR", "SM_PDSliding_Handle", "SM_PDSliding_Monitor", "SM_PDSliding_Camera"] STATIC = PARTS[:2] # dimensions (as sd_build.py) OW, OH, OZ0, ZC = 0.70, 1.05, 0.16, 1.21 PX, BEZ = 1.645, 0.18 COL_W = 0.78 LX, RX = -(PX + COL_W / 2), PX + COL_W / 2 RAIL_X = OW + BEZ + 0.10 Z_TOP = OZ0 + 2 * OH def coll(): top = bpy.data.collections["PressureDoorSliding"] c = bpy.data.collections.get("PDS | collision") if c is None: c = bpy.data.collections.new("PDS | collision") top.children.link(c) return c def hull(name, pts, owner, C): """convex hull of world points, stored in the owner's local frame""" bm = bmesh.new() vs = [bm.verts.new(p) for p in pts] bmesh.ops.convex_hull(bm, input=vs) loose = [v for v in bm.verts if not v.link_faces] bmesh.ops.delete(bm, geom=loose, context='VERTS') me = bpy.data.meshes.get(name) or bpy.data.meshes.new(name) me.clear_geometry() bm.to_mesh(me) bm.free() me.transform(owner.matrix_world.inverted()) ob = bpy.data.objects.get(name) or bpy.data.objects.new(name, me) ob.data = me if ob.name not in C.objects: C.objects.link(ob) ob.parent = owner ob.matrix_parent_inverse = Matrix.Identity(4) ob.matrix_basis = Matrix.Identity(4) ob.display_type = 'WIRE' ob.hide_render = True return ob def box(lo, hi): return [Vector((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 make_hulls(set_open): set_open(0.0) C = coll() for o in list(C.objects): bpy.data.objects.remove(o) ob = {n: bpy.data.objects[n] for n in PARTS} fr, wl = ob["SM_PDSliding_Frame"], ob["SM_PDSliding_Wall"] bx = OW + BEZ boxes = [((-PX, -0.075, Z_TOP), (PX, 0.30, 3.00)), ((-PX, -0.075, 0.0), (PX, 0.30, OZ0)), ((-PX, -0.075, OZ0), (-OW, 0.30, Z_TOP)), ((OW, -0.075, OZ0), (PX, 0.30, Z_TOP)), ((-bx, -0.125, Z_TOP), (bx, -0.075, Z_TOP + BEZ)), ((-bx, -0.125, 0.0), (bx, -0.075, OZ0)), ((-bx, -0.125, 0.0), (-OW, -0.075, Z_TOP + BEZ)), ((OW, -0.125, 0.0), (bx, -0.075, Z_TOP + BEZ)), ((-RAIL_X - 0.03, -0.165, 0.48), (-RAIL_X + 0.03, -0.085, 2.04)), ((RAIL_X - 0.03, -0.165, 0.48), (RAIL_X + 0.03, -0.085, 2.04)), ((-0.34, -0.105, 2.65), (0.34, -0.085, 2.83)), ((0.81, -0.185, 2.76), (0.89, -0.085, 2.84))] for i, (lo, hi) in enumerate(boxes): hull("UCX_SM_PDSliding_Frame_%02d" % i, box(lo, hi), fr, C) boxes = [((LX - COL_W / 2, -0.045, 0.0), (LX + COL_W / 2, 0.30, 3.00)), ((RX - COL_W / 2, -0.045, 0.0), (RX + COL_W / 2, 0.30, 3.00)), ((LX - COL_W / 2, 0.02, 3.00), (RX + COL_W / 2, 0.30, 3.40)), ((LX - 0.09, -0.21, 1.15), (LX + 0.09, -0.045, 1.37)), ((LX - 0.34, -0.105, 0.17), (LX, -0.045, 0.43)), ((LX - 0.325, -0.095, 0.42), (LX - 0.16, -0.045, 3.10)), ((RX - 0.28, -0.065, 2.38), (RX + 0.28, -0.045, 2.62)), ((RX - 0.25, -0.061, 1.19), (RX + 0.25, -0.045, 1.65))] for i, (lo, hi) in enumerate(boxes): hull("UCX_SM_PDSliding_Wall_%02d" % i, box(lo, hi), wl, C) hull("UCX_SM_PDSliding_LeafL_00", box((-OW - 0.04, -0.047, OZ0 - 0.04), (-0.002, 0.04, Z_TOP + 0.04)), ob["SM_PDSliding_LeafL"], C) hull("UCX_SM_PDSliding_LeafR_00", box((0.002, -0.047, OZ0 - 0.04), (OW + 0.04, 0.04, Z_TOP + 0.04)), ob["SM_PDSliding_LeafR"], C) hull("UCX_SM_PDSliding_Handle_00", box((-0.125, -0.050, ZC - 0.125), (0.125, -0.017, ZC + 0.125)), ob["SM_PDSliding_Handle"], C) mon = ob["SM_PDSliding_Monitor"] for i, (lo, hi) in enumerate((((LX - 0.25, -0.33, 1.33), (LX + 0.25, -0.26, 1.71)), ((LX - 0.20, -0.40, 1.06), (LX + 0.20, -0.26, 1.10)), ((LX - 0.02, -0.20, 1.10), (LX + 0.02, -0.16, 1.42)))): hull("UCX_SM_PDSliding_Monitor_%02d" % i, box(lo, hi), mon, C) hull("UCX_SM_PDSliding_Camera_00", box((0.815, -0.38, 2.70), (0.885, -0.16, 2.83)), ob["SM_PDSliding_Camera"], C) return len(C.objects) 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, use_tspace=True) def export(set_open): os.makedirs(FBX, exist_ok=True) set_open(0.0) C = coll() tmpc = bpy.data.collections.new("_pds_export") bpy.context.scene.collection.children.link(tmpc) was_excluded = None lc = bpy.context.view_layer.layer_collection.children["PressureDoorSliding"].children.get("PDS | collision") if lc: was_excluded = lc.exclude lc.exclude = False written = [] temps = [] try: for name in PARTS: src = bpy.data.objects[name] me = src.data.copy() me.transform(PRE) t = bpy.data.objects.new("_tmp_" + name, me) tmpc.objects.link(t) temps.append(t) objs = [t] for h in [o for o in C.objects if o.name.startswith("UCX_" + name + "_")]: hm = h.data.copy() hm.transform(PRE) ht = bpy.data.objects.new("_tmp_" + h.name, hm) tmpc.objects.link(ht) temps.append(ht) objs.append(ht) real = {} for o in objs: want = o.name.replace("_tmp_", "", 1) holder = bpy.data.objects.get(want) if holder is not None and holder is not o: real[holder] = want holder.name = want + "__hold" o.name = want bpy.context.view_layer.update() path = os.path.join(FBX, name + ".fbx") _fbx(path, objs, {"MESH"}) written.append(os.path.basename(path)) for o in objs: o.name = "_tmp_" + o.name for holder, want in real.items(): holder.name = want root = bpy.data.objects[ROOT] rz = root.rotation_euler.z root.rotation_euler.z = -math.pi / 2 bpy.context.view_layer.update() objs = [root] + [o for o in root.children_recursive if o.type == 'MESH' and not o.name.startswith("UCX_")] _fbx(os.path.join(FBX, "PressureDoorSliding_Assembled.fbx"), objs, {"MESH", "EMPTY"}) written.append("PressureDoorSliding_Assembled.fbx") root.rotation_euler.z = rz bpy.context.view_layer.update() finally: for o in temps: me = o.data bpy.data.objects.remove(o) if me.users == 0: bpy.data.meshes.remove(me) bpy.data.collections.remove(tmpc) if lc is not None and was_excluded is not None: lc.exclude = True return written def ue(v): return [round(v[0] * 100, 2), round(-v[1] * 100, 2), round(v[2] * 100, 2)] def layout(check=None): def mats(n): return [m.name for m in bpy.data.objects[n].data.materials] ob = {n: bpy.data.objects[n] for n in PARTS} check = check or {} hw = ob["SM_PDSliding_Handle"].matrix_world.translation d = { "asset": "Sliding pressure door for the vessel's main entrance (an alternative to the hinged pressure door for the boarding gate)", "units": "Unreal cm; ue = (x, -y, z) * 100 from Blender. Origin: floor, on the door's axis, in the plain wall's front face.", "front": "the side the references show is Unreal +Y (Blender -Y); the leaves slide along X", "opening": {"clear_width_cm": round(2 * OW * 100), "clear_height_cm": round(2 * OH * 100), "corner_radius_cm": 32, "sill_cm": round(OZ0 * 100), "centre_height_cm": round(ZC * 100), "reveal_depth_cm": 45.5, "note": "bezel front at ue y 12.5, wall back at y -33 (its back bezel)"}, "module": {"door_plate_x_cm": [-PX * 100, PX * 100], "with_columns_x_cm": [-(PX + COL_W) * 100, (PX + COL_W) * 100], "height_cm": 340, "pockets": "the leaves slide into an 11 cm slot inside the plate (ue y -6 .. 5); the plate is %d cm wide for that" % round(2 * PX * 100)}, "static_meshes": {n: {"mesh": "/Game/Vessel/PressureDoorSliding/" + n, "location": [0, 0, 0], "materials": mats(n)} for n in STATIC}, "moving": { "SM_PDSliding_LeafL": {"parent": None, "pivot": ue(ob["SM_PDSliding_LeafL"].location), "motion": "translate along X", "shut_cm": 0.0, "open_cm": -84.5, "materials": mats("SM_PDSliding_LeafL")}, "SM_PDSliding_LeafR": {"parent": None, "pivot": ue(ob["SM_PDSliding_LeafR"].location), "motion": "translate along X", "shut_cm": 0.0, "open_cm": 84.5, "materials": mats("SM_PDSliding_LeafR")}, "SM_PDSliding_Handle": {"parent": "SM_PDSliding_LeafL", "pivot_world_shut": ue(hw), "pivot_in_leaf": ue(ob["SM_PDSliding_Handle"].location), "motion": "rotation about the door axis (Blender Y = Unreal Y, which Unreal calls pitch); it rides on the left leaf and overhangs the right leaf's half of the well", "lock_turn": "a quarter turn clockwise seen from the front while the ring light changes, and it stays: the bar lies " "across the band when shut (lower left to upper right), along it when open; Blender +90 deg about Y = " "Unreal -90 deg about Y (the y flip)", "free_turn_deg": [-90.0, 90.0], "materials": mats("SM_PDSliding_Handle")}, "SM_PDSliding_Monitor": {"parent": None, "pivot": ue(ob["SM_PDSliding_Monitor"].location), "motion": "yaw about Z (the post)", "range_deg": [-25.0, 25.0], "note": "Blender +25 deg about Z = Unreal yaw -25 deg (the y flip)", "materials": mats("SM_PDSliding_Monitor")}, "SM_PDSliding_Camera": {"parent": None, "pivot": ue(ob["SM_PDSliding_Camera"].location), "motion": "yaw about Z (the ball)", "range_deg": [-30.0, 30.0], "note": "the body is pitched 20 deg down in the mesh", "materials": mats("SM_PDSliding_Camera")}, }, "lock_light": {"material": "M_PDS_Lock", "parameter": "LockRamp Fac 0..1: 0 red (locked), 0.5 amber, 1 green (unlocked)", "driven_by": "the first 30 % of the open slider in Blender; in Unreal a scalar parameter on the material instance"}, "sequence_open_0_to_1": {"lock_light": [0.0, 0.30], "handle_quarter_turn": [0.0, 0.30], "slide": [0.36, 1.0], "easing": "smoothstep inside each span", "blender_timeline": "shut 1, open 60, held to 120, shut 180 at 30 fps (2 s to open, as the video)"}, "for_boarding_gate_script": {"note": "models/vessel-interior/build/boarding_gate.py (another session) appends this door with these numbers written in; " "its 'sliding' entry held the first version's (1.40 x 2.20, R 0.30, zc 1.26, plate 1.52, four movers); the plate is 5 mm inside the gate's +-1.650", "opening": {"kind": "rrect", "w": round(2 * OW, 2), "h": round(2 * OH, 2), "r": 0.32, "zc": round(ZC, 2)}, "plate_hole": [PX, 3.0], "sleeve_y0": 0.33, "movers": ["SM_PDSliding_LeafL", "SM_PDSliding_LeafR", "SM_PDSliding_Handle", "SM_PDSliding_Monitor", "SM_PDSliding_Camera"]}, "check": check, "textures": ["T_PDS_Masks (UV0, non-colour: R occlusion, G edges, B crevices)", "T_PDS_Screens (UV0, emissive: monitor, keypad, sign ground)", "pack: ModSci_Engineer Base/ScratchedMetal, Base/StainedSteel, Base/Glass (UV1, metres)"], } p = os.path.join(FOLDER, "PressureDoorSliding_layout.json") with open(p, "w", encoding="utf-8") as f: json.dump(d, f, indent=1) return p