Files
gig-line/Tools/blender/fbx_export_preset.py
zwitschi 4d27109668 refactor(tools): group the sixty scripts by what they talk to
One directory with sixty scripts stopped being navigable, and the grouping is also the sandbox boundary:

  Tools/unreal/    commandlets and the two map builders, with ue_common.py and ue_placement.py as the
                   shared imports (they need the engine)
  Tools/blender/   builders, exporters, the FBX preset, the MPFB character pipeline, blender_screenshots
                   (they need the Blender Flatpak)
  Tools/review/    the visual loop: render_screenshots.sh, judge_screenshot.py, hero_compare.py,
                   build_comparison_md.py, e2e_asset_check.py, hero_iterate.sh, visual_loop.py,
                   visual_targets.json
  Tools/gen/       image and texture generation: the ComfyUI driver, deterministic texture writers,
                   reference fetching
  Tools/input/     OS-level input injection
  root             _common.py, check-lfs.sh, addon_compat_probe.py, sibling_asset_audit.py, tests/

Every moved script now resolves the repository root as ../.. and puts the tools root plus its siblings on
sys.path, so 'import ue_common' still works from unreal/. The test suite bootstrap does the same for the five
folders. render_screenshots.sh takes the folder as a third argument to run_cmdlet, and the e2e driver,
hero_iterate.sh and the comparison generator were repointed.

51 README rows now link to their new location, the README gains a Layout section naming each folder's
sandbox, and the architecture notes and Blender README follow the moves.

Verified: 108 tests pass, prettier is clean, judge_screenshot.py and hero_compare.py run from their new
homes, and the e2e run was aborted mid-way (it had been rebuilding an empty DemoMap for 15 minutes) and is
being rerun against the new layout.
2026-11-10 12:40:30 +01:00

108 lines
4.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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.
#
# Animation export (added for the 0.2.0 animation set): without `bake_anim` the
# exporter silently writes a static bind pose even when the armature carries
# actions, and Unreal then imports a character with no AnimSequence and no error.
# `bake_anim_use_all_actions` exports every action as its own clip; NLA strips are
# off because the project does not author NLA. `bake_anim_simplify_factor=0.0`
# keeps the sampled keys exactly as authored - the clips are short, so the size
# cost is irrelevant and a simplified curve would drift from the source poses.
_CHARACTER = dict(
_METRIC,
object_types={"ARMATURE", "MESH"},
add_leaf_bones=False,
path_mode="COPY",
embed_textures=False,
bake_anim=True,
bake_anim_use_all_actions=True,
bake_anim_use_nla_strips=False,
bake_anim_force_startend_keying=True,
bake_anim_simplify_factor=0.0,
)
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)