Files
gig-line/Tools/ue_character_anim_import.py
T
zwitschi 54c579ab1a feat(tex): ComfyUI tool, procedural character textures, hair cap behind a flag
Tools/comfyui_generate.py drives the local ComfyUI instance for generated textures
and writes a sidecar with model, prompt, seed and workflow, so a generated asset can
be regenerated or rejected instead of being an unexplained PNG. Models list and
generation both verified against 127.0.0.1:8188.

Stable Diffusion is not usable for flat skin swatches, and that is worth recording:
three attempts produced an anatomical close-up twice and a leather-scale surface once.
Skin detail is generated procedurally instead - tileable octave noise for pores, plus
a strand sheet for hair - in Tools/generate_character_textures.py, with sidecars.

Hair geometry exists (cap conforming to the head, weighted to the head bone, second
material slot in Blender) but Unreal's import collapses the second material slot, so
the cap renders with the skin material and reads as a bald skull. It is behind
--hair and off; the next step is a separate skeletal mesh attached as a second
component.
2026-11-09 13:09:39 +01:00

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 = "CharacterV6"
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")