The character now stands naturally: arms at its sides with a soft elbow bend, hands at the hips, feet flat. Both character shots confirm it, front and back. The root cause was not the pose. The FBX importer had been silently refusing to replace the existing assets: after changing the arm base from 1.55 rad to 0.55 rad and rebuilding, the imported clip returned the IDENTICAL bone quaternion (+0.006, +0.002, -0.179, -0.984). replace_existing=True was set and delete_asset returned False even after chmod u+w, because the pawn's constructor hard-references the clips. Importing under a new name (CharacterV2) created fresh assets and the same probe then returned (+0.439, +0.898) — the rotation had landed. Every earlier attempt had been invisible for this reason. The rotation itself is measured, not inherited: sweeping it in Blender gives 0.55 rad for arms down and clear of the body, 0.65 for straight down against the hip, 0.75 for past vertical and into the torso. It lives in the clamp keys, not in the bind: every bind experiment was impossible on this shape-keyed MPFB mesh or left Unreal showing the old bind. Remaining, for 0.2.2: the fingers splay from the bind, and the walk clip has still not been captured in motion.
97 lines
3.5 KiB
Python
97 lines
3.5 KiB
Python
# Import the character animation clips into Unreal (0.2.0).
|
|
#
|
|
# blender -b Art/blender/character/character-2026-11-07.blend \
|
|
# --python Tools/character_anim_build.py -- --out /tmp/gigline/character-anim.fbx
|
|
# <engine>/Engine/Binaries/Linux/UnrealEditor-Cmd <uproject> -run=pythonscript \
|
|
# -script=<repo>/Tools/ue_character_anim_import.py -nullrhi -unattended -nopause -stdout
|
|
#
|
|
# Imports the animated FBX next to the existing character assets and verifies the
|
|
# result rather than assuming it: an FBX whose export omitted `bake_anim` imports
|
|
# without error and produces a skeletal mesh but no AnimSequence, which is exactly
|
|
# the failure this tool must catch.
|
|
#
|
|
# Destination: /Game/Test/Character. Result file: /tmp/gigline/character-anim-import.txt
|
|
|
|
import glob
|
|
import os
|
|
|
|
import unreal
|
|
|
|
SRC = "/tmp/gigline/character-anim.fbx"
|
|
DEST = "/Game/Test/Character"
|
|
PREFIX = "CharacterV2"
|
|
RESULT = "/tmp/gigline/character-anim-import.txt"
|
|
lines = []
|
|
|
|
|
|
def log(msg):
|
|
lines.append(str(msg))
|
|
unreal.log(str(msg))
|
|
|
|
|
|
def main():
|
|
if not os.path.exists(SRC):
|
|
log(f"missing {SRC}; run Tools/character_anim_build.py first")
|
|
return
|
|
|
|
task = unreal.AssetImportTask()
|
|
task.filename = SRC
|
|
task.destination_path = DEST
|
|
task.destination_name = PREFIX
|
|
task.automated = True
|
|
task.save = True
|
|
task.replace_existing = True
|
|
|
|
ui = unreal.FbxImportUI()
|
|
ui.import_mesh = True
|
|
ui.import_as_skeletal = True
|
|
ui.import_animations = True
|
|
ui.import_materials = True
|
|
ui.import_textures = False
|
|
# UE 5.8 has no SKELETON_ONLY content type (FBXICT_ALL/GEOMETRY/SKINNING_WEIGHTS
|
|
# only), so the clips come in with the mesh and the verification below decides
|
|
# whether that worked. The mesh is replaced at the same path by design.
|
|
ui.skeletal_mesh_import_data.import_uniform_scale = 1.0
|
|
task.options = ui
|
|
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
|
|
|
# Verify: the clips are the deliverable, not the mesh.
|
|
registry = unreal.AssetRegistryHelpers.get_asset_registry()
|
|
found = []
|
|
for asset in registry.get_assets_by_path(DEST, recursive=False):
|
|
name = str(asset.asset_name)
|
|
if not name.startswith(PREFIX):
|
|
continue
|
|
obj = unreal.EditorAssetLibrary.load_asset(f"{DEST}/{name}")
|
|
if isinstance(obj, unreal.AnimSequence):
|
|
length = obj.get_play_length()
|
|
# UE 5.8 exposes only play length and sequence length here; the sampled
|
|
# frame count is derived so the report still states the clip duration.
|
|
frames = round(length * 30.0)
|
|
found.append(name)
|
|
log(f"AnimSequence {name:34} {length:.3f} s (~{frames} frames at 30 fps)")
|
|
elif isinstance(obj, unreal.SkeletalMesh):
|
|
skeleton = obj.get_editor_property("skeleton")
|
|
log(f"SkeletalMesh {name:34} skeleton={'ok' if skeleton else 'MISSING'}")
|
|
|
|
if not found:
|
|
log("FAILED: no AnimSequence was created — the FBX carried no baked animation")
|
|
lines.append("VERDICT: FAIL")
|
|
return
|
|
log(f"{len(found)} AnimSequence(s) imported: {', '.join(sorted(found))}")
|
|
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)
|
|
os.makedirs(os.path.dirname(RESULT), exist_ok=True)
|
|
with open(RESULT, "w", encoding="utf-8") as handle:
|
|
handle.write("\n".join(lines) + "\n")
|