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