advanced example

This commit is contained in:
Vysyr committed 2026-10-31 20:16:32 +01:00
1 parent 8c0a639fba
commit 6fe57915e8
99 files changed
+1329 -56

No files matched your search

+134
View File
@@ -0,0 +1,134 @@
"""FBX skeletal meshes and animation clips, including exports from Blender."""
from dataclasses import dataclass
from pathlib import Path
import unreal
from .paths import GENERATED, PROJECT, slug
from .characters import _clips
@dataclass
class SkeletalImport:
mesh: unreal.SkeletalMesh
animations: dict[str, unreal.AnimSequence]
def _source(filename):
source = Path(filename).expanduser()
source = (source if source.is_absolute() else PROJECT / source).resolve()
if not source.is_file() or source.suffix.lower() != ".fbx":
raise ValueError(f"Wymagany istniejacy FBX ze szkieletem: {source}")
return source
def _cached(destination, name, skeleton=None):
asset = unreal.load_asset(destination + "/" + name)
expected = unreal.SkeletalMesh if skeleton is None else unreal.AnimSequence
if not isinstance(asset, expected):
return None
objects = [asset]
if skeleton is None:
for path in unreal.EditorAssetLibrary.list_assets(destination, recursive=False, include_folder=False):
clip = unreal.load_asset(path)
if isinstance(clip, unreal.AnimSequence):
objects.append(clip)
elif asset.get_editor_property("skeleton") != skeleton:
return None
return objects
def _import(source, destination, name, skeleton=None, reimport=False):
if not reimport:
objects = _cached(destination, name, skeleton)
if objects is not None:
return objects
options = unreal.FbxImportUI()
options.set_editor_property("automated_import_should_detect_type", False)
options.set_editor_property("import_as_skeletal", True)
options.set_editor_property("import_mesh", skeleton is None)
options.set_editor_property("import_animations", True)
options.set_editor_property("create_physics_asset", False)
options.set_editor_property("import_materials", skeleton is None)
options.set_editor_property("import_textures", skeleton is None)
options.set_editor_property("override_animation_name", name + "_Anim" if skeleton is None else name)
options.set_editor_property("mesh_type_to_import", unreal.FBXImportType.FBXIT_SKELETAL_MESH
if skeleton is None else unreal.FBXImportType.FBXIT_ANIMATION)
if skeleton is not None:
options.set_editor_property("skeleton", skeleton)
options.anim_sequence_import_data.set_editor_property(
"animation_length", unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME)
options.anim_sequence_import_data.set_editor_property("snap_to_closest_frame_boundary", True)
task = unreal.AssetImportTask()
for key, value in {"filename": str(source), "destination_path": destination,
"destination_name": name, "automated": True, "replace_existing": True,
"replace_existing_settings": True, "save": True, "async_": False,
"options": options, "factory": unreal.FbxFactory()}.items():
task.set_editor_property(key, value)
# UE 5.7 otherwise routes FBX reimports through Interchange, which does not
# consistently honor FbxImportUI (notably the target animation skeleton).
flag = "Interchange.FeatureFlags.Import.FBX"
previous = unreal.SystemLibrary.get_console_variable_int_value(flag)
unreal.SystemLibrary.execute_console_command(None, flag + " 0")
try:
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
objects = list(task.get_objects())
# Saving the mesh alone can leave its new skeleton/materials unsaved.
unreal.EditorAssetLibrary.save_directory(destination, only_if_is_dirty=True, recursive=True)
# Legacy FBX reports the primary mesh only; embedded animation assets
# are registered alongside it rather than included in task.get_objects().
paths = {obj.get_path_name() for obj in objects}
embedded = (unreal.EditorAssetLibrary.list_assets(destination, recursive=False, include_folder=False)
if skeleton is None else [])
for path in embedded:
obj = unreal.load_asset(path)
if isinstance(obj, unreal.AnimSequence) and obj.get_path_name() not in paths:
objects.append(obj)
return objects
finally:
unreal.SystemLibrary.execute_console_command(None, f"{flag} {previous}")
def import_animation(filename, skeleton, name="Clip", destination=None, reimport=False):
"""Import a clip for this exact skeleton; no automatic retargeting."""
source = _source(filename)
name = slug(name)
objects = _import(source, destination or f"{GENERATED}/Skeletal/{name}/Animations", name, skeleton,
reimport=reimport)
clips = [obj for obj in objects if isinstance(obj, unreal.AnimSequence)]
if len(clips) != 1:
raise RuntimeError(f"{source.name}: oczekiwano jednej animacji, otrzymano {len(clips)}.")
if clips[0].get_editor_property("skeleton") != skeleton:
raise ValueError("Animacja ma inny szkielet. Uzyj animacji dla tej samej postaci z Mixamo.")
return clips[0]
def import_model(filename, name="Character", animations=None, collection="Character", reimport=False):
"""Import an FBX/.blend and discover animation FBXs beside the source model.
animations=None loads all neighboring clips using names derived from filenames.
Pass a dictionary to select files explicitly; {} skips neighboring clips.
"""
source = Path(filename).expanduser()
source = (source if source.is_absolute() else PROJECT / source).resolve()
if animations is None:
animations = _clips(source)
name = slug(name)
destination = f"{GENERATED}/Skeletal/{name}"
objects = None if reimport else _cached(destination, name)
if objects is None:
if source.suffix.lower() == ".blend":
from .blender import export
source = export(source, name=name, collection=collection, skeletal=True)
source = _source(source)
objects = _import(source, destination, name, reimport=True)
meshes = [obj for obj in objects if isinstance(obj, unreal.SkeletalMesh)]
if len(meshes) != 1:
raise RuntimeError(f"{source.name}: oczekiwano jednego SkeletalMesh, otrzymano {len(meshes)}. "
"Eksportuj jedna postac ze szkieletem i wagami (Mixamo: With Skin).")
clips = {obj.get_name(): obj for obj in objects if isinstance(obj, unreal.AnimSequence)}
skeleton = meshes[0].get_editor_property("skeleton")
for alias, filename in (animations or {}).items():
clips[alias] = import_animation(filename, skeleton, name=slug(alias),
destination=destination + "/Animations", reimport=reimport)
return SkeletalImport(meshes[0], clips)