From db23c995e23c2ac14f39b39fa265873996ff1305 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 13 Oct 2021 17:29:31 +0100 Subject: [PATCH] Layout now includes animations --- .../hosts/blender/plugins/load/load_rig.py | 2 + .../blender/plugins/publish/extract_layout.py | 122 +++++++++++- .../hosts/unreal/plugins/load/load_layout.py | 185 +++++++++++++++--- 3 files changed, 271 insertions(+), 38 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 2d974a3205..4e92704703 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -110,6 +110,8 @@ class BlendRigLoader(plugin.AssetLoader): plugin.prepare_data(local_obj.data, group_name) if action is not None: + if local_obj.animation_data is None: + local_obj.animation_data_create() local_obj.animation_data.action = action elif (local_obj.animation_data and local_obj.animation_data.action is not None): diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index f54215800f..54656dc7fd 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -2,9 +2,12 @@ import os import json import bpy +import bpy_extras +import bpy_extras.anim_utils from avalon import io from avalon.blender.pipeline import AVALON_PROPERTY +from openpype.hosts.blender.api import plugin import openpype.api @@ -16,6 +19,99 @@ class ExtractLayout(openpype.api.Extractor): families = ["layout"] optional = True + def _export_animation(self, asset, instance, stagingdir): + file_names = [] + + for obj in asset.children: + if obj.type != "ARMATURE": + continue + + asset_group_name = asset.name + asset.name = asset.get(AVALON_PROPERTY).get("asset_name") + + armature_name = obj.name + original_name = armature_name.split(':')[1] + obj.name = original_name + + object_action_pairs = [] + original_actions = [] + + starting_frames = [] + ending_frames = [] + + # For each armature, we make a copy of the current action + curr_action = None + copy_action = None + + if obj.animation_data and obj.animation_data.action: + curr_action = obj.animation_data.action + copy_action = curr_action.copy() + + curr_frame_range = curr_action.frame_range + + starting_frames.append(curr_frame_range[0]) + ending_frames.append(curr_frame_range[1]) + else: + self.log.info("Object have no animation.") + continue + + object_action_pairs.append((obj, copy_action)) + original_actions.append(curr_action) + + # We compute the starting and ending frames + max_frame = min(starting_frames) + min_frame = max(ending_frames) + + # We bake the copy of the current action for each object + bpy_extras.anim_utils.bake_action_objects( + object_action_pairs, + frames=range(int(min_frame), int(max_frame)), + do_object=False, + do_clean=False + ) + + for o in bpy.data.objects: + o.select_set(False) + + asset.select_set(True) + obj.select_set(True) + fbx_filename = f"{instance.name}_{asset_group_name}.fbx" + filepath = os.path.join(stagingdir, fbx_filename) + + override = plugin.create_blender_context( + active=asset, selected=[asset, obj]) + bpy.ops.export_scene.fbx( + override, + filepath=filepath, + use_active_collection=False, + use_selection=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + add_leaf_bones=False, + armature_nodetype='ROOT', + object_types={'EMPTY', 'ARMATURE'} + ) + obj.name = armature_name + asset.name = asset_group_name + asset.select_set(False) + obj.select_set(False) + + # We delete the baked action and set the original one back + for i in range(0, len(object_action_pairs)): + pair = object_action_pairs[i] + action = original_actions[i] + + if action: + pair[0].animation_data.action = action + + if pair[1]: + pair[1].user_clear() + bpy.data.actions.remove(pair[1]) + + file_names.append(fbx_filename) + + return file_names + def process(self, instance): # Define extract output file path stagingdir = self.staging_dir(instance) @@ -23,7 +119,11 @@ class ExtractLayout(openpype.api.Extractor): # Perform extraction self.log.info("Performing extraction..") + if "representations" not in instance.data: + instance.data["representations"] = [] + json_data = [] + fbx_files = [] asset_group = bpy.data.objects[str(instance)] @@ -78,22 +178,32 @@ class ExtractLayout(openpype.api.Extractor): } json_data.append(json_element) + # Extract the animation as well + if family == "rig": + fbx_files.extend( + self._export_animation( + asset, instance, stagingdir)) + json_filename = "{}.json".format(instance.name) json_path = os.path.join(stagingdir, json_filename) with open(json_path, "w+") as file: json.dump(json_data, fp=file, indent=2) - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { + json_representation = { 'name': 'json', 'ext': 'json', 'files': json_filename, "stagingDir": stagingdir, } - instance.data["representations"].append(representation) + fbx_representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': fbx_files, + "stagingDir": stagingdir, + } + instance.data["representations"].append(json_representation) + instance.data["representations"].append(fbx_representation) self.log.info("Extracted instance '%s' to: %s", - instance.name, representation) + instance.name, json_representation) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index e51e760776..77691949bf 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -1,9 +1,12 @@ import os import json +from pathlib import Path import unreal from unreal import EditorAssetLibrary from unreal import EditorLevelLibrary +from unreal import AssetToolsHelpers +from unreal import FBXImportType from unreal import MathLibrary as umath from avalon import api, pipeline @@ -58,6 +61,8 @@ class LayoutLoader(api.Loader): def _process_family(self, assets, classname, transform): ar = unreal.AssetRegistryHelpers.get_asset_registry() + actors = [] + for asset in assets: obj = ar.get_asset_by_object_path(asset).get_asset() if obj.get_class().get_name() == classname: @@ -75,7 +80,91 @@ class LayoutLoader(api.Loader): ), False) actor.set_actor_scale3d(transform.get('scale')) + actors.append(actor) + + return actors + + def _import_animation( + self, asset_dir, path, rig_count, instance_name, skeleton, + actors_dict): + anim_path = f"{asset_dir}/animations/{path.with_suffix('').name}_{rig_count:02d}" + + # Import animation + task = unreal.AssetImportTask() + task.options = unreal.FbxImportUI() + + task.set_editor_property( + 'filename', str(path.with_suffix(f".{rig_count:02d}.fbx"))) + task.set_editor_property('destination_path', anim_path) + task.set_editor_property( + 'destination_name', f"{instance_name}_animation") + task.set_editor_property('replace_existing', False) + task.set_editor_property('automated', True) + task.set_editor_property('save', False) + + # set import options here + task.options.set_editor_property( + 'automated_import_should_detect_type', False) + task.options.set_editor_property( + 'original_import_type', FBXImportType.FBXIT_SKELETAL_MESH) + task.options.set_editor_property( + 'mesh_type_to_import', FBXImportType.FBXIT_ANIMATION) + task.options.set_editor_property('import_mesh', False) + task.options.set_editor_property('import_animations', True) + task.options.set_editor_property('override_full_name', True) + task.options.set_editor_property('skeleton', skeleton) + + task.options.anim_sequence_import_data.set_editor_property( + 'animation_length', + unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME + ) + task.options.anim_sequence_import_data.set_editor_property( + 'import_meshes_in_bone_hierarchy', False) + task.options.anim_sequence_import_data.set_editor_property( + 'use_default_sample_rate', True) + task.options.anim_sequence_import_data.set_editor_property( + 'import_custom_attribute', True) + task.options.anim_sequence_import_data.set_editor_property( + 'import_bone_tracks', True) + task.options.anim_sequence_import_data.set_editor_property( + 'remove_redundant_keys', True) + task.options.anim_sequence_import_data.set_editor_property( + 'convert_scene', True) + + AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) + + asset_content = unreal.EditorAssetLibrary.list_assets( + anim_path, recursive=False, include_folder=False + ) + + animation = None + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a) + imported_asset = unreal.AssetRegistryHelpers.get_asset( + imported_asset_data) + if imported_asset.__class__ == unreal.AnimSequence: + animation = imported_asset + break + + if animation: + actor = None + if actors_dict.get(instance_name): + for a in actors_dict.get(instance_name): + if a.get_class().get_name() == 'SkeletalMeshActor': + actor = a + break + + animation.set_editor_property('enable_root_motion', True) + actor.skeletal_mesh_component.set_editor_property( + 'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE) + actor.skeletal_mesh_component.animation_data.set_editor_property( + 'anim_to_play', animation) + def _process(self, libpath, asset_dir, loaded=None): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + with open(libpath, "r") as fp: data = json.load(fp) @@ -84,46 +173,78 @@ class LayoutLoader(api.Loader): if not loaded: loaded = [] + path = Path(libpath) + rig_count = 0 + + skeleton_dict = {} + actors_dict = {} + for element in data: reference = element.get('reference_fbx') - - if reference in loaded: - continue - - loaded.append(reference) - - family = element.get('family') - loaders = api.loaders_from_representation( - all_loaders, reference) - loader = self._get_loader(loaders, family) - - if not loader: - continue - instance_name = element.get('instance_name') - options = { - "asset_dir": asset_dir - } + skeleton = None - assets = api.load( - loader, - reference, - namespace=instance_name, - options=options - ) + if reference not in loaded: + loaded.append(reference) - instances = [ - item for item in data - if item.get('reference_fbx') == reference] + family = element.get('family') + loaders = api.loaders_from_representation( + all_loaders, reference) + loader = self._get_loader(loaders, family) - for instance in instances: - transform = instance.get('transform') + if not loader: + continue - if family == 'model': - self._process_family(assets, 'StaticMesh', transform) - elif family == 'rig': - self._process_family(assets, 'SkeletalMesh', transform) + options = { + "asset_dir": asset_dir + } + + assets = api.load( + loader, + reference, + namespace=instance_name, + options=options + ) + + instances = [ + item for item in data + if item.get('reference_fbx') == reference] + + for instance in instances: + transform = instance.get('transform') + inst = instance.get('instance_name') + + actors = [] + + if family == 'model': + actors = self._process_family( + assets, 'StaticMesh', transform) + elif family == 'rig': + actors = self._process_family( + assets, 'SkeletalMesh', transform) + actors_dict[inst] = actors + + if family == 'rig': + # Finds skeleton among the imported assets + for asset in assets: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() == 'Skeleton': + skeleton = obj + if skeleton: + break + + if skeleton: + skeleton_dict[reference] = skeleton + else: + skeleton = skeleton_dict.get(reference) + + if skeleton: + self._import_animation( + asset_dir, path, rig_count, instance_name, skeleton, + actors_dict) + + rig_count += 1 def _remove_family(self, assets, components, classname, propname): ar = unreal.AssetRegistryHelpers.get_asset_registry()