advanced example
This commit is contained in:
1 parent
8c0a639fba
commit
6fe57915e8
99 files changed
+1329
-56
No files matched your search
@@ -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)
|
||||
Reference in new issue
Block a user