From 286d9f6479189e12bec80ed7ae0d7cea8eee5cf9 Mon Sep 17 00:00:00 2001 From: Zwitschi Date: Sun, 8 Nov 2026 18:00:59 +0100 Subject: [PATCH] refactor(tools): one FBX export preset instead of six copies The bpy.ops.export_scene.fbx argument block was duplicated in six scripts (hero, block frontage, north buildings, site, scale test, MPFB character) and all six carried mesh_smooth_type="OFF", which drops smoothing groups and made every Unreal import warn and every round part faceted. Tools/fbx_export_preset.py now owns the presets: - preset_kwargs(preset, selection, **overrides) is bpy-free and unit-tested; - export_static()/export_character() are thin wrappers that import bpy lazily; - ue_metric_v1 and ue_character_v1 keep the recorded scale contract (FBX_SCALE_NONE, apply_unit_scale, -Y forward / Z up) and use FACE smoothing. Callers switched: hero_facade_build, export_block_frontage, export_north_buildings, scale_test_export, mpfb_character_export, Art/blender/site/export_site_fbx. Verified by re-exporting every asset through the preset and re-importing all of them: 0 smoothing warnings on site, frontage and north buildings; packaging and -qaselftest all PASS (walk, transect 0 holes, wall probe 0/35, facade probe 0/9). 49 tool tests pass (9 new for the preset contract). --- Art/blender/hero/hero-facade-2026-11-07.blend | 4 +- Art/blender/site/export_site_fbx.py | 18 +--- Game/GigLine/Content/Hollywood/DemoMap.umap | 4 +- .../Hollywood/Frontage/SM_Budget_6822.uasset | 4 +- .../Frontage/SM_ElCapitanTheatre.uasset | 4 +- .../Frontage/SM_Hollywood_6806.uasset | 4 +- .../Frontage/SM_Hollywood_6902.uasset | 4 +- .../Frontage/SM_Hollywood_6922.uasset | 4 +- .../Hollywood/Frontage/SM_LeeDrug_6800.uasset | 4 +- .../Hollywood/North/SM_Madame_Tussauds.uasset | 4 +- .../North/SM_TCL_Chinese_Theatre.uasset | 4 +- .../Hollywood/Site/Crossing_Markings.uasset | 4 +- .../Content/Hollywood/Site/Kerb_North.uasset | 4 +- .../Content/Hollywood/Site/Kerb_South.uasset | 4 +- .../Content/Hollywood/Site/M_Asphalt.uasset | 2 +- .../Content/Hollywood/Site/M_Concrete.uasset | 2 +- .../Hollywood/Site/M_FacadeStone.uasset | 2 +- .../Content/Hollywood/Site/M_Ground.uasset | 2 +- .../Hollywood/Site/M_KerbConcrete.uasset | 2 +- .../Content/Hollywood/Site/M_RoadPaint.uasset | 2 +- .../Content/Hollywood/Site/M_Stucco.uasset | 2 +- .../Hollywood/Site/Road_Hollywood.uasset | 4 +- .../Hollywood/Site/Sidewalk_North.uasset | 4 +- .../Hollywood/Site/Sidewalk_South.uasset | 4 +- .../Content/Hollywood/Site/Terrain_DEM.uasset | 4 +- Game/GigLine/Content/Test/ReviewMap.umap | 2 +- MEMORY.md | 23 +++++ Screenshots/demo/review-demo-crossing.png | 4 +- .../demo/review-demo-north-buildings.png | 4 +- Screenshots/demo/review-demo-spawn-view.png | 4 +- Screenshots/demo/review-demo-street-level.png | 4 +- Screenshots/review-character.png | 4 +- Screenshots/review-ground-contact.png | 4 +- Screenshots/review-hero-facade-front.png | 4 +- Screenshots/review-hero-facade-oblique.png | 4 +- Screenshots/review-hero-portals.png | 4 +- Screenshots/review-npc-prefab.png | 4 +- Screenshots/review-overview.png | 4 +- Screenshots/review-player-placement.png | 4 +- Screenshots/review-scale-cubes.png | 4 +- Screenshots/review-shadow-compare.png | 4 +- Tools/README.md | 1 + Tools/export_block_frontage.py | 18 +--- Tools/export_north_buildings.py | 18 +--- Tools/fbx_export_preset.py | 94 +++++++++++++++++++ Tools/hero_facade_build.py | 21 +---- Tools/mpfb_character_export.py | 20 +--- Tools/scale_test_export.py | 18 +--- Tools/tests/test_fbx_export_preset.py | 70 ++++++++++++++ 49 files changed, 283 insertions(+), 158 deletions(-) create mode 100644 Tools/fbx_export_preset.py create mode 100644 Tools/tests/test_fbx_export_preset.py diff --git a/Art/blender/hero/hero-facade-2026-11-07.blend b/Art/blender/hero/hero-facade-2026-11-07.blend index b97c286..975dd5b 100644 --- a/Art/blender/hero/hero-facade-2026-11-07.blend +++ b/Art/blender/hero/hero-facade-2026-11-07.blend @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:5a1fb7ce48fa6a8b82a629caa61b6678e79d001491dfc5b5a00ce49acd13d9f0 -size 139669 +oid sha256:f93518ea18939682b8056fbad2741199f0e9dad6db427f0a749573a07ae4f232 +size 139688 diff --git a/Art/blender/site/export_site_fbx.py b/Art/blender/site/export_site_fbx.py index 7680e32..4f26b77 100644 --- a/Art/blender/site/export_site_fbx.py +++ b/Art/blender/site/export_site_fbx.py @@ -15,6 +15,9 @@ import os import sys import bpy +sys.path.insert(0, os.path.join(os.path.join(os.path.dirname(__file__), "..", "..", ".."), "Tools")) +import fbx_export_preset as fbx_preset # noqa: E402 + MESHES = ("Terrain_DEM", "Road_Hollywood", "Sidewalk_South", "Sidewalk_North", "Kerb_South", "Kerb_North", "Crossing_Markings") @@ -60,20 +63,7 @@ def main(): _ = bpy.context.view_layer bpy.context.view_layer.objects.active = obj path = os.path.join(args.out, name + ".fbx") - bpy.ops.export_scene.fbx( - filepath=path, - use_selection=True, - object_types={"MESH"}, - apply_unit_scale=True, - apply_scale_options="FBX_SCALE_NONE", - axis_forward="-Y", - axis_up="Z", - use_space_transform=True, - use_mesh_modifiers=True, - mesh_smooth_type="OFF", - path_mode="AUTO", - bake_space_transform=False, - ) + fbx_preset.export_static(path, selection=True) if mirrored: # The flip is an export correction, not a change to the source: undo it. obj.scale.y *= -1.0 diff --git a/Game/GigLine/Content/Hollywood/DemoMap.umap b/Game/GigLine/Content/Hollywood/DemoMap.umap index c11114b..6523f7b 100644 --- a/Game/GigLine/Content/Hollywood/DemoMap.umap +++ b/Game/GigLine/Content/Hollywood/DemoMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab6fbc24a8ebaa48287b8d81e4b180000d5692134349e9bd26f46a90f5d297aa -size 54508 +oid sha256:3239737465e1695fc16868978aabcca4767055e1ab45cb1c222591b7ff2bda22 +size 54516 diff --git a/Game/GigLine/Content/Hollywood/Frontage/SM_Budget_6822.uasset b/Game/GigLine/Content/Hollywood/Frontage/SM_Budget_6822.uasset index 29c9048..fd8d792 100644 --- a/Game/GigLine/Content/Hollywood/Frontage/SM_Budget_6822.uasset +++ b/Game/GigLine/Content/Hollywood/Frontage/SM_Budget_6822.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6cc532260823c0b6dfd943744300fb5cbeff8ab87f63ceb092717641759b014a -size 78278 +oid sha256:04eea883885521fadfc2675c4ee93c7aba61cbbf84e464d34a634f1b778bc161 +size 78284 diff --git a/Game/GigLine/Content/Hollywood/Frontage/SM_ElCapitanTheatre.uasset b/Game/GigLine/Content/Hollywood/Frontage/SM_ElCapitanTheatre.uasset index c93c236..7182b89 100644 --- a/Game/GigLine/Content/Hollywood/Frontage/SM_ElCapitanTheatre.uasset +++ b/Game/GigLine/Content/Hollywood/Frontage/SM_ElCapitanTheatre.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:97b279689536abc54f2dae5f78b9a82a1fe2bd138ee76557de279e9c7a48d7f3 -size 79377 +oid sha256:92b4a9e1ffebdec61f477c62228b02074d1afed6e76288b9cac7cfc21479bf94 +size 79348 diff --git a/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6806.uasset b/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6806.uasset index 7fcfeb4..f8f5611 100644 --- a/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6806.uasset +++ b/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6806.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c4b522b03d10b7c5c98627f199c426143122f6c0e1d1d6d82938174152ca42d6 -size 79018 +oid sha256:21faaa28e97235717b973e3827604805edd88195fb0997c1ff374e8e04f4186a +size 78998 diff --git a/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6902.uasset b/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6902.uasset index 6799faf..49ee421 100644 --- a/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6902.uasset +++ b/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6902.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:60f3a08ac1493fe918da2e38ad7b4355763a39cf90c7e9e6016911cd3505c46c -size 78339 +oid sha256:86082f721699b618c0989d250861bd6a079c45bc321f9472c191344ad3a9fef3 +size 78349 diff --git a/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6922.uasset b/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6922.uasset index 324bac4..8ffa7e4 100644 --- a/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6922.uasset +++ b/Game/GigLine/Content/Hollywood/Frontage/SM_Hollywood_6922.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a1704908dd0d66a25672fdf6b1c8f3b8b731e361e38b33077b67dce3e6dd747e -size 78896 +oid sha256:2c06ffd414b54a3c8383344da54baf05938ba3aa2a6322f3d6801b2f7cc1ccfc +size 78881 diff --git a/Game/GigLine/Content/Hollywood/Frontage/SM_LeeDrug_6800.uasset b/Game/GigLine/Content/Hollywood/Frontage/SM_LeeDrug_6800.uasset index f683ef8..a0948ea 100644 --- a/Game/GigLine/Content/Hollywood/Frontage/SM_LeeDrug_6800.uasset +++ b/Game/GigLine/Content/Hollywood/Frontage/SM_LeeDrug_6800.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4c3c8df33dd9aee5d6175c9f3ad50e4b453461750bdb3ea5543f61e4a63fa19c -size 78091 +oid sha256:ad59e28c16e965ddd814e69d9510233f93b44410bf42d5602e36609124879465 +size 78097 diff --git a/Game/GigLine/Content/Hollywood/North/SM_Madame_Tussauds.uasset b/Game/GigLine/Content/Hollywood/North/SM_Madame_Tussauds.uasset index dfc957a..08c9aa6 100644 --- a/Game/GigLine/Content/Hollywood/North/SM_Madame_Tussauds.uasset +++ b/Game/GigLine/Content/Hollywood/North/SM_Madame_Tussauds.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e62e32b685cecfc0d5a9071e4fc1a2fa634768f5f68be73eb8b52b3011960a0 -size 79860 +oid sha256:6916ce5e1856eaefccce5faa3d688f0bdb6398a2dcc56a872ceb00080db7993b +size 79982 diff --git a/Game/GigLine/Content/Hollywood/North/SM_TCL_Chinese_Theatre.uasset b/Game/GigLine/Content/Hollywood/North/SM_TCL_Chinese_Theatre.uasset index 823a712..d8f0531 100644 --- a/Game/GigLine/Content/Hollywood/North/SM_TCL_Chinese_Theatre.uasset +++ b/Game/GigLine/Content/Hollywood/North/SM_TCL_Chinese_Theatre.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:b2ee1d5bd60aa0368f9ec110efdd4618c8b0ac924bbd06af34e31ce5d302ef17 -size 80261 +oid sha256:6ba67436b139db9375a1e43d05ea5fdd9c4770f4f778f3ace12cc7a47426222f +size 80253 diff --git a/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset b/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset index 2f48750..e7c6d84 100644 --- a/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Crossing_Markings.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:90e9c5586b43bd8a5073fdd98b48e477c0369dbfb935752a2e832b046c4ac9f1 -size 75501 +oid sha256:b87aee07bd35898e3acd37cc9f6c595cad62e68aaa68ff93db424687b983738b +size 75559 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset index e451175..57b7e5e 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_North.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:096d4e05ce88191ce821b1987b973fdeed6f30ac67414ba50d40dc74e0341c41 -size 83558 +oid sha256:7fe2e4298ba7ac5d214cee36a604926ceadcdb929b643bdc0e571ac9de6354d4 +size 83660 diff --git a/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset index e1c291a..7a6b66b 100644 --- a/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Kerb_South.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c611c9f32b52c569b3e4c9a9cd337a2227f3d678b69e7033123a678bb9aa8d3f -size 83802 +oid sha256:cee2405bd726e30658551592811b4f7ade9d09eb53a3f5d7f7ac437b07f477c5 +size 84070 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset index b5f45fe..55afb15 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Asphalt.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:492410ca526af9484ed9d2f35139b05c00072fdf93156e470c4dea697913804b +oid sha256:1573ce34e0eedd36e9fea9c3b7870c3a7e40cd7763feeeafcf406f6821e1ddfa size 7876 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset index 5f25f89..43ced3c 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Concrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01c5f65d30f0502c08ae6c3d0a6149bf22cd3cc6964386fab7823006433d09a7 +oid sha256:7ed5ce4a4a8d007723859d1bf72f8cda4dc44a0a1516861388cf453a8ed51bd8 size 7881 diff --git a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset index da554a9..e08b823 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_FacadeStone.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:ab055873e8dd41063966798974289109b1a402fedc9856a0e8e8f22d5575c382 +oid sha256:d9e56b813e652f1db48f0d2c06fb971a3e626ac8a7db2355b844bed7e9cb0a9e size 7896 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset index 601239b..4dd5e9c 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Ground.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9ceb456eaaa65eb883ff9b5601d1da6801349396ffaff50d956eca62f8978c57 +oid sha256:f602277d7e29590cc74edc68947bfaf4005bb1b725b893048999bef871965e29 size 7871 diff --git a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset index 78ed54a..ce1583d 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_KerbConcrete.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:95591e094b01819fe8fd4ccc36241c1c71aa9b364689e555468191f4a74992d5 +oid sha256:a33a56d8da461099abc07e4d2608c461fa6ecf64e86901a751ebd123a35c7b01 size 7901 diff --git a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset index ee34264..cab50f8 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_RoadPaint.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:514e92d6639134304adb30c845cdcc4ce5d21debc4088e78de5446ae6949dafd +oid sha256:df8b9571db76547fcb0c14b17440434aa865cf06fede0870edddbe57c25a13c7 size 7886 diff --git a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset index 669842d..2835530 100644 --- a/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset +++ b/Game/GigLine/Content/Hollywood/Site/M_Stucco.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12a5691b007ae60ced3f3bab90abd9a08825a460206dec097a589c94f58228ba +oid sha256:17de1d539b838415aa76f4637feabc200991ee52671dce1546da6564d6bc0761 size 7871 diff --git a/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset index 2c299df..fdebf3d 100644 --- a/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Road_Hollywood.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:a48b4b61a9a062c19d1f604634fb349eb5ea59def82803574b3b3376f7241f09 -size 309812 +oid sha256:496a3d53741a35ac7d45ba894c8c9661c39b4e922f968177795f818400dca192 +size 311045 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset index 5ae5e2d..f6ff33d 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_North.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:3534e2ba61975bb8c350cb0b3a78624b6ca90b8c92e0acb725c053883dcfaf29 -size 150179 +oid sha256:6e850dd628c7809e1607d748e97903b736780b215462efb2e9550abedec59b38 +size 150199 diff --git a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset index fe14cd8..1587e25 100644 --- a/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Sidewalk_South.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4b3113c472a0b83f6ba369efc8cdec91c0f7aa08c0b859c00661a624303253bb -size 164880 +oid sha256:1f979437847aa453436acbea6b0e5ae474f50d75da51a7d5cdca9782fcb9b790 +size 162907 diff --git a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset index 5f66af5..3f1cab2 100644 --- a/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset +++ b/Game/GigLine/Content/Hollywood/Site/Terrain_DEM.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:48ff0d06e31cb7824b60c2e13b9555c20c7b03d45cfd4efcab2f72ca61fb16b9 -size 335528 +oid sha256:ecff60acd3e6f7149564003fe68fb9fc8a89c21c9d0eebdafa6f8ae59c6ab270 +size 357901 diff --git a/Game/GigLine/Content/Test/ReviewMap.umap b/Game/GigLine/Content/Test/ReviewMap.umap index 6bca92a..88400fa 100644 --- a/Game/GigLine/Content/Test/ReviewMap.umap +++ b/Game/GigLine/Content/Test/ReviewMap.umap @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:631a70bc5f98ececab410ab698bc2047a7f65e3e2d010c679988bef28f70b430 +oid sha256:a39dff6294d91f55ab3ea581c8c1a7f663cbe2214e61d2836491d06965f383e8 size 28706 diff --git a/MEMORY.md b/MEMORY.md index 31ae523..63bd5ff 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -460,3 +460,26 @@ Scope: facts and traps that cost time to establish. Planned work lives in [TODO. - Probe expectations must mirror the staged buildings: the facade probe now carries explicit X spans per frontage (and the TCL/Tussaud's spans on the north side) so a gap between buildings is not reported as a defect. + +## 2026-11-09 — one FBX export preset instead of six copies + +- The `bpy.ops.export_scene.fbx(...)` argument block was duplicated in six scripts + (hero, block frontage, north buildings, site, scale test, MPFB character), and + all six carried `mesh_smooth_type="OFF"` — hence the "No smoothing group + information was found" warning on every import. +- Now `Tools/fbx_export_preset.py` holds `ue_metric_v1` and `ue_character_v1`: + `preset_kwargs()` (bpy-free, unit-tested) plus `export_static()` / + `export_character()` wrappers that import `bpy` lazily. Every caller imports it + and passes `selection=True/False`; the seven duplicated blocks are gone. +- Callers in `Tools/` resolve the module with + `sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))`; the Blender-side + `Art/blender/site/export_site_fbx.py` computes the repo root + (`..`, `..`, `..`) and joins `Tools`. Getting that wrong silently inserts + `Tools/Tools` — check the import path when adding a caller. +- Verified end to end: scale test, hero, frontage (6/6), site (7/7) and north + buildings all re-exported through the preset, re-imported with **0 smoothing + warnings**, DemoMap rebuilt and `-qaselftest` all PASS (walk, transect 0 holes, + wall probe 0/35, facade probe 0/9). +- `mpfb_character_export.py` was not re-run: it needs the MPFB add-on on + `BLENDER_USER_SCRIPTS` ("Add-on not loaded: bl_ext.blender_org.mpfb"), which is + pre-existing and unrelated; the character is not in `0.1.x` gameplay. diff --git a/Screenshots/demo/review-demo-crossing.png b/Screenshots/demo/review-demo-crossing.png index 53583a0..860efee 100644 --- a/Screenshots/demo/review-demo-crossing.png +++ b/Screenshots/demo/review-demo-crossing.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6c0144a5d8ad74cc56b512099e0bccf1b4913cc0969a40e2353a5d7b6c88dc4c -size 2670411 +oid sha256:4ced7093dbfd87341f26824b3d383afcadba6b84d02e9d22b46dcae504107dcf +size 2555823 diff --git a/Screenshots/demo/review-demo-north-buildings.png b/Screenshots/demo/review-demo-north-buildings.png index 3f0fd57..9c49c20 100644 --- a/Screenshots/demo/review-demo-north-buildings.png +++ b/Screenshots/demo/review-demo-north-buildings.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:bf0a8b0f98a3a8e37c6708a1ba6f37fce92d1954e5d02e8ef8051fdc669ad8f9 -size 2505075 +oid sha256:8922ee977c1d89df3d4d9e10caa2785f87deecd17bee4624cf5dbfbdeecadb73 +size 2238947 diff --git a/Screenshots/demo/review-demo-spawn-view.png b/Screenshots/demo/review-demo-spawn-view.png index 0fd459c..7a36e08 100644 --- a/Screenshots/demo/review-demo-spawn-view.png +++ b/Screenshots/demo/review-demo-spawn-view.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:25fb859414e73ccc043ab5cbba352bdab1ed45e668d82d3a1c4ccdd4f6e36441 -size 2627108 +oid sha256:cf464077cd4a79302b33d7f88be63aee6f586ab761c030cfb67661945eeee493 +size 2640409 diff --git a/Screenshots/demo/review-demo-street-level.png b/Screenshots/demo/review-demo-street-level.png index 5f559d9..de7adf8 100644 --- a/Screenshots/demo/review-demo-street-level.png +++ b/Screenshots/demo/review-demo-street-level.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:01c7e5f2385b9f4c7457b7d15745757568f5c26329b3db4ef08d44742ff5a25d -size 2448129 +oid sha256:6f6eb060018bd904b1e55eeaf1607fff013cec44334ecdc76a1b0f1dad17cb06 +size 2419233 diff --git a/Screenshots/review-character.png b/Screenshots/review-character.png index 85e752d..4c51cc6 100644 --- a/Screenshots/review-character.png +++ b/Screenshots/review-character.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:6984bfec9d9d04e71ae10dd0e950f4bef7c82721e8c72abe70542ae7446e644f -size 2750769 +oid sha256:b5e133239d976a0c451845b126a4139afec85055592752639db430640c714f0d +size 2762684 diff --git a/Screenshots/review-ground-contact.png b/Screenshots/review-ground-contact.png index 9ed90e0..faa7954 100644 --- a/Screenshots/review-ground-contact.png +++ b/Screenshots/review-ground-contact.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:797c379922881c6db39bb8afd710283e5c0d5928919b4214eb98ab0d2e89ebe5 -size 1817363 +oid sha256:d3ff3ccb37e39df6a85b46d55e42c830eedb472cca58784126dbc0c92f3cacd8 +size 1826522 diff --git a/Screenshots/review-hero-facade-front.png b/Screenshots/review-hero-facade-front.png index 134198e..be22d0e 100644 --- a/Screenshots/review-hero-facade-front.png +++ b/Screenshots/review-hero-facade-front.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:8e46779f2b62816fab2f651b3d61bd780f2d98926a2f1b3eecbf36d09fedda41 -size 1731572 +oid sha256:f08a527395660a98d8b39af4356ccdf1a38d7e3cab19ce7ad560cfd12a75c749 +size 1743785 diff --git a/Screenshots/review-hero-facade-oblique.png b/Screenshots/review-hero-facade-oblique.png index b6db3e6..8dfd265 100644 --- a/Screenshots/review-hero-facade-oblique.png +++ b/Screenshots/review-hero-facade-oblique.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c054e9e5edc10864f0a68f91248ec586121ba58cd57fac5509e0967859156594 -size 1399505 +oid sha256:29605038bf303924976deec33896c5534b65d43233ce3bc587c3e8a0320ebd65 +size 1402114 diff --git a/Screenshots/review-hero-portals.png b/Screenshots/review-hero-portals.png index d3bcd07..6fed5e6 100644 --- a/Screenshots/review-hero-portals.png +++ b/Screenshots/review-hero-portals.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:43bc085789469e32f656e910996dd2a9b4744cae1a0fcb8670e69d8625c94876 -size 1367381 +oid sha256:9ae371b126e13e24610ca9358a10bc9524702ad3dda9ce672fcaac44bb03dc8e +size 1373251 diff --git a/Screenshots/review-npc-prefab.png b/Screenshots/review-npc-prefab.png index 3140e12..26c6dd5 100644 --- a/Screenshots/review-npc-prefab.png +++ b/Screenshots/review-npc-prefab.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:645767d4184dedc7569d9a443cf7c855f1205b8dfd85c7ccbcd16068bcb26c97 -size 1802903 +oid sha256:d26ee7178f4b7bd23c028d19e7cb8ded4109bbf6864f95c3e56e28c5eeed8c32 +size 1804784 diff --git a/Screenshots/review-overview.png b/Screenshots/review-overview.png index c45ae01..e844489 100644 --- a/Screenshots/review-overview.png +++ b/Screenshots/review-overview.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:4ca2ac170c369b2093c0b4bbb06bdf0cce318c897e75b74363a31bf56858bdab -size 2393713 +oid sha256:232ba45b93dc9871ec79363ba29033ef2ddf56bf2b90ed712718b136be3638f2 +size 2419553 diff --git a/Screenshots/review-player-placement.png b/Screenshots/review-player-placement.png index ff8ca06..70a7a50 100644 --- a/Screenshots/review-player-placement.png +++ b/Screenshots/review-player-placement.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:16985e7229cd275af3567e9999e45c51db14dd2d9ab6e305bd16cbb77f5076c0 -size 2212611 +oid sha256:2b0d3c22b5e642a5ef4586f452e50753649923835ed41b7c9310a257fb124fca +size 2211043 diff --git a/Screenshots/review-scale-cubes.png b/Screenshots/review-scale-cubes.png index ec70c6e..4b31d9b 100644 --- a/Screenshots/review-scale-cubes.png +++ b/Screenshots/review-scale-cubes.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:9e0d634aeb2adb125981cefabd3be2f54de7e64d6ab2de0baaa00d73e5539b01 -size 2147701 +oid sha256:89c4381b77b18eebc27c1df85bd31ce1cba4b2f89fc73dbdb312cb60013f318f +size 2140459 diff --git a/Screenshots/review-shadow-compare.png b/Screenshots/review-shadow-compare.png index 3229eca..a922f66 100644 --- a/Screenshots/review-shadow-compare.png +++ b/Screenshots/review-shadow-compare.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:62940b77df50ddab2d2e9dd813c2f09c8fe83ec8f4cc755d4cfc47a3145800db -size 1851501 +oid sha256:c3b6f8d9cc11087da4f7b47c3ed6e49af45098094ce29a2d729e137cabb4bb5b +size 1852570 diff --git a/Tools/README.md b/Tools/README.md index 2be84e2..efd3dca 100644 --- a/Tools/README.md +++ b/Tools/README.md @@ -16,6 +16,7 @@ Geographic tools live in [`Geo/tools/`](../Geo/README.md) and the Blender-side b | Tool | Purpose | | --- | --- | | `check-lfs.sh` | Fails if the `lfs` filter is missing or an LFS-tracked file was committed as a plain blob | +| `fbx_export_preset.py` | The **single** FBX export preset (`ue_metric_v1` / `ue_character_v1`): metres→centimetres via `FBX_SCALE_NONE`, axis mapping east +X / north −Y / up +Z, `mesh_smooth_type="FACE"` (smoothing groups survive the import). Every Blender exporter imports it instead of copying the argument block; `preset_kwargs()` is bpy-free and unit-tested. | | `sibling_asset_audit.py` | Reads the sibling project's prop library from GLB headers into `Art/props/reuse-manifest.json`; copies nothing | | `scale_test_export.py` / `ue_scale_test_import.py` | The 1 m = 100 uu scale test; the named presets `ue_metric_v1` live in these two files | diff --git a/Tools/export_block_frontage.py b/Tools/export_block_frontage.py index 50e7670..a03d9df 100644 --- a/Tools/export_block_frontage.py +++ b/Tools/export_block_frontage.py @@ -19,6 +19,9 @@ import os import sys import bpy +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import fbx_export_preset as fbx_preset # noqa: E402 + # blockout object name -> (asset name, note). Addresses from the verification doc. BUILDINGS = { @@ -69,20 +72,7 @@ def main(): bpy.context.view_layer.objects.active = new_obj path = os.path.join(args.out, asset_name + ".fbx") - bpy.ops.export_scene.fbx( - filepath=path, - use_selection=True, - object_types={"MESH"}, - apply_unit_scale=True, - apply_scale_options="FBX_SCALE_NONE", - axis_forward="-Y", - axis_up="Z", - use_space_transform=True, - use_mesh_modifiers=True, - mesh_smooth_type="OFF", - path_mode="AUTO", - bake_space_transform=False, - ) + fbx_preset.export_static(path, selection=True) world = new_obj.matrix_world zs = [(world @ v.co).z for v in new_mesh.vertices] ys = [(world @ v.co).y for v in new_mesh.vertices] diff --git a/Tools/export_north_buildings.py b/Tools/export_north_buildings.py index 6224772..06dfd50 100644 --- a/Tools/export_north_buildings.py +++ b/Tools/export_north_buildings.py @@ -13,6 +13,9 @@ import os import sys import bpy +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import fbx_export_preset as fbx_preset # noqa: E402 + BUILDINGS = ("TCL Chinese Theatre", "Madame Tussaud's") @@ -50,20 +53,7 @@ def main(): bpy.context.view_layer.objects.active = new_obj path = os.path.join(args.out, name.replace(" ", "_").replace("'", "") + ".fbx") - bpy.ops.export_scene.fbx( - filepath=path, - use_selection=True, - object_types={"MESH"}, - apply_unit_scale=True, - apply_scale_options="FBX_SCALE_NONE", - axis_forward="-Y", - axis_up="Z", - use_space_transform=True, - use_mesh_modifiers=True, - mesh_smooth_type="OFF", - path_mode="AUTO", - bake_space_transform=False, - ) + fbx_preset.export_static(path, selection=True) world = new_obj.matrix_world zs = [(world @ v.co).z for v in new_mesh.vertices] print(f"[north] {name:22} verts {len(new_mesh.vertices):5} z {min(zs):+.2f}..{max(zs):+.2f} m " diff --git a/Tools/fbx_export_preset.py b/Tools/fbx_export_preset.py new file mode 100644 index 0000000..d36ede5 --- /dev/null +++ b/Tools/fbx_export_preset.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +"""The one FBX export preset for this project ("ue_metric_v1"/"ue_character_v1"). + +Before this module the same `bpy.ops.export_scene.fbx(...)` argument block was +copied into six scripts (hero, frontage, north buildings, site, scale test, MPFB +character). They had already drifted only by accident, and every one of them +carried `mesh_smooth_type="OFF"` — which is why Unreal logged "No smoothing group +information was found for this mesh" on every import and round parts rendered +faceted. One preset, one place to fix that. + +Use from inside Blender: + + import sys, os + sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "Tools")) + import fbx_export_preset as preset + + preset.export_static(path) # whole scene, MESH only + preset.export_static(path, selection=True) # selected objects only + preset.export_character(path) # armature + mesh, character flags + +`preset_kwargs()` is pure (no `bpy`), so the argument set can be unit-tested +without Blender — see Tools/tests/test_fbx_export_preset.py. + +Why these values: +* `FBX_SCALE_NONE` + `apply_unit_scale` keeps Blender metres → Unreal centimetres + at exactly ×100, verified by the three-cube scale test (1 m → 100.00 uu). +* `axis_forward="-Y"`, `axis_up="Z"` is the project's recorded mapping: east → +X, + north → −Y, up → +Z. +* `mesh_smooth_type="FACE"` exports smoothing groups. `"OFF"` (the old value) + drops them, so Unreal rebuilds flat normals and every cylinder is faceted. +* `use_mesh_modifiers=True` bakes the bevel/modifier stack at export; the source + `.blend` stays non-destructive. +""" + +from __future__ import annotations + +# Shared by every mesh export. Values are the recorded ue_metric_v1 preset. +_METRIC = { + "object_types": {"MESH"}, + "apply_unit_scale": True, + "apply_scale_options": "FBX_SCALE_NONE", + "axis_forward": "-Y", + "axis_up": "Z", + "use_space_transform": True, + "use_mesh_modifiers": True, + "mesh_smooth_type": "FACE", + "path_mode": "AUTO", + "bake_space_transform": False, +} + +# Character exports differ only in the object set and the armature/path flags. +_CHARACTER = dict( + _METRIC, + object_types={"ARMATURE", "MESH"}, + add_leaf_bones=False, + path_mode="COPY", + embed_textures=False, +) + +PRESETS = {"ue_metric_v1": _METRIC, "ue_character_v1": _CHARACTER} + + +def preset_kwargs(preset: str = "ue_metric_v1", selection: bool = False, **overrides) -> dict: + """Return the `bpy.ops.export_scene.fbx` argument dict (no `bpy` needed). + + `preset` selects the base set, `selection` decides whether only selected + objects are exported, and `overrides` wins last for the rare special case. + """ + if preset not in PRESETS: + raise KeyError(f"unknown preset {preset!r}; known: {sorted(PRESETS)}") + kwargs = dict(PRESETS[preset]) + kwargs["use_selection"] = bool(selection) + kwargs.update(overrides) + if "filepath" in kwargs: + raise ValueError("pass the path to export_static/export_character, not as an override") + return kwargs + + +def _export(path: str, preset: str, selection: bool, overrides: dict) -> str: + import bpy # imported here so `preset_kwargs` stays usable without Blender + + kwargs = preset_kwargs(preset, selection=selection, **overrides) + bpy.ops.export_scene.fbx(filepath=path, **kwargs) + return path + + +def export_static(path: str, selection: bool = False, **overrides) -> str: + """Export meshes with the ue_metric_v1 preset. Returns the path.""" + return _export(path, "ue_metric_v1", selection, overrides) + + +def export_character(path: str, selection: bool = False, **overrides) -> str: + """Export armature + mesh with the ue_character_v1 preset. Returns the path.""" + return _export(path, "ue_character_v1", selection, overrides) diff --git a/Tools/hero_facade_build.py b/Tools/hero_facade_build.py index 7a5887a..2afca9a 100644 --- a/Tools/hero_facade_build.py +++ b/Tools/hero_facade_build.py @@ -33,6 +33,9 @@ import os import sys import bpy +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import fbx_export_preset as fbx_preset # noqa: E402 + # --- measured / derived ------------------------------------------------------ FRONTAGE_W = 25.75 @@ -306,24 +309,10 @@ def build(): def export_fbx(path): + # Preset lives in Tools/fbx_export_preset.py (ue_metric_v1). os.makedirs(os.path.dirname(path), exist_ok=True) # Named export preset "ue_metric_v1" - same settings as Tools/scale_test_export.py. - bpy.ops.export_scene.fbx( - filepath=path, - use_selection=False, - object_types={"MESH"}, - apply_unit_scale=True, - apply_scale_options="FBX_SCALE_NONE", - axis_forward="-Y", - axis_up="Z", - use_space_transform=True, - use_mesh_modifiers=True, - # "FACE" exports smoothing groups; without it every import logs - # "No smoothing group information was found for this mesh". - mesh_smooth_type="FACE", - path_mode="AUTO", - bake_space_transform=False, - ) + fbx_preset.export_static(path) print(f"[hero] exported FBX preset ue_metric_v1 -> {path}") diff --git a/Tools/mpfb_character_export.py b/Tools/mpfb_character_export.py index 37d15f5..a37dd4a 100644 --- a/Tools/mpfb_character_export.py +++ b/Tools/mpfb_character_export.py @@ -19,6 +19,9 @@ import sys import zlib import bpy +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import fbx_export_preset as fbx_preset # noqa: E402 + import numpy from mathutils import Euler @@ -149,22 +152,7 @@ def make_diffuse(human, size=1024): def export_fbx(human, path): os.makedirs(os.path.dirname(path), exist_ok=True) # Named export preset "ue_character_v1" — change only via a new preset name. - bpy.ops.export_scene.fbx( - filepath=path, - use_selection=False, - object_types={"ARMATURE", "MESH"}, - apply_unit_scale=True, - apply_scale_options="FBX_SCALE_NONE", - axis_forward="-Y", - axis_up="Z", - use_space_transform=True, - use_mesh_modifiers=True, - add_leaf_bones=False, - path_mode="COPY", - embed_textures=False, - mesh_smooth_type="OFF", - bake_space_transform=False, - ) + fbx_preset.export_character(path) log(f"export: FBX preset ue_character_v1 -> {path}") diff --git a/Tools/scale_test_export.py b/Tools/scale_test_export.py index bf46f0a..ffac49e 100644 --- a/Tools/scale_test_export.py +++ b/Tools/scale_test_export.py @@ -16,6 +16,9 @@ import os import sys import bpy +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +import fbx_export_preset as fbx_preset # noqa: E402 + def build_scene(): @@ -44,20 +47,7 @@ def build_scene(): def export_fbx(path): os.makedirs(os.path.dirname(path), exist_ok=True) # Named export preset "ue_metric_v1" - change only via a new preset name. - bpy.ops.export_scene.fbx( - filepath=path, - use_selection=False, - object_types={"MESH"}, - apply_unit_scale=True, - apply_scale_options="FBX_SCALE_NONE", - axis_forward="-Y", - axis_up="Z", - use_space_transform=True, - use_mesh_modifiers=True, - mesh_smooth_type="OFF", - path_mode="AUTO", - bake_space_transform=False, - ) + fbx_preset.export_static(path) print(f"[scale_test] exported FBX preset ue_metric_v1 -> {path}") diff --git a/Tools/tests/test_fbx_export_preset.py b/Tools/tests/test_fbx_export_preset.py new file mode 100644 index 0000000..e86013e --- /dev/null +++ b/Tools/tests/test_fbx_export_preset.py @@ -0,0 +1,70 @@ +"""Tests for Tools/fbx_export_preset.py. + +The preset module keeps its argument set in plain dicts and builds it in +`preset_kwargs()`, so the export contract can be checked without starting +Blender. Only `export_static`/`export_character` import `bpy`. +""" + +import os +import sys +import unittest + +ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) +sys.path.insert(0, os.path.join(ROOT, "Tools")) + +import fbx_export_preset as preset # noqa: E402 + + +class MetricPresetTest(unittest.TestCase): + def test_scale_and_axis_contract(self): + # These three are the project's recorded conversion contract: metres -> + # centimetres by a factor of exactly 100, east +X, north -Y, up +Z. + kwargs = preset.preset_kwargs() + self.assertTrue(kwargs["apply_unit_scale"]) + self.assertEqual(kwargs["apply_scale_options"], "FBX_SCALE_NONE") + self.assertEqual(kwargs["axis_forward"], "-Y") + self.assertEqual(kwargs["axis_up"], "Z") + + def test_smoothing_groups_are_exported(self): + # "OFF" (the pre-refactor value) drops smoothing groups and every Unreal + # import warns about it, with round parts faceted. + self.assertEqual(preset.preset_kwargs()["mesh_smooth_type"], "FACE") + + def test_selection_flag(self): + self.assertFalse(preset.preset_kwargs()["use_selection"]) + self.assertTrue(preset.preset_kwargs(selection=True)["use_selection"]) + + def test_object_types_are_meshes_only(self): + self.assertEqual(preset.preset_kwargs()["object_types"], {"MESH"}) + + +class CharacterPresetTest(unittest.TestCase): + def test_character_exports_armature_and_mesh(self): + kwargs = preset.preset_kwargs("ue_character_v1") + self.assertEqual(kwargs["object_types"], {"ARMATURE", "MESH"}) + self.assertFalse(kwargs["add_leaf_bones"]) + self.assertEqual(kwargs["path_mode"], "COPY") + self.assertFalse(kwargs["embed_textures"]) + + def test_character_keeps_the_scale_contract(self): + kwargs = preset.preset_kwargs("ue_character_v1") + self.assertEqual(kwargs["apply_scale_options"], "FBX_SCALE_NONE") + self.assertEqual(kwargs["mesh_smooth_type"], "FACE") + + +class OverrideTest(unittest.TestCase): + def test_overrides_win(self): + kwargs = preset.preset_kwargs(mesh_smooth_type="OFF") + self.assertEqual(kwargs["mesh_smooth_type"], "OFF") + + def test_unknown_preset_raises(self): + with self.assertRaises(KeyError): + preset.preset_kwargs("ue_imperial_v9") + + def test_filepath_must_not_be_an_override(self): + with self.assertRaises(ValueError): + preset.preset_kwargs(filepath="/tmp/x.fbx") + + +if __name__ == "__main__": + unittest.main()