diff --git a/openpype/hosts/blender/plugins/create/create_rig.py b/openpype/hosts/blender/plugins/create/create_rig.py index 116fb9f742..45c0f836d1 100644 --- a/openpype/hosts/blender/plugins/create/create_rig.py +++ b/openpype/hosts/blender/plugins/create/create_rig.py @@ -4,10 +4,11 @@ import bpy from avalon import api from avalon.blender import lib -import openpype.hosts.blender.api.plugin +from avalon.blender.pipeline import AVALON_INSTANCES +from openpype.hosts.blender.api import plugin -class CreateRig(openpype.hosts.blender.api.plugin.Creator): +class CreateRig(plugin.Creator): """Artist-friendly rig with controls to direct motion""" name = "rigMain" @@ -16,26 +17,30 @@ class CreateRig(openpype.hosts.blender.api.plugin.Creator): icon = "wheelchair" def process(self): + # Get Instance Containter or create it if it does not exist + instances = bpy.data.collections.get(AVALON_INSTANCES) + if not instances: + instances = bpy.data.collections.new(name=AVALON_INSTANCES) + bpy.context.scene.collection.children.link(instances) + # Create instance object asset = self.data["asset"] subset = self.data["subset"] - name = openpype.hosts.blender.api.plugin.asset_name(asset, subset) - collection = bpy.data.collections.new(name=name) - bpy.context.scene.collection.children.link(collection) + name = plugin.asset_name(asset, subset) + asset_group = bpy.data.objects.new(name=name, object_data=None) + instances.objects.link(asset_group) self.data['task'] = api.Session.get('AVALON_TASK') - lib.imprint(collection, self.data) - - # Add the rig object and all the children meshes to - # a set and link them all at the end to avoid duplicates. - # Blender crashes if trying to link an object that is already linked. - # This links automatically the children meshes if they were not - # selected, and doesn't link them twice if they, insted, - # were manually selected by the user. + lib.imprint(asset_group, self.data) + # Add selected objects to instance if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - for child in obj.users_collection[0].children: - collection.children.link(child) - collection.objects.link(obj) + bpy.context.view_layer.objects.active = asset_group + selected = lib.get_selection() + 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) - return collection + return asset_group diff --git a/openpype/hosts/blender/plugins/load/load_rig.py b/openpype/hosts/blender/plugins/load/load_rig.py index b6be8f4cf6..6fa7460d76 100644 --- a/openpype/hosts/blender/plugins/load/load_rig.py +++ b/openpype/hosts/blender/plugins/load/load_rig.py @@ -1,21 +1,20 @@ """Load a rig asset in Blender.""" -import logging from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -from avalon import api, blender import bpy -import openpype.hosts.blender.api.plugin as plugin + +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.hosts.blender.api import plugin class BlendRigLoader(plugin.AssetLoader): - """Load rigs from a .blend file. - - Because they come from a .blend file we can simply link the collection that - contains the model. There is no further need to 'containerise' it. - """ + """Load rigs from a .blend file.""" families = ["rig"] representations = ["blend"] @@ -24,105 +23,110 @@ class BlendRigLoader(plugin.AssetLoader): icon = "code-fork" color = "orange" - def _remove(self, objects, obj_container): - for obj in list(objects): - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': + def _remove(self, asset_group): + objects = list(asset_group.children) + + for obj in objects: + if obj.type == 'MESH': + for material_slot in list(obj.material_slots): + 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) + bpy.data.objects.remove(obj) - for child in obj_container.children: - bpy.data.collections.remove(child) - - bpy.data.collections.remove(obj_container) - - def make_local_and_metadata(self, obj, collection_name): - local_obj = plugin.prepare_data(obj, collection_name) - plugin.prepare_data(local_obj.data, collection_name) - - if not local_obj.get(blender.pipeline.AVALON_PROPERTY): - local_obj[blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection_name + '_CON'}) - - return local_obj - - def _process( - self, libpath, lib_container, collection_name, - action, parent_collection - ): + def _process(self, libpath, asset_group, group_name, action): relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] + ) as (data_from, data_to): + data_to.objects = data_from.objects - parent = parent_collection + parent = bpy.context.scene.collection - if parent is None: - parent = bpy.context.scene.collection + empties = [obj for obj in data_to.objects if obj.type == 'EMPTY'] - parent.children.link(bpy.data.collections[lib_container]) + container = None - rig_container = parent.children[lib_container].make_local() - rig_container.name = collection_name + for empty in empties: + if empty.get(AVALON_PROPERTY): + container = empty + break + assert container, "No asset group found" + + # Children must be linked before parents, + # otherwise the hierarchy will break objects = [] - armatures = [ - obj for obj in rig_container.objects - if obj.type == 'ARMATURE' - ] + nodes = list(container.children) - for child in rig_container.children: - local_child = plugin.prepare_data(child, collection_name) - objects.extend(local_child.objects) + for obj in nodes: + obj.parent = asset_group - # for obj in bpy.data.objects: - # obj.select_set(False) + for obj in nodes: + objects.append(obj) + nodes.extend(list(obj.children)) + + objects.reverse() constraints = [] + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] + for armature in armatures: for bone in armature.pose.bones: for constraint in bone.constraints: if hasattr(constraint, 'target'): constraints.append(constraint) - # Link armatures after other objects. - # The armature is unparented for all the non-local meshes, - # when it is made local. for obj in objects: - local_obj = self.make_local_and_metadata(obj, collection_name) + parent.objects.link(obj) - if obj != local_obj: - for constraint in constraints: - if constraint.target == obj: - constraint.target = local_obj + for obj in objects: + local_obj = plugin.prepare_data(obj, group_name) - for armature in armatures: - local_obj = self.make_local_and_metadata(armature, collection_name) + if obj.type == 'MESH': + plugin.prepare_data(local_obj.data, group_name) - if action is not None: - local_obj.animation_data.action = action - elif local_obj.animation_data.action is not None: - plugin.prepare_data( - local_obj.animation_data.action, collection_name) + if obj != local_obj: + for constraint in constraints: + if constraint.target == obj: + constraint.target = local_obj - # Set link the drivers to the local object - if local_obj.data.animation_data: - for d in local_obj.data.animation_data.drivers: - for v in d.driver.variables: - for t in v.targets: - t.id = local_obj + for material_slot in local_obj.material_slots: + plugin.prepare_data(material_slot.material, group_name) + elif obj.type == 'ARMATURE': + plugin.prepare_data(local_obj.data, group_name) - rig_container.pop(blender.pipeline.AVALON_PROPERTY) + if action is not None: + local_obj.animation_data.action = action + elif local_obj.animation_data.action is not None: + plugin.prepare_data( + local_obj.animation_data.action, group_name) + + # Set link the drivers to the local object + if local_obj.data.animation_data: + for d in local_obj.data.animation_data.drivers: + for v in d.driver.variables: + for t in v.targets: + t.id = local_obj + + if not obj.get(AVALON_PROPERTY): + local_obj[AVALON_PROPERTY] = dict() + + avalon_info = local_obj[AVALON_PROPERTY] + avalon_info.update({"container_name": group_name}) + + objects.reverse() bpy.ops.object.select_all(action='DESELECT') - return rig_container + return objects def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -138,61 +142,48 @@ class BlendRigLoader(plugin.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = plugin.asset_name( - asset, subset - ) - unique_number = plugin.get_unique_number( - asset, subset - ) + + asset_name = plugin.asset_name(asset, subset) + unique_number = plugin.get_unique_number(asset, subset) + group_name = plugin.asset_name(asset, subset, unique_number) namespace = namespace or f"{asset}_{unique_number}" - collection_name = plugin.asset_name( - asset, subset, unique_number - ) - container = bpy.data.collections.new(collection_name) - blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) + avalon_container = bpy.data.collections.get(AVALON_CONTAINERS) + if not avalon_container: + avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS) + bpy.context.scene.collection.children.link(avalon_container) - metadata = container.get(blender.pipeline.AVALON_PROPERTY) + asset_group = bpy.data.objects.new(group_name, object_data=None) + avalon_container.objects.link(asset_group) - metadata["libpath"] = libpath - metadata["lib_container"] = lib_container + objects = self._process(libpath, asset_group, group_name, None) - obj_container = self._process( - libpath, lib_container, collection_name, None, None) + bpy.context.scene.collection.objects.link(asset_group) - metadata["obj_container"] = obj_container - # Save the list of objects in the metadata container - metadata["objects"] = obj_container.all_objects + asset_group[AVALON_PROPERTY] = { + "schema": "openpype:container-2.0", + "id": AVALON_CONTAINER_ID, + "name": name, + "namespace": namespace or '', + "loader": str(self.__class__.__name__), + "representation": str(context["representation"]["_id"]), + "libpath": libpath, + "asset_name": asset_name, + "parent": str(context["representation"]["parent"]), + "family": context["representation"]["context"]["family"] + } - metadata["parent"] = str(context["representation"]["parent"]) - metadata["family"] = context["representation"]["context"]["family"] - - nodes = list(container.objects) - nodes.append(container) - self[:] = nodes - return nodes + self[:] = objects + return objects def update(self, container: Dict, representation: Dict): """Update the loaded asset. - This will remove all objects of the current collection, load the new - ones and add them to the collection. - If the objects of the collection are used in another collection they - will not be removed, only unlinked. Normally this should not be the - case though. - - Warning: - No nested collections are supported at the moment! + This will remove all children of the asset group, load the new ones + and add them as children of the group. """ - collection = bpy.data.collections.get( - container["objectName"] - ) + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() @@ -202,12 +193,9 @@ class BlendRigLoader(plugin.AssetLoader): pformat(representation, indent=2), ) - assert collection, ( + assert asset_group, ( f"The asset is not loaded: {container['objectName']}" ) - assert not (collection.children), ( - "Nested collections are not supported." - ) assert libpath, ( "No existing library file found for {container['objectName']}" ) @@ -218,89 +206,84 @@ class BlendRigLoader(plugin.AssetLoader): f"Unsupported file: {libpath}" ) - collection_metadata = collection.get( - blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] - lib_container = collection_metadata["lib_container"] + metadata = asset_group.get(AVALON_PROPERTY) + group_libpath = metadata["libpath"] - obj_container = plugin.get_local_collection_with_name( - collection_metadata["obj_container"].name - ) - objects = obj_container.all_objects - - container_name = obj_container.name - - normalized_collection_libpath = ( - str(Path(bpy.path.abspath(collection_libpath)).resolve()) + normalized_group_libpath = ( + str(Path(bpy.path.abspath(group_libpath)).resolve()) ) normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) self.log.debug( - "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", - normalized_collection_libpath, + "normalized_group_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_group_libpath, normalized_libpath, ) - if normalized_collection_libpath == normalized_libpath: + if normalized_group_libpath == normalized_libpath: self.log.info("Library already loaded, not updating...") return - # Get the armature of the rig - armatures = [obj for obj in objects if obj.type == 'ARMATURE'] - assert(len(armatures) == 1) + # Check how many assets use the same library + count = 0 + for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: + if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath: + count += 1 + + # # Get the armature of the rig + objects = asset_group.children + armature = [obj for obj in objects if obj.type == 'ARMATURE'][0] action = None - if armatures[0].animation_data and armatures[0].animation_data.action: - action = armatures[0].animation_data.action + if armature.animation_data and armature.animation_data.action: + action = armature.animation_data.action - parent = plugin.get_parent_collection(obj_container) + mat = asset_group.matrix_basis.copy() - self._remove(objects, obj_container) + self._remove(asset_group) - obj_container = self._process( - str(libpath), lib_container, container_name, action, parent) + # 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) - # Save the list of objects in the metadata container - collection_metadata["obj_container"] = obj_container - collection_metadata["objects"] = obj_container.all_objects - collection_metadata["libpath"] = str(libpath) - collection_metadata["representation"] = str(representation["_id"]) + self._process(str(libpath), asset_group, object_name, action) - bpy.ops.object.select_all(action='DESELECT') + asset_group.matrix_basis = mat + + metadata["libpath"] = str(libpath) + metadata["representation"] = str(representation["_id"]) def remove(self, container: Dict) -> bool: - """Remove an existing container from a Blender scene. + """Remove an existing asset group from a Blender scene. Arguments: container (openpype:container-1.0): Container to remove, from `host.ls()`. Returns: - bool: Whether the container was deleted. - - Warning: - No nested collections are supported at the moment! + bool: Whether the asset group was deleted. """ + object_name = container["objectName"] + asset_group = bpy.data.objects.get(object_name) + libpath = asset_group.get(AVALON_PROPERTY).get('libpath') - collection = bpy.data.collections.get( - container["objectName"] - ) - if not collection: + # Check how many assets use the same library + count = 0 + for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects: + if obj.get(AVALON_PROPERTY).get('libpath') == libpath: + count += 1 + + if not asset_group: return False - assert not (collection.children), ( - "Nested collections are not supported." - ) - collection_metadata = collection.get( - blender.pipeline.AVALON_PROPERTY) + self._remove(asset_group) - obj_container = plugin.get_local_collection_with_name( - collection_metadata["obj_container"].name - ) - objects = obj_container.all_objects + bpy.data.objects.remove(asset_group) - self._remove(objects, obj_container) - - bpy.data.collections.remove(collection) + # If it is the last object to use that library, remove it + if count == 1: + library = bpy.data.libraries.get(bpy.path.basename(libpath)) + bpy.data.libraries.remove(library) return True diff --git a/openpype/hosts/blender/plugins/publish/extract_fbx.py b/openpype/hosts/blender/plugins/publish/extract_fbx.py index f95a0a3283..b91f2a75ef 100644 --- a/openpype/hosts/blender/plugins/publish/extract_fbx.py +++ b/openpype/hosts/blender/plugins/publish/extract_fbx.py @@ -21,8 +21,6 @@ class ExtractFBX(api.Extractor): filename = f"{instance.name}.fbx" filepath = os.path.join(stagingdir, filename) - scene = bpy.context.scene - # Perform extraction self.log.info("Performing extraction..") @@ -41,12 +39,16 @@ class ExtractFBX(api.Extractor): active=asset_group, selected=selected) new_materials = [] + new_materials_objs = [] + objects = list(asset_group.children) - for obj in collections[0].all_objects: - if obj.type == 'MESH': + for obj in objects: + objects.extend(obj.children) + if obj.type == 'MESH' and len(obj.data.materials) == 0: mat = bpy.data.materials.new(obj.name) obj.data.materials.append(mat) new_materials.append(mat) + new_materials_objs.append(obj) # We export the fbx bpy.ops.export_scene.fbx( @@ -63,9 +65,8 @@ class ExtractFBX(api.Extractor): for mat in new_materials: bpy.data.materials.remove(mat) - for obj in collections[0].all_objects: - if obj.type == 'MESH': - obj.data.materials.pop() + for obj in new_materials_objs: + obj.data.materials.pop() if "representations" not in instance.data: instance.data["representations"] = []