From ec411a4b5dfd4b1bef1553236f7c0820de9581b2 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 4 Mar 2020 17:00:00 +0000 Subject: [PATCH] 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 = [