diff --git a/openpype/hosts/blender/api/plugin.py b/openpype/hosts/blender/api/plugin.py index f3cf2b88cd..b0df2d7109 100644 --- a/openpype/hosts/blender/api/plugin.py +++ b/openpype/hosts/blender/api/plugin.py @@ -6,10 +6,11 @@ from typing import Dict, List, Optional import bpy from avalon import api, blender +from avalon.blender import ops from avalon.blender.pipeline import AVALON_CONTAINERS from openpype.api import PypeCreatorMixin -VALID_EXTENSIONS = [".blend", ".json", ".abc"] +VALID_EXTENSIONS = [".blend", ".json", ".abc", ".fbx"] def asset_name( @@ -161,6 +162,15 @@ class AssetLoader(api.Loader): raise NotImplementedError("Must be implemented by a sub-class") def load(self, + context: dict, + name: Optional[str] = None, + namespace: Optional[str] = None, + options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: + """ Run the loader on Blender main thread""" + mti = ops.MainThreadItem(self._load, context, name, namespace, options) + ops.execute_in_main_thread(mti) + + def _load(self, context: dict, name: Optional[str] = None, namespace: Optional[str] = None, @@ -216,10 +226,20 @@ class AssetLoader(api.Loader): return self._get_instance_collection(instance_name, nodes) + def exec_update(self, container: Dict, representation: Dict): + """Must be implemented by a sub-class""" + raise NotImplementedError("Must be implemented by a sub-class") + def update(self, container: Dict, representation: Dict): + """ Run the update on Blender main thread""" + mti = ops.MainThreadItem(self.exec_update, container, representation) + ops.execute_in_main_thread(mti) + + def exec_remove(self, container: Dict) -> bool: """Must be implemented by a sub-class""" raise NotImplementedError("Must be implemented by a sub-class") def remove(self, container: Dict) -> bool: - """Must be implemented by a sub-class""" - raise NotImplementedError("Must be implemented by a sub-class") + """ Run the remove on Blender main thread""" + mti = ops.MainThreadItem(self.exec_remove, container) + ops.execute_in_main_thread(mti) diff --git a/openpype/hosts/blender/plugins/create/create_model.py b/openpype/hosts/blender/plugins/create/create_model.py index ecc6f4bf22..e778f5b74f 100644 --- a/openpype/hosts/blender/plugins/create/create_model.py +++ b/openpype/hosts/blender/plugins/create/create_model.py @@ -3,7 +3,7 @@ import bpy from avalon import api -from avalon.blender import lib +from avalon.blender import lib, ops from avalon.blender.pipeline import AVALON_INSTANCES from openpype.hosts.blender.api import plugin @@ -17,6 +17,11 @@ class CreateModel(plugin.Creator): icon = "cube" def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + + def _process(self): # Get Instance Containter or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -40,8 +45,6 @@ class CreateModel(plugin.Creator): for obj in selected: obj.select_set(True) selected.append(asset_group) - context = plugin.create_blender_context( - active=asset_group, selected=selected) - bpy.ops.object.parent_set(context, keep_transform=True) + bpy.ops.object.parent_set(keep_transform=True) return asset_group diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 0f1c686816..2e1c71f570 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -3,7 +3,7 @@ import bpy from avalon import api -from avalon.blender import lib +from avalon.blender import lib, ops from avalon.blender.pipeline import AVALON_INSTANCES from openpype.hosts.blender.api import plugin @@ -17,6 +17,11 @@ class CreateRig(plugin.Creator): icon = "wheelchair" def process(self): + """ Run the creator on Blender main thread""" + mti = ops.MainThreadItem(self._process) + ops.execute_in_main_thread(mti) + + def _process(self): # Get Instance Containter or create it if it does not exist instances = bpy.data.collections.get(AVALON_INSTANCES) if not instances: @@ -40,8 +45,6 @@ class CreateRig(plugin.Creator): for obj in selected: obj.select_set(True) selected.append(asset_group) - context = plugin.create_blender_context( - active=asset_group, selected=selected) - bpy.ops.object.parent_set(context, keep_transform=True) + bpy.ops.object.parent_set(keep_transform=True) return asset_group diff --git a/openpype/hosts/blender/plugins/load/load_abc.py b/openpype/hosts/blender/plugins/load/load_abc.py index 522a7fd63a..a985ae684d 100644 --- a/openpype/hosts/blender/plugins/load/load_abc.py +++ b/openpype/hosts/blender/plugins/load/load_abc.py @@ -52,9 +52,7 @@ class CacheModelLoader(plugin.AssetLoader): collection = bpy.context.view_layer.active_layer_collection.collection relative = bpy.context.preferences.filepaths.use_relative_paths - context = plugin.create_blender_context() bpy.ops.wm.alembic_import( - context, filepath=libpath, relative_path=relative ) @@ -164,7 +162,7 @@ class CacheModelLoader(plugin.AssetLoader): self[:] = objects return objects - def update(self, container: Dict, representation: Dict): + def exec_update(self, container: Dict, representation: Dict): """Update the loaded asset. This will remove all objects of the current collection, load the new @@ -227,7 +225,7 @@ class CacheModelLoader(plugin.AssetLoader): metadata["libpath"] = str(libpath) metadata["representation"] = str(representation["_id"]) - def remove(self, container: Dict) -> bool: + def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. Arguments: diff --git a/openpype/hosts/blender/plugins/load/load_fbx_model.py b/openpype/hosts/blender/plugins/load/load_fbx.py similarity index 82% rename from openpype/hosts/blender/plugins/load/load_fbx_model.py rename to openpype/hosts/blender/plugins/load/load_fbx.py index 0e49eada3f..f267bef586 100644 --- a/openpype/hosts/blender/plugins/load/load_fbx_model.py +++ b/openpype/hosts/blender/plugins/load/load_fbx.py @@ -7,7 +7,7 @@ from typing import Dict, List, Optional import bpy from avalon import api -from avalon.blender import lib +from avalon.blender import lib, ops from avalon.blender.pipeline import AVALON_CONTAINERS from avalon.blender.pipeline import AVALON_CONTAINER_ID from avalon.blender.pipeline import AVALON_PROPERTY @@ -20,7 +20,7 @@ class FbxModelLoader(plugin.AssetLoader): Stores the imported asset in an empty named after the asset. """ - families = ["model"] + families = ["model", "rig"] representations = ["fbx"] label = "Load FBX" @@ -29,7 +29,6 @@ class FbxModelLoader(plugin.AssetLoader): def _remove(self, asset_group): objects = list(asset_group.children) - empties = [] for obj in objects: if obj.type == 'MESH': @@ -37,23 +36,21 @@ class FbxModelLoader(plugin.AssetLoader): if material_slot.material: bpy.data.materials.remove(material_slot.material) bpy.data.meshes.remove(obj.data) + elif obj.type == 'ARMATURE': + objects.extend(obj.children) + bpy.data.armatures.remove(obj.data) + elif obj.type == 'CURVE': + bpy.data.curves.remove(obj.data) elif obj.type == 'EMPTY': objects.extend(obj.children) - empties.append(obj) + bpy.data.objects.remove(obj) - for empty in empties: - bpy.data.objects.remove(empty) - - def _process(self, libpath, asset_group, group_name): + def _process(self, libpath, asset_group, group_name, action): bpy.ops.object.select_all(action='DESELECT') collection = bpy.context.view_layer.active_layer_collection.collection - context = plugin.create_blender_context() - bpy.ops.import_scene.fbx( - context, - filepath=libpath - ) + bpy.ops.import_scene.fbx(filepath=libpath) parent = bpy.context.scene.collection @@ -97,9 +94,17 @@ class FbxModelLoader(plugin.AssetLoader): name_data = obj.data.name obj.data.name = f"{group_name}:{name_data}" + if obj.type == 'MESH': for material_slot in obj.material_slots: name_mat = material_slot.material.name material_slot.material.name = f"{group_name}:{name_mat}" + elif obj.type == 'ARMATURE': + anim_data = obj.animation_data + if action is not None: + anim_data.action = action + elif anim_data.action is not None: + name_action = anim_data.action.name + anim_data.action.name = f"{group_name}:{name_action}" if not obj.get(AVALON_PROPERTY): obj[AVALON_PROPERTY] = dict() @@ -122,7 +127,6 @@ class FbxModelLoader(plugin.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] @@ -140,7 +144,14 @@ class FbxModelLoader(plugin.AssetLoader): asset_group = bpy.data.objects.new(group_name, object_data=None) avalon_container.objects.link(asset_group) - objects = self._process(libpath, asset_group, group_name) + objects = self._process(libpath, asset_group, group_name, None) + + objects = [] + nodes = list(asset_group.children) + + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) bpy.context.scene.collection.objects.link(asset_group) @@ -160,7 +171,7 @@ class FbxModelLoader(plugin.AssetLoader): self[:] = objects return objects - def update(self, container: Dict, representation: Dict): + def exec_update(self, container: Dict, representation: Dict): """Update the loaded asset. This will remove all objects of the current collection, load the new @@ -214,16 +225,28 @@ class FbxModelLoader(plugin.AssetLoader): self.log.info("Library already loaded, not updating...") return + # Get the armature of the rig + objects = asset_group.children + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] + action = None + + if armatures: + armature = armatures[0] + + if armature.animation_data and armature.animation_data.action: + action = armature.animation_data.action + mat = asset_group.matrix_basis.copy() self._remove(asset_group) - self._process(str(libpath), asset_group, object_name) + self._process(str(libpath), asset_group, object_name, action) + asset_group.matrix_basis = mat metadata["libpath"] = str(libpath) metadata["representation"] = str(representation["_id"]) - def remove(self, container: Dict) -> bool: + def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. Arguments: diff --git a/openpype/hosts/blender/plugins/load/load_model.py b/openpype/hosts/blender/plugins/load/load_model.py index 3f7967cd0d..37618b164d 100644 --- a/openpype/hosts/blender/plugins/load/load_model.py +++ b/openpype/hosts/blender/plugins/load/load_model.py @@ -145,7 +145,7 @@ class BlendModelLoader(plugin.AssetLoader): self[:] = objects return objects - def update(self, container: Dict, representation: Dict): + def exec_update(self, container: Dict, representation: Dict): """Update the loaded asset. This will remove all objects of the current collection, load the new @@ -218,7 +218,7 @@ class BlendModelLoader(plugin.AssetLoader): metadata["libpath"] = str(libpath) metadata["representation"] = str(representation["_id"]) - def remove(self, container: Dict) -> bool: + def exec_remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. Arguments: diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index 306c4aa03b..3909a1b4aa 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -29,7 +29,8 @@ class BlendRigLoader(plugin.AssetLoader): for obj in objects: if obj.type == 'MESH': for material_slot in list(obj.material_slots): - bpy.data.materials.remove(material_slot.material) + if material_slot.material: + bpy.data.materials.remove(material_slot.material) bpy.data.meshes.remove(obj.data) elif obj.type == 'ARMATURE': objects.extend(obj.children) @@ -178,7 +179,7 @@ class BlendRigLoader(plugin.AssetLoader): self[:] = objects return objects - def update(self, container: Dict, representation: Dict): + def exec_update(self, container: Dict, representation: Dict): """Update the loaded asset. This will remove all children of the asset group, load the new ones @@ -232,7 +233,7 @@ class BlendRigLoader(plugin.AssetLoader): if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: count += 1 - # # Get the armature of the rig + # Get the armature of the rig objects = asset_group.children armature = [obj for obj in objects if obj.type == 'ARMATURE'][0] @@ -256,7 +257,7 @@ class BlendRigLoader(plugin.AssetLoader): metadata["libpath"] = str(libpath) metadata["representation"] = str(representation["_id"]) - def remove(self, container: Dict) -> bool: + def exec_remove(self, container: Dict) -> bool: """Remove an existing asset group from a Blender scene. Arguments: