Added 'action' family and small adjustments

This commit is contained in:
Simone Barbieri 2020-03-04 17:00:00 +00:00
parent f2733c0a1b
commit ec411a4b5d
9 changed files with 401 additions and 7 deletions

View file

@ -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

View file

@ -1,3 +1,5 @@
"""Create an animation asset."""
import bpy
from avalon import api

View file

@ -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

View file

@ -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()

View file

@ -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()

View file

@ -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)

View file

@ -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:

View file

@ -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):

View file

@ -78,7 +78,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
"matchmove",
"image"
"source",
"assembly"
"assembly",
"action"
]
exclude_families = ["clip"]
db_representation_context_keys = [