From 657d107e3b850dc1f98cca10a9c2383f0b2ce4e0 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 16:05:20 +0100 Subject: [PATCH 01/20] Implemented JSON layout loading for Unreal --- .../blender/plugins/publish/extract_fbx.py | 9 +- .../blender/plugins/publish/extract_layout.py | 9 + .../hosts/unreal/plugins/load/load_layout.py | 328 ++++++++++++++++++ .../hosts/unreal/plugins/load/load_rig.py | 4 +- .../unreal/plugins/load/load_staticmeshfbx.py | 4 +- 5 files changed, 350 insertions(+), 4 deletions(-) create mode 100644 openpype/hosts/unreal/plugins/load/load_layout.py diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index b91f2a75ef..0d2b5c1000 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -1,11 +1,11 @@ import os +import bpy + from openpype import api from openpype.hosts.blender.api import plugin from avalon.blender.pipeline import AVALON_PROPERTY -import bpy - class ExtractFBX(api.Extractor): """Extract as FBX.""" @@ -50,6 +50,9 @@ class ExtractFBX(api.Extractor): new_materials.append(mat) new_materials_objs.append(obj) + scale_length = bpy.context.scene.unit_settings.scale_length + bpy.context.scene.unit_settings.scale_length = 0.01 + # We export the fbx bpy.ops.export_scene.fbx( context, @@ -60,6 +63,8 @@ class ExtractFBX(api.Extractor): add_leaf_bones=False ) + bpy.context.scene.unit_settings.scale_length = scale_length + bpy.ops.object.select_all(action='DESELECT') for mat in new_materials: diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index cd081b4479..f54215800f 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -42,9 +42,18 @@ class ExtractLayout(openpype.api.Extractor): }, projection={"_id": True}) blend_id = blend["_id"] + fbx = io.find_one( + { + "type": "representation", + "parent": io.ObjectId(parent), + "name": "fbx" + }, + projection={"_id": True}) + fbx_id = fbx["_id"] json_element = {} json_element["reference"] = str(blend_id) + json_element["reference_fbx"] = str(fbx_id) json_element["family"] = family json_element["instance_name"] = asset.name json_element["asset_name"] = metadata["asset_name"] diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py new file mode 100644 index 0000000000..0419fb3759 --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -0,0 +1,328 @@ +import os +import json + +import unreal +from unreal import EditorAssetLibrary +from unreal import EditorLevelLibrary +from unreal import MathLibrary as umath + +from avalon import api, pipeline +from avalon.unreal import lib +from avalon.unreal import pipeline as unreal_pipeline + + +class LayoutLoader(api.Loader): + """Load Layout from a JSON file""" + + families = ["layout"] + representations = ["json"] + + label = "Load Layout" + icon = "code-fork" + color = "orange" + + def _get_asset_containers(self, path): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset_content = EditorAssetLibrary.list_assets( + path, recursive=True) + + asset_containers = [] + + # Get all the asset containers + for a in asset_content: + obj = ar.get_asset_by_object_path(a) + if obj.get_asset().get_class().get_name() == 'AssetContainer': + asset_containers.append(obj) + + return asset_containers + + def _get_loader(self, loaders, family): + name = "" + if family == 'rig': + name = "SkeletalMeshFBXLoader" + elif family == 'model': + name = "StaticMeshFBXLoader" + elif family == 'camera': + name = "CameraLoader" + + if name == "": + return None + + for loader in loaders: + if loader.__name__ == name: + return loader + + return None + + def _process_family(self, assets, classname, transform): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + for asset in assets: + obj = ar.get_asset_by_object_path(asset).get_asset() + if obj.get_class().get_name() == classname: + actor = EditorLevelLibrary.spawn_actor_from_object( + obj, + transform.get('translation') + ) + actor.set_actor_rotation(unreal.Rotator( + umath.radians_to_degrees( + transform.get('rotation').get('x')), + -umath.radians_to_degrees( + transform.get('rotation').get('y')), + umath.radians_to_degrees( + transform.get('rotation').get('z')), + ), False) + actor.set_actor_scale3d(transform.get('scale')) + + def _process(self, libpath, asset_dir, loaded = []): + with open(libpath, "r") as fp: + data = json.load(fp) + + all_loaders = api.discover(api.Loader) + + # loaded = [] + + 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 + } + + 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') + + if family == 'model': + self._process_family(assets, 'StaticMesh', transform) + elif family == 'rig': + self._process_family(assets, 'SkeletalMesh', transform) + + def _remove_family(self, assets, components, classname, propname): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + objects = [] + for a in assets: + obj = ar.get_asset_by_object_path(a) + if obj.get_asset().get_class().get_name() == classname: + objects.append(obj) + for obj in objects: + for comp in components: + if comp.get_editor_property(propname) == obj.get_asset(): + comp.get_owner().destroy_actor() + + def _remove_actors(self, path): + asset_containers = self._get_asset_containers(path) + + # Get all the static and skeletal meshes components in the level + components = EditorLevelLibrary.get_all_level_actors_components() + static_meshes_comp = [ + c for c in components + if c.get_class().get_name() == 'StaticMeshComponent'] + skel_meshes_comp = [ + c for c in components + if c.get_class().get_name() == 'SkeletalMeshComponent'] + + # For all the asset containers, get the static and skeletal meshes. + # Then, check the components in the level and destroy the matching + # actors. + for asset_container in asset_containers: + package_path = asset_container.get_editor_property('package_path') + family = EditorAssetLibrary.get_metadata_tag( + asset_container.get_asset(), 'family') + assets = EditorAssetLibrary.list_assets( + str(package_path), recursive=False) + if family == 'model': + self._remove_family( + assets, static_meshes_comp, 'StaticMesh', 'static_mesh') + elif family == 'rig': + self._remove_family( + assets, skel_meshes_comp, 'SkeletalMesh', 'skeletal_mesh') + + def load(self, context, name, namespace, options): + """ + Load and containerise representation into Content Browser. + + This is two step process. First, import FBX to temporary path and + then call `containerise()` on it - this moves all content to new + directory and then it will create AssetContainer there and imprint it + with metadata. This will mark this path as container. + + Args: + context (dict): application context + name (str): subset name + namespace (str): in Unreal this is basically path to container. + This is not passed here, so namespace is set + by `containerise()` because only then we know + real path. + data (dict): Those would be data to be imprinted. This is not used + now, data are imprinted by `containerise()`. + + Returns: + list(str): list of container content + """ + # Create directory for asset and avalon container + root = "/Game/Avalon/Assets" + asset = context.get('asset').get('name') + suffix = "_CON" + if asset: + asset_name = "{}_{}".format(asset, name) + else: + asset_name = "{}".format(name) + + tools = unreal.AssetToolsHelpers().get_asset_tools() + asset_dir, container_name = tools.create_unique_asset_name( + "{}/{}/{}".format(root, asset, name), suffix="") + + container_name += suffix + + EditorAssetLibrary.make_directory(asset_dir) + + self._process(self.fname, asset_dir) + + # Create Asset Container + lib.create_avalon_container( + container=container_name, path=asset_dir) + + data = { + "schema": "openpype:container-2.0", + "id": pipeline.AVALON_CONTAINER_ID, + "asset": asset, + "namespace": asset_dir, + "container_name": container_name, + "asset_name": asset_name, + "loader": str(self.__class__.__name__), + "representation": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + asset_content = EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=False) + + for a in asset_content: + EditorAssetLibrary.save_asset(a) + + return asset_content + + def update(self, container, representation): + source_path = api.get_representation_path(representation) + destination_path = container["namespace"] + + self._remove_actors(destination_path) + + with open(source_path, "r") as fp: + data = json.load(fp) + + references = [e.get('reference_fbx') for e in data] + asset_containers = self._get_asset_containers(destination_path) + loaded = [] + + # Delete all the assets imported with the previous version of the + # layout, if they're not in the new layout. + for asset_container in asset_containers: + if asset_container.get_editor_property( + 'asset_name') == container["objectName"]: + continue + ref = EditorAssetLibrary.get_metadata_tag( + asset_container.get_asset(), 'representation') + ppath = asset_container.get_editor_property('package_path') + + if ref not in references: + # If the asset is not in the new layout, delete it. + # Also check if the parent directory is empty, and delete that + # as well, if it is. + EditorAssetLibrary.delete_directory(ppath) + + parent = os.path.dirname(str(ppath)) + parent_content = EditorAssetLibrary.list_assets( + parent, recursive=False, include_folder=True + ) + + if len(parent_content) == 0: + EditorAssetLibrary.delete_directory(parent) + else: + # If the asset is in the new layout, search the instances in + # the JSON file, and create actors for them. + for element in data: + if element.get('reference_fbx') == ref: + if ref not in loaded: + loaded.append(ref) + + assets = EditorAssetLibrary.list_assets( + ppath, recursive=True, include_folder=False) + + transform = element.get('transform') + family = element.get('family') + + if family == 'model': + self._process_family( + assets, 'StaticMesh', transform) + elif family == 'rig': + self._process_family( + assets, 'SkeletalMesh', transform) + + self._process(source_path, destination_path, loaded) + + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = EditorAssetLibrary.list_assets( + destination_path, recursive=True, include_folder=False) + + for a in asset_content: + EditorAssetLibrary.save_asset(a) + + def remove(self, container): + """ + First, destroy all actors of the assets to be removed. Then, deletes + the asset's directory. + """ + path = container["namespace"] + parent_path = os.path.dirname(path) + + self._remove_actors(path) + + EditorAssetLibrary.delete_directory(path) + + asset_content = EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + EditorAssetLibrary.delete_directory(parent_path) diff --git a/openpype/hosts/unreal/plugins/load/load_rig.py b/openpype/hosts/unreal/plugins/load/load_rig.py index 7f6e31618a..c7d095aa21 100644 --- a/openpype/hosts/unreal/plugins/load/load_rig.py +++ b/openpype/hosts/unreal/plugins/load/load_rig.py @@ -15,7 +15,7 @@ class SkeletalMeshFBXLoader(api.Loader): icon = "cube" color = "orange" - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, options): """ Load and containerise representation into Content Browser. @@ -40,6 +40,8 @@ class SkeletalMeshFBXLoader(api.Loader): # Create directory for asset and avalon container root = "/Game/Avalon/Assets" + if options and options.get("asset_dir"): + root = options["asset_dir"] asset = context.get('asset').get('name') suffix = "_CON" if asset: diff --git a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py index d25f84ea69..510c4331ad 100644 --- a/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py +++ b/openpype/hosts/unreal/plugins/load/load_staticmeshfbx.py @@ -40,7 +40,7 @@ class StaticMeshFBXLoader(api.Loader): return task - def load(self, context, name, namespace, data): + def load(self, context, name, namespace, options): """ Load and containerise representation into Content Browser. @@ -65,6 +65,8 @@ class StaticMeshFBXLoader(api.Loader): # Create directory for asset and avalon container root = "/Game/Avalon/Assets" + if options and options.get("asset_dir"): + root = options["asset_dir"] asset = context.get('asset').get('name') suffix = "_CON" if asset: From 6026b4b955e7c53153b570016cae7a6f14db1d8b Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 16:34:23 +0100 Subject: [PATCH 02/20] Fix for minor problem when handling assets in Blender with layout --- openpype/hosts/blender/plugins/load/load_model.py | 7 +++++-- openpype/hosts/blender/plugins/load/load_rig.py | 1 + 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index af5591c299..6097fab7cd 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -81,7 +81,8 @@ class BlendModelLoader(plugin.AssetLoader): plugin.prepare_data(local_obj.data, group_name) for material_slot in local_obj.material_slots: - plugin.prepare_data(material_slot.material, group_name) + if material_slot.material: + plugin.prepare_data(material_slot.material, group_name) if not local_obj.get(AVALON_PROPERTY): local_obj[AVALON_PROPERTY] = dict() @@ -245,7 +246,8 @@ class BlendModelLoader(plugin.AssetLoader): # If it is the last object to use that library, remove it if count == 1: library = bpy.data.libraries.get(bpy.path.basename(group_libpath)) - bpy.data.libraries.remove(library) + if library: + bpy.data.libraries.remove(library) self._process(str(libpath), asset_group, object_name) @@ -253,6 +255,7 @@ class BlendModelLoader(plugin.AssetLoader): metadata["libpath"] = str(libpath) metadata["representation"] = str(representation["_id"]) + metadata["parent"] = str(representation["parent"]) def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 5573c081e1..8c8a2a2324 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -320,6 +320,7 @@ class BlendRigLoader(plugin.AssetLoader): metadata["libpath"] = str(libpath) metadata["representation"] = str(representation["_id"]) + metadata["parent"] = str(representation["parent"]) def exec_remove(self, container: Dict) -> bool: """Remove an existing asset group from a Blender scene. From d63e1caedef3ca48237f768c42b57b07fdb3e8fb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 23 Sep 2021 17:02:49 +0100 Subject: [PATCH 03/20] Hound fixes --- .../hosts/unreal/plugins/load/load_layout.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 0419fb3759..e51e760776 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -75,13 +75,14 @@ class LayoutLoader(api.Loader): ), False) actor.set_actor_scale3d(transform.get('scale')) - def _process(self, libpath, asset_dir, loaded = []): + def _process(self, libpath, asset_dir, loaded=None): with open(libpath, "r") as fp: data = json.load(fp) all_loaders = api.discover(api.Loader) - # loaded = [] + if not loaded: + loaded = [] for element in data: reference = element.get('reference_fbx') @@ -113,7 +114,7 @@ class LayoutLoader(api.Loader): ) instances = [ - item for item in data + item for item in data if item.get('reference_fbx') == reference] for instance in instances: @@ -143,10 +144,10 @@ class LayoutLoader(api.Loader): # Get all the static and skeletal meshes components in the level components = EditorLevelLibrary.get_all_level_actors_components() static_meshes_comp = [ - c for c in components + c for c in components if c.get_class().get_name() == 'StaticMeshComponent'] skel_meshes_comp = [ - c for c in components + c for c in components if c.get_class().get_name() == 'SkeletalMeshComponent'] # For all the asset containers, get the static and skeletal meshes. @@ -246,12 +247,12 @@ class LayoutLoader(api.Loader): asset_containers = self._get_asset_containers(destination_path) loaded = [] - # Delete all the assets imported with the previous version of the + # Delete all the assets imported with the previous version of the # layout, if they're not in the new layout. for asset_container in asset_containers: if asset_container.get_editor_property( - 'asset_name') == container["objectName"]: - continue + 'asset_name') == container["objectName"]: + continue ref = EditorAssetLibrary.get_metadata_tag( asset_container.get_asset(), 'representation') ppath = asset_container.get_editor_property('package_path') @@ -270,7 +271,7 @@ class LayoutLoader(api.Loader): if len(parent_content) == 0: EditorAssetLibrary.delete_directory(parent) else: - # If the asset is in the new layout, search the instances in + # If the asset is in the new layout, search the instances in # the JSON file, and create actors for them. for element in data: if element.get('reference_fbx') == ref: From db23c995e23c2ac14f39b39fa265873996ff1305 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 13 Oct 2021 17:29:31 +0100 Subject: [PATCH 04/20] 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() From de468b34fafcd9455927292494dc4c2cb7edf3dc Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 14 Oct 2021 15:53:46 +0100 Subject: [PATCH 05/20] Fixed problem with object names if export fails --- .../plugins/publish/extract_fbx_animation.py | 14 +++++++------- .../blender/plugins/publish/extract_layout.py | 14 +++++++------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 16443b760c..96fa37c07e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -37,13 +37,6 @@ class ExtractAnimationFBX(api.Extractor): armature = [ obj for obj in asset_group.children if obj.type == 'ARMATURE'][0] - asset_group_name = asset_group.name - asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name") - - armature_name = armature.name - original_name = armature_name.split(':')[1] - armature.name = original_name - object_action_pairs = [] original_actions = [] @@ -66,6 +59,13 @@ class ExtractAnimationFBX(api.Extractor): self.log.info("Object have no animation.") return + asset_group_name = asset_group.name + asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name") + + armature_name = armature.name + original_name = armature_name.split(':')[1] + armature.name = original_name + object_action_pairs.append((armature, copy_action)) original_actions.append(curr_action) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 54656dc7fd..93df5fe01c 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -26,13 +26,6 @@ class ExtractLayout(openpype.api.Extractor): 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 = [] @@ -55,6 +48,13 @@ class ExtractLayout(openpype.api.Extractor): self.log.info("Object have no animation.") 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.append((obj, copy_action)) original_actions.append(curr_action) From e7f6bf03209b6813046bd078fd912581d4c46efb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 14 Oct 2021 15:55:51 +0100 Subject: [PATCH 06/20] Changed how the fbx animations are handled. There was a problem with no animations or if only some of the rigs were animated. Now, the animation file is referenced in the json. --- .../blender/plugins/publish/extract_layout.py | 50 +++++++++++++------ .../hosts/unreal/plugins/load/load_layout.py | 23 +++++---- 2 files changed, 47 insertions(+), 26 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 93df5fe01c..804004bf4e 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -19,8 +19,8 @@ class ExtractLayout(openpype.api.Extractor): families = ["layout"] optional = True - def _export_animation(self, asset, instance, stagingdir): - file_names = [] + def _export_animation(self, asset, instance, stagingdir, fbx_count): + n = fbx_count for obj in asset.children: if obj.type != "ARMATURE": @@ -75,7 +75,7 @@ class ExtractLayout(openpype.api.Extractor): asset.select_set(True) obj.select_set(True) - fbx_filename = f"{instance.name}_{asset_group_name}.fbx" + fbx_filename = f"{n:03d}.fbx" filepath = os.path.join(stagingdir, fbx_filename) override = plugin.create_blender_context( @@ -108,9 +108,9 @@ class ExtractLayout(openpype.api.Extractor): pair[1].user_clear() bpy.data.actions.remove(pair[1]) - file_names.append(fbx_filename) + return fbx_filename, n + 1 - return file_names + return None, n def process(self, instance): # Define extract output file path @@ -127,6 +127,8 @@ class ExtractLayout(openpype.api.Extractor): asset_group = bpy.data.objects[str(instance)] + fbx_count = 0 + for asset in asset_group.children: metadata = asset.get(AVALON_PROPERTY) @@ -176,13 +178,17 @@ class ExtractLayout(openpype.api.Extractor): "z": asset.scale.z } } - json_data.append(json_element) # Extract the animation as well if family == "rig": - fbx_files.extend( - self._export_animation( - asset, instance, stagingdir)) + f, n = self._export_animation( + asset, instance, stagingdir, fbx_count) + if f: + fbx_files.append(f) + json_element["animation"] = f + fbx_count = n + + json_data.append(json_element) json_filename = "{}.json".format(instance.name) json_path = os.path.join(stagingdir, json_filename) @@ -196,14 +202,26 @@ class ExtractLayout(openpype.api.Extractor): 'files': json_filename, "stagingDir": stagingdir, } - 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.debug(fbx_files) + + if len(fbx_files) == 1: + fbx_representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': fbx_files[0], + "stagingDir": stagingdir, + } + instance.data["representations"].append(fbx_representation) + elif len(fbx_files) > 1: + fbx_representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': fbx_files, + "stagingDir": stagingdir, + } + instance.data["representations"].append(fbx_representation) self.log.info("Extracted instance '%s' to: %s", 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 77691949bf..0ef24e42f4 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -85,16 +85,19 @@ class LayoutLoader(api.Loader): 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}" + self, asset_dir, path, instance_name, skeleton, actors_dict, + animation_file): + anim_file = Path(animation_file) + anim_file_name = anim_file.with_suffix('') + + anim_path = f"{asset_dir}/animations/{anim_file_name}" # Import animation task = unreal.AssetImportTask() task.options = unreal.FbxImportUI() task.set_editor_property( - 'filename', str(path.with_suffix(f".{rig_count:02d}.fbx"))) + 'filename', str(path.with_suffix(f".{animation_file}"))) task.set_editor_property('destination_path', anim_path) task.set_editor_property( 'destination_name', f"{instance_name}_animation") @@ -174,7 +177,6 @@ class LayoutLoader(api.Loader): loaded = [] path = Path(libpath) - rig_count = 0 skeleton_dict = {} actors_dict = {} @@ -239,12 +241,12 @@ class LayoutLoader(api.Loader): else: skeleton = skeleton_dict.get(reference) - if skeleton: - self._import_animation( - asset_dir, path, rig_count, instance_name, skeleton, - actors_dict) + animation_file = element.get('animation') - rig_count += 1 + if animation_file and skeleton: + self._import_animation( + asset_dir, path, instance_name, skeleton, + actors_dict, animation_file) def _remove_family(self, assets, components, classname, propname): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -356,6 +358,7 @@ class LayoutLoader(api.Loader): return asset_content def update(self, container, representation): + assert False, "Update not working for now. Delete and reload the layout." source_path = api.get_representation_path(representation) destination_path = container["namespace"] From f810e68cf9295f90908933cfb82e8b4bb7fd5492 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Oct 2021 16:11:45 +0100 Subject: [PATCH 07/20] Blender: increment workfile version for layouts as well --- .../hosts/blender/plugins/publish/increment_workfile_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py index db73842323..32cbcf0834 100644 --- a/openpype/hosts/blender/plugins/publish/increment_workfile_version.py +++ b/openpype/hosts/blender/plugins/publish/increment_workfile_version.py @@ -9,7 +9,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin): label = "Increment Workfile Version" optional = True hosts = ["blender"] - families = ["animation", "model", "rig", "action"] + families = ["animation", "model", "rig", "action", "layout"] def process(self, context): From ef14d4269b8de07d5c4872717f6ca7eefdb24a1f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 18 Oct 2021 16:12:17 +0100 Subject: [PATCH 08/20] Blender: added validation for object mode to layout tasks --- .../hosts/blender/plugins/publish/validate_object_mode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/validate_object_mode.py b/openpype/hosts/blender/plugins/publish/validate_object_mode.py index 1c82628c1c..90ef0b7c41 100644 --- a/openpype/hosts/blender/plugins/publish/validate_object_mode.py +++ b/openpype/hosts/blender/plugins/publish/validate_object_mode.py @@ -5,15 +5,15 @@ import openpype.hosts.blender.api.action class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin): - """Validate that the current object is in Object Mode.""" + """Validate that the objects in the instance are in Object Mode.""" order = pyblish.api.ValidatorOrder - 0.01 hosts = ["blender"] - families = ["model", "rig"] + families = ["model", "rig", "layout"] category = "geometry" - label = "Object is in Object Mode" + label = "Validate Object Mode" actions = [openpype.hosts.blender.api.action.SelectInvalidAction] - optional = True + optional = False @classmethod def get_invalid(cls, instance) -> List: From cb9b11e7260c41d9abca8792f74a6b81027dcc48 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 27 Oct 2021 15:39:56 +0100 Subject: [PATCH 09/20] Implemented update for layouts with animations --- .../hosts/unreal/plugins/load/load_layout.py | 70 +++++++++++++++---- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index 0ef24e42f4..af80a2bae6 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -358,12 +358,18 @@ class LayoutLoader(api.Loader): return asset_content def update(self, container, representation): - assert False, "Update not working for now. Delete and reload the layout." + ar = unreal.AssetRegistryHelpers.get_asset_registry() + source_path = api.get_representation_path(representation) destination_path = container["namespace"] + libpath = Path(api.get_representation_path(representation)) self._remove_actors(destination_path) + # Delete old animations + anim_path = f"{destination_path}/animations/" + EditorAssetLibrary.delete_directory(anim_path) + with open(source_path, "r") as fp: data = json.load(fp) @@ -397,23 +403,63 @@ class LayoutLoader(api.Loader): else: # If the asset is in the new layout, search the instances in # the JSON file, and create actors for them. + + actors_dict = {} + skeleton_dict = {} + for element in data: - if element.get('reference_fbx') == ref: - if ref not in loaded: - loaded.append(ref) + reference = element.get('reference_fbx') + instance_name = element.get('instance_name') + + skeleton = None + + if reference == ref and ref not in loaded: + loaded.append(ref) + + family = element.get('family') assets = EditorAssetLibrary.list_assets( ppath, recursive=True, include_folder=False) - transform = element.get('transform') - family = element.get('family') + instances = [ + item for item in data + if item.get('reference_fbx') == reference] - if family == 'model': - self._process_family( - assets, 'StaticMesh', transform) - elif family == 'rig': - self._process_family( - assets, 'SkeletalMesh', transform) + 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) + + animation_file = element.get('animation') + + if animation_file and skeleton: + self._import_animation( + destination_path, libpath, + instance_name, skeleton, + actors_dict, animation_file) self._process(source_path, destination_path, loaded) From 14e2c07be43ae9a7e9661c868a396c4c087d86f9 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 Nov 2021 10:03:57 +0000 Subject: [PATCH 10/20] Unreal changes the name of the actor to the instance name in the JSON This is needed to keep the actors in Unreal mirrored to the Blender scene --- .../hosts/unreal/plugins/load/load_layout.py | 24 ++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index af80a2bae6..c32c810c36 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -58,7 +58,7 @@ class LayoutLoader(api.Loader): return None - def _process_family(self, assets, classname, transform): + def _process_family(self, assets, classname, transform, inst_name=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() actors = [] @@ -70,6 +70,17 @@ class LayoutLoader(api.Loader): obj, transform.get('translation') ) + if inst_name: + try: + # Rename method leads to crash + # actor.rename(name=inst_name) + + # The label works, although it make it slightly more + # complicated to check for the names, as we need to + # loop through all the actors in the level + actor.set_actor_label(inst_name) + except Exception as e: + print(e) actor.set_actor_rotation(unreal.Rotator( umath.radians_to_degrees( transform.get('rotation').get('x')), @@ -221,10 +232,10 @@ class LayoutLoader(api.Loader): if family == 'model': actors = self._process_family( - assets, 'StaticMesh', transform) + assets, 'StaticMesh', transform, inst) elif family == 'rig': actors = self._process_family( - assets, 'SkeletalMesh', transform) + assets, 'SkeletalMesh', transform, inst) actors_dict[inst] = actors if family == 'rig': @@ -433,16 +444,17 @@ class LayoutLoader(api.Loader): if family == 'model': actors = self._process_family( - assets, 'StaticMesh', transform) + assets, 'StaticMesh', transform, inst) elif family == 'rig': actors = self._process_family( - assets, 'SkeletalMesh', transform) + assets, 'SkeletalMesh', transform, inst) 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() + obj = ar.get_asset_by_object_path( + asset).get_asset() if obj.get_class().get_name() == 'Skeleton': skeleton = obj if skeleton: From 38ce006a0ee73cf1241b996c114bcd352ca35acb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 Nov 2021 10:05:35 +0000 Subject: [PATCH 11/20] Blender now loads the animation included with the layout --- .../blender/plugins/load/load_layout_json.py | 4 ++++ .../hosts/blender/plugins/load/load_rig.py | 23 +++++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 442cf05d85..281343924a 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -92,6 +92,10 @@ class JsonLayoutLoader(plugin.AssetLoader): 'animation_asset': asset } + if element.get('animation'): + options['animation_file'] = str(Path(libpath).with_suffix( + '')) + "." + element.get('animation') + # This should return the loaded asset, but the load call will be # added to the queue to run in the Blender main thread, so # at this time it will not return anything. The assets will be diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 07ab5ce6d6..f5c9d8edc5 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -7,6 +7,7 @@ from typing import Dict, List, Optional import bpy from avalon import api +from avalon.blender import lib as avalon_lib from avalon.blender.pipeline import AVALON_CONTAINERS from avalon.blender.pipeline import AVALON_CONTAINER_ID from avalon.blender.pipeline import AVALON_PROPERTY @@ -196,12 +197,14 @@ class BlendRigLoader(plugin.AssetLoader): plugin.deselect_all() create_animation = False + anim_file = None if options is not None: parent = options.get('parent') transform = options.get('transform') action = options.get('action') create_animation = options.get('create_animation') + anim_file = options.get('animation_file') if parent and transform: location = transform.get('translation') @@ -254,6 +257,26 @@ class BlendRigLoader(plugin.AssetLoader): plugin.deselect_all() + if anim_file: + bpy.ops.import_scene.fbx(filepath=anim_file) + + imported = avalon_lib.get_selection() + + armature = [ + o for o in asset_group.children if o.type == 'ARMATURE'][0] + + imported_group = [ + o for o in imported if o.type == 'EMPTY'][0] + + for obj in imported: + if obj.type == 'ARMATURE': + if not armature.animation_data: + armature.animation_data_create() + armature.animation_data.action = obj.animation_data.action + + self._remove(imported_group) + bpy.data.objects.remove(imported_group) + bpy.context.scene.collection.objects.link(asset_group) asset_group[AVALON_PROPERTY] = { From e7fbefd6f2b5b563639ed3a61bcd9f11ad141645 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 9 Nov 2021 12:28:09 +0000 Subject: [PATCH 12/20] Added parameter to fbx import for layout animation in Blender --- openpype/hosts/blender/plugins/load/load_rig.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index f5c9d8edc5..7e3cde1735 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -258,7 +258,7 @@ class BlendRigLoader(plugin.AssetLoader): plugin.deselect_all() if anim_file: - bpy.ops.import_scene.fbx(filepath=anim_file) + bpy.ops.import_scene.fbx(filepath=anim_file, anim_offset=0.0) imported = avalon_lib.get_selection() From 92d2bee2d4a85702c02b8cb2e9c3203a2bc38394 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Nov 2021 10:39:26 +0000 Subject: [PATCH 13/20] Unreal can now loads animations to layout previously loaded --- .../hosts/unreal/plugins/load/load_animation.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index 481285d603..f6037bee0a 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -71,8 +71,18 @@ class AnimationFBXLoader(api.Loader): if instance_name: automated = True - actor_name = 'PersistentLevel.' + instance_name - actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name) + # Old method to get the actor + # actor_name = 'PersistentLevel.' + instance_name + # actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name) + actors = unreal.EditorLevelLibrary.get_all_level_actors() + for a in actors: + if a.get_class().get_name() != "SkeletalMeshActor": + continue + if a.get_actor_label() == instance_name: + actor = a + break + if not actor: + raise Exception(f"Could not find actor {instance_name}") skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton task.options.set_editor_property('skeleton', skeleton) From 578ab1c7f298246bcb36e7d347c50f6cc8188360 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Nov 2021 16:39:00 +0000 Subject: [PATCH 14/20] Fixed problem if only one animation in the layout --- openpype/hosts/blender/plugins/publish/extract_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index 804004bf4e..f397e70892 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -209,7 +209,7 @@ class ExtractLayout(openpype.api.Extractor): if len(fbx_files) == 1: fbx_representation = { 'name': 'fbx', - 'ext': 'fbx', + 'ext': '000.fbx', 'files': fbx_files[0], "stagingDir": stagingdir, } From d2ae004b494eafafa6f95df86989406493f45c66 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 11 Nov 2021 10:33:11 +0000 Subject: [PATCH 15/20] Disabled camera creation in Blender when loading layout from JSON --- .../blender/plugins/load/load_layout_json.py | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_layout_json.py b/openpype/hosts/blender/plugins/load/load_layout_json.py index 281343924a..e09861859a 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_json.py +++ b/openpype/hosts/blender/plugins/load/load_layout_json.py @@ -108,20 +108,22 @@ class JsonLayoutLoader(plugin.AssetLoader): options=options ) - # Create the camera asset and the camera instance - creator_plugin = lib.get_creator_by_name("CreateCamera") - if not creator_plugin: - raise ValueError("Creator plugin \"CreateCamera\" was " - "not found.") + # Camera creation when loading a layout is not necessary for now, + # but the code is worth keeping in case we need it in the future. + # # Create the camera asset and the camera instance + # creator_plugin = lib.get_creator_by_name("CreateCamera") + # if not creator_plugin: + # raise ValueError("Creator plugin \"CreateCamera\" was " + # "not found.") - api.create( - creator_plugin, - name="camera", - # name=f"{unique_number}_{subset}_animation", - asset=asset, - options={"useSelection": False} - # data={"dependencies": str(context["representation"]["_id"])} - ) + # api.create( + # creator_plugin, + # name="camera", + # # name=f"{unique_number}_{subset}_animation", + # asset=asset, + # options={"useSelection": False} + # # data={"dependencies": str(context["representation"]["_id"])} + # ) def process_asset(self, context: dict, From 41220adfcaf59f529f1fe794c71575ffdeb55301 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 12 Nov 2021 15:42:10 +0000 Subject: [PATCH 16/20] Improved loading of blend layouts in Blender to behave like to JSON one --- openpype/hosts/blender/api/plugin.py | 7 +++-- .../blender/plugins/load/load_layout_blend.py | 31 ++++++++++++++++--- .../publish/extract_blend_animation.py | 13 ++++---- .../plugins/publish/extract_fbx_animation.py | 2 +- .../unreal/plugins/load/load_animation.py | 29 ++++++++++++----- .../hosts/unreal/plugins/load/load_layout.py | 2 +- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index 6d437059b8..9056bd0686 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -42,10 +42,13 @@ def get_unique_number( return f"{count:0>2}" -def prepare_data(data, container_name): +def prepare_data(data, container_name=None): name = data.name local_data = data.make_local() - local_data.name = f"{container_name}:{name}" + if container_name: + local_data.name = f"{container_name}:{name}" + else: + local_data.name = f"{name}" return local_data diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 4c1f751a77..84457fbd7c 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -10,6 +10,7 @@ from avalon import api from avalon.blender.pipeline import AVALON_CONTAINERS from avalon.blender.pipeline import AVALON_CONTAINER_ID from avalon.blender.pipeline import AVALON_PROPERTY +from openpype import lib from openpype.hosts.blender.api import plugin @@ -59,7 +60,9 @@ class BlendLayoutLoader(plugin.AssetLoader): library = bpy.data.libraries.get(bpy.path.basename(libpath)) bpy.data.libraries.remove(library) - def _process(self, libpath, asset_group, group_name, actions): + def _process( + self, libpath, asset_group, group_name, asset, representation, actions + ): with bpy.data.libraries.load( libpath, link=True, relative=False ) as (data_from, data_to): @@ -106,7 +109,7 @@ class BlendLayoutLoader(plugin.AssetLoader): parent.objects.link(obj) for obj in objects: - local_obj = plugin.prepare_data(obj, group_name) + local_obj = plugin.prepare_data(obj) action = None @@ -125,11 +128,12 @@ class BlendLayoutLoader(plugin.AssetLoader): if material_slot.material: plugin.prepare_data(material_slot.material, group_name) elif local_obj.type == 'ARMATURE': - plugin.prepare_data(local_obj.data, group_name) + plugin.prepare_data(local_obj.data) if action is not None: local_obj.animation_data.action = action - elif local_obj.animation_data.action is not None: + elif (local_obj.animation_data and + local_obj.animation_data.action is not None): plugin.prepare_data( local_obj.animation_data.action, group_name) @@ -140,6 +144,21 @@ class BlendLayoutLoader(plugin.AssetLoader): for t in v.targets: t.id = local_obj + elif local_obj.type == 'EMPTY': + creator_plugin = lib.get_creator_by_name("CreateAnimation") + if not creator_plugin: + raise ValueError("Creator plugin \"CreateAnimation\" was " + "not found.") + + api.create( + creator_plugin, + name=local_obj.name.split(':')[-1] + "_animation", + asset=asset, + options={"useSelection": False, + "asset_group": local_obj}, + data={"dependencies": representation} + ) + if not local_obj.get(AVALON_PROPERTY): local_obj[AVALON_PROPERTY] = dict() @@ -168,6 +187,7 @@ class BlendLayoutLoader(plugin.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] + representation = str(context["representation"]["_id"]) asset_name = plugin.asset_name(asset, subset) unique_number = plugin.get_unique_number(asset, subset) @@ -183,7 +203,8 @@ class BlendLayoutLoader(plugin.AssetLoader): asset_group.empty_display_type = 'SINGLE_ARROW' avalon_container.objects.link(asset_group) - objects = self._process(libpath, asset_group, group_name, None) + objects = self._process( + libpath, asset_group, group_name, asset, representation, None) for child in asset_group.children: if child.get(AVALON_PROPERTY): diff --git a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py index 239ca53f98..4917223331 100644 --- a/openpype/hosts/blender/plugins/publish/extract_blend_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_blend_animation.py @@ -29,12 +29,13 @@ class ExtractBlendAnimation(openpype.api.Extractor): if isinstance(obj, bpy.types.Object) and obj.type == 'EMPTY': child = obj.children[0] if child and child.type == 'ARMATURE': - if not obj.animation_data: - obj.animation_data_create() - obj.animation_data.action = child.animation_data.action - obj.animation_data_clear() - data_blocks.add(child.animation_data.action) - data_blocks.add(obj) + if child.animation_data and child.animation_data.action: + if not obj.animation_data: + obj.animation_data_create() + obj.animation_data.action = child.animation_data.action + obj.animation_data_clear() + data_blocks.add(child.animation_data.action) + data_blocks.add(obj) bpy.data.libraries.write(filepath, data_blocks) diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py index 96fa37c07e..00346a39f2 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx_animation.py @@ -123,7 +123,7 @@ class ExtractAnimationFBX(api.Extractor): json_path = os.path.join(stagingdir, json_filename) json_dict = { - "instance_name": asset_group.get(AVALON_PROPERTY).get("namespace") + "instance_name": asset_group.get(AVALON_PROPERTY).get("objectName") } # collection = instance.data.get("name") diff --git a/openpype/hosts/unreal/plugins/load/load_animation.py b/openpype/hosts/unreal/plugins/load/load_animation.py index f6037bee0a..20baa30847 100644 --- a/openpype/hosts/unreal/plugins/load/load_animation.py +++ b/openpype/hosts/unreal/plugins/load/load_animation.py @@ -183,20 +183,35 @@ class AnimationFBXLoader(api.Loader): task.set_editor_property('destination_name', name) task.set_editor_property('replace_existing', True) task.set_editor_property('automated', True) - task.set_editor_property('save', False) + task.set_editor_property('save', True) # set import options here task.options.set_editor_property( - 'automated_import_should_detect_type', True) + 'automated_import_should_detect_type', False) task.options.set_editor_property( - 'original_import_type', unreal.FBXImportType.FBXIT_ANIMATION) + 'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH) + task.options.set_editor_property( + 'mesh_type_to_import', unreal.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.skeletal_mesh_import_data.set_editor_property( - 'import_content_type', - unreal.FBXImportContentType.FBXICT_SKINNING_WEIGHTS + 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) skeletal_mesh = unreal.EditorAssetLibrary.load_asset( container.get('namespace') + "/" + container.get('asset_name')) @@ -229,7 +244,7 @@ class AnimationFBXLoader(api.Loader): unreal.EditorAssetLibrary.delete_directory(path) asset_content = unreal.EditorAssetLibrary.list_assets( - parent_path, recursive=False + parent_path, recursive=False, include_folder=True ) if len(asset_content) == 0: diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index c32c810c36..ce15e49ad8 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -504,7 +504,7 @@ class LayoutLoader(api.Loader): EditorAssetLibrary.delete_directory(path) asset_content = EditorAssetLibrary.list_assets( - parent_path, recursive=False + parent_path, recursive=False, include_folder=True ) if len(asset_content) == 0: From 6aed38324bfbee479e86bbdb8ec447aacbb180b2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 18 Nov 2021 11:53:56 +0000 Subject: [PATCH 17/20] Fix loading layout with rigs with animation --- .../blender/plugins/load/load_layout_blend.py | 77 +++++++++++++++++-- 1 file changed, 69 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 84457fbd7c..102541e9b6 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -86,12 +86,16 @@ class BlendLayoutLoader(plugin.AssetLoader): objects = [] nodes = list(container.children) - for obj in nodes: - obj.parent = asset_group + allowed_types = ['ARMATURE', 'MESH', 'EMPTY'] for obj in nodes: - objects.append(obj) - nodes.extend(list(obj.children)) + if obj.type in allowed_types: + obj.parent = asset_group + + for obj in nodes: + if obj.type in allowed_types: + objects.append(obj) + nodes.extend(list(obj.children)) objects.reverse() @@ -117,7 +121,7 @@ class BlendLayoutLoader(plugin.AssetLoader): action = actions.get(local_obj.name, None) if local_obj.type == 'MESH': - plugin.prepare_data(local_obj.data, group_name) + plugin.prepare_data(local_obj.data) if obj != local_obj: for constraint in constraints: @@ -126,16 +130,18 @@ class BlendLayoutLoader(plugin.AssetLoader): for material_slot in local_obj.material_slots: if material_slot.material: - plugin.prepare_data(material_slot.material, group_name) + plugin.prepare_data(material_slot.material) elif local_obj.type == 'ARMATURE': plugin.prepare_data(local_obj.data) 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): plugin.prepare_data( - local_obj.animation_data.action, group_name) + local_obj.animation_data.action) # Set link the drivers to the local object if local_obj.data.animation_data: @@ -167,7 +173,62 @@ class BlendLayoutLoader(plugin.AssetLoader): objects.reverse() - bpy.data.orphans_purge(do_local_ids=False) + armatures = [ + obj for obj in bpy.data.objects + if obj.type == 'ARMATURE' and obj.library is None] + arm_act = {} + + # The armatures with an animation need to be at the center of the + # scene to be hooked correctly by the curves modifiers. + for armature in armatures: + if armature.animation_data and armature.animation_data.action: + arm_act[armature] = armature.animation_data.action + armature.animation_data.action = None + armature.location = (0.0, 0.0, 0.0) + for bone in armature.pose.bones: + bone.location = (0.0, 0.0, 0.0) + bone.rotation_euler = (0.0, 0.0, 0.0) + + curves = [obj for obj in data_to.objects if obj.type == 'CURVE'] + + for curve in curves: + rig_name = curve.name.split(':')[0] + rig_obj = bpy.data.objects.get(rig_name) + + local_obj = plugin.prepare_data(curve) + plugin.prepare_data(local_obj.data) + + # Curves need to reset the hook, but to do that they need to be + # in the view layer. + parent.objects.link(local_obj) + plugin.deselect_all() + local_obj.select_set(True) + bpy.context.view_layer.objects.active = local_obj + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.object.hook_reset() + bpy.ops.object.mode_set(mode='OBJECT') + parent.objects.unlink(local_obj) + + local_obj.use_fake_user = True + + for mod in local_obj.modifiers: + mod.object = bpy.data.objects.get(f"{mod.object.name}") + + if not local_obj.get(AVALON_PROPERTY): + local_obj[AVALON_PROPERTY] = dict() + + avalon_info = local_obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + local_obj.parent = rig_obj + objects.append(local_obj) + + for armature in armatures: + if arm_act.get(armature): + armature.animation_data.action = arm_act[armature] + + while bpy.data.orphans_purge(do_local_ids=False): + pass plugin.deselect_all() From dd7d08590658ebf1f44cd9a55bd292830665ca2f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 30 Nov 2021 15:20:25 +0000 Subject: [PATCH 18/20] Fix problem with the loaded container --- .../blender/plugins/load/load_layout_blend.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index 102541e9b6..b7981e0fc8 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -75,7 +75,8 @@ class BlendLayoutLoader(plugin.AssetLoader): container = None for empty in empties: - if empty.get(AVALON_PROPERTY): + if (empty.get(AVALON_PROPERTY) and + empty.get(AVALON_PROPERTY).get('family') == 'layout'): container = empty break @@ -174,7 +175,7 @@ class BlendLayoutLoader(plugin.AssetLoader): objects.reverse() armatures = [ - obj for obj in bpy.data.objects + obj for obj in bpy.data.objects if obj.type == 'ARMATURE' and obj.library is None] arm_act = {} @@ -192,8 +193,8 @@ class BlendLayoutLoader(plugin.AssetLoader): curves = [obj for obj in data_to.objects if obj.type == 'CURVE'] for curve in curves: - rig_name = curve.name.split(':')[0] - rig_obj = bpy.data.objects.get(rig_name) + curve_name = curve.name.split(':')[0] + curve_obj = bpy.data.objects.get(curve_name) local_obj = plugin.prepare_data(curve) plugin.prepare_data(local_obj.data) @@ -204,9 +205,10 @@ class BlendLayoutLoader(plugin.AssetLoader): plugin.deselect_all() local_obj.select_set(True) bpy.context.view_layer.objects.active = local_obj - bpy.ops.object.mode_set(mode='EDIT') - bpy.ops.object.hook_reset() - bpy.ops.object.mode_set(mode='OBJECT') + if local_obj.library == None: + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.object.hook_reset() + bpy.ops.object.mode_set(mode='OBJECT') parent.objects.unlink(local_obj) local_obj.use_fake_user = True @@ -220,7 +222,7 @@ class BlendLayoutLoader(plugin.AssetLoader): avalon_info = local_obj[AVALON_PROPERTY] avalon_info.update({"container_name": group_name}) - local_obj.parent = rig_obj + local_obj.parent = curve_obj objects.append(local_obj) for armature in armatures: From 0941a8d9ed55a8fb16d56bc589c14b6e0fe3470a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jan 2022 11:27:40 +0000 Subject: [PATCH 19/20] Added alembic support --- .../blender/plugins/publish/extract_layout.py | 29 +++++++++++-- .../hosts/unreal/plugins/load/load_layout.py | 41 +++++++++++++++++-- 2 files changed, 62 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/blender/plugins/publish/extract_layout.py b/openpype/hosts/blender/plugins/publish/extract_layout.py index f397e70892..af87204a6d 100644 --- a/openpype/hosts/blender/plugins/publish/extract_layout.py +++ b/openpype/hosts/blender/plugins/publish/extract_layout.py @@ -136,6 +136,7 @@ class ExtractLayout(openpype.api.Extractor): family = metadata["family"] self.log.debug("Parent: {}".format(parent)) + # Get blend reference blend = io.find_one( { "type": "representation", @@ -143,7 +144,10 @@ class ExtractLayout(openpype.api.Extractor): "name": "blend" }, projection={"_id": True}) - blend_id = blend["_id"] + blend_id = None + if blend: + blend_id = blend["_id"] + # Get fbx reference fbx = io.find_one( { "type": "representation", @@ -151,11 +155,28 @@ class ExtractLayout(openpype.api.Extractor): "name": "fbx" }, projection={"_id": True}) - fbx_id = fbx["_id"] + fbx_id = None + if fbx: + fbx_id = fbx["_id"] + # Get abc reference + abc = io.find_one( + { + "type": "representation", + "parent": io.ObjectId(parent), + "name": "abc" + }, + projection={"_id": True}) + abc_id = None + if abc: + abc_id = abc["_id"] json_element = {} - json_element["reference"] = str(blend_id) - json_element["reference_fbx"] = str(fbx_id) + if blend_id: + json_element["reference"] = str(blend_id) + if fbx_id: + json_element["reference_fbx"] = str(fbx_id) + if abc_id: + json_element["reference_abc"] = str(abc_id) json_element["family"] = family json_element["instance_name"] = asset.name json_element["asset_name"] = metadata["asset_name"] diff --git a/openpype/hosts/unreal/plugins/load/load_layout.py b/openpype/hosts/unreal/plugins/load/load_layout.py index ce15e49ad8..19d0b74e3e 100644 --- a/openpype/hosts/unreal/plugins/load/load_layout.py +++ b/openpype/hosts/unreal/plugins/load/load_layout.py @@ -40,7 +40,7 @@ class LayoutLoader(api.Loader): return asset_containers - def _get_loader(self, loaders, family): + def _get_fbx_loader(self, loaders, family): name = "" if family == 'rig': name = "SkeletalMeshFBXLoader" @@ -58,6 +58,22 @@ class LayoutLoader(api.Loader): return None + def _get_abc_loader(self, loaders, family): + name = "" + if family == 'rig': + name = "SkeletalMeshAlembicLoader" + elif family == 'model': + name = "StaticMeshAlembicLoader" + + if name == "": + return None + + for loader in loaders: + if loader.__name__ == name: + return loader + + return None + def _process_family(self, assets, classname, transform, inst_name=None): ar = unreal.AssetRegistryHelpers.get_asset_registry() @@ -193,7 +209,17 @@ class LayoutLoader(api.Loader): actors_dict = {} for element in data: - reference = element.get('reference_fbx') + reference = None + if element.get('reference_fbx'): + reference = element.get('reference_fbx') + elif element.get('reference_abc'): + reference = element.get('reference_abc') + + # If reference is None, this element is skipped, as it cannot be + # imported in Unreal + if not reference: + continue + instance_name = element.get('instance_name') skeleton = None @@ -204,7 +230,13 @@ class LayoutLoader(api.Loader): family = element.get('family') loaders = api.loaders_from_representation( all_loaders, reference) - loader = self._get_loader(loaders, family) + + loader = None + + if reference == element.get('reference_fbx'): + loader = self._get_fbx_loader(loaders, family) + elif reference == element.get('reference_abc'): + loader = self._get_abc_loader(loaders, family) if not loader: continue @@ -222,7 +254,8 @@ class LayoutLoader(api.Loader): instances = [ item for item in data - if item.get('reference_fbx') == reference] + if (item.get('reference_fbx') == reference or + item.get('reference_abc') == reference)] for instance in instances: transform = instance.get('transform') From 66c762c63504ebdefbb2234fcdbd905f163beff6 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 14 Jan 2022 11:31:45 +0000 Subject: [PATCH 20/20] Hound fix --- openpype/hosts/blender/plugins/load/load_layout_blend.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/blender/plugins/load/load_layout_blend.py b/openpype/hosts/blender/plugins/load/load_layout_blend.py index b7981e0fc8..0f5ae01a99 100644 --- a/openpype/hosts/blender/plugins/load/load_layout_blend.py +++ b/openpype/hosts/blender/plugins/load/load_layout_blend.py @@ -205,7 +205,7 @@ class BlendLayoutLoader(plugin.AssetLoader): plugin.deselect_all() local_obj.select_set(True) bpy.context.view_layer.objects.active = local_obj - if local_obj.library == None: + if local_obj.library is None: bpy.ops.object.mode_set(mode='EDIT') bpy.ops.object.hook_reset() bpy.ops.object.mode_set(mode='OBJECT')