"""Tiger: FBX export and the layout file (headless, the .blend is not changed). blender.exe -b Tender_Tiger.blend -t 0 --python build/tg_export.py -> fbx/SM_Tiger_Hull.fbx (with its UCX_ hulls), fbx/SM_Tiger_Glass.fbx, fbx/SM_Tiger_Cabin.fbx, fbx/Tiger_Assembled.fbx, Tiger_layout.json FBX forward -Y, up Z, 1 unit = 1 m (the Kazah's and the Mako's axes); Unreal import with Force Front X Axis, Convert Scene. """ import bpy import json import math import os HERE = os.path.dirname(os.path.abspath(__file__)) ROOT = os.path.dirname(HERE) FBX = os.path.join(ROOT, "fbx") os.makedirs(FBX, exist_ok=True) MESHES = ["SM_Tiger_Hull", "SM_Tiger_Glass", "SM_Tiger_Cabin", "SM_Tiger_ToolArm"] UCX = [o for o in bpy.data.objects if o.name.startswith("UCX_SM_Tiger_Hull")] UCX_ARM = [o for o in bpy.data.objects if o.name.startswith("UCX_SM_Tiger_ToolArm")] CHECKS = os.path.join(HERE, "checks") def fbx(path, objs): for o in bpy.context.view_layer.objects: o.select_set(False) for o in objs: o.hide_set(False) o.hide_viewport = False 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, use_tspace=True) for c in bpy.data.collections: c.hide_viewport = False for lc in bpy.context.view_layer.layer_collection.children: lc.hide_viewport = False lc.exclude = False objs = {n: bpy.data.objects[n] for n in MESHES} fbx(os.path.join(FBX, "SM_Tiger_Hull.fbx"), [objs["SM_Tiger_Hull"]] + UCX) fbx(os.path.join(FBX, "SM_Tiger_Glass.fbx"), [objs["SM_Tiger_Glass"]]) fbx(os.path.join(FBX, "SM_Tiger_Cabin.fbx"), [objs["SM_Tiger_Cabin"]]) fbx(os.path.join(FBX, "Tiger_Assembled.fbx"), list(objs.values())) # the tool arm on its own: at the world origin, unturned, so its vertices are relative to its hinge (its UCX goes with it) arm = objs["SM_Tiger_ToolArm"] arm_drv = arm.animation_data.drivers[0] if arm.animation_data and arm.animation_data.drivers else None if arm_drv: arm_drv.mute = True loc, rot = arm.location.copy(), arm.rotation_euler.copy() arm.location, arm.rotation_euler = (0, 0, 0), (0, 0, 0) bpy.context.view_layer.update() fbx(os.path.join(FBX, "SM_Tiger_ToolArm.fbx"), [arm] + UCX_ARM) arm.location, arm.rotation_euler = loc, rot if arm_drv: arm_drv.mute = False lo, hi = [1e9] * 3, [-1e9] * 3 info = {} bpy.context.view_layer.update() for n, o in objs.items(): me = o.data for v in me.vertices: w = o.matrix_world @ v.co # the tool arm at its hinge, pointing aft (the default) for k in range(3): lo[k] = min(lo[k], w[k]) hi[k] = max(hi[k], w[k]) sizes = [len(p.vertices) for p in me.polygons] info[n] = dict(triangles=sum(s - 2 for s in sizes), quads=sizes.count(4), tris=sizes.count(3), ngons=sum(1 for s in sizes if s > 4), uv_layers=[u.name for u in me.uv_layers], materials=[m.name for m in me.materials if m]) cam = json.load(open(os.path.join(HERE, "cam.json"))) import sys sys.path.insert(0, HERE) import tg_geom tg_y_clamp, tg_shift = tg_geom.Y_CLAMP, round(tg_geom.TENDER_SHIFT_Y, 4) _, clip_info = tg_geom.part_clips() def _j(name): f = os.path.join(CHECKS, name) return json.load(open(f)) if os.path.exists(f) else None _sw = _j("sweep_and_moonpool.json") or {} checks = { "moon_pool_at_board_tool_arm_folded": _sw.get("moonpool_board_tiger"), "moon_pool_at_board_tool_arm_aft": _sw.get("moonpool_board_tiger_toolarm_aft"), "moon_pool_limit_m": 1.0, "carousel_sweep_tiger": (_sw.get("tiger") or [None])[0], "carousel_sweep_kazah": (_sw.get("kazah") or [None])[0], "clamp_docking_arm_file": (_j("clamp_DockArm_Carousel.json") or {}).get("summary"), "clamp_ship_file": (_j("clamp_M_Ship_v2_interior_season2.json") or {}).get("summary"), "tool_arm_travel": (_j("motion.json") or {}).get("summary"), "carousel_triangles": _j("carousel_bvh.json"), "descent_board_to_pool": (_j("descent.json") or {}).get("summary"), "cockpit_and_bay_inside_the_skin": _j("cabin.json"), } textures = sorted(f for f in os.listdir(os.path.join(ROOT, "textures")) if f.startswith("T_Tiger_")) def ue(v): return [round(-v[1] * 100, 1), round(-v[0] * 100, 1), round(v[2] * 100, 1)] layout = { "name": "Tiger (working name)", "source": "reference/user_picture.jpg, one perspective concept picture (827 x 803), the user's 2026-10-22", "frame": "1 unit = 1 m, nose to -Y, port +X, Z up (the Kazah's and the Mako's); origin on the centre line 9 cm behind the " "docking clamp's axis, z 0 the Mako's height datum (the docking arm's wrist 1.94 m over it)", "fbx": {"forward": "-Y", "up": "Z", "unreal_import": "Force Front X Axis on, Convert Scene on, scale 1", "to_unreal_cm": "ue = (-y, -x, z) * 100 (nose +X)"}, "bounds_m": {"min": [round(x, 3) for x in lo], "max": [round(x, 3) for x in hi], "length": round(hi[1] - lo[1], 3), "span": round(hi[0] - lo[0], 3), "height": round(hi[2] - lo[2], 3)}, "bounds_ue_cm": {"min": ue([hi[0], hi[1], lo[2]]), "max": ue([lo[0], lo[1], hi[2]])}, "meshes": info, "collision": {"SM_Tiger_Hull": [o.name for o in UCX]}, "moving_parts": {"SM_Tiger_ToolArm": { "pivot_m": [round(float(c), 4) for c in loc], "pivot_ue_cm": ue(list(loc)), "axis": "Blender X (Unreal Y)", "range_deg": [0, 90], "handle": "Tiger_Root[\"tool_arm_down_deg\"]: 0 = pointing aft as the picture draws it, 90 = folded straight down", "blender": "rotation_euler.x = -deg (a simple-expression driver)", "docking": "fold it (90) before BOARD: pointing aft it ends 0.634 m from the deck well's lip"}}, "clamp": { "axis_m": [0.0, -0.09], "convention": "the Kazah's: the docking arm's wrist 1.94 m over the origin, the clamp axis 9 cm " "ahead of it", "picture_frame_station": tg_y_clamp, "model_moved_aft_m": tg_shift, "clips": {"azimuths_deg": [45, 135, 225, 315], "plate_r_m": [0.506, 0.535], "lip_r_m": 0.552, "z_m": {"plate_bottom": 1.188, "wall_top": 1.335, "lip_top": 1.375}, "arc_deg": 22, "fore_pair": "on raked orange crests from the shell's front edge behind the glass, over the visor", "aft_pair": "on orange fairings on the shell behind the collar", "detail": clip_info}, "why_here": "aft of about -0.20 the dorsal fin comes within 0.1 m of the arm's ore hose (over the aft side); forward " "of -0.25 the fore clips would stand over the canopy glass; raising the Tiger on the arm brought the claws " "down on the shell (build/tg_clamp_place.py)", "hold": "the collar's profile only, as the Kazah and the Mako: the shut claws stand 14 mm off the lip, no catch"}, "checks": checks, "textures": textures, "picture_camera": {"location": [round(x, 4) for x in cam["params"][:3]], "rotation_euler_xyz_deg": [round(math.degrees(x), 3) for x in cam["params"][3:6]], "lens_mm_36mm_sensor": round(cam["lens_mm"], 2), "resolution": [827, 803]}, } json.dump(layout, open(os.path.join(ROOT, "Tiger_layout.json"), "w"), indent=1) print("TIGER EXPORT OK", json.dumps(layout["bounds_m"]), json.dumps({k: v["triangles"] for k, v in info.items()}))