From 02cadd4964a304348b0cf4016ba772141db3593e Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Mon, 9 Nov 2026 14:46:02 +0100 Subject: [PATCH] feat(vehicle): split into systems, add real LODs, import to Unreal Follows the vehicle-artist object split, which the first build violated: body, glass, front and rear lights, mirrors and a single wheel mesh are now separate objects and separate assets in /Game/Hollywood/Vehicle. A single merged mesh across all systems is what the skill forbids for a game vehicle, and it also meant glass could not be made transparent or lights emissive independently. LODs decimate rather than duplicate: LOD1 38 triangles, LOD2 14, against 132 for LOD0. The first pass copied the shell unchanged and produced 84-triangle 'LODs' that saved nothing. Imports verified with bounds: body 4.76 x 1.76 x 1.28 m, wheel 0.22 x 0.66 x 0.66 m, which match the modelled dimensions. Open, recorded: LOD group wiring needs StaticMeshEditorSubsystem.import_lod, which is unavailable in a headless commandlet, so the LOD meshes sit as their own assets for now. --- Art/blender/vehicle/gigline-sedan.blend | 4 +- .../Vehicle/M_Vehicle_Collision.uasset | 3 + .../Hollywood/Vehicle/M_Vehicle_Glass.uasset | 3 + .../Hollywood/Vehicle/M_Vehicle_Lamp.uasset | 3 + .../Hollywood/Vehicle/M_Vehicle_Paint.uasset | 3 + .../Hollywood/Vehicle/M_Vehicle_Tail.uasset | 3 + .../Hollywood/Vehicle/M_Vehicle_Trim.uasset | 3 + .../Hollywood/Vehicle/M_Vehicle_Tyre.uasset | 3 + .../Hollywood/Vehicle/SM_Vehicle_Body.uasset | 3 + .../Vehicle/SM_Vehicle_Body_LOD1.uasset | 3 + .../Vehicle/SM_Vehicle_Body_LOD2.uasset | 3 + .../Hollywood/Vehicle/SM_Vehicle_Glass.uasset | 3 + .../Vehicle/SM_Vehicle_Light_Front.uasset | 3 + .../Vehicle/SM_Vehicle_Light_Rear.uasset | 3 + .../Vehicle/SM_Vehicle_Mirrors.uasset | 3 + .../Hollywood/Vehicle/SM_Vehicle_Wheel.uasset | 3 + TODO.md | 4 +- Tools/README.md | 1 + Tools/ue_vehicle_import.py | 118 ++++++++++++++++++ Tools/vehicle_build.py | 109 ++++++++++++++-- 20 files changed, 268 insertions(+), 13 deletions(-) create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Collision.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Glass.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Lamp.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Paint.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tail.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Trim.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tyre.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD1.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD2.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Glass.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Front.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Rear.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Mirrors.uasset create mode 100644 Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Wheel.uasset create mode 100644 Tools/ue_vehicle_import.py diff --git a/Art/blender/vehicle/gigline-sedan.blend b/Art/blender/vehicle/gigline-sedan.blend index bcea865..d58052e 100644 --- a/Art/blender/vehicle/gigline-sedan.blend +++ b/Art/blender/vehicle/gigline-sedan.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ec66602a4fcfc8f5a992336321cda2025c6e0357f387d12838e8a7f494a06870 -size 101931 +oid sha256:7a2296338fcd7e7b4ea2264a2c99b1c1437d4c3d323dae46a9fac9430dacc24b +size 102442 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Collision.uasset b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Collision.uasset new file mode 100644 index 0000000..a4297c7 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Collision.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:770af69266779476e7fb7e0aaafdadc688bdd9de84af4997b4b3921c17ef4884 +size 56718 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Glass.uasset b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Glass.uasset new file mode 100644 index 0000000..5a02b2d --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Glass.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:554d4ee13f307ab7265b4e963a106fff846ffe1173e477e22ceeb02150c97471 +size 56665 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Lamp.uasset b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Lamp.uasset new file mode 100644 index 0000000..2d6fa77 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Lamp.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86a811b95ea7875249df1a2c7060cfdbc5d2c26826202c4f28d0009393e8d324 +size 56667 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Paint.uasset b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Paint.uasset new file mode 100644 index 0000000..e6aac43 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Paint.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:164566bb91b6f1a4e7be961fd6d9a86a34c8654bfd8ef6b585a1a57564324307 +size 56654 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tail.uasset b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tail.uasset new file mode 100644 index 0000000..f35f5e1 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tail.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a28208bab5de0ef1572bafbe78d80a5c67f266bf7a8f57688d49d8034d4df4f +size 56664 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Trim.uasset b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Trim.uasset new file mode 100644 index 0000000..3fc9ed3 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Trim.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7bd774f618fb3a3c80599bad994aca79dd30bb811236bd7931bafdd4c9063973 +size 56638 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tyre.uasset b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tyre.uasset new file mode 100644 index 0000000..d84f8b5 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/M_Vehicle_Tyre.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b0994f0650775090b04ea4328231ad1c21bcba437274a0b02873ed9c47c1ac69 +size 56649 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body.uasset new file mode 100644 index 0000000..507ea43 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a0df31c43c5716c654d2f4913f6d4648a97b08dd77f20d980e5baee427200942 +size 84317 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD1.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD1.uasset new file mode 100644 index 0000000..02bc981 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD1.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9015eeab8e1a56e047c1bfaef2a7dcc6ae2099565624b23fc3970581da8c81f2 +size 79710 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD2.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD2.uasset new file mode 100644 index 0000000..6e01d6e --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Body_LOD2.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9e4fcd96a037de9222dbb64109dad5beadbbaebc49190bd993d21b4457c4eda0 +size 75223 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Glass.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Glass.uasset new file mode 100644 index 0000000..551966d --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Glass.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3386071f9781e97040141cc327f817bb6a2429a6d5b6f8a5b2823c287451c4df +size 75795 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Front.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Front.uasset new file mode 100644 index 0000000..c5dfda8 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Front.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1bf2ca90dff4313f3ad4b762419b50e6de233efb9fd80236cd46bef5de035d4 +size 74377 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Rear.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Rear.uasset new file mode 100644 index 0000000..c292896 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Light_Rear.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2abfbaba56d9da4924f992b3961616af18640411424618ac524ba4d6871c6f17 +size 74371 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Mirrors.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Mirrors.uasset new file mode 100644 index 0000000..5c3e74e --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Mirrors.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:26492303a0a8ee16ffefb15b7113acadd9de0f6c8fe12125cb8f1608d4e31cf2 +size 74303 diff --git a/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Wheel.uasset b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Wheel.uasset new file mode 100644 index 0000000..56e3cc1 --- /dev/null +++ b/Game/GigLine/Content/Hollywood/Vehicle/SM_Vehicle_Wheel.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:78415d0eff6f59821c30eb15b850b7dd80a3f80b1212a18ed7265dbd67de2a47 +size 76772 diff --git a/TODO.md b/TODO.md index 7e4edba..04cab6d 100644 --- a/TODO.md +++ b/TODO.md @@ -13,7 +13,9 @@ Actionable task list for this project. This file is the single, central place fo ### Vehicle production - active - session-5598ce7e-27e3-4c64-bb49-c04a4a31ee58 (branch `vehicle-0.3.0`) -- [A] Model one vehicle in Blender with collision meshes; export to Unreal. **Model done** (`Tools/vehicle_build.py` → `Art/blender/vehicle/gigline-sedan.blend`, preview in `Art/blender/vehicle/preview.png`): 4.72 x 1.80 x 1.44 m, 2.78 m wheelbase, 1.62 m track, 0.33 m wheels, 252 triangles, four `UCX_` convex hulls. Dimensions come from real sedan figures and are logged to the report, so the vehicle config can read them rather than restating them. Remaining: import into Unreal (`0.3.0` next step). +- [A] Model one vehicle in Blender with collision meshes; export to Unreal. **Model done** (`Tools/vehicle_build.py` → `Art/blender/vehicle/gigline-sedan.blend`, preview in `Art/blender/vehicle/preview.png`): 4.72 x 1.80 x 1.44 m, 2.78 m wheelbase, 1.62 m track, 0.33 m wheels, 252 triangles, four `UCX_` convex hulls. Dimensions come from real sedan figures and are logged to the report, so the vehicle config can read them rather than restating them. - **Imported into Unreal** (`Tools/ue_vehicle_import.py` → `/Game/Hollywood/Vehicle`), split per the vehicle-artist object split: `SM_Vehicle_Body` (3 slots, 4.76 x 1.76 x 1.28 m), `SM_Vehicle_Glass`, `SM_Vehicle_Light_Front`, `SM_Vehicle_Light_Rear`, `SM_Vehicle_Mirrors`, and one `SM_Vehicle_Wheel` (0.22 x 0.66 x 0.66 m) reused four times. The body FBX carries the `UCX_` hulls as simple collision. + - **LODs cut for real:** decimating the duplicate gives LOD1 at 38 triangles and LOD2 at 14, against 132 for LOD0. The first attempt duplicated the shell unchanged and produced 84-triangle LODs that saved nothing - worth remembering as the failure mode of a "LOD" step nobody measures. + - **LOD group wiring is open:** `StaticMeshEditorSubsystem.import_lod` is unavailable in a headless commandlet (`get_editor_subsystem` returns None), so the LOD meshes import as their own assets and the group still needs an editor session. - [ ] Set up Chaos Vehicle with wheel and physics configuration. - [ ] Implement enter/exit interaction. - [ ] Build a closed driving route with boundaries. diff --git a/Tools/README.md b/Tools/README.md index 0205a57..9e11c14 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -95,6 +95,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b | `character_match.py` | Compares a rendered character against a blueprint: figure aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette distance, and a side-by-side montage. | `--render`, `--blueprint`, `--render-box`, `--out`, `--montage` | `python3 Tools/character_match.py --render Screenshots/demo/review-demo-third-person-front.png --blueprint Art/characters/blueprints/front.png --render-box 0.42,0.46,0.56,0.97 --montage /tmp/gigline/match.png` | | `generate_character_textures.py` | Procedural skin-pore and hair-strand textures with sidecars; no model involved. | `--out` | `python3 Tools/generate_character_textures.py --out Art/textures/generated` | | `vehicle_build.py` | Models the drivable sedan: three-tier body, four wheels, lamps and bumpers, plus four `UCX_` convex collision hulls; saves the .blend, exports an FBX with the `ue_metric_v1` preset, and writes a report with every dimension the vehicle config needs. | `--out`, `--fbx`, `--report` | `blender -b --factory-startup --python Tools/vehicle_build.py -- --out Art/blender/vehicle/gigline-sedan.blend --fbx /tmp/gigline/vehicle/sedan.fbx` | +| `ue_vehicle_import.py` | Imports the sedan's six systems into `/Game/Hollywood/Vehicle` as separate static meshes, plus the two LOD meshes; the body carries the `UCX_` hulls as simple collision. | (no arguments; reads `/tmp/gigline/vehicle`) | `UnrealEditor-Cmd -run=pythonscript -script=Tools/ue_vehicle_import.py -nullrhi -unattended -nopause` | ## Tests diff --git a/Tools/ue_vehicle_import.py b/Tools/ue_vehicle_import.py new file mode 100644 index 0000000..056b6b4 --- /dev/null +++ b/Tools/ue_vehicle_import.py @@ -0,0 +1,118 @@ +# Import the sedan systems as separate static meshes (0.3.0). +# +# blender -b --factory-startup --python Tools/vehicle_build.py -- \ +# --out Art/blender/vehicle/gigline-sedan.blend --fbx /tmp/gigline/vehicle/sedan.fbx +# /Engine/Binaries/Linux/UnrealEditor-Cmd -run=pythonscript \ +# -script=/Tools/ue_vehicle_import.py -nullrhi -unattended -nopause -stdout +# +# One asset per system, per the vehicle-artist skill's object split: body, glass, front +# and rear lights, mirrors, and one wheel reused four times. The body FBX also carries +# the UCX_ hulls, which Unreal consumes as simple collision. +# +# Result file: /tmp/gigline/vehicle-import.txt + +import os + +import unreal + +DEST = "/Game/Hollywood/Vehicle" +SOURCE = "/tmp/gigline/vehicle" +SYSTEMS = ( + ("body", "SM_Vehicle_Body"), + ("glass", "SM_Vehicle_Glass"), + ("light_front", "SM_Vehicle_Light_Front"), + ("light_rear", "SM_Vehicle_Light_Rear"), + ("mirrors", "SM_Vehicle_Mirrors"), + ("wheel", "SM_Vehicle_Wheel"), +) +RESULT = "/tmp/gigline/vehicle-import.txt" +lines = [] + + +def log(msg): + lines.append(str(msg)) + unreal.log(str(msg)) + + +def main(): + for source, name in SYSTEMS: + path = os.path.join(SOURCE, f"{source}.fbx") + if not os.path.exists(path): + log(f"{name}: missing source {path}") + lines.append("VERDICT: FAIL") + return + task = unreal.AssetImportTask() + task.filename = path + task.destination_path = DEST + task.destination_name = name + task.automated = True + task.save = True + task.replace_existing = True + + ui = unreal.FbxImportUI() + ui.import_mesh = True + ui.import_as_skeletal = False + ui.import_materials = True + ui.import_textures = False + ui.static_mesh_import_data.combine_meshes = True + ui.static_mesh_import_data.generate_lightmap_u_vs = True + ui.static_mesh_import_data.auto_generate_collision = True + ui.static_mesh_import_data.import_uniform_scale = 1.0 + task.options = ui + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}") + if mesh is None: + log(f"{name}: MISSING after import") + lines.append("VERDICT: FAIL") + return + bounds = mesh.get_bounding_box() if hasattr(mesh, "get_bounding_box") else None + size = "" + if bounds: + extent = bounds.max - bounds.min + size = f"{extent.x/100:.2f} x {extent.y/100:.2f} x {extent.z/100:.2f} m" + mats = mesh.get_editor_property("static_materials") + log(f"{name}: imported, {len(mats)} material slot(s), bounds {size}") + + # Wire the two LOD meshes into the body's LOD group. import_lod is the supported + # path; copying bytes between assets is not, and the LOD group is what the engine + # uses to pick by screen size. + # LOD meshes are imported as their own assets. Wiring them into the body's LOD group + # needs StaticMeshEditorSubsystem.import_lod, which is unavailable in this headless + # commandlet (get_editor_subsystem returns None), so the group is a follow-up. + for source, name in (("body_lod1", "SM_Vehicle_Body_LOD1"), + ("body_lod2", "SM_Vehicle_Body_LOD2")): + path = os.path.join(SOURCE, f"{source}.fbx") + if not os.path.exists(path): + log(f"{name}: missing {path}") + continue + task = unreal.AssetImportTask() + task.filename = path + task.destination_path = DEST + task.destination_name = name + task.automated = True + task.save = True + task.replace_existing = True + ui = unreal.FbxImportUI() + ui.import_mesh = True + ui.import_materials = True + ui.static_mesh_import_data.combine_meshes = True + task.options = ui + unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + mesh = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}") + log(f"{name}: imported as its own asset ({'ok' if mesh else 'MISSING'}); " + "LOD group wiring needs an editor session") + lines.append("VERDICT: PASS") + + +try: + main() +except Exception as exc: # noqa: BLE001 + import traceback + log(f"FAILED: {type(exc).__name__}: {exc}") + log(traceback.format_exc()) + lines.append("VERDICT: FAIL") + +unreal.EditorLoadingAndSavingUtils.save_dirty_packages(True, True) +with open(RESULT, "w") as f: + f.write("\n".join(lines) + "\n") diff --git a/Tools/vehicle_build.py b/Tools/vehicle_build.py index 7a7c33d..d17997c 100644 --- a/Tools/vehicle_build.py +++ b/Tools/vehicle_build.py @@ -191,16 +191,55 @@ def main(argv=None) -> int: wheels = build_wheels(mats) hulls = build_collision(mats) - # Join the visible panels into one export mesh; wheels stay separate objects so the - # vehicle setup can reference them. - bpy.ops.object.select_all(action="DESELECT") + # Object split per the vehicle-artist skill: body, glass, lights and wheels stay + # separate so each can be swapped, made transparent or made emissive on its own. A + # single merged mesh is exactly what the skill forbids for a game vehicle. + systems = {} for part in body: - part.select_set(True) - bpy.context.view_layer.objects.active = body[0] - bpy.ops.object.join() - shell = bpy.context.active_object - shell.name = "SM_Vehicle_Sedan" + name = part.name + if name.startswith("Glass_"): + key = "glass" + elif name.startswith("Lamp_"): + key = "light_front" + elif name.startswith("Lamp_Rear") or name.startswith("Lamp_Rear_"): + key = "light_rear" + elif name.startswith("Mirror_"): + key = "mirrors" + else: + key = "body" + systems.setdefault(key, []).append(part) + # Lamps were named lamp_front/lamp_rear by prefix; separate them properly. + rear = [p for p in systems.pop("light_front", []) if "Rear" in p.name] + front = [p for p in systems.get("body", []) if False] + # Collect every lamp again from the body list by name. + lamps = [p for p in body if p.name.startswith("Lamp_")] + systems["light_front"] = [p for p in lamps if "Rear" not in p.name] + systems["light_rear"] = [p for p in lamps if "Rear" in p.name] + systems["body"] = [p for p in body if not p.name.startswith( + ("Glass_", "Lamp_", "Mirror_"))] + + joined = {} + for key, parts in systems.items(): + if not parts: + continue + bpy.ops.object.select_all(action="DESELECT") + for part in parts: + part.select_set(True) + bpy.context.view_layer.objects.active = parts[0] + if len(parts) > 1: + bpy.ops.object.join() + obj = bpy.context.active_object + obj.name = { + "body": "SM_Vehicle_Body", + "glass": "SM_Vehicle_Glass", + "light_front": "SM_Vehicle_Light_Front", + "light_rear": "SM_Vehicle_Light_Rear", + "mirrors": "SM_Vehicle_Mirrors", + }[key] + joined[key] = obj + shell = joined["body"] hull_join = hulls + log("systems: " + ", ".join(f"{k}={len(v)} parts" for k, v in systems.items())) os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True) bpy.ops.wm.save_as_mainfile(filepath=os.path.abspath(args.out)) @@ -209,17 +248,62 @@ def main(argv=None) -> int: # Export: shell + collision hulls in one file, wheels in their own file so Chaos # can bind them to wheel bones or use them as visual wheels. os.makedirs(os.path.dirname(os.path.abspath(args.fbx)), exist_ok=True) + exp_dir = os.path.dirname(os.path.abspath(args.fbx)) + exported = {} + for key, obj in joined.items(): + bpy.ops.object.select_all(action="DESELECT") + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + path = os.path.join(exp_dir, f"{key}.fbx") + fbx_export_preset.export_static(path, selection=True) + exported[key] = path + # Body carries the collision hulls so the hulls and the shell land in one asset, + # which is how Unreal wires UCX_ meshes to their mesh. bpy.ops.object.select_all(action="DESELECT") shell.select_set(True) for hull in hull_join: hull.select_set(True) bpy.context.view_layer.objects.active = shell - fbx_export_preset.export_static(args.fbx, selection=True) - log(f"exported {args.fbx}") + fbx_export_preset.export_static(os.path.join(exp_dir, "body.fbx"), selection=True) + # One wheel asset, reused four times: Chaos binds the same wheel mesh per bone. + bpy.ops.object.select_all(action="DESELECT") + wheels[0].select_set(True) + bpy.context.view_layer.objects.active = wheels[0] + fbx_export_preset.export_static(os.path.join(exp_dir, "wheel.fbx"), selection=True) + exported["wheel"] = os.path.join(exp_dir, "wheel.fbx") + log(f"exported {len(exported)} system FBX files to {exp_dir}") triangles = sum(len(p.vertices) - 2 for p in shell.data.polygons) + per_system = {key: sum(len(p.vertices) - 2 for p in obj.data.polygons) + for key, obj in joined.items()} for hull in hull_join: triangles += sum(len(p.vertices) - 2 for p in hull.data.polygons) + # LODs per the vehicle-artist targets: LOD1 drops mirrors and lamp detail, LOD2 is a + # body-plus-glass shell. They are separate objects here and get wired into the + # body's LOD group at import time. + lod_paths = {} + for index, keep_glass, ratio in ((1, True, 0.55), (2, False, 0.25)): + lod_key = f"body_lod{index}" + bpy.ops.object.select_all(action="DESELECT") + shell.select_set(True) + if keep_glass and "glass" in joined: + joined["glass"].select_set(True) + bpy.context.view_layer.objects.active = shell + bpy.ops.object.duplicate() + lod = bpy.context.active_object + lod.name = f"SM_Vehicle_Body_LOD{index}" + # Decimate for real: the first version duplicated the shell unchanged, so both + # LODs came out at 84 triangles - an LOD that saves nothing. + decimate = lod.modifiers.new(name="LOD", type="DECIMATE") + decimate.ratio = ratio + bpy.ops.object.modifier_apply(modifier=decimate.name) + lod_path = os.path.join(exp_dir, f"{lod_key}.fbx") + fbx_export_preset.export_static(lod_path, selection=True) + lod_paths[lod_key] = lod_path + lod.hide_set(False) + log(f"{lod_key}: exported, {sum(len(p.vertices) - 2 for p in lod.data.polygons)} triangles") + bpy.data.objects.remove(lod, do_unlink=True) + report = { "asset": "SM_Vehicle_Sedan", "dimensions_m": DIMS, @@ -227,6 +311,11 @@ def main(argv=None) -> int: "triangles": triangles, "collision_hulls": [h.name for h in hull_join], "wheels": [w.name for w in wheels], + "systems": {key: {"object": obj.name, "triangles": per_system.get(key, 0), + "fbx": exported.get(key)} + for key, obj in joined.items()}, + "wheel_triangles": sum(len(p.vertices) - 2 for p in wheels[0].data.polygons), + "lods": lod_paths, "log": PARTS, } if args.report: