#!/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)