Improved rig assets handling

This commit is contained in:
Simone Barbieri 2021-06-09 10:44:44 +01:00
parent 96b3e063ba
commit 015f001d73
3 changed files with 187 additions and 198 deletions

View file

@ -4,10 +4,11 @@ import bpy
from avalon import api
from avalon.blender import lib
import openpype.hosts.blender.api.plugin
from avalon.blender.pipeline import AVALON_INSTANCES
from openpype.hosts.blender.api import plugin
class CreateRig(openpype.hosts.blender.api.plugin.Creator):
class CreateRig(plugin.Creator):
"""Artist-friendly rig with controls to direct motion"""
name = "rigMain"
@ -16,26 +17,30 @@ class CreateRig(openpype.hosts.blender.api.plugin.Creator):
icon = "wheelchair"
def process(self):
# Get Instance Containter or create it if it does not exist
instances = bpy.data.collections.get(AVALON_INSTANCES)
if not instances:
instances = bpy.data.collections.new(name=AVALON_INSTANCES)
bpy.context.scene.collection.children.link(instances)
# Create instance object
asset = self.data["asset"]
subset = self.data["subset"]
name = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
collection = bpy.data.collections.new(name=name)
bpy.context.scene.collection.children.link(collection)
name = plugin.asset_name(asset, subset)
asset_group = bpy.data.objects.new(name=name, object_data=None)
instances.objects.link(asset_group)
self.data['task'] = api.Session.get('AVALON_TASK')
lib.imprint(collection, self.data)
# Add the rig object and all the children meshes to
# a set and link them all at the end to avoid duplicates.
# Blender crashes if trying to link an object that is already linked.
# This links automatically the children meshes if they were not
# selected, and doesn't link them twice if they, insted,
# were manually selected by the user.
lib.imprint(asset_group, self.data)
# Add selected objects to instance
if (self.options or {}).get("useSelection"):
for obj in lib.get_selection():
for child in obj.users_collection[0].children:
collection.children.link(child)
collection.objects.link(obj)
bpy.context.view_layer.objects.active = asset_group
selected = lib.get_selection()
for obj in selected:
obj.select_set(True)
selected.append(asset_group)
context = plugin.create_blender_context(
active=asset_group, selected=selected)
bpy.ops.object.parent_set(context, keep_transform=True)
return collection
return asset_group

View file

@ -1,21 +1,20 @@
"""Load a rig asset in Blender."""
import logging
from pathlib import Path
from pprint import pformat
from typing import Dict, List, Optional
from avalon import api, blender
import bpy
import openpype.hosts.blender.api.plugin as plugin
from avalon import api
from avalon.blender.pipeline import AVALON_CONTAINERS
from avalon.blender.pipeline import AVALON_CONTAINER_ID
from avalon.blender.pipeline import AVALON_PROPERTY
from openpype.hosts.blender.api import plugin
class BlendRigLoader(plugin.AssetLoader):
"""Load rigs from a .blend file.
Because they come from a .blend file we can simply link the collection that
contains the model. There is no further need to 'containerise' it.
"""
"""Load rigs from a .blend file."""
families = ["rig"]
representations = ["blend"]
@ -24,105 +23,110 @@ class BlendRigLoader(plugin.AssetLoader):
icon = "code-fork"
color = "orange"
def _remove(self, objects, obj_container):
for obj in list(objects):
if obj.type == 'ARMATURE':
bpy.data.armatures.remove(obj.data)
elif obj.type == 'MESH':
def _remove(self, asset_group):
objects = list(asset_group.children)
for obj in objects:
if obj.type == 'MESH':
for material_slot in list(obj.material_slots):
bpy.data.materials.remove(material_slot.material)
bpy.data.meshes.remove(obj.data)
elif obj.type == 'ARMATURE':
objects.extend(obj.children)
bpy.data.armatures.remove(obj.data)
elif obj.type == 'CURVE':
bpy.data.curves.remove(obj.data)
elif obj.type == 'EMPTY':
objects.extend(obj.children)
bpy.data.objects.remove(obj)
for child in obj_container.children:
bpy.data.collections.remove(child)
bpy.data.collections.remove(obj_container)
def make_local_and_metadata(self, obj, collection_name):
local_obj = plugin.prepare_data(obj, collection_name)
plugin.prepare_data(local_obj.data, collection_name)
if not local_obj.get(blender.pipeline.AVALON_PROPERTY):
local_obj[blender.pipeline.AVALON_PROPERTY] = dict()
avalon_info = local_obj[blender.pipeline.AVALON_PROPERTY]
avalon_info.update({"container_name": collection_name + '_CON'})
return local_obj
def _process(
self, libpath, lib_container, collection_name,
action, parent_collection
):
def _process(self, libpath, asset_group, group_name, action):
relative = bpy.context.preferences.filepaths.use_relative_paths
with bpy.data.libraries.load(
libpath, link=True, relative=relative
) as (_, data_to):
data_to.collections = [lib_container]
) as (data_from, data_to):
data_to.objects = data_from.objects
parent = parent_collection
parent = bpy.context.scene.collection
if parent is None:
parent = bpy.context.scene.collection
empties = [obj for obj in data_to.objects if obj.type == 'EMPTY']
parent.children.link(bpy.data.collections[lib_container])
container = None
rig_container = parent.children[lib_container].make_local()
rig_container.name = collection_name
for empty in empties:
if empty.get(AVALON_PROPERTY):
container = empty
break
assert container, "No asset group found"
# Children must be linked before parents,
# otherwise the hierarchy will break
objects = []
armatures = [
obj for obj in rig_container.objects
if obj.type == 'ARMATURE'
]
nodes = list(container.children)
for child in rig_container.children:
local_child = plugin.prepare_data(child, collection_name)
objects.extend(local_child.objects)
for obj in nodes:
obj.parent = asset_group
# for obj in bpy.data.objects:
# obj.select_set(False)
for obj in nodes:
objects.append(obj)
nodes.extend(list(obj.children))
objects.reverse()
constraints = []
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
for armature in armatures:
for bone in armature.pose.bones:
for constraint in bone.constraints:
if hasattr(constraint, 'target'):
constraints.append(constraint)
# Link armatures after other objects.
# The armature is unparented for all the non-local meshes,
# when it is made local.
for obj in objects:
local_obj = self.make_local_and_metadata(obj, collection_name)
parent.objects.link(obj)
if obj != local_obj:
for constraint in constraints:
if constraint.target == obj:
constraint.target = local_obj
for obj in objects:
local_obj = plugin.prepare_data(obj, group_name)
for armature in armatures:
local_obj = self.make_local_and_metadata(armature, collection_name)
if obj.type == 'MESH':
plugin.prepare_data(local_obj.data, group_name)
if action is not None:
local_obj.animation_data.action = action
elif local_obj.animation_data.action is not None:
plugin.prepare_data(
local_obj.animation_data.action, collection_name)
if obj != local_obj:
for constraint in constraints:
if constraint.target == obj:
constraint.target = local_obj
# Set link the drivers to the local object
if local_obj.data.animation_data:
for d in local_obj.data.animation_data.drivers:
for v in d.driver.variables:
for t in v.targets:
t.id = local_obj
for material_slot in local_obj.material_slots:
plugin.prepare_data(material_slot.material, group_name)
elif obj.type == 'ARMATURE':
plugin.prepare_data(local_obj.data, group_name)
rig_container.pop(blender.pipeline.AVALON_PROPERTY)
if action is not None:
local_obj.animation_data.action = action
elif local_obj.animation_data.action is not None:
plugin.prepare_data(
local_obj.animation_data.action, group_name)
# Set link the drivers to the local object
if local_obj.data.animation_data:
for d in local_obj.data.animation_data.drivers:
for v in d.driver.variables:
for t in v.targets:
t.id = local_obj
if not obj.get(AVALON_PROPERTY):
local_obj[AVALON_PROPERTY] = dict()
avalon_info = local_obj[AVALON_PROPERTY]
avalon_info.update({"container_name": group_name})
objects.reverse()
bpy.ops.object.select_all(action='DESELECT')
return rig_container
return objects
def process_asset(
self, context: dict, name: str, namespace: Optional[str] = None,
@ -138,61 +142,48 @@ class BlendRigLoader(plugin.AssetLoader):
libpath = self.fname
asset = context["asset"]["name"]
subset = context["subset"]["name"]
lib_container = plugin.asset_name(
asset, subset
)
unique_number = plugin.get_unique_number(
asset, subset
)
asset_name = plugin.asset_name(asset, subset)
unique_number = plugin.get_unique_number(asset, subset)
group_name = plugin.asset_name(asset, subset, unique_number)
namespace = namespace or f"{asset}_{unique_number}"
collection_name = plugin.asset_name(
asset, subset, unique_number
)
container = bpy.data.collections.new(collection_name)
blender.pipeline.containerise_existing(
container,
name,
namespace,
context,
self.__class__.__name__,
)
avalon_container = bpy.data.collections.get(AVALON_CONTAINERS)
if not avalon_container:
avalon_container = bpy.data.collections.new(name=AVALON_CONTAINERS)
bpy.context.scene.collection.children.link(avalon_container)
metadata = container.get(blender.pipeline.AVALON_PROPERTY)
asset_group = bpy.data.objects.new(group_name, object_data=None)
avalon_container.objects.link(asset_group)
metadata["libpath"] = libpath
metadata["lib_container"] = lib_container
objects = self._process(libpath, asset_group, group_name, None)
obj_container = self._process(
libpath, lib_container, collection_name, None, None)
bpy.context.scene.collection.objects.link(asset_group)
metadata["obj_container"] = obj_container
# Save the list of objects in the metadata container
metadata["objects"] = obj_container.all_objects
asset_group[AVALON_PROPERTY] = {
"schema": "openpype:container-2.0",
"id": AVALON_CONTAINER_ID,
"name": name,
"namespace": namespace or '',
"loader": str(self.__class__.__name__),
"representation": str(context["representation"]["_id"]),
"libpath": libpath,
"asset_name": asset_name,
"parent": str(context["representation"]["parent"]),
"family": context["representation"]["context"]["family"]
}
metadata["parent"] = str(context["representation"]["parent"])
metadata["family"] = context["representation"]["context"]["family"]
nodes = list(container.objects)
nodes.append(container)
self[:] = nodes
return nodes
self[:] = objects
return objects
def update(self, container: Dict, representation: Dict):
"""Update the loaded asset.
This will remove all objects of the current collection, load the new
ones and add them to the collection.
If the objects of the collection are used in another collection they
will not be removed, only unlinked. Normally this should not be the
case though.
Warning:
No nested collections are supported at the moment!
This will remove all children of the asset group, load the new ones
and add them as children of the group.
"""
collection = bpy.data.collections.get(
container["objectName"]
)
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = Path(api.get_representation_path(representation))
extension = libpath.suffix.lower()
@ -202,12 +193,9 @@ class BlendRigLoader(plugin.AssetLoader):
pformat(representation, indent=2),
)
assert collection, (
assert asset_group, (
f"The asset is not loaded: {container['objectName']}"
)
assert not (collection.children), (
"Nested collections are not supported."
)
assert libpath, (
"No existing library file found for {container['objectName']}"
)
@ -218,89 +206,84 @@ class BlendRigLoader(plugin.AssetLoader):
f"Unsupported file: {libpath}"
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
collection_libpath = collection_metadata["libpath"]
lib_container = collection_metadata["lib_container"]
metadata = asset_group.get(AVALON_PROPERTY)
group_libpath = metadata["libpath"]
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
container_name = obj_container.name
normalized_collection_libpath = (
str(Path(bpy.path.abspath(collection_libpath)).resolve())
normalized_group_libpath = (
str(Path(bpy.path.abspath(group_libpath)).resolve())
)
normalized_libpath = (
str(Path(bpy.path.abspath(str(libpath))).resolve())
)
self.log.debug(
"normalized_collection_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_collection_libpath,
"normalized_group_libpath:\n %s\nnormalized_libpath:\n %s",
normalized_group_libpath,
normalized_libpath,
)
if normalized_collection_libpath == normalized_libpath:
if normalized_group_libpath == normalized_libpath:
self.log.info("Library already loaded, not updating...")
return
# Get the armature of the rig
armatures = [obj for obj in objects if obj.type == 'ARMATURE']
assert(len(armatures) == 1)
# Check how many assets use the same library
count = 0
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
if obj.get(AVALON_PROPERTY).get('libpath') == group_libpath:
count += 1
# # Get the armature of the rig
objects = asset_group.children
armature = [obj for obj in objects if obj.type == 'ARMATURE'][0]
action = None
if armatures[0].animation_data and armatures[0].animation_data.action:
action = armatures[0].animation_data.action
if armature.animation_data and armature.animation_data.action:
action = armature.animation_data.action
parent = plugin.get_parent_collection(obj_container)
mat = asset_group.matrix_basis.copy()
self._remove(objects, obj_container)
self._remove(asset_group)
obj_container = self._process(
str(libpath), lib_container, container_name, action, parent)
# If it is the last object to use that library, remove it
if count == 1:
library = bpy.data.libraries.get(bpy.path.basename(group_libpath))
bpy.data.libraries.remove(library)
# Save the list of objects in the metadata container
collection_metadata["obj_container"] = obj_container
collection_metadata["objects"] = obj_container.all_objects
collection_metadata["libpath"] = str(libpath)
collection_metadata["representation"] = str(representation["_id"])
self._process(str(libpath), asset_group, object_name, action)
bpy.ops.object.select_all(action='DESELECT')
asset_group.matrix_basis = mat
metadata["libpath"] = str(libpath)
metadata["representation"] = str(representation["_id"])
def remove(self, container: Dict) -> bool:
"""Remove an existing container from a Blender scene.
"""Remove an existing asset group from a Blender scene.
Arguments:
container (openpype:container-1.0): Container to remove,
from `host.ls()`.
Returns:
bool: Whether the container was deleted.
Warning:
No nested collections are supported at the moment!
bool: Whether the asset group was deleted.
"""
object_name = container["objectName"]
asset_group = bpy.data.objects.get(object_name)
libpath = asset_group.get(AVALON_PROPERTY).get('libpath')
collection = bpy.data.collections.get(
container["objectName"]
)
if not collection:
# Check how many assets use the same library
count = 0
for obj in bpy.data.collections.get(AVALON_CONTAINERS).objects:
if obj.get(AVALON_PROPERTY).get('libpath') == libpath:
count += 1
if not asset_group:
return False
assert not (collection.children), (
"Nested collections are not supported."
)
collection_metadata = collection.get(
blender.pipeline.AVALON_PROPERTY)
self._remove(asset_group)
obj_container = plugin.get_local_collection_with_name(
collection_metadata["obj_container"].name
)
objects = obj_container.all_objects
bpy.data.objects.remove(asset_group)
self._remove(objects, obj_container)
bpy.data.collections.remove(collection)
# If it is the last object to use that library, remove it
if count == 1:
library = bpy.data.libraries.get(bpy.path.basename(libpath))
bpy.data.libraries.remove(library)
return True

View file

@ -21,8 +21,6 @@ class ExtractFBX(api.Extractor):
filename = f"{instance.name}.fbx"
filepath = os.path.join(stagingdir, filename)
scene = bpy.context.scene
# Perform extraction
self.log.info("Performing extraction..")
@ -41,12 +39,16 @@ class ExtractFBX(api.Extractor):
active=asset_group, selected=selected)
new_materials = []
new_materials_objs = []
objects = list(asset_group.children)
for obj in collections[0].all_objects:
if obj.type == 'MESH':
for obj in objects:
objects.extend(obj.children)
if obj.type == 'MESH' and len(obj.data.materials) == 0:
mat = bpy.data.materials.new(obj.name)
obj.data.materials.append(mat)
new_materials.append(mat)
new_materials_objs.append(obj)
# We export the fbx
bpy.ops.export_scene.fbx(
@ -63,9 +65,8 @@ class ExtractFBX(api.Extractor):
for mat in new_materials:
bpy.data.materials.remove(mat)
for obj in collections[0].all_objects:
if obj.type == 'MESH':
obj.data.materials.pop()
for obj in new_materials_objs:
obj.data.materials.pop()
if "representations" not in instance.data:
instance.data["representations"] = []