Wheel axles were vertical in Unreal (the FBX armature conversion rolled them 90 deg about X). Fix: axis_forward:X in the character preset and a Blender-Z wheel tail so the axle lands on Unreal Y. This is correct (reference pose FL roll 180 / FR roll 0) but does not fix the reverse: -drivetest still -2737 cm/s, and FinalRatio sign is a no-op. The remaining cause is the Chaos wheel spin-axis convention or engine torque sign.
109 lines
4.5 KiB
Python
109 lines
4.5 KiB
Python
#!/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"},
|
||
axis_forward="X",
|
||
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)
|