mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-25 05:14:40 +01:00
Added 'action' family and small adjustments
This commit is contained in:
parent
f2733c0a1b
commit
ec411a4b5d
9 changed files with 401 additions and 7 deletions
38
pype/plugins/blender/create/create_action.py
Normal file
38
pype/plugins/blender/create/create_action.py
Normal 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
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
"""Create an animation asset."""
|
||||
|
||||
import bpy
|
||||
|
||||
from avalon import api
|
||||
|
|
|
|||
295
pype/plugins/blender/load/load_action.py
Normal file
295
pype/plugins/blender/load/load_action.py
Normal 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
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
53
pype/plugins/blender/publish/collect_action.py
Normal file
53
pype/plugins/blender/publish/collect_action.py
Normal 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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
|
|
@ -78,7 +78,8 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin):
|
|||
"matchmove",
|
||||
"image"
|
||||
"source",
|
||||
"assembly"
|
||||
"assembly",
|
||||
"action"
|
||||
]
|
||||
exclude_families = ["clip"]
|
||||
db_representation_context_keys = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue