From 631942327edc0469834c5cbb2e328a4caae0586e Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 10:43:51 +0000 Subject: [PATCH 01/27] Creation and publishing of a rig in Blender --- pype/blender/plugin.py | 7 +++ pype/plugins/blender/create/create_rig.py | 32 +++++++++++++ pype/plugins/blender/publish/collect_rig.py | 53 +++++++++++++++++++++ pype/plugins/blender/publish/extract_rig.py | 47 ++++++++++++++++++ 4 files changed, 139 insertions(+) create mode 100644 pype/plugins/blender/create/create_rig.py create mode 100644 pype/plugins/blender/publish/collect_rig.py create mode 100644 pype/plugins/blender/publish/extract_rig.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index ad5a259785..eaa429c989 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -17,6 +17,13 @@ def model_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: name = f"{namespace}:{name}" return name +def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: + """Return a consistent name for a rig asset.""" + name = f"{asset}_{subset}" + if namespace: + name = f"{namespace}:{name}" + return name + class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py new file mode 100644 index 0000000000..01eb524eef --- /dev/null +++ b/pype/plugins/blender/create/create_rig.py @@ -0,0 +1,32 @@ +"""Create a rig asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateRig(Creator): + """Artist-friendly rig with controls to direct motion""" + + name = "rigMain" + label = "Rig" + family = "rig" + icon = "cube" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.rig_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py new file mode 100644 index 0000000000..a4b30541f6 --- /dev/null +++ b/pype/plugins/blender/publish/collect_rig.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectRig(pyblish.api.ContextPlugin): + """Collect the data of a rig.""" + + hosts = ["blender"] + label = "Collect Rig" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_rig_collections() -> Generator: + """Return all 'rig' collections. + + Check if the family is 'rig' and if it doesn't have the + representation set. If the representation is set, it is a loaded rig + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'rig' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the rigs from the current Blender scene.""" + collections = self.get_rig_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/extract_rig.py b/pype/plugins/blender/publish/extract_rig.py new file mode 100644 index 0000000000..8a3c83d07c --- /dev/null +++ b/pype/plugins/blender/publish/extract_rig.py @@ -0,0 +1,47 @@ +import os +import avalon.blender.workio + +import pype.api + + +class ExtractRig(pype.api.Extractor): + """Extract as rig.""" + + label = "Rig" + hosts = ["blender"] + families = ["rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From fd6bdc9aa5bb48a3ca252e493281d26f45120470 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 10:45:50 +0000 Subject: [PATCH 02/27] Loading and removal of rigs from Blender scenes --- pype/blender/plugin.py | 20 ++ pype/plugins/blender/load/load_rig.py | 337 ++++++++++++++++++++++++++ 2 files changed, 357 insertions(+) create mode 100644 pype/plugins/blender/load/load_rig.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index eaa429c989..c85e6df990 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -24,6 +24,26 @@ def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: name = f"{namespace}:{name}" return name +def create_blender_context( obj: Optional[bpy.types.Object] = None ): + """Create a new Blender context. If an object is passed as + parameter, it is set as selected and active. + """ + for win in bpy.context.window_manager.windows: + for area in win.screen.areas: + if area.type == 'VIEW_3D': + for region in area.regions: + if region.type == 'WINDOW': + override_context = { + 'window': win, + 'screen': win.screen, + 'area': area, + 'region': region, + 'scene': bpy.context.scene, + 'active_object': obj, + 'selected_objects': [obj] + } + return override_context + raise Exception( "Could not create a custom Blender context." ) class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py new file mode 100644 index 0000000000..f3c9e49f53 --- /dev/null +++ b/pype/plugins/blender/load/load_rig.py @@ -0,0 +1,337 @@ +"""Load a rig asset in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + +class BlendRigLoader(pype.blender.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. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["rig"] + representations = ["blend"] + + label = "Link Rig" + icon = "code-fork" + color = "orange" + + @staticmethod + def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: + """Find the collection(s) with name, loaded from libpath. + + Note: + It is assumed that only 1 matching collection is found. + """ + for collection in bpy.data.collections: + if collection.name != name: + continue + if collection.library is None: + continue + if not collection.library.filepath: + continue + collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) + normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) + if collection_lib_path == normalized_libpath: + return collection + return None + + @staticmethod + def _collection_contains_object( + collection: bpy.types.Collection, object: bpy.types.Object + ) -> bool: + """Check if the collection contains the object.""" + for obj in collection.objects: + if obj == object: + return True + return False + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.rig_name(asset, subset) + container_name = pype.blender.plugin.rig_name( + asset, subset, namespace + ) + 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] + + scene = bpy.context.scene + + container = bpy.data.collections[lib_container] + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + override_context = pype.blender.plugin.create_blender_context() + bpy.ops.object.collection_instance_add( override_context, name = container_name + "_CON" ) + + override_context = pype.blender.plugin.create_blender_context( bpy.data.objects[container_name + "_CON"] ) + bpy.ops.object.make_override_library( override_context ) + bpy.ops.object.delete( override_context ) + + container_metadata = container.get( 'avalon' ) + + object_names_list = [] + + for c in bpy.data.collections: + + if c.name == container_name + "_CON" and c.library is None: + + for obj in c.objects: + + scene.collection.objects.link( obj ) + c.objects.unlink( obj ) + + if not obj.get("avalon"): + obj["avalon"] = dict() + + avalon_info = obj["avalon"] + avalon_info.update( { "container_name": container_name } ) + + object_names_list.append( obj.name ) + + bpy.data.collections.remove( c ) + + container_metadata["objects"] = object_names_list + + bpy.ops.object.select_all( action = 'DESELECT' ) + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + def load(self, + context: dict, + name: Optional[str] = None, + namespace: Optional[str] = None, + options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: + """Load asset via database + + Arguments: + context: Full parenthood of representation to load + name: Use pre-defined name + namespace: Use pre-defined namespace + options: Additional settings dictionary + """ + # TODO (jasper): make it possible to add the asset several times by + # just re-using the collection + assert Path(self.fname).exists(), f"{self.fname} doesn't exist." + + self.process_asset( + context=context, + name=name, + namespace=namespace, + options=options, + ) + + # Only containerise if anything was loaded by the Loader. + nodes = self[:] + if not nodes: + return None + + # Only containerise if it's not already a collection from a .blend file. + representation = context["representation"]["name"] + if representation != "blend": + from avalon.blender.pipeline import containerise + return containerise( + name=name, + namespace=namespace, + nodes=nodes, + context=context, + loader=self.__class__.__name__, + ) + + asset = context["asset"]["name"] + subset = context["subset"]["name"] + instance_name = pype.blender.plugin.rig_name(asset, subset, namespace) + + return self._get_instance_collection(instance_name, nodes) + + 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! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + 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']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + collection_libpath = ( + self._get_library_from_container(collection).filepath + ) + print( collection_libpath ) + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + print( normalized_collection_libpath ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + print( normalized_libpath ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + # Let Blender's garbage collection take care of removing the library + # itself after removing the objects. + objects_to_remove = set() + collection_objects = list() + collection_objects[:] = collection.objects + for obj in collection_objects: + # Unlink every object + collection.objects.unlink(obj) + remove_obj = True + for coll in [ + coll for coll in bpy.data.collections + if coll != collection + ]: + if ( + coll.objects and + self._collection_contains_object(coll, obj) + ): + remove_obj = False + if remove_obj: + objects_to_remove.add(obj) + + for obj in objects_to_remove: + # Only delete objects that are not used elsewhere + bpy.data.objects.remove(obj) + + instance_empties = [ + obj for obj in collection.users_dupli_group + if obj.name in collection.name + ] + if instance_empties: + instance_empty = instance_empties[0] + container_name = instance_empty["avalon"]["container_name"] + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [container_name] + + new_collection = self._get_lib_collection(container_name, libpath) + if new_collection is None: + raise ValueError( + "A matching collection '{container_name}' " + "should have been found in: {libpath}" + ) + + for obj in new_collection.objects: + collection.objects.link(obj) + bpy.data.collections.remove(new_collection) + # Update the representation on the collection + avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_prop["representation"] = str(representation["_id"]) + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core: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! + """ + + print( container["objectName"] ) + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + instance_objects = list(collection.objects) + + data = collection.get( "avalon" ) + object_names = data["objects"] + + for obj in instance_objects: + bpy.data.objects.remove(obj) + + for name in object_names: + bpy.data.objects.remove( bpy.data.objects[name] ) + + bpy.data.collections.remove(collection) + + return True From e88fc5cb4891f0de74822551258687d5e29226ce Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 11:49:38 +0000 Subject: [PATCH 03/27] We now use references to objects in the metadata instead of names --- pype/plugins/blender/load/load_rig.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index f3c9e49f53..70cf6e781a 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -107,7 +107,7 @@ class BlendRigLoader(pype.blender.AssetLoader): container_metadata = container.get( 'avalon' ) - object_names_list = [] + objects_list = [] for c in bpy.data.collections: @@ -124,11 +124,11 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info = obj["avalon"] avalon_info.update( { "container_name": container_name } ) - object_names_list.append( obj.name ) + objects_list.append( obj ) bpy.data.collections.remove( c ) - container_metadata["objects"] = object_names_list + container_metadata["objects"] = objects_list bpy.ops.object.select_all( action = 'DESELECT' ) @@ -324,13 +324,13 @@ class BlendRigLoader(pype.blender.AssetLoader): instance_objects = list(collection.objects) data = collection.get( "avalon" ) - object_names = data["objects"] + objects = data["objects"] for obj in instance_objects: bpy.data.objects.remove(obj) - for name in object_names: - bpy.data.objects.remove( bpy.data.objects[name] ) + for obj in objects: + bpy.data.objects.remove( obj ) bpy.data.collections.remove(collection) From c123ed757ced7a0fc7bd9b82ce4824e03561573d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 15:31:44 +0000 Subject: [PATCH 04/27] Improved handling of rigs using avalon container for metadata only --- pype/plugins/blender/load/load_rig.py | 46 +++++++++++---------------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 70cf6e781a..b348f99728 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -81,12 +81,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) 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] - - scene = bpy.context.scene + bpy.data.collections.new( lib_container ) container = bpy.data.collections[lib_container] container.name = container_name @@ -98,35 +93,34 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - override_context = pype.blender.plugin.create_blender_context() - bpy.ops.object.collection_instance_add( override_context, name = container_name + "_CON" ) - - override_context = pype.blender.plugin.create_blender_context( bpy.data.objects[container_name + "_CON"] ) - bpy.ops.object.make_override_library( override_context ) - bpy.ops.object.delete( override_context ) - container_metadata = container.get( 'avalon' ) objects_list = [] - for c in bpy.data.collections: + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (data_from, data_to): - if c.name == container_name + "_CON" and c.library is None: + data_to.collections = [lib_container] - for obj in c.objects: + scene = bpy.context.scene - scene.collection.objects.link( obj ) - c.objects.unlink( obj ) + models = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] - if not obj.get("avalon"): - obj["avalon"] = dict() + for obj in models + armatures: - avalon_info = obj["avalon"] - avalon_info.update( { "container_name": container_name } ) + scene.collection.objects.link( obj ) - objects_list.append( obj ) + obj = obj.make_local() - bpy.data.collections.remove( c ) + if not obj.get("avalon"): + + obj["avalon"] = dict() + + avalon_info = obj["avalon"] + avalon_info.update( { "container_name": container_name } ) + objects_list.append( obj ) container_metadata["objects"] = objects_list @@ -321,14 +315,10 @@ class BlendRigLoader(pype.blender.AssetLoader): assert not (collection.children), ( "Nested collections are not supported." ) - instance_objects = list(collection.objects) data = collection.get( "avalon" ) objects = data["objects"] - for obj in instance_objects: - bpy.data.objects.remove(obj) - for obj in objects: bpy.data.objects.remove( obj ) From 88b9ceccab25d1785ab73849d83b8ba7c21e8f98 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 4 Feb 2020 15:45:17 +0000 Subject: [PATCH 05/27] More comments for clarity --- pype/plugins/blender/load/load_rig.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index b348f99728..75aa515c66 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -105,10 +105,13 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - models = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] - for obj in models + armatures: + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: scene.collection.objects.link( obj ) @@ -122,6 +125,7 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info.update( { "container_name": container_name } ) objects_list.append( obj ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list bpy.ops.object.select_all( action = 'DESELECT' ) From fc1779387149902705034f9d2101e7cf5a1acf72 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 5 Feb 2020 15:20:15 +0000 Subject: [PATCH 06/27] Implemented update for rigs --- pype/plugins/blender/load/load_rig.py | 124 +++++++++++++------------- 1 file changed, 64 insertions(+), 60 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 75aa515c66..294366c41b 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -93,14 +93,14 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( 'avalon' ) + container_metadata = container.get( avalon.blender.pipeline.AVALON_PROPERTY ) - objects_list = [] + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (data_from, data_to): - data_to.collections = [lib_container] scene = bpy.context.scene @@ -108,6 +108,8 @@ class BlendRigLoader(pype.blender.AssetLoader): meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + objects_list = [] + # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. @@ -117,17 +119,19 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() - if not obj.get("avalon"): + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - obj["avalon"] = dict() + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj["avalon"] + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] avalon_info.update( { "container_name": container_name } ) objects_list.append( obj ) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list + bpy.data.collections.remove( bpy.data.collections[lib_container] ) + bpy.ops.object.select_all( action = 'DESELECT' ) nodes = list(container.objects) @@ -198,6 +202,7 @@ class BlendRigLoader(pype.blender.AssetLoader): collection = bpy.data.collections.get( container["objectName"] ) + libpath = Path(api.get_representation_path(representation)) extension = libpath.suffix.lower() @@ -222,18 +227,14 @@ class BlendRigLoader(pype.blender.AssetLoader): assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) - collection_libpath = ( - self._get_library_from_container(collection).filepath - ) - print( collection_libpath ) + + collection_libpath = container["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) - print( normalized_collection_libpath ) normalized_libpath = ( str(Path(bpy.path.abspath(str(libpath))).resolve()) ) - print( normalized_libpath ) logger.debug( "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", normalized_collection_libpath, @@ -242,58 +243,63 @@ class BlendRigLoader(pype.blender.AssetLoader): if normalized_collection_libpath == normalized_libpath: logger.info("Library already loaded, not updating...") return - # Let Blender's garbage collection take care of removing the library - # itself after removing the objects. - objects_to_remove = set() - collection_objects = list() - collection_objects[:] = collection.objects - for obj in collection_objects: - # Unlink every object - collection.objects.unlink(obj) - remove_obj = True - for coll in [ - coll for coll in bpy.data.collections - if coll != collection - ]: - if ( - coll.objects and - self._collection_contains_object(coll, obj) - ): - remove_obj = False - if remove_obj: - objects_to_remove.add(obj) - for obj in objects_to_remove: - # Only delete objects that are not used elsewhere - bpy.data.objects.remove(obj) + # Get the armature of the rig + armatures = [ obj for obj in container["objects"] if obj.type == 'ARMATURE' ] + assert( len( armatures ) == 1 ) - instance_empties = [ - obj for obj in collection.users_dupli_group - if obj.name in collection.name - ] - if instance_empties: - instance_empty = instance_empties[0] - container_name = instance_empty["avalon"]["container_name"] + action = armatures[0].animation_data.action + + for obj in container["objects"]: + bpy.data.objects.remove( obj ) + + lib_container = container["lib_container"] relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative ) as (_, data_to): - data_to.collections = [container_name] + data_to.collections = [lib_container] - new_collection = self._get_lib_collection(container_name, libpath) - if new_collection is None: - raise ValueError( - "A matching collection '{container_name}' " - "should have been found in: {libpath}" - ) + scene = bpy.context.scene - for obj in new_collection.objects: - collection.objects.link(obj) - bpy.data.collections.remove(new_collection) - # Update the representation on the collection - avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_prop["representation"] = str(representation["_id"]) + meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] + armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + objects_list = [] + + assert( len( armatures ) == 1 ) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + scene.collection.objects.link( obj ) + + obj = obj.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update( { "container_name": collection.name } ) + objects_list.append( obj ) + + if obj.type == 'ARMATURE' and action is not None: + + obj.animation_data.action = action + + collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.data.collections.remove( bpy.data.collections[lib_container] ) + + bpy.ops.object.select_all( action = 'DESELECT' ) def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -309,8 +315,6 @@ class BlendRigLoader(pype.blender.AssetLoader): No nested collections are supported at the moment! """ - print( container["objectName"] ) - collection = bpy.data.collections.get( container["objectName"] ) @@ -320,12 +324,12 @@ class BlendRigLoader(pype.blender.AssetLoader): "Nested collections are not supported." ) - data = collection.get( "avalon" ) - objects = data["objects"] + collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY ) + objects = collection_metadata["objects"] for obj in objects: bpy.data.objects.remove( obj ) - bpy.data.collections.remove(collection) + bpy.data.collections.remove( collection ) return True From 4bd4c6e811f52a4cfc74551d53b3a9fde2e857a3 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 5 Feb 2020 16:49:27 +0000 Subject: [PATCH 07/27] The data in the objects is made local as well. Not having this would cause problems with the keyframing of shape keys and custom data. --- pype/plugins/blender/load/load_rig.py | 28 +++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 294366c41b..fa0e1c52b2 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -119,6 +119,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() + obj.data.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() @@ -228,7 +230,9 @@ class BlendRigLoader(pype.blender.AssetLoader): f"Unsupported file: {libpath}" ) - collection_libpath = container["libpath"] + collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -245,15 +249,19 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [ obj for obj in container["objects"] if obj.type == 'ARMATURE' ] + armatures = [ obj for obj in collection_metadata["objects"] if obj.type == 'ARMATURE' ] assert( len( armatures ) == 1 ) action = armatures[0].animation_data.action - for obj in container["objects"]: - bpy.data.objects.remove( obj ) + for obj in collection_metadata["objects"]: - lib_container = container["lib_container"] + if obj.type == 'ARMATURE': + bpy.data.armatures.remove( obj.data ) + elif obj.type == 'MESH': + bpy.data.meshes.remove( obj.data ) + + lib_container = collection_metadata["lib_container"] relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( @@ -278,6 +286,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj = obj.make_local() + obj.data.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() @@ -290,8 +300,6 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action - collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) - # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) @@ -328,7 +336,11 @@ class BlendRigLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] for obj in objects: - bpy.data.objects.remove( obj ) + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove( obj.data ) + elif obj.type == 'MESH': + bpy.data.meshes.remove( obj.data ) bpy.data.collections.remove( collection ) From 6f6482a58b4aa328af07ccaae9c2e66ccf7ecc7f Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 7 Feb 2020 11:39:14 +0000 Subject: [PATCH 08/27] Rig is linked in a collection, instead of the generic scene collection --- pype/plugins/blender/load/load_rig.py | 79 ++++++++++++++++----------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index fa0e1c52b2..7ea131a54c 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -12,6 +12,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + class BlendRigLoader(pype.blender.AssetLoader): """Load rigs from a .blend file. @@ -44,8 +45,10 @@ class BlendRigLoader(pype.blender.AssetLoader): continue if not collection.library.filepath: continue - collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) + collection_lib_path = str( + Path(bpy.path.abspath(collection.library.filepath)).resolve()) + normalized_libpath = str( + Path(bpy.path.abspath(str(libpath))).resolve()) if collection_lib_path == normalized_libpath: return collection return None @@ -81,7 +84,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.data.collections.new( lib_container ) + bpy.data.collections.new(lib_container) container = bpy.data.collections[lib_container] container.name = container_name @@ -93,7 +96,8 @@ class BlendRigLoader(pype.blender.AssetLoader): self.__class__.__name__, ) - container_metadata = container.get( avalon.blender.pipeline.AVALON_PROPERTY ) + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -105,8 +109,13 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] - armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] @@ -115,8 +124,6 @@ class BlendRigLoader(pype.blender.AssetLoader): # when it is made local. for obj in meshes + armatures: - scene.collection.objects.link( obj ) - obj = obj.make_local() obj.data.make_local() @@ -126,15 +133,13 @@ class BlendRigLoader(pype.blender.AssetLoader): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update( { "container_name": container_name } ) - objects_list.append( obj ) + avalon_info.update({"container_name": container_name}) + objects_list.append(obj) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.data.collections.remove( bpy.data.collections[lib_container] ) - - bpy.ops.object.select_all( action = 'DESELECT' ) + bpy.ops.object.select_all(action='DESELECT') nodes = list(container.objects) nodes.append(container) @@ -230,7 +235,8 @@ class BlendRigLoader(pype.blender.AssetLoader): f"Unsupported file: {libpath}" ) - collection_metadata = collection.get(avalon.blender.pipeline.AVALON_PROPERTY) + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -249,20 +255,23 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [ obj for obj in collection_metadata["objects"] if obj.type == 'ARMATURE' ] - assert( len( armatures ) == 1 ) + armatures = [obj for obj in collection_metadata["objects"] + if obj.type == 'ARMATURE'] + assert(len(armatures) == 1) action = armatures[0].animation_data.action for obj in collection_metadata["objects"]: if obj.type == 'ARMATURE': - bpy.data.armatures.remove( obj.data ) + bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': - bpy.data.meshes.remove( obj.data ) + bpy.data.meshes.remove(obj.data) lib_container = collection_metadata["lib_container"] + bpy.data.collections.remove(bpy.data.collections[lib_container]) + relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative @@ -271,19 +280,22 @@ class BlendRigLoader(pype.blender.AssetLoader): scene = bpy.context.scene - meshes = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'MESH' ] - armatures = [ obj for obj in bpy.data.collections[lib_container].objects if obj.type == 'ARMATURE' ] + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] - assert( len( armatures ) == 1 ) + assert(len(armatures) == 1) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - scene.collection.objects.link( obj ) - obj = obj.make_local() obj.data.make_local() @@ -293,8 +305,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update( { "container_name": collection.name } ) - objects_list.append( obj ) + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) if obj.type == 'ARMATURE' and action is not None: @@ -305,9 +317,7 @@ class BlendRigLoader(pype.blender.AssetLoader): collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) - bpy.data.collections.remove( bpy.data.collections[lib_container] ) - - bpy.ops.object.select_all( action = 'DESELECT' ) + bpy.ops.object.select_all(action='DESELECT') def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -332,16 +342,19 @@ class BlendRigLoader(pype.blender.AssetLoader): "Nested collections are not supported." ) - collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY ) + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] for obj in objects: if obj.type == 'ARMATURE': - bpy.data.armatures.remove( obj.data ) + bpy.data.armatures.remove(obj.data) elif obj.type == 'MESH': - bpy.data.meshes.remove( obj.data ) - - bpy.data.collections.remove( collection ) + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) return True From 65104882db7a44ba3d5e213aae9f7cee0c572c93 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 7 Feb 2020 15:09:21 +0000 Subject: [PATCH 09/27] Changed handling of models for consistency --- pype/plugins/blender/load/load_model.py | 196 ++++++++++++------------ pype/plugins/blender/load/load_rig.py | 39 +---- 2 files changed, 98 insertions(+), 137 deletions(-) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index bd6db17650..bb9f2250be 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -12,7 +12,6 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") - class BlendModelLoader(pype.blender.AssetLoader): """Load models from a .blend file. @@ -31,36 +30,6 @@ class BlendModelLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod - def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: - """Find the collection(s) with name, loaded from libpath. - - Note: - It is assumed that only 1 matching collection is found. - """ - for collection in bpy.data.collections: - if collection.name != name: - continue - if collection.library is None: - continue - if not collection.library.filepath: - continue - collection_lib_path = str(Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str(Path(bpy.path.abspath(str(libpath))).resolve()) - if collection_lib_path == normalized_libpath: - return collection - return None - - @staticmethod - def _collection_contains_object( - collection: bpy.types.Collection, object: bpy.types.Object - ) -> bool: - """Check if the collection contains the object.""" - for obj in collection.objects: - if obj == object: - return True - return False - def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -82,25 +51,8 @@ class BlendModelLoader(pype.blender.AssetLoader): ) 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] - - scene = bpy.context.scene - instance_empty = bpy.data.objects.new( - container_name, None - ) - if not instance_empty.get("avalon"): - instance_empty["avalon"] = dict() - avalon_info = instance_empty["avalon"] - avalon_info.update({"container_name": container_name}) - scene.collection.objects.link(instance_empty) - instance_empty.instance_type = 'COLLECTION' - container = bpy.data.collections[lib_container] + container = bpy.data.collections.new(lib_container) container.name = container_name - instance_empty.instance_collection = container - container.make_local() avalon.blender.pipeline.containerise_existing( container, name, @@ -109,9 +61,47 @@ class BlendModelLoader(pype.blender.AssetLoader): self.__class__.__name__, ) + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + for obj in rig_container.objects: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + nodes = list(container.objects) nodes.append(container) - nodes.append(instance_empty) self[:] = nodes return nodes @@ -154,9 +144,11 @@ class BlendModelLoader(pype.blender.AssetLoader): assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( f"Unsupported file: {libpath}" ) - collection_libpath = ( - self._get_library_from_container(collection).filepath - ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -171,58 +163,52 @@ class BlendModelLoader(pype.blender.AssetLoader): if normalized_collection_libpath == normalized_libpath: logger.info("Library already loaded, not updating...") return - # Let Blender's garbage collection take care of removing the library - # itself after removing the objects. - objects_to_remove = set() - collection_objects = list() - collection_objects[:] = collection.objects - for obj in collection_objects: - # Unlink every object - collection.objects.unlink(obj) - remove_obj = True - for coll in [ - coll for coll in bpy.data.collections - if coll != collection - ]: - if ( - coll.objects and - self._collection_contains_object(coll, obj) - ): - remove_obj = False - if remove_obj: - objects_to_remove.add(obj) - for obj in objects_to_remove: - # Only delete objects that are not used elsewhere - bpy.data.objects.remove(obj) + for obj in collection_metadata["objects"]: - instance_empties = [ - obj for obj in collection.users_dupli_group - if obj.name in collection.name - ] - if instance_empties: - instance_empty = instance_empties[0] - container_name = instance_empty["avalon"]["container_name"] + bpy.data.meshes.remove(obj.data) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( str(libpath), link=True, relative=relative ) as (_, data_to): - data_to.collections = [container_name] + data_to.collections = [lib_container] - new_collection = self._get_lib_collection(container_name, libpath) - if new_collection is None: - raise ValueError( - "A matching collection '{container_name}' " - "should have been found in: {libpath}" - ) + scene = bpy.context.scene - for obj in new_collection.objects: - collection.objects.link(obj) - bpy.data.collections.remove(new_collection) - # Update the representation on the collection - avalon_prop = collection[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_prop["representation"] = str(representation["_id"]) + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in rig_container.objects: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -245,10 +231,17 @@ class BlendModelLoader(pype.blender.AssetLoader): assert not (collection.children), ( "Nested collections are not supported." ) - instance_parents = list(collection.users_dupli_group) - instance_objects = list(collection.objects) - for obj in instance_objects + instance_parents: - bpy.data.objects.remove(obj) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) bpy.data.collections.remove(collection) return True @@ -281,7 +274,8 @@ class CacheModelLoader(pype.blender.AssetLoader): context: Full parenthood of representation to load options: Additional settings dictionary """ - raise NotImplementedError("Loading of Alembic files is not yet implemented.") + raise NotImplementedError( + "Loading of Alembic files is not yet implemented.") # TODO (jasper): implement Alembic import. libpath = self.fname diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 7ea131a54c..8593440624 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -31,38 +31,6 @@ class BlendRigLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod - def _get_lib_collection(name: str, libpath: Path) -> Optional[bpy.types.Collection]: - """Find the collection(s) with name, loaded from libpath. - - Note: - It is assumed that only 1 matching collection is found. - """ - for collection in bpy.data.collections: - if collection.name != name: - continue - if collection.library is None: - continue - if not collection.library.filepath: - continue - collection_lib_path = str( - Path(bpy.path.abspath(collection.library.filepath)).resolve()) - normalized_libpath = str( - Path(bpy.path.abspath(str(libpath))).resolve()) - if collection_lib_path == normalized_libpath: - return collection - return None - - @staticmethod - def _collection_contains_object( - collection: bpy.types.Collection, object: bpy.types.Object - ) -> bool: - """Check if the collection contains the object.""" - for obj in collection.objects: - if obj == object: - return True - return False - def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, options: Optional[Dict] = None @@ -84,9 +52,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) relative = bpy.context.preferences.filepaths.use_relative_paths - bpy.data.collections.new(lib_container) - - container = bpy.data.collections[lib_container] + container = bpy.data.collections.new(lib_container) container.name = container_name avalon.blender.pipeline.containerise_existing( container, @@ -104,7 +70,7 @@ class BlendRigLoader(pype.blender.AssetLoader): with bpy.data.libraries.load( libpath, link=True, relative=relative - ) as (data_from, data_to): + ) as (_, data_to): data_to.collections = [lib_container] scene = bpy.context.scene @@ -134,6 +100,7 @@ class BlendRigLoader(pype.blender.AssetLoader): avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) + objects_list.append(obj) # Save the list of objects in the metadata container From 951dcfca3e7f9c02e8d878fea4d693f950b7fada Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 10 Feb 2020 14:45:50 +0000 Subject: [PATCH 10/27] Code optimization --- pype/blender/plugin.py | 13 ++---- pype/plugins/blender/create/create_model.py | 2 +- pype/plugins/blender/create/create_rig.py | 4 +- pype/plugins/blender/load/load_model.py | 15 +++--- pype/plugins/blender/load/load_rig.py | 51 +-------------------- 5 files changed, 16 insertions(+), 69 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index c85e6df990..b441714c0d 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -10,15 +10,8 @@ from avalon import api VALID_EXTENSIONS = [".blend"] -def model_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: - """Return a consistent name for a model asset.""" - name = f"{asset}_{subset}" - if namespace: - name = f"{namespace}:{name}" - return name - -def rig_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: - """Return a consistent name for a rig asset.""" +def asset_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: + """Return a consistent name for an asset.""" name = f"{asset}_{subset}" if namespace: name = f"{namespace}:{name}" @@ -149,7 +142,7 @@ class AssetLoader(api.Loader): asset = context["asset"]["name"] subset = context["subset"]["name"] - instance_name = model_name(asset, subset, namespace) + instance_name = asset_name(asset, subset, namespace) return self._get_instance_collection(instance_name, nodes) diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index 7301073f05..a3b2ffc55b 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -19,7 +19,7 @@ class CreateModel(Creator): asset = self.data["asset"] subset = self.data["subset"] - name = pype.blender.plugin.model_name(asset, subset) + name = pype.blender.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) self.data['task'] = api.Session.get('AVALON_TASK') diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 01eb524eef..5d83fafdd3 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -12,14 +12,14 @@ class CreateRig(Creator): name = "rigMain" label = "Rig" family = "rig" - icon = "cube" + icon = "wheelchair" def process(self): import pype.blender asset = self.data["asset"] subset = self.data["subset"] - name = pype.blender.plugin.rig_name(asset, subset) + name = pype.blender.plugin.asset_name(asset, subset) collection = bpy.data.collections.new(name=name) bpy.context.scene.collection.children.link(collection) self.data['task'] = api.Session.get('AVALON_TASK') diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index bb9f2250be..cde4109a7c 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -12,6 +12,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + class BlendModelLoader(pype.blender.AssetLoader): """Load models from a .blend file. @@ -45,8 +46,8 @@ class BlendModelLoader(pype.blender.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.blender.plugin.model_name(asset, subset) - container_name = pype.blender.plugin.model_name( + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) relative = bpy.context.preferences.filepaths.use_relative_paths @@ -76,11 +77,11 @@ class BlendModelLoader(pype.blender.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - rig_container = scene.collection.children[lib_container].make_local() + model_container = scene.collection.children[lib_container].make_local() objects_list = [] - for obj in rig_container.objects: + for obj in model_container.objects: obj = obj.make_local() @@ -182,14 +183,14 @@ class BlendModelLoader(pype.blender.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - rig_container = scene.collection.children[lib_container].make_local() + model_container = scene.collection.children[lib_container].make_local() objects_list = [] # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in rig_container.objects: + for obj in model_container.objects: obj = obj.make_local() @@ -283,7 +284,7 @@ class CacheModelLoader(pype.blender.AssetLoader): subset = context["subset"]["name"] # TODO (jasper): evaluate use of namespace which is 'alien' to Blender. lib_container = container_name = ( - pype.blender.plugin.model_name(asset, subset, namespace) + pype.blender.plugin.asset_name(asset, subset, namespace) ) relative = bpy.context.preferences.filepaths.use_relative_paths diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 8593440624..361850c51b 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -46,8 +46,8 @@ class BlendRigLoader(pype.blender.AssetLoader): libpath = self.fname asset = context["asset"]["name"] subset = context["subset"]["name"] - lib_container = pype.blender.plugin.rig_name(asset, subset) - container_name = pype.blender.plugin.rig_name( + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) relative = bpy.context.preferences.filepaths.use_relative_paths @@ -113,53 +113,6 @@ class BlendRigLoader(pype.blender.AssetLoader): self[:] = nodes return nodes - def load(self, - context: dict, - name: Optional[str] = None, - namespace: Optional[str] = None, - options: Optional[Dict] = None) -> Optional[bpy.types.Collection]: - """Load asset via database - - Arguments: - context: Full parenthood of representation to load - name: Use pre-defined name - namespace: Use pre-defined namespace - options: Additional settings dictionary - """ - # TODO (jasper): make it possible to add the asset several times by - # just re-using the collection - assert Path(self.fname).exists(), f"{self.fname} doesn't exist." - - self.process_asset( - context=context, - name=name, - namespace=namespace, - options=options, - ) - - # Only containerise if anything was loaded by the Loader. - nodes = self[:] - if not nodes: - return None - - # Only containerise if it's not already a collection from a .blend file. - representation = context["representation"]["name"] - if representation != "blend": - from avalon.blender.pipeline import containerise - return containerise( - name=name, - namespace=namespace, - nodes=nodes, - context=context, - loader=self.__class__.__name__, - ) - - asset = context["asset"]["name"] - subset = context["subset"]["name"] - instance_name = pype.blender.plugin.rig_name(asset, subset, namespace) - - return self._get_instance_collection(instance_name, nodes) - def update(self, container: Dict, representation: Dict): """Update the loaded asset. From 2b6e90ffe00a44a7f9e972389a50c3c8b20fb972 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 10 Feb 2020 14:55:04 +0000 Subject: [PATCH 11/27] Creation, loading, and management of animations --- .../blender/create/create_animation.py | 30 ++ pype/plugins/blender/load/load_animation.py | 274 ++++++++++++++++++ .../blender/publish/collect_animation.py | 53 ++++ .../blender/publish/extract_animation.py | 47 +++ 4 files changed, 404 insertions(+) create mode 100644 pype/plugins/blender/create/create_animation.py create mode 100644 pype/plugins/blender/load/load_animation.py create mode 100644 pype/plugins/blender/publish/collect_animation.py create mode 100644 pype/plugins/blender/publish/extract_animation.py diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py new file mode 100644 index 0000000000..cfe569f918 --- /dev/null +++ b/pype/plugins/blender/create/create_animation.py @@ -0,0 +1,30 @@ +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateAnimation(Creator): + """Animation output for character rigs""" + + name = "animationMain" + label = "Animation" + family = "animation" + icon = "male" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + collection.objects.link(obj) + + return collection diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py new file mode 100644 index 0000000000..5b527e1717 --- /dev/null +++ b/pype/plugins/blender/load/load_animation.py @@ -0,0 +1,274 @@ +"""Load an animation in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_model") + + +class BlendAnimationLoader(pype.blender.AssetLoader): + """Load animations 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. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["animation"] + representations = ["blend"] + + label = "Link Animation" + icon = "code-fork" + color = "orange" + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + 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! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + 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']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + # Get the armature of the rig + armatures = [obj for obj in collection_metadata["objects"] + if obj.type == 'ARMATURE'] + assert(len(armatures) == 1) + + for obj in collection_metadata["objects"]: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + objects_list = [] + + assert(len(armatures) == 1) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + objects_list.append(obj) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core: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! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py new file mode 100644 index 0000000000..9bc0b02227 --- /dev/null +++ b/pype/plugins/blender/publish/collect_animation.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectAnimation(pyblish.api.ContextPlugin): + """Collect the data of an animation.""" + + hosts = ["blender"] + label = "Collect Animation" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_animation_collections() -> Generator: + """Return all 'animation' collections. + + Check if the family is 'animation' and if it doesn't have the + representation set. If the representation is set, it is a loaded rig + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'animation' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the animations from the current Blender scene.""" + collections = self.get_animation_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/extract_animation.py b/pype/plugins/blender/publish/extract_animation.py new file mode 100644 index 0000000000..dbfe29af83 --- /dev/null +++ b/pype/plugins/blender/publish/extract_animation.py @@ -0,0 +1,47 @@ +import os +import avalon.blender.workio + +import pype.api + + +class ExtractAnimation(pype.api.Extractor): + """Extract as animation.""" + + label = "Animation" + hosts = ["blender"] + families = ["animation"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From 0a561382f2dcb716fc2c311c7b94ee712383ff26 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 12 Feb 2020 12:53:48 +0000 Subject: [PATCH 12/27] Fixed a problem where loaded assets were collected for publishing --- pype/plugins/blender/load/load_animation.py | 4 ++++ pype/plugins/blender/load/load_model.py | 4 ++++ pype/plugins/blender/load/load_rig.py | 4 ++++ 3 files changed, 12 insertions(+) diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 5b527e1717..58a0e94665 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -103,6 +103,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): objects_list.append(obj) + animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -226,6 +228,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): avalon_info.update({"container_name": collection.name}) objects_list.append(obj) + animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index cde4109a7c..40d6c3434c 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -96,6 +96,8 @@ class BlendModelLoader(pype.blender.AssetLoader): objects_list.append(obj) + model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -204,6 +206,8 @@ class BlendModelLoader(pype.blender.AssetLoader): avalon_info.update({"container_name": collection.name}) objects_list.append(obj) + model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 361850c51b..c19717cd82 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -103,6 +103,8 @@ class BlendRigLoader(pype.blender.AssetLoader): objects_list.append(obj) + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -232,6 +234,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) From 875ca5cd6fd4f0da67b2dba80897dd27b6505bda Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Feb 2020 16:26:10 +0000 Subject: [PATCH 13/27] Extraction for blend files is now handled by a single class --- .../blender/publish/collect_current_file.py | 2 + ...{extract_animation.py => extract_blend.py} | 93 +++++++++---------- pype/plugins/blender/publish/extract_model.py | 47 ---------- pype/plugins/blender/publish/extract_rig.py | 47 ---------- 4 files changed, 48 insertions(+), 141 deletions(-) rename pype/plugins/blender/publish/{extract_animation.py => extract_blend.py} (86%) delete mode 100644 pype/plugins/blender/publish/extract_model.py delete mode 100644 pype/plugins/blender/publish/extract_rig.py diff --git a/pype/plugins/blender/publish/collect_current_file.py b/pype/plugins/blender/publish/collect_current_file.py index a097c72047..926d290b31 100644 --- a/pype/plugins/blender/publish/collect_current_file.py +++ b/pype/plugins/blender/publish/collect_current_file.py @@ -14,3 +14,5 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): """Inject the current working file""" current_file = bpy.data.filepath context.data['currentFile'] = current_file + + assert current_file != '', "Current file is empty. Save the file before continuing." diff --git a/pype/plugins/blender/publish/extract_animation.py b/pype/plugins/blender/publish/extract_blend.py similarity index 86% rename from pype/plugins/blender/publish/extract_animation.py rename to pype/plugins/blender/publish/extract_blend.py index dbfe29af83..7e11e9ef8d 100644 --- a/pype/plugins/blender/publish/extract_animation.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -1,47 +1,46 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractAnimation(pype.api.Extractor): - """Extract as animation.""" - - label = "Animation" - hosts = ["blender"] - families = ["animation"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) +import os +import avalon.blender.workio + +import pype.api + + +class ExtractBlend(pype.api.Extractor): + """Extract a blend file.""" + + label = "Extract Blend" + hosts = ["blender"] + families = ["animation", "model", "rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.blend" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + # Just save the file to a temporary location. At least for now it's no + # problem to have (possibly) extra stuff in the file. + avalon.blender.workio.save_file(filepath, copy=True) + # + # # Store reference for integration + # if "files" not in instance.data: + # instance.data["files"] = list() + # + # # instance.data["files"].append(filename) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'blend', + 'ext': 'blend', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_model.py b/pype/plugins/blender/publish/extract_model.py deleted file mode 100644 index 501c4d9d5c..0000000000 --- a/pype/plugins/blender/publish/extract_model.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractModel(pype.api.Extractor): - """Extract as model.""" - - label = "Model" - hosts = ["blender"] - families = ["model"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_rig.py b/pype/plugins/blender/publish/extract_rig.py deleted file mode 100644 index 8a3c83d07c..0000000000 --- a/pype/plugins/blender/publish/extract_rig.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -import avalon.blender.workio - -import pype.api - - -class ExtractRig(pype.api.Extractor): - """Extract as rig.""" - - label = "Rig" - hosts = ["blender"] - families = ["rig"] - optional = True - - def process(self, instance): - # Define extract output file path - - stagingdir = self.staging_dir(instance) - filename = f"{instance.name}.blend" - filepath = os.path.join(stagingdir, filename) - - # Perform extraction - self.log.info("Performing extraction..") - - # Just save the file to a temporary location. At least for now it's no - # problem to have (possibly) extra stuff in the file. - avalon.blender.workio.save_file(filepath, copy=True) - # - # # Store reference for integration - # if "files" not in instance.data: - # instance.data["files"] = list() - # - # # instance.data["files"].append(filename) - - if "representations" not in instance.data: - instance.data["representations"] = [] - - representation = { - 'name': 'blend', - 'ext': 'blend', - 'files': filename, - "stagingDir": stagingdir, - } - instance.data["representations"].append(representation) - - - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) From f2733c0a1b05bff6afa7b21a00c84beb436ca31d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Mon, 24 Feb 2020 16:26:40 +0000 Subject: [PATCH 14/27] Implemented extraction to FBX files --- pype/plugins/blender/publish/extract_fbx.py | 71 +++++++++++ .../blender/publish/extract_fbx_animation.py | 118 ++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 pype/plugins/blender/publish/extract_fbx.py create mode 100644 pype/plugins/blender/publish/extract_fbx_animation.py diff --git a/pype/plugins/blender/publish/extract_fbx.py b/pype/plugins/blender/publish/extract_fbx.py new file mode 100644 index 0000000000..95466c1d2b --- /dev/null +++ b/pype/plugins/blender/publish/extract_fbx.py @@ -0,0 +1,71 @@ +import os +import avalon.blender.workio + +import pype.api + +import bpy + +class ExtractFBX(pype.api.Extractor): + """Extract as FBX.""" + + label = "Extract FBX" + hosts = ["blender"] + families = ["model", "rig"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one collection collected for this asset" + + old_active_layer_collection = bpy.context.view_layer.active_layer_collection + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection (but there is the vice versa). + layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + bpy.context.view_layer.active_layer_collection = layer_collections[0] + + old_scale = bpy.context.scene.unit_settings.scale_length + + # We set the scale of the scene for the export + bpy.context.scene.unit_settings.scale_length = 0.01 + + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=True, + mesh_smooth_type='FACE', + add_leaf_bones=False + ) + + bpy.context.view_layer.active_layer_collection = old_active_layer_collection + + bpy.context.scene.unit_settings.scale_length = old_scale + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py new file mode 100644 index 0000000000..bc088f8bb7 --- /dev/null +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -0,0 +1,118 @@ +import os +import avalon.blender.workio + +import pype.api + +import bpy +import bpy_extras +import bpy_extras.anim_utils + + +class ExtractAnimationFBX(pype.api.Extractor): + """Extract as animation.""" + + label = "Extract FBX" + hosts = ["blender"] + families = ["animation"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one collection collected for this asset" + + old_active_layer_collection = bpy.context.view_layer.active_layer_collection + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection (but there is the vice versa). + layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + bpy.context.view_layer.active_layer_collection = layer_collections[0] + + old_scale = bpy.context.scene.unit_settings.scale_length + + # We set the scale of the scene for the export + bpy.context.scene.unit_settings.scale_length = 0.01 + + # We export all the objects in the collection + objects_to_export = collections[0].objects + + object_action_pairs = [] + original_actions = [] + + starting_frames = [] + ending_frames = [] + + # For each object, we make a copy of the current action + for obj in objects_to_export: + + curr_action = obj.animation_data.action + copy_action = curr_action.copy() + + object_action_pairs.append((obj, copy_action)) + original_actions.append(curr_action) + + curr_frame_range = curr_action.frame_range + + starting_frames.append( curr_frame_range[0] ) + ending_frames.append( curr_frame_range[1] ) + + # 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 + ) + + # We export the fbx + bpy.ops.export_scene.fbx( + filepath=filepath, + use_active_collection=True, + bake_anim_use_nla_strips=False, + bake_anim_use_all_actions=False, + add_leaf_bones=False + ) + + bpy.context.view_layer.active_layer_collection = old_active_layer_collection + + bpy.context.scene.unit_settings.scale_length = old_scale + + # We delete the baked action and set the original one back + for i in range(0, len(object_action_pairs)): + + object_action_pairs[i][0].animation_data.action = original_actions[i] + + object_action_pairs[i][1].user_clear() + bpy.data.actions.remove(object_action_pairs[i][1]) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'fbx', + 'ext': 'fbx', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) From ec411a4b5dfd4b1bef1553236f7c0820de9581b2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 4 Mar 2020 17:00:00 +0000 Subject: [PATCH 15/27] Added 'action' family and small adjustments --- pype/plugins/blender/create/create_action.py | 38 +++ .../blender/create/create_animation.py | 2 + pype/plugins/blender/load/load_action.py | 295 ++++++++++++++++++ pype/plugins/blender/load/load_animation.py | 9 +- pype/plugins/blender/load/load_model.py | 4 + .../plugins/blender/publish/collect_action.py | 53 ++++ .../blender/publish/collect_animation.py | 2 +- pype/plugins/blender/publish/extract_blend.py | 2 +- pype/plugins/global/publish/integrate_new.py | 3 +- 9 files changed, 401 insertions(+), 7 deletions(-) create mode 100644 pype/plugins/blender/create/create_action.py create mode 100644 pype/plugins/blender/load/load_action.py create mode 100644 pype/plugins/blender/publish/collect_action.py diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py new file mode 100644 index 0000000000..88ecebdfff --- /dev/null +++ b/pype/plugins/blender/create/create_action.py @@ -0,0 +1,38 @@ +"""Create an animation asset.""" + +import bpy + +from avalon import api +from avalon.blender import Creator, lib + + +class CreateAction(Creator): + """Action output for character rigs""" + + name = "actionMain" + label = "Action" + family = "action" + icon = "male" + + def process(self): + import pype.blender + + asset = self.data["asset"] + subset = self.data["subset"] + name = pype.blender.plugin.asset_name(asset, subset) + collection = bpy.data.collections.new(name=name) + bpy.context.scene.collection.children.link(collection) + self.data['task'] = api.Session.get('AVALON_TASK') + lib.imprint(collection, self.data) + + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): + if obj.animation_data is not None and obj.animation_data.action is not None: + + empty_obj = bpy.data.objects.new( name = name, object_data = None ) + empty_obj.animation_data_create() + empty_obj.animation_data.action = obj.animation_data.action + empty_obj.animation_data.action.name = name + collection.objects.link(empty_obj) + + return collection diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index cfe569f918..14a50ba5ea 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -1,3 +1,5 @@ +"""Create an animation asset.""" + import bpy from avalon import api diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py new file mode 100644 index 0000000000..6094f712ae --- /dev/null +++ b/pype/plugins/blender/load/load_action.py @@ -0,0 +1,295 @@ +"""Load an action in Blender.""" + +import logging +from pathlib import Path +from pprint import pformat +from typing import Dict, List, Optional + +import avalon.blender.pipeline +import bpy +import pype.blender +from avalon import api + +logger = logging.getLogger("pype").getChild("blender").getChild("load_action") + + +class BlendAnimationLoader(pype.blender.AssetLoader): + """Load action from a .blend file. + + Warning: + Loading the same asset more then once is not properly supported at the + moment. + """ + + families = ["action"] + representations = ["blend"] + + label = "Link Action" + icon = "code-fork" + color = "orange" + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + relative = bpy.context.preferences.filepaths.use_relative_paths + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + with bpy.data.libraries.load( + libpath, link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in animation_container.objects: + + obj = obj.make_local() + + # obj.data.make_local() + + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + objects_list.append(obj) + + animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + container_metadata["objects"] = objects_list + + bpy.ops.object.select_all(action='DESELECT') + + nodes = list(container.objects) + nodes.append(container) + self[:] = nodes + return nodes + + 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! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + + libpath = Path(api.get_representation_path(representation)) + extension = libpath.suffix.lower() + + logger.info( + "Container: %s\nRepresentation: %s", + pformat(container, indent=2), + pformat(representation, indent=2), + ) + + assert collection, ( + 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']}" + ) + assert libpath.is_file(), ( + f"The file doesn't exist: {libpath}" + ) + assert extension in pype.blender.plugin.VALID_EXTENSIONS, ( + f"Unsupported file: {libpath}" + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + collection_libpath = collection_metadata["libpath"] + normalized_collection_libpath = ( + str(Path(bpy.path.abspath(collection_libpath)).resolve()) + ) + normalized_libpath = ( + str(Path(bpy.path.abspath(str(libpath))).resolve()) + ) + logger.debug( + "normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s", + normalized_collection_libpath, + normalized_libpath, + ) + if normalized_collection_libpath == normalized_libpath: + logger.info("Library already loaded, not updating...") + return + + strips = [] + + for obj in collection_metadata["objects"]: + + for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + + if armature_obj.animation_data is not None: + + for track in armature_obj.animation_data.nla_tracks: + + for strip in track.strips: + + if strip.action == obj.animation_data.action: + + strips.append(strip) + + bpy.data.actions.remove(obj.animation_data.action) + bpy.data.objects.remove(obj) + + lib_container = collection_metadata["lib_container"] + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + relative = bpy.context.preferences.filepaths.use_relative_paths + with bpy.data.libraries.load( + str(libpath), link=True, relative=relative + ) as (_, data_to): + data_to.collections = [lib_container] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + animation_container = scene.collection.children[lib_container].make_local() + + objects_list = [] + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in animation_container.objects: + + obj = obj.make_local() + + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + + for strip in strips: + + strip.action = obj.animation_data.action + strip.action_frame_end = obj.animation_data.action.frame_range[1] + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": collection.name}) + + objects_list.append(obj) + + animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + + # Save the list of objects in the metadata container + collection_metadata["objects"] = objects_list + collection_metadata["libpath"] = str(libpath) + collection_metadata["representation"] = str(representation["_id"]) + + bpy.ops.object.select_all(action='DESELECT') + + def remove(self, container: Dict) -> bool: + """Remove an existing container from a Blender scene. + + Arguments: + container (avalon-core: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! + """ + + collection = bpy.data.collections.get( + container["objectName"] + ) + if not collection: + return False + assert not (collection.children), ( + "Nested collections are not supported." + ) + + collection_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + + for obj in objects: + + for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + + if armature_obj.animation_data is not None: + + for track in armature_obj.animation_data.nla_tracks: + + for strip in track.strips: + + if strip.action == obj.animation_data.action: + + track.strips.remove(strip) + + bpy.data.actions.remove(obj.animation_data.action) + bpy.data.objects.remove(obj) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + bpy.data.collections.remove(collection) + + return True diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 58a0e94665..c6d18fb1a9 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -10,15 +10,12 @@ import bpy import pype.blender from avalon import api -logger = logging.getLogger("pype").getChild("blender").getChild("load_model") +logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") class BlendAnimationLoader(pype.blender.AssetLoader): """Load animations 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. - Warning: Loading the same asset more then once is not properly supported at the moment. @@ -94,6 +91,10 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj.data.make_local() + if obj.animation_data is not None and obj.animation_data.action is not None: + + obj.animation_data.action.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 40d6c3434c..8ba8c5cfc8 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -87,6 +87,10 @@ class BlendModelLoader(pype.blender.AssetLoader): obj.data.make_local() + for material_slot in obj.material_slots: + + material_slot.material.make_local() + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py new file mode 100644 index 0000000000..0b5e468920 --- /dev/null +++ b/pype/plugins/blender/publish/collect_action.py @@ -0,0 +1,53 @@ +import typing +from typing import Generator + +import bpy + +import avalon.api +import pyblish.api +from avalon.blender.pipeline import AVALON_PROPERTY + + +class CollectAnimation(pyblish.api.ContextPlugin): + """Collect the data of an action.""" + + hosts = ["blender"] + label = "Collect Action" + order = pyblish.api.CollectorOrder + + @staticmethod + def get_action_collections() -> Generator: + """Return all 'animation' collections. + + Check if the family is 'action' and if it doesn't have the + representation set. If the representation is set, it is a loaded action + and we don't want to publish it. + """ + for collection in bpy.data.collections: + avalon_prop = collection.get(AVALON_PROPERTY) or dict() + if (avalon_prop.get('family') == 'action' + and not avalon_prop.get('representation')): + yield collection + + def process(self, context): + """Collect the actions from the current Blender scene.""" + collections = self.get_action_collections() + for collection in collections: + avalon_prop = collection[AVALON_PROPERTY] + asset = avalon_prop['asset'] + family = avalon_prop['family'] + subset = avalon_prop['subset'] + task = avalon_prop['task'] + name = f"{asset}_{subset}" + instance = context.create_instance( + name=name, + family=family, + families=[family], + subset=subset, + asset=asset, + task=task, + ) + members = list(collection.objects) + members.append(collection) + instance[:] = members + self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 9bc0b02227..109ae98e6f 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -20,7 +20,7 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded rig + representation set. If the representation is set, it is a loaded animation and we don't want to publish it. """ for collection in bpy.data.collections: diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 7e11e9ef8d..032f85897d 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -9,7 +9,7 @@ class ExtractBlend(pype.api.Extractor): label = "Extract Blend" hosts = ["blender"] - families = ["animation", "model", "rig"] + families = ["animation", "model", "rig", "action"] optional = True def process(self, instance): diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index 813417bdfc..86ada2f111 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -78,7 +78,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "matchmove", "image" "source", - "assembly" + "assembly", + "action" ] exclude_families = ["clip"] db_representation_context_keys = [ From de972a2fa45c22cc6b5337e342028372261028ac Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 5 Mar 2020 10:27:57 +0000 Subject: [PATCH 16/27] Fixed a naming issue --- pype/plugins/blender/publish/collect_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index 0b5e468920..9a54045cea 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -8,7 +8,7 @@ import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY -class CollectAnimation(pyblish.api.ContextPlugin): +class CollectAction(pyblish.api.ContextPlugin): """Collect the data of an action.""" hosts = ["blender"] From 5bf25ffd3ee322e16c508bda88322faafa198e04 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 11 Mar 2020 12:39:25 +0000 Subject: [PATCH 17/27] Bug fixing and code optimization --- pype/plugins/blender/create/create_rig.py | 63 ++++++- pype/plugins/blender/load/load_action.py | 6 +- pype/plugins/blender/load/load_animation.py | 161 ++++++++---------- pype/plugins/blender/load/load_model.py | 146 +++++++---------- pype/plugins/blender/load/load_rig.py | 172 ++++++++------------ 5 files changed, 262 insertions(+), 286 deletions(-) diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 5d83fafdd3..f630c63966 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -14,6 +14,23 @@ class CreateRig(Creator): family = "rig" icon = "wheelchair" + # @staticmethod + # def _find_layer_collection(self, layer_collection, collection): + + # found = None + + # if (layer_collection.collection == collection): + + # return layer_collection + + # for layer in layer_collection.children: + + # found = self._find_layer_collection(layer, collection) + + # if found: + + # return found + def process(self): import pype.blender @@ -25,8 +42,52 @@ class CreateRig(Creator): 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. + objects_to_link = set() + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): - collection.objects.link(obj) + + objects_to_link.add( obj ) + + if obj.type == 'ARMATURE': + + for subobj in obj.children: + + objects_to_link.add( subobj ) + + # Create a new collection and link the widgets that + # the rig uses. + # custom_shapes = set() + + # for posebone in obj.pose.bones: + + # if posebone.custom_shape is not None: + + # custom_shapes.add( posebone.custom_shape ) + + # if len( custom_shapes ) > 0: + + # widgets_collection = bpy.data.collections.new(name="Widgets") + + # collection.children.link(widgets_collection) + + # for custom_shape in custom_shapes: + + # widgets_collection.objects.link( custom_shape ) + + # layer_collection = self._find_layer_collection(bpy.context.view_layer.layer_collection, widgets_collection) + + # layer_collection.exclude = True + + for obj in objects_to_link: + + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 6094f712ae..747bcd47f5 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -13,7 +13,7 @@ from avalon import api logger = logging.getLogger("pype").getChild("blender").getChild("load_action") -class BlendAnimationLoader(pype.blender.AssetLoader): +class BlendActionLoader(pype.blender.AssetLoader): """Load action from a .blend file. Warning: @@ -47,7 +47,6 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) - relative = bpy.context.preferences.filepaths.use_relative_paths container = bpy.data.collections.new(lib_container) container.name = container_name @@ -65,6 +64,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container + relative = bpy.context.preferences.filepaths.use_relative_paths with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -85,8 +85,6 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj = obj.make_local() - # obj.data.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: obj.animation_data.action.make_local() diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index c6d18fb1a9..0610517b67 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -28,43 +28,22 @@ class BlendAnimationLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name): - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - lib_container = pype.blender.plugin.asset_name(asset, subset) - container_name = pype.blender.plugin.asset_name( - asset, subset, namespace - ) relative = bpy.context.preferences.filepaths.use_relative_paths - - container = bpy.data.collections.new(lib_container) - container.name = container_name - avalon.blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) - - container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) - - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -77,8 +56,9 @@ class BlendAnimationLoader(pype.blender.AssetLoader): animation_container = scene.collection.children[lib_container].make_local() meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + armatures = [obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + + # Should check if there is only an armature? objects_list = [] @@ -106,11 +86,51 @@ class BlendAnimationLoader(pype.blender.AssetLoader): animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + + container = bpy.data.collections.new(lib_container) + container.name = container_name + avalon.blender.pipeline.containerise_existing( + container, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = container.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process(self, libpath, lib_container, container_name) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - nodes = list(container.objects) nodes.append(container) self[:] = nodes @@ -177,59 +197,16 @@ class BlendAnimationLoader(pype.blender.AssetLoader): logger.info("Library already loaded, not updating...") return - # Get the armature of the rig - armatures = [obj for obj in collection_metadata["objects"] - if obj.type == 'ARMATURE'] - assert(len(armatures) == 1) - - for obj in collection_metadata["objects"]: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - + objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - animation_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in animation_container.objects if obj.type == 'ARMATURE'] - objects_list = [] - + # Get the armature of the rig + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] assert(len(armatures) == 1) - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: + self._remove(self, objects, lib_container) - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -266,14 +243,8 @@ class BlendAnimationLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) + self._remove(self, objects, lib_container) + bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 8ba8c5cfc8..10904a1f7b 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -31,43 +31,19 @@ class BlendModelLoader(pype.blender.AssetLoader): icon = "code-fork" color = "orange" - def process_asset( - self, context: dict, name: str, namespace: Optional[str] = None, - options: Optional[Dict] = None - ) -> Optional[List]: - """ - Arguments: - name: Use pre-defined name - namespace: Use pre-defined namespace - context: Full parenthood of representation to load - options: Additional settings dictionary - """ + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_name): - libpath = self.fname - asset = context["asset"]["name"] - subset = context["subset"]["name"] - lib_container = pype.blender.plugin.asset_name(asset, subset) - container_name = pype.blender.plugin.asset_name( - asset, subset, namespace - ) relative = bpy.context.preferences.filepaths.use_relative_paths - - container = bpy.data.collections.new(lib_container) - container.name = container_name - avalon.blender.pipeline.containerise_existing( - container, - name, - namespace, - context, - self.__class__.__name__, - ) - - container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) - - container_metadata["libpath"] = libpath - container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( libpath, link=True, relative=relative ) as (_, data_to): @@ -102,13 +78,53 @@ class BlendModelLoader(pype.blender.AssetLoader): model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + bpy.ops.object.select_all(action='DESELECT') + + return objects_list + + def process_asset( + self, context: dict, name: str, namespace: Optional[str] = None, + options: Optional[Dict] = None + ) -> Optional[List]: + """ + Arguments: + name: Use pre-defined name + namespace: Use pre-defined namespace + context: Full parenthood of representation to load + options: Additional settings dictionary + """ + + libpath = self.fname + asset = context["asset"]["name"] + subset = context["subset"]["name"] + lib_container = pype.blender.plugin.asset_name(asset, subset) + container_name = pype.blender.plugin.asset_name( + asset, subset, namespace + ) + + collection = bpy.data.collections.new(lib_container) + collection.name = container_name + avalon.blender.pipeline.containerise_existing( + collection, + name, + namespace, + context, + self.__class__.__name__, + ) + + container_metadata = collection.get( + avalon.blender.pipeline.AVALON_PROPERTY) + + container_metadata["libpath"] = libpath + container_metadata["lib_container"] = lib_container + + objects_list = self._process(self, libpath, lib_container, container_name) + # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - - nodes = list(container.objects) - nodes.append(container) + nodes = list(collection.objects) + nodes.append(collection) self[:] = nodes return nodes @@ -154,8 +170,10 @@ class BlendModelLoader(pype.blender.AssetLoader): collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -171,54 +189,15 @@ class BlendModelLoader(pype.blender.AssetLoader): logger.info("Library already loaded, not updating...") return - for obj in collection_metadata["objects"]: + self._remove(self, objects, lib_container) - bpy.data.meshes.remove(obj.data) - - lib_container = collection_metadata["lib_container"] - - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - model_container = scene.collection.children[lib_container].make_local() - - objects_list = [] - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in model_container.objects: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list collection_metadata["libpath"] = str(libpath) collection_metadata["representation"] = str(representation["_id"]) - bpy.ops.object.select_all(action='DESELECT') - def remove(self, container: Dict) -> bool: """Remove an existing container from a Blender scene. @@ -246,11 +225,8 @@ class BlendModelLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: + self._remove(self, objects, lib_container) - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index c19717cd82..dcb70da6d8 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -30,6 +30,68 @@ class BlendRigLoader(pype.blender.AssetLoader): label = "Link Rig" icon = "code-fork" color = "orange" + + @staticmethod + def _remove(self, objects, lib_container): + + for obj in objects: + + if obj.type == 'ARMATURE': + bpy.data.armatures.remove(obj.data) + elif obj.type == 'MESH': + bpy.data.meshes.remove(obj.data) + + bpy.data.collections.remove(bpy.data.collections[lib_container]) + + @staticmethod + def _process(self, libpath, lib_container, container_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] + + scene = bpy.context.scene + + scene.collection.children.link(bpy.data.collections[lib_container]) + + rig_container = scene.collection.children[lib_container].make_local() + + meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] + + objects_list = [] + + assert(len(armatures) == 1) + + # Link meshes first, then armatures. + # The armature is unparented for all the non-local meshes, + # when it is made local. + for obj in meshes + armatures: + + obj = obj.make_local() + + obj.data.make_local() + + if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + + obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + + avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info.update({"container_name": container_name}) + + if obj.type == 'ARMATURE' and action is not None: + + obj.animation_data.action = action + + objects_list.append(obj) + + rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + + bpy.ops.object.select_all(action='DESELECT') + + return objects_list def process_asset( self, context: dict, name: str, namespace: Optional[str] = None, @@ -50,7 +112,6 @@ class BlendRigLoader(pype.blender.AssetLoader): container_name = pype.blender.plugin.asset_name( asset, subset, namespace ) - relative = bpy.context.preferences.filepaths.use_relative_paths container = bpy.data.collections.new(lib_container) container.name = container_name @@ -68,48 +129,11 @@ class BlendRigLoader(pype.blender.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - with bpy.data.libraries.load( - libpath, link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - rig_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - - objects_list = [] - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": container_name}) - - objects_list.append(obj) - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list - bpy.ops.object.select_all(action='DESELECT') - nodes = list(container.objects) nodes.append(container) self[:] = nodes @@ -159,8 +183,10 @@ class BlendRigLoader(pype.blender.AssetLoader): collection_metadata = collection.get( avalon.blender.pipeline.AVALON_PROPERTY) - collection_libpath = collection_metadata["libpath"] + objects = collection_metadata["objects"] + lib_container = collection_metadata["lib_container"] + normalized_collection_libpath = ( str(Path(bpy.path.abspath(collection_libpath)).resolve()) ) @@ -177,64 +203,14 @@ class BlendRigLoader(pype.blender.AssetLoader): return # Get the armature of the rig - armatures = [obj for obj in collection_metadata["objects"] - if obj.type == 'ARMATURE'] + armatures = [obj for obj in objects if obj.type == 'ARMATURE'] assert(len(armatures) == 1) action = armatures[0].animation_data.action - for obj in collection_metadata["objects"]: + self._remove(self, objects, lib_container) - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - lib_container = collection_metadata["lib_container"] - - bpy.data.collections.remove(bpy.data.collections[lib_container]) - - relative = bpy.context.preferences.filepaths.use_relative_paths - with bpy.data.libraries.load( - str(libpath), link=True, relative=relative - ) as (_, data_to): - data_to.collections = [lib_container] - - scene = bpy.context.scene - - scene.collection.children.link(bpy.data.collections[lib_container]) - - rig_container = scene.collection.children[lib_container].make_local() - - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - objects_list = [] - - assert(len(armatures) == 1) - - # Link meshes first, then armatures. - # The armature is unparented for all the non-local meshes, - # when it is made local. - for obj in meshes + armatures: - - obj = obj.make_local() - - obj.data.make_local() - - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): - - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() - - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] - avalon_info.update({"container_name": collection.name}) - objects_list.append(obj) - - if obj.type == 'ARMATURE' and action is not None: - - obj.animation_data.action = action - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + objects_list = self._process(self, str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -271,14 +247,8 @@ class BlendRigLoader(pype.blender.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - for obj in objects: - - if obj.type == 'ARMATURE': - bpy.data.armatures.remove(obj.data) - elif obj.type == 'MESH': - bpy.data.meshes.remove(obj.data) - - bpy.data.collections.remove(bpy.data.collections[lib_container]) + self._remove(self, objects, lib_container) + bpy.data.collections.remove(collection) return True From 05441eb2fe3df8b44e465118d6ef319ba774d535 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 Mar 2020 18:07:36 +0100 Subject: [PATCH 18/27] fix imports --- pype/plugins/blender/create/create_action.py | 2 ++ .../blender/create/create_animation.py | 2 ++ pype/plugins/blender/create/create_model.py | 3 +- pype/plugins/blender/create/create_rig.py | 11 ++++--- pype/plugins/blender/load/load_action.py | 29 +++++++++-------- pype/plugins/blender/load/load_animation.py | 26 ++++++++-------- pype/plugins/blender/load/load_model.py | 25 +++++++-------- pype/plugins/blender/load/load_rig.py | 31 +++++++++---------- 8 files changed, 65 insertions(+), 64 deletions(-) diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 88ecebdfff..64dfe9ff90 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -4,6 +4,8 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin + class CreateAction(Creator): diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 14a50ba5ea..0758db280f 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -4,6 +4,8 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin + class CreateAnimation(Creator): diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index a3b2ffc55b..7a53f215f2 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -4,7 +4,7 @@ import bpy from avalon import api from avalon.blender import Creator, lib - +import pype.blender.plugin class CreateModel(Creator): """Polygonal static geometry""" @@ -15,7 +15,6 @@ class CreateModel(Creator): icon = "cube" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index f630c63966..b5860787ea 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -4,6 +4,7 @@ import bpy from avalon import api from avalon.blender import Creator, lib +import pype.blender.plugin class CreateRig(Creator): @@ -42,16 +43,16 @@ class CreateRig(Creator): 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. + # 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 + # 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. objects_to_link = set() if (self.options or {}).get("useSelection"): - + for obj in lib.get_selection(): objects_to_link.add( obj ) @@ -75,7 +76,7 @@ class CreateRig(Creator): # if len( custom_shapes ) > 0: # widgets_collection = bpy.data.collections.new(name="Widgets") - + # collection.children.link(widgets_collection) # for custom_shape in custom_shapes: diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 747bcd47f5..afde8b90a1 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -5,10 +5,9 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_action") @@ -50,7 +49,7 @@ class BlendActionLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -59,7 +58,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -89,16 +88,16 @@ class BlendActionLoader(pype.blender.AssetLoader): obj.animation_data.action.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + animation_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -153,7 +152,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -224,16 +223,16 @@ class BlendActionLoader(pype.blender.AssetLoader): strip.action = obj.animation_data.action strip.action_frame_end = obj.animation_data.action.frame_range[1] - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": collection.name}) objects_list.append(obj) - animation_container.pop(avalon.blender.pipeline.AVALON_PROPERTY) + animation_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -266,7 +265,7 @@ class BlendActionLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index 0610517b67..ec3e24443f 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -5,15 +5,15 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin + logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") -class BlendAnimationLoader(pype.blender.AssetLoader): +class BlendAnimationLoader(pype.blender.plugin.AssetLoader): """Load animations from a .blend file. Warning: @@ -75,16 +75,16 @@ class BlendAnimationLoader(pype.blender.AssetLoader): obj.animation_data.action.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - animation_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + animation_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -112,7 +112,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -121,7 +121,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -179,7 +179,7 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] normalized_collection_libpath = ( @@ -239,12 +239,12 @@ class BlendAnimationLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] self._remove(self, objects, lib_container) - + bpy.data.collections.remove(collection) return True diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 10904a1f7b..b8b6b9b956 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -5,15 +5,14 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_model") -class BlendModelLoader(pype.blender.AssetLoader): +class BlendModelLoader(pype.blender.plugin.AssetLoader): """Load models from a .blend file. Because they come from a .blend file we can simply link the collection that @@ -67,16 +66,16 @@ class BlendModelLoader(pype.blender.AssetLoader): material_slot.material.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) objects_list.append(obj) - model_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + model_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -104,7 +103,7 @@ class BlendModelLoader(pype.blender.AssetLoader): collection = bpy.data.collections.new(lib_container) collection.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( collection, name, namespace, @@ -113,7 +112,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) container_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -169,7 +168,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -221,7 +220,7 @@ class BlendModelLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -232,7 +231,7 @@ class BlendModelLoader(pype.blender.AssetLoader): return True -class CacheModelLoader(pype.blender.AssetLoader): +class CacheModelLoader(pype.blender.plugin.AssetLoader): """Load cache models. Stores the imported asset in a collection named after the asset. diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index dcb70da6d8..44d47b41a1 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -5,15 +5,14 @@ from pathlib import Path from pprint import pformat from typing import Dict, List, Optional -import avalon.blender.pipeline +from avalon import api, blender import bpy -import pype.blender -from avalon import api +import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_model") -class BlendRigLoader(pype.blender.AssetLoader): +class BlendRigLoader(pype.blender.plugin.AssetLoader): """Load rigs from a .blend file. Because they come from a .blend file we can simply link the collection that @@ -30,7 +29,7 @@ class BlendRigLoader(pype.blender.AssetLoader): label = "Link Rig" icon = "code-fork" color = "orange" - + @staticmethod def _remove(self, objects, lib_container): @@ -60,7 +59,7 @@ class BlendRigLoader(pype.blender.AssetLoader): meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] - + objects_list = [] assert(len(armatures) == 1) @@ -74,11 +73,11 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.data.make_local() - if not obj.get(avalon.blender.pipeline.AVALON_PROPERTY): + if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[avalon.blender.pipeline.AVALON_PROPERTY] = dict() + obj[blender.pipeline.AVALON_PROPERTY] = dict() - avalon_info = obj[avalon.blender.pipeline.AVALON_PROPERTY] + avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) if obj.type == 'ARMATURE' and action is not None: @@ -86,8 +85,8 @@ class BlendRigLoader(pype.blender.AssetLoader): obj.animation_data.action = action objects_list.append(obj) - - rig_container.pop( avalon.blender.pipeline.AVALON_PROPERTY ) + + rig_container.pop( blender.pipeline.AVALON_PROPERTY ) bpy.ops.object.select_all(action='DESELECT') @@ -115,7 +114,7 @@ class BlendRigLoader(pype.blender.AssetLoader): container = bpy.data.collections.new(lib_container) container.name = container_name - avalon.blender.pipeline.containerise_existing( + blender.pipeline.containerise_existing( container, name, namespace, @@ -124,7 +123,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) container_metadata = container.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container @@ -182,7 +181,7 @@ class BlendRigLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) collection_libpath = collection_metadata["libpath"] objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] @@ -243,12 +242,12 @@ class BlendRigLoader(pype.blender.AssetLoader): ) collection_metadata = collection.get( - avalon.blender.pipeline.AVALON_PROPERTY) + blender.pipeline.AVALON_PROPERTY) objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] self._remove(self, objects, lib_container) - + bpy.data.collections.remove(collection) return True From 5ff73c064ecae24e0470529503bda8ab76875ddd Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Thu, 19 Mar 2020 19:40:40 +0100 Subject: [PATCH 19/27] remove extra imports --- pype/plugins/blender/create/create_action.py | 8 ++++---- pype/plugins/blender/create/create_animation.py | 2 -- pype/plugins/blender/create/create_model.py | 1 + pype/plugins/blender/create/create_rig.py | 3 +-- 4 files changed, 6 insertions(+), 8 deletions(-) diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 64dfe9ff90..68e2a50b61 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -7,7 +7,6 @@ from avalon.blender import Creator, lib import pype.blender.plugin - class CreateAction(Creator): """Action output for character rigs""" @@ -17,7 +16,6 @@ class CreateAction(Creator): icon = "male" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] @@ -29,9 +27,11 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if obj.animation_data is not None and obj.animation_data.action is not None: + if (obj.animation_data is not None + and obj.animation_data.action is not None): - empty_obj = bpy.data.objects.new( name = name, object_data = None ) + empty_obj = bpy.data.objects.new(name=name, + object_data=None) empty_obj.animation_data_create() empty_obj.animation_data.action = obj.animation_data.action empty_obj.animation_data.action.name = name diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 0758db280f..b40a456c8f 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -7,7 +7,6 @@ from avalon.blender import Creator, lib import pype.blender.plugin - class CreateAnimation(Creator): """Animation output for character rigs""" @@ -17,7 +16,6 @@ class CreateAnimation(Creator): icon = "male" def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] diff --git a/pype/plugins/blender/create/create_model.py b/pype/plugins/blender/create/create_model.py index 7a53f215f2..303a7a63a1 100644 --- a/pype/plugins/blender/create/create_model.py +++ b/pype/plugins/blender/create/create_model.py @@ -6,6 +6,7 @@ from avalon import api from avalon.blender import Creator, lib import pype.blender.plugin + class CreateModel(Creator): """Polygonal static geometry""" diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index b5860787ea..d28e854232 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -33,7 +33,6 @@ class CreateRig(Creator): # return found def process(self): - import pype.blender asset = self.data["asset"] subset = self.data["subset"] @@ -61,7 +60,7 @@ class CreateRig(Creator): for subobj in obj.children: - objects_to_link.add( subobj ) + objects_to_link.add(subobj) # Create a new collection and link the widgets that # the rig uses. From 0f8b35c831420dc55ff07699ab54f1064b9a3004 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 11:31:59 +0000 Subject: [PATCH 20/27] Fixed Loader's parent class --- pype/plugins/blender/load/load_action.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index afde8b90a1..e185bff7a8 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -12,7 +12,7 @@ import pype.blender.plugin logger = logging.getLogger("pype").getChild("blender").getChild("load_action") -class BlendActionLoader(pype.blender.AssetLoader): +class BlendActionLoader(pype.blender.plugin.AssetLoader): """Load action from a .blend file. Warning: From 7e8bb47ed727a5798427e393dac2361ba555b065 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 12:29:34 +0000 Subject: [PATCH 21/27] Fixed creation of the animation and fbx publishing --- .../blender/create/create_animation.py | 22 +++++++++++- .../blender/publish/extract_fbx_animation.py | 36 +++++++++++-------- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index b40a456c8f..6b7616bbfd 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -25,8 +25,28 @@ class CreateAnimation(Creator): 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. + objects_to_link = set() + if (self.options or {}).get("useSelection"): + for obj in lib.get_selection(): - collection.objects.link(obj) + + objects_to_link.add( obj ) + + if obj.type == 'ARMATURE': + + for subobj in obj.children: + + objects_to_link.add(subobj) + + for obj in objects_to_link: + + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py index bc088f8bb7..4b1fe98c2f 100644 --- a/pype/plugins/blender/publish/extract_fbx_animation.py +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -47,8 +47,7 @@ class ExtractAnimationFBX(pype.api.Extractor): # We set the scale of the scene for the export bpy.context.scene.unit_settings.scale_length = 0.01 - # We export all the objects in the collection - objects_to_export = collections[0].objects + armatures = [obj for obj in collections[0].objects if obj.type == 'ARMATURE'] object_action_pairs = [] original_actions = [] @@ -56,20 +55,25 @@ class ExtractAnimationFBX(pype.api.Extractor): starting_frames = [] ending_frames = [] - # For each object, we make a copy of the current action - for obj in objects_to_export: + # For each armature, we make a copy of the current action + for obj in armatures: - curr_action = obj.animation_data.action - copy_action = curr_action.copy() + 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] ) object_action_pairs.append((obj, copy_action)) original_actions.append(curr_action) - curr_frame_range = curr_action.frame_range - - starting_frames.append( curr_frame_range[0] ) - ending_frames.append( curr_frame_range[1] ) - # We compute the starting and ending frames max_frame = min( starting_frames ) min_frame = max( ending_frames ) @@ -98,10 +102,14 @@ class ExtractAnimationFBX(pype.api.Extractor): # We delete the baked action and set the original one back for i in range(0, len(object_action_pairs)): - object_action_pairs[i][0].animation_data.action = original_actions[i] + if original_actions[i]: - object_action_pairs[i][1].user_clear() - bpy.data.actions.remove(object_action_pairs[i][1]) + object_action_pairs[i][0].animation_data.action = original_actions[i] + + if object_action_pairs[i][1]: + + object_action_pairs[i][1].user_clear() + bpy.data.actions.remove(object_action_pairs[i][1]) if "representations" not in instance.data: instance.data["representations"] = [] From a3d025f7845ad3ef30f04fe5b7fcb7f6b0c8ab20 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 16:13:55 +0000 Subject: [PATCH 22/27] PEP8 compliance --- pype/blender/plugin.py | 16 +++--- pype/plugins/blender/create/create_action.py | 4 +- .../blender/create/create_animation.py | 2 +- pype/plugins/blender/create/create_rig.py | 43 +------------- pype/plugins/blender/load/load_action.py | 40 ++++++++----- pype/plugins/blender/load/load_animation.py | 24 +++++--- pype/plugins/blender/load/load_model.py | 8 ++- pype/plugins/blender/load/load_rig.py | 11 ++-- .../plugins/blender/publish/collect_action.py | 6 +- .../blender/publish/collect_animation.py | 10 ++-- .../blender/publish/collect_current_file.py | 3 +- pype/plugins/blender/publish/collect_model.py | 6 +- pype/plugins/blender/publish/collect_rig.py | 6 +- pype/plugins/blender/publish/extract_blend.py | 3 +- pype/plugins/blender/publish/extract_fbx.py | 34 +++++++---- .../blender/publish/extract_fbx_animation.py | 57 ++++++++++++------- 16 files changed, 137 insertions(+), 136 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index b441714c0d..5e98d8314b 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -10,14 +10,16 @@ from avalon import api VALID_EXTENSIONS = [".blend"] -def asset_name(asset: str, subset: str, namespace: Optional[str] = None) -> str: +def asset_name( + asset: str, subset: str, namespace: Optional[str] = None +) -> str: """Return a consistent name for an asset.""" name = f"{asset}_{subset}" if namespace: name = f"{namespace}:{name}" return name -def create_blender_context( obj: Optional[bpy.types.Object] = None ): +def create_blender_context(obj: Optional[bpy.types.Object] = None): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. """ @@ -27,16 +29,16 @@ def create_blender_context( obj: Optional[bpy.types.Object] = None ): for region in area.regions: if region.type == 'WINDOW': override_context = { - 'window': win, - 'screen': win.screen, - 'area': area, - 'region': region, + 'window': win, + 'screen': win.screen, + 'area': area, + 'region': region, 'scene': bpy.context.scene, 'active_object': obj, 'selected_objects': [obj] } return override_context - raise Exception( "Could not create a custom Blender context." ) + raise Exception("Could not create a custom Blender context.") class AssetLoader(api.Loader): """A basic AssetLoader for Blender diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 68e2a50b61..6c24065f81 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -27,8 +27,8 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if (obj.animation_data is not None - and obj.animation_data.action is not None): + if (obj.animation_data is not None and + obj.animation_data.action is not None): empty_obj = bpy.data.objects.new(name=name, object_data=None) diff --git a/pype/plugins/blender/create/create_animation.py b/pype/plugins/blender/create/create_animation.py index 6b7616bbfd..3a5985d7a2 100644 --- a/pype/plugins/blender/create/create_animation.py +++ b/pype/plugins/blender/create/create_animation.py @@ -37,7 +37,7 @@ class CreateAnimation(Creator): for obj in lib.get_selection(): - objects_to_link.add( obj ) + objects_to_link.add(obj) if obj.type == 'ARMATURE': diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index d28e854232..dc97d8b4ce 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -15,23 +15,6 @@ class CreateRig(Creator): family = "rig" icon = "wheelchair" - # @staticmethod - # def _find_layer_collection(self, layer_collection, collection): - - # found = None - - # if (layer_collection.collection == collection): - - # return layer_collection - - # for layer in layer_collection.children: - - # found = self._find_layer_collection(layer, collection) - - # if found: - - # return found - def process(self): asset = self.data["asset"] @@ -54,7 +37,7 @@ class CreateRig(Creator): for obj in lib.get_selection(): - objects_to_link.add( obj ) + objects_to_link.add(obj) if obj.type == 'ARMATURE': @@ -62,30 +45,6 @@ class CreateRig(Creator): objects_to_link.add(subobj) - # Create a new collection and link the widgets that - # the rig uses. - # custom_shapes = set() - - # for posebone in obj.pose.bones: - - # if posebone.custom_shape is not None: - - # custom_shapes.add( posebone.custom_shape ) - - # if len( custom_shapes ) > 0: - - # widgets_collection = bpy.data.collections.new(name="Widgets") - - # collection.children.link(widgets_collection) - - # for custom_shape in custom_shapes: - - # widgets_collection.objects.link( custom_shape ) - - # layer_collection = self._find_layer_collection(bpy.context.view_layer.layer_collection, widgets_collection) - - # layer_collection.exclude = True - for obj in objects_to_link: collection.objects.link(obj) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index e185bff7a8..303d1ead4d 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -69,11 +69,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): ) as (_, data_to): data_to.collections = [lib_container] - scene = bpy.context.scene + collection = bpy.context.scene.collection - scene.collection.children.link(bpy.data.collections[lib_container]) + collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + animation_container = collection.children[lib_container].make_local() objects_list = [] @@ -84,9 +84,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): obj = obj.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -173,8 +175,12 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): strips = [] for obj in collection_metadata["objects"]: + + # Get all the strips that use the action + arm_objs = [ + arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] - for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + for armature_obj in arm_objs: if armature_obj.animation_data is not None: @@ -203,25 +209,27 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + anim_container = scene.collection.children[lib_container].make_local() objects_list = [] # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. - for obj in animation_container.objects: + for obj in anim_container.objects: obj = obj.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() for strip in strips: - strip.action = obj.animation_data.action - strip.action_frame_end = obj.animation_data.action.frame_range[1] + strip.action = anim_data.action + strip.action_frame_end = anim_data.action.frame_range[1] if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -232,7 +240,7 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - animation_container.pop(blender.pipeline.AVALON_PROPERTY) + anim_container.pop(blender.pipeline.AVALON_PROPERTY) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -271,7 +279,11 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): for obj in objects: - for armature_obj in [ objj for objj in bpy.data.objects if objj.type == 'ARMATURE' ]: + # Get all the strips that use the action + arm_objs = [ + arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] + + for armature_obj in arm_objs: if armature_obj.animation_data is not None: diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index ec3e24443f..395684a3ba 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -10,7 +10,8 @@ import bpy import pype.blender.plugin -logger = logging.getLogger("pype").getChild("blender").getChild("load_animation") +logger = logging.getLogger("pype").getChild( + "blender").getChild("load_animation") class BlendAnimationLoader(pype.blender.plugin.AssetLoader): @@ -53,10 +54,11 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): scene.collection.children.link(bpy.data.collections[lib_container]) - animation_container = scene.collection.children[lib_container].make_local() + anim_container = scene.collection.children[lib_container].make_local() - meshes = [obj for obj in animation_container.objects if obj.type == 'MESH'] - armatures = [obj for obj in animation_container.objects if obj.type == 'ARMATURE'] + meshes = [obj for obj in anim_container.objects if obj.type == 'MESH'] + armatures = [ + obj for obj in anim_container.objects if obj.type == 'ARMATURE'] # Should check if there is only an armature? @@ -71,9 +73,11 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): obj.data.make_local() - if obj.animation_data is not None and obj.animation_data.action is not None: + anim_data = obj.animation_data - obj.animation_data.action.make_local() + if anim_data is not None and anim_data.action is not None: + + anim_data.action.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): @@ -84,7 +88,7 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - animation_container.pop( blender.pipeline.AVALON_PROPERTY ) + anim_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -126,7 +130,8 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name) + objects_list = self._process( + self, libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -206,7 +211,8 @@ class BlendAnimationLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name) + objects_list = self._process( + self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index b8b6b9b956..ff7c6c49c2 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -75,7 +75,7 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - model_container.pop( blender.pipeline.AVALON_PROPERTY ) + model_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -117,7 +117,8 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name) + objects_list = self._process( + self, libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -190,7 +191,8 @@ class BlendModelLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name) + objects_list = self._process( + self, str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 44d47b41a1..d14a868722 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -58,7 +58,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): rig_container = scene.collection.children[lib_container].make_local() meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] - armatures = [obj for obj in rig_container.objects if obj.type == 'ARMATURE'] + armatures = [ + obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] @@ -86,7 +87,7 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): objects_list.append(obj) - rig_container.pop( blender.pipeline.AVALON_PROPERTY ) + rig_container.pop(blender.pipeline.AVALON_PROPERTY) bpy.ops.object.select_all(action='DESELECT') @@ -128,7 +129,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): container_metadata["libpath"] = libpath container_metadata["lib_container"] = lib_container - objects_list = self._process(self, libpath, lib_container, container_name, None) + objects_list = self._process( + self, libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -209,7 +211,8 @@ class BlendRigLoader(pype.blender.plugin.AssetLoader): self._remove(self, objects, lib_container) - objects_list = self._process(self, str(libpath), lib_container, collection.name, action) + objects_list = self._process( + self, str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index 9a54045cea..a8ceed9c82 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectAction(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'action' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 109ae98e6f..50d49692b8 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -20,13 +18,13 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded animation - and we don't want to publish it. + representation set. If the representation is set, it is a loaded + animation and we don't want to publish it. """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'animation' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_current_file.py b/pype/plugins/blender/publish/collect_current_file.py index 926d290b31..72976c490b 100644 --- a/pype/plugins/blender/publish/collect_current_file.py +++ b/pype/plugins/blender/publish/collect_current_file.py @@ -15,4 +15,5 @@ class CollectBlenderCurrentFile(pyblish.api.ContextPlugin): current_file = bpy.data.filepath context.data['currentFile'] = current_file - assert current_file != '', "Current file is empty. Save the file before continuing." + assert current_file != '', "Current file is empty. " \ + "Save the file before continuing." diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_model.py index ee10eaf7f2..df5c1e709a 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_model.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'model' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py index a4b30541f6..01958da37a 100644 --- a/pype/plugins/blender/publish/collect_rig.py +++ b/pype/plugins/blender/publish/collect_rig.py @@ -1,9 +1,7 @@ -import typing from typing import Generator import bpy -import avalon.api import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY @@ -25,8 +23,8 @@ class CollectRig(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' - and not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'rig' and + not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/extract_blend.py b/pype/plugins/blender/publish/extract_blend.py index 032f85897d..5f3fdac293 100644 --- a/pype/plugins/blender/publish/extract_blend.py +++ b/pype/plugins/blender/publish/extract_blend.py @@ -43,4 +43,5 @@ class ExtractBlend(pype.api.Extractor): } instance.data["representations"].append(representation) - self.log.info("Extracted instance '%s' to: %s", instance.name, representation) + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) diff --git a/pype/plugins/blender/publish/extract_fbx.py b/pype/plugins/blender/publish/extract_fbx.py index 95466c1d2b..231bfdde24 100644 --- a/pype/plugins/blender/publish/extract_fbx.py +++ b/pype/plugins/blender/publish/extract_fbx.py @@ -1,10 +1,10 @@ import os -import avalon.blender.workio import pype.api import bpy + class ExtractFBX(pype.api.Extractor): """Extract as FBX.""" @@ -20,29 +20,39 @@ class ExtractFBX(pype.api.Extractor): filename = f"{instance.name}.fbx" filepath = os.path.join(stagingdir, filename) + context = bpy.context + scene = context.scene + view_layer = context.view_layer + # Perform extraction self.log.info("Performing extraction..") - collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] - assert len(collections) == 1, "There should be one and only one collection collected for this asset" + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" - old_active_layer_collection = bpy.context.view_layer.active_layer_collection + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children # Get the layer collection from the collection we need to export. - # This is needed because in Blender you can only set the active + # This is needed because in Blender you can only set the active # collection with the layer collection, and there is no way to get - # the layer collection from the collection (but there is the vice versa). - layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] assert len(layer_collections) == 1 - bpy.context.view_layer.active_layer_collection = layer_collections[0] + view_layer.active_layer_collection = layer_collections[0] - old_scale = bpy.context.scene.unit_settings.scale_length + old_scale = scene.unit_settings.scale_length # We set the scale of the scene for the export - bpy.context.scene.unit_settings.scale_length = 0.01 + scene.unit_settings.scale_length = 0.01 # We export the fbx bpy.ops.export_scene.fbx( @@ -52,9 +62,9 @@ class ExtractFBX(pype.api.Extractor): add_leaf_bones=False ) - bpy.context.view_layer.active_layer_collection = old_active_layer_collection + view_layer.active_layer_collection = old_active_layer_collection - bpy.context.scene.unit_settings.scale_length = old_scale + scene.unit_settings.scale_length = old_scale if "representations" not in instance.data: instance.data["representations"] = [] diff --git a/pype/plugins/blender/publish/extract_fbx_animation.py b/pype/plugins/blender/publish/extract_fbx_animation.py index 4b1fe98c2f..d51c641e9c 100644 --- a/pype/plugins/blender/publish/extract_fbx_animation.py +++ b/pype/plugins/blender/publish/extract_fbx_animation.py @@ -1,5 +1,4 @@ import os -import avalon.blender.workio import pype.api @@ -23,31 +22,42 @@ class ExtractAnimationFBX(pype.api.Extractor): filename = f"{instance.name}.fbx" filepath = os.path.join(stagingdir, filename) + context = bpy.context + scene = context.scene + view_layer = context.view_layer + # Perform extraction self.log.info("Performing extraction..") - collections = [obj for obj in instance if type(obj) is bpy.types.Collection] + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] - assert len(collections) == 1, "There should be one and only one collection collected for this asset" + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" - old_active_layer_collection = bpy.context.view_layer.active_layer_collection + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children # Get the layer collection from the collection we need to export. - # This is needed because in Blender you can only set the active + # This is needed because in Blender you can only set the active # collection with the layer collection, and there is no way to get - # the layer collection from the collection (but there is the vice versa). - layer_collections = [layer for layer in bpy.context.view_layer.layer_collection.children if layer.collection == collections[0]] + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] assert len(layer_collections) == 1 - bpy.context.view_layer.active_layer_collection = layer_collections[0] + view_layer.active_layer_collection = layer_collections[0] - old_scale = bpy.context.scene.unit_settings.scale_length + old_scale = scene.unit_settings.scale_length # We set the scale of the scene for the export - bpy.context.scene.unit_settings.scale_length = 0.01 + scene.unit_settings.scale_length = 0.01 - armatures = [obj for obj in collections[0].objects if obj.type == 'ARMATURE'] + armatures = [ + obj for obj in collections[0].objects if obj.type == 'ARMATURE'] object_action_pairs = [] original_actions = [] @@ -68,15 +78,15 @@ class ExtractAnimationFBX(pype.api.Extractor): curr_frame_range = curr_action.frame_range - starting_frames.append( curr_frame_range[0] ) - ending_frames.append( curr_frame_range[1] ) + starting_frames.append(curr_frame_range[0]) + ending_frames.append(curr_frame_range[1]) 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 ) + 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( @@ -95,21 +105,24 @@ class ExtractAnimationFBX(pype.api.Extractor): add_leaf_bones=False ) - bpy.context.view_layer.active_layer_collection = old_active_layer_collection + view_layer.active_layer_collection = old_active_layer_collection - bpy.context.scene.unit_settings.scale_length = old_scale + scene.unit_settings.scale_length = old_scale # We delete the baked action and set the original one back for i in range(0, len(object_action_pairs)): - if original_actions[i]: + pair = object_action_pairs[i] + action = original_actions[i] - object_action_pairs[i][0].animation_data.action = original_actions[i] + if action: - if object_action_pairs[i][1]: + pair[0].animation_data.action = action - object_action_pairs[i][1].user_clear() - bpy.data.actions.remove(object_action_pairs[i][1]) + if pair[1]: + + pair[1].user_clear() + bpy.data.actions.remove(pair[1]) if "representations" not in instance.data: instance.data["representations"] = [] From 40cf778f106c32deec2d850d0c72c906537ebfcf Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Fri, 20 Mar 2020 16:46:45 +0000 Subject: [PATCH 23/27] More PEP8 compliance --- pype/blender/plugin.py | 5 ++++- pype/plugins/blender/create/create_action.py | 4 ++-- pype/plugins/blender/load/load_action.py | 2 +- pype/plugins/blender/publish/collect_action.py | 4 ++-- pype/plugins/blender/publish/collect_animation.py | 6 +++--- pype/plugins/blender/publish/collect_model.py | 4 ++-- pype/plugins/blender/publish/collect_rig.py | 4 ++-- 7 files changed, 16 insertions(+), 13 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index 5e98d8314b..8f72d04a1d 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -19,6 +19,7 @@ def asset_name( name = f"{namespace}:{name}" return name + def create_blender_context(obj: Optional[bpy.types.Object] = None): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. @@ -40,6 +41,7 @@ def create_blender_context(obj: Optional[bpy.types.Object] = None): return override_context raise Exception("Could not create a custom Blender context.") + class AssetLoader(api.Loader): """A basic AssetLoader for Blender @@ -89,7 +91,8 @@ class AssetLoader(api.Loader): assert obj.library, f"'{obj.name}' is not linked." libraries.add(obj.library) - assert len(libraries) == 1, "'{container.name}' contains objects from more then 1 library." + assert len( + libraries) == 1, "'{container.name}' contains objects from more then 1 library." return list(libraries)[0] diff --git a/pype/plugins/blender/create/create_action.py b/pype/plugins/blender/create/create_action.py index 6c24065f81..68e2a50b61 100644 --- a/pype/plugins/blender/create/create_action.py +++ b/pype/plugins/blender/create/create_action.py @@ -27,8 +27,8 @@ class CreateAction(Creator): if (self.options or {}).get("useSelection"): for obj in lib.get_selection(): - if (obj.animation_data is not None and - obj.animation_data.action is not None): + if (obj.animation_data is not None + and obj.animation_data.action is not None): empty_obj = bpy.data.objects.new(name=name, object_data=None) diff --git a/pype/plugins/blender/load/load_action.py b/pype/plugins/blender/load/load_action.py index 303d1ead4d..a1b1ad3cea 100644 --- a/pype/plugins/blender/load/load_action.py +++ b/pype/plugins/blender/load/load_action.py @@ -175,7 +175,7 @@ class BlendActionLoader(pype.blender.plugin.AssetLoader): strips = [] for obj in collection_metadata["objects"]: - + # Get all the strips that use the action arm_objs = [ arm for arm in bpy.data.objects if arm.type == 'ARMATURE'] diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py index a8ceed9c82..c359198490 100644 --- a/pype/plugins/blender/publish/collect_action.py +++ b/pype/plugins/blender/publish/collect_action.py @@ -23,8 +23,8 @@ class CollectAction(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'action' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py index 50d49692b8..681f945f25 100644 --- a/pype/plugins/blender/publish/collect_animation.py +++ b/pype/plugins/blender/publish/collect_animation.py @@ -18,13 +18,13 @@ class CollectAnimation(pyblish.api.ContextPlugin): """Return all 'animation' collections. Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded + representation set. If the representation is set, it is a loaded animation and we don't want to publish it. """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'animation' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_model.py index df5c1e709a..5cbd097a4e 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_model.py @@ -23,8 +23,8 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'model' + and not avalon_prop.get('representation')): yield collection def process(self, context): diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py index 01958da37a..730f209e89 100644 --- a/pype/plugins/blender/publish/collect_rig.py +++ b/pype/plugins/blender/publish/collect_rig.py @@ -23,8 +23,8 @@ class CollectRig(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' and - not avalon_prop.get('representation')): + if (avalon_prop.get('family') == 'rig' + and not avalon_prop.get('representation')): yield collection def process(self, context): From 043d8225a368510acb3427a92ae2672e23501367 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:02 +0100 Subject: [PATCH 24/27] unify collectors --- .../plugins/blender/publish/collect_action.py | 51 ------------------- .../blender/publish/collect_animation.py | 51 ------------------- ...{collect_model.py => collect_instances.py} | 17 ++++--- pype/plugins/blender/publish/collect_rig.py | 51 ------------------- 4 files changed, 10 insertions(+), 160 deletions(-) delete mode 100644 pype/plugins/blender/publish/collect_action.py delete mode 100644 pype/plugins/blender/publish/collect_animation.py rename pype/plugins/blender/publish/{collect_model.py => collect_instances.py} (78%) delete mode 100644 pype/plugins/blender/publish/collect_rig.py diff --git a/pype/plugins/blender/publish/collect_action.py b/pype/plugins/blender/publish/collect_action.py deleted file mode 100644 index c359198490..0000000000 --- a/pype/plugins/blender/publish/collect_action.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectAction(pyblish.api.ContextPlugin): - """Collect the data of an action.""" - - hosts = ["blender"] - label = "Collect Action" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_action_collections() -> Generator: - """Return all 'animation' collections. - - Check if the family is 'action' and if it doesn't have the - representation set. If the representation is set, it is a loaded action - and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'action' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the actions from the current Blender scene.""" - collections = self.get_action_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_animation.py b/pype/plugins/blender/publish/collect_animation.py deleted file mode 100644 index 681f945f25..0000000000 --- a/pype/plugins/blender/publish/collect_animation.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectAnimation(pyblish.api.ContextPlugin): - """Collect the data of an animation.""" - - hosts = ["blender"] - label = "Collect Animation" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_animation_collections() -> Generator: - """Return all 'animation' collections. - - Check if the family is 'animation' and if it doesn't have the - representation set. If the representation is set, it is a loaded - animation and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'animation' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the animations from the current Blender scene.""" - collections = self.get_animation_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) diff --git a/pype/plugins/blender/publish/collect_model.py b/pype/plugins/blender/publish/collect_instances.py similarity index 78% rename from pype/plugins/blender/publish/collect_model.py rename to pype/plugins/blender/publish/collect_instances.py index 5cbd097a4e..1d3693216d 100644 --- a/pype/plugins/blender/publish/collect_model.py +++ b/pype/plugins/blender/publish/collect_instances.py @@ -1,20 +1,21 @@ from typing import Generator import bpy +import json import pyblish.api from avalon.blender.pipeline import AVALON_PROPERTY -class CollectModel(pyblish.api.ContextPlugin): +class CollectInstances(pyblish.api.ContextPlugin): """Collect the data of a model.""" hosts = ["blender"] - label = "Collect Model" + label = "Collect Instances" order = pyblish.api.CollectorOrder @staticmethod - def get_model_collections() -> Generator: + def get_collections() -> Generator: """Return all 'model' collections. Check if the family is 'model' and if it doesn't have the @@ -23,13 +24,13 @@ class CollectModel(pyblish.api.ContextPlugin): """ for collection in bpy.data.collections: avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'model' - and not avalon_prop.get('representation')): + if avalon_prop.get('id') == 'pyblish.avalon.instance': yield collection def process(self, context): """Collect the models from the current Blender scene.""" - collections = self.get_model_collections() + collections = self.get_collections() + for collection in collections: avalon_prop = collection[AVALON_PROPERTY] asset = avalon_prop['asset'] @@ -48,4 +49,6 @@ class CollectModel(pyblish.api.ContextPlugin): members = list(collection.objects) members.append(collection) instance[:] = members - self.log.debug(instance.data) + self.log.debug(json.dumps(instance.data, indent=4)) + for obj in instance: + self.log.debug(obj) diff --git a/pype/plugins/blender/publish/collect_rig.py b/pype/plugins/blender/publish/collect_rig.py deleted file mode 100644 index 730f209e89..0000000000 --- a/pype/plugins/blender/publish/collect_rig.py +++ /dev/null @@ -1,51 +0,0 @@ -from typing import Generator - -import bpy - -import pyblish.api -from avalon.blender.pipeline import AVALON_PROPERTY - - -class CollectRig(pyblish.api.ContextPlugin): - """Collect the data of a rig.""" - - hosts = ["blender"] - label = "Collect Rig" - order = pyblish.api.CollectorOrder - - @staticmethod - def get_rig_collections() -> Generator: - """Return all 'rig' collections. - - Check if the family is 'rig' and if it doesn't have the - representation set. If the representation is set, it is a loaded rig - and we don't want to publish it. - """ - for collection in bpy.data.collections: - avalon_prop = collection.get(AVALON_PROPERTY) or dict() - if (avalon_prop.get('family') == 'rig' - and not avalon_prop.get('representation')): - yield collection - - def process(self, context): - """Collect the rigs from the current Blender scene.""" - collections = self.get_rig_collections() - for collection in collections: - avalon_prop = collection[AVALON_PROPERTY] - asset = avalon_prop['asset'] - family = avalon_prop['family'] - subset = avalon_prop['subset'] - task = avalon_prop['task'] - name = f"{asset}_{subset}" - instance = context.create_instance( - name=name, - family=family, - families=[family], - subset=subset, - asset=asset, - task=task, - ) - members = list(collection.objects) - members.append(collection) - instance[:] = members - self.log.debug(instance.data) From 00bec457481636d48be8ec516786b83594061a82 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:34 +0100 Subject: [PATCH 25/27] naive fix to crashing uv validator --- .../blender/publish/validate_mesh_has_uv.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pype/plugins/blender/publish/validate_mesh_has_uv.py b/pype/plugins/blender/publish/validate_mesh_has_uv.py index b71a40ad8f..d0cd33645b 100644 --- a/pype/plugins/blender/publish/validate_mesh_has_uv.py +++ b/pype/plugins/blender/publish/validate_mesh_has_uv.py @@ -35,12 +35,15 @@ class ValidateMeshHasUvs(pyblish.api.InstancePlugin): invalid = [] # TODO (jasper): only check objects in the collection that will be published? for obj in [ - obj for obj in bpy.data.objects if obj.type == 'MESH' - ]: - # Make sure we are in object mode. - bpy.ops.object.mode_set(mode='OBJECT') - if not cls.has_uvs(obj): - invalid.append(obj) + obj for obj in instance]: + try: + if obj.type == 'MESH': + # Make sure we are in object mode. + bpy.ops.object.mode_set(mode='OBJECT') + if not cls.has_uvs(obj): + invalid.append(obj) + except: + continue return invalid def process(self, instance): From 28c626b69469c1b46e3939022ab7e997c2c7c09c Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:07:59 +0100 Subject: [PATCH 26/27] attempt at alembic extractor --- pype/blender/plugin.py | 11 ++- pype/plugins/blender/publish/extract_abc.py | 91 +++++++++++++++++++++ 2 files changed, 99 insertions(+), 3 deletions(-) create mode 100644 pype/plugins/blender/publish/extract_abc.py diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index 8f72d04a1d..f27bf0daab 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -20,10 +20,15 @@ def asset_name( return name -def create_blender_context(obj: Optional[bpy.types.Object] = None): +def create_blender_context(active: Optional[bpy.types.Object] = None, + selected: Optional[bpy.types.Object] = None,): """Create a new Blender context. If an object is passed as parameter, it is set as selected and active. """ + + if not isinstance(selected, list): + selected = [selected] + for win in bpy.context.window_manager.windows: for area in win.screen.areas: if area.type == 'VIEW_3D': @@ -35,8 +40,8 @@ def create_blender_context(obj: Optional[bpy.types.Object] = None): 'area': area, 'region': region, 'scene': bpy.context.scene, - 'active_object': obj, - 'selected_objects': [obj] + 'active_object': selected[0], + 'selected_objects': selected } return override_context raise Exception("Could not create a custom Blender context.") diff --git a/pype/plugins/blender/publish/extract_abc.py b/pype/plugins/blender/publish/extract_abc.py new file mode 100644 index 0000000000..b953d41ba2 --- /dev/null +++ b/pype/plugins/blender/publish/extract_abc.py @@ -0,0 +1,91 @@ +import os + +import pype.api +import pype.blender.plugin + +import bpy + + +class ExtractABC(pype.api.Extractor): + """Extract as ABC.""" + + label = "Extract ABC" + hosts = ["blender"] + families = ["model"] + optional = True + + def process(self, instance): + # Define extract output file path + + stagingdir = self.staging_dir(instance) + filename = f"{instance.name}.fbx" + filepath = os.path.join(stagingdir, filename) + + context = bpy.context + scene = context.scene + view_layer = context.view_layer + + # Perform extraction + self.log.info("Performing extraction..") + + collections = [ + obj for obj in instance if type(obj) is bpy.types.Collection] + + assert len(collections) == 1, "There should be one and only one " \ + "collection collected for this asset" + + old_active_layer_collection = view_layer.active_layer_collection + + layers = view_layer.layer_collection.children + + # Get the layer collection from the collection we need to export. + # This is needed because in Blender you can only set the active + # collection with the layer collection, and there is no way to get + # the layer collection from the collection + # (but there is the vice versa). + layer_collections = [ + layer for layer in layers if layer.collection == collections[0]] + + assert len(layer_collections) == 1 + + view_layer.active_layer_collection = layer_collections[0] + + old_scale = scene.unit_settings.scale_length + + selected = list() + + for obj in instance: + selected.append(obj) + + new_context = pype.blender.plugin.create_blender_context(active=None, selected=selected) + + # We set the scale of the scene for the export + scene.unit_settings.scale_length = 0.01 + + self.log.info(new_context) + + # We export the abc + bpy.ops.wm.alembic_export( + new_context, + filepath=filepath, + start=1, + end=1 + ) + + view_layer.active_layer_collection = old_active_layer_collection + + scene.unit_settings.scale_length = old_scale + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'abc', + 'ext': 'abc', + 'files': filename, + "stagingDir": stagingdir, + } + instance.data["representations"].append(representation) + + self.log.info("Extracted instance '%s' to: %s", + instance.name, representation) From 59dd912a7626e3e98238c11a4c38ebaf53162f23 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Sat, 21 Mar 2020 01:26:40 +0100 Subject: [PATCH 27/27] tweak context override --- pype/blender/plugin.py | 2 +- pype/plugins/blender/publish/extract_abc.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pype/blender/plugin.py b/pype/blender/plugin.py index f27bf0daab..77fce90d65 100644 --- a/pype/blender/plugin.py +++ b/pype/blender/plugin.py @@ -40,7 +40,7 @@ def create_blender_context(active: Optional[bpy.types.Object] = None, 'area': area, 'region': region, 'scene': bpy.context.scene, - 'active_object': selected[0], + 'active_object': active, 'selected_objects': selected } return override_context diff --git a/pype/plugins/blender/publish/extract_abc.py b/pype/plugins/blender/publish/extract_abc.py index b953d41ba2..d2c0c769ae 100644 --- a/pype/plugins/blender/publish/extract_abc.py +++ b/pype/plugins/blender/publish/extract_abc.py @@ -55,9 +55,13 @@ class ExtractABC(pype.api.Extractor): selected = list() for obj in instance: - selected.append(obj) + try: + obj.select_set(True) + selected.append(obj) + except: + continue - new_context = pype.blender.plugin.create_blender_context(active=None, selected=selected) + new_context = pype.blender.plugin.create_blender_context(active=selected[0], selected=selected) # We set the scale of the scene for the export scene.unit_settings.scale_length = 0.01