mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Improved animation workflow
This commit is contained in:
parent
78e10b9d1a
commit
07fd5ba12c
10 changed files with 235 additions and 262 deletions
|
|
@ -2,11 +2,13 @@
|
|||
|
||||
import bpy
|
||||
|
||||
from avalon import api, blender
|
||||
import openpype.hosts.blender.api.plugin
|
||||
from avalon import api
|
||||
from avalon.blender import lib, ops
|
||||
from avalon.blender.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
class CreateAnimation(openpype.hosts.blender.api.plugin.Creator):
|
||||
class CreateAnimation(plugin.Creator):
|
||||
"""Animation output for character rigs"""
|
||||
|
||||
name = "animationMain"
|
||||
|
|
@ -15,16 +17,36 @@ class CreateAnimation(openpype.hosts.blender.api.plugin.Creator):
|
|||
icon = "male"
|
||||
|
||||
def process(self):
|
||||
""" Run the creator on Blender main thread"""
|
||||
mti = ops.MainThreadItem(self._process)
|
||||
ops.execute_in_main_thread(mti)
|
||||
|
||||
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
|
||||
# name = self.name
|
||||
# if not name:
|
||||
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)
|
||||
# asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
asset_group = bpy.data.collections.new(name=name)
|
||||
instances.children.link(asset_group)
|
||||
self.data['task'] = api.Session.get('AVALON_TASK')
|
||||
blender.lib.imprint(collection, self.data)
|
||||
lib.imprint(asset_group, self.data)
|
||||
|
||||
if (self.options or {}).get("useSelection"):
|
||||
for obj in blender.lib.get_selection():
|
||||
collection.objects.link(obj)
|
||||
selected = lib.get_selection()
|
||||
for obj in selected:
|
||||
asset_group.objects.link(obj)
|
||||
elif (self.options or {}).get("asset_group"):
|
||||
obj = (self.options or {}).get("asset_group")
|
||||
asset_group.objects.link(obj)
|
||||
|
||||
return collection
|
||||
return asset_group
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
"""Load an animation 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
|
||||
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
logger = logging.getLogger("openpype").getChild(
|
||||
"blender").getChild("load_animation")
|
||||
|
||||
|
||||
class BlendAnimationLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
||||
class BlendAnimationLoader(plugin.AssetLoader):
|
||||
"""Load animations from a .blend file.
|
||||
|
||||
Warning:
|
||||
|
|
@ -29,67 +28,6 @@ class BlendAnimationLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _remove(self, objects, lib_container):
|
||||
for obj in list(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])
|
||||
|
||||
def _process(self, libpath, lib_container, container_name):
|
||||
|
||||
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])
|
||||
|
||||
anim_container = scene.collection.children[lib_container].make_local()
|
||||
|
||||
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?
|
||||
|
||||
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()
|
||||
|
||||
anim_data = obj.animation_data
|
||||
|
||||
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):
|
||||
|
||||
obj[blender.pipeline.AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = obj[blender.pipeline.AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": container_name})
|
||||
|
||||
objects_list.append(obj)
|
||||
|
||||
anim_container.pop(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
|
||||
|
|
@ -101,148 +39,32 @@ class BlendAnimationLoader(openpype.hosts.blender.api.plugin.AssetLoader):
|
|||
context: Full parenthood of representation to load
|
||||
options: Additional settings dictionary
|
||||
"""
|
||||
|
||||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
lib_container = openpype.hosts.blender.api.plugin.asset_name(asset, subset)
|
||||
container_name = openpype.hosts.blender.api.plugin.asset_name(
|
||||
asset, subset, namespace
|
||||
)
|
||||
|
||||
container = bpy.data.collections.new(lib_container)
|
||||
container.name = container_name
|
||||
blender.pipeline.containerise_existing(
|
||||
container,
|
||||
name,
|
||||
namespace,
|
||||
context,
|
||||
self.__class__.__name__,
|
||||
)
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=False
|
||||
) as (data_from, data_to):
|
||||
data_to.objects = data_from.objects
|
||||
data_to.actions = data_from.actions
|
||||
|
||||
container_metadata = container.get(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
container = data_to.objects[0]
|
||||
|
||||
container_metadata["libpath"] = libpath
|
||||
container_metadata["lib_container"] = lib_container
|
||||
assert container, "No asset group found"
|
||||
|
||||
objects_list = self._process(
|
||||
libpath, lib_container, container_name)
|
||||
target_namespace = container.get(AVALON_PROPERTY).get('namespace')
|
||||
|
||||
# Save the list of objects in the metadata container
|
||||
container_metadata["objects"] = objects_list
|
||||
action = data_to.actions[0].make_local().copy()
|
||||
|
||||
nodes = list(container.objects)
|
||||
nodes.append(container)
|
||||
self[:] = nodes
|
||||
return nodes
|
||||
for obj in bpy.data.objects:
|
||||
if obj.get(AVALON_PROPERTY) and obj.get(AVALON_PROPERTY).get(
|
||||
'namespace') == target_namespace:
|
||||
if obj.children[0]:
|
||||
if not obj.children[0].animation_data:
|
||||
obj.children[0].animation_data_create()
|
||||
obj.children[0].animation_data.action = action
|
||||
break
|
||||
|
||||
def update(self, container: Dict, representation: Dict):
|
||||
"""Update the loaded asset.
|
||||
bpy.data.objects.remove(container)
|
||||
|
||||
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 openpype.hosts.blender.api.plugin.VALID_EXTENSIONS, (
|
||||
f"Unsupported file: {libpath}"
|
||||
)
|
||||
|
||||
collection_metadata = collection.get(
|
||||
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
|
||||
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
objects_list = self._process(
|
||||
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.
|
||||
|
||||
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!
|
||||
"""
|
||||
|
||||
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(
|
||||
blender.pipeline.AVALON_PROPERTY)
|
||||
objects = collection_metadata["objects"]
|
||||
lib_container = collection_metadata["lib_container"]
|
||||
|
||||
self._remove(objects, lib_container)
|
||||
|
||||
bpy.data.collections.remove(collection)
|
||||
|
||||
return True
|
||||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ 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 avalon.blender.pipeline import AVALON_INSTANCES
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
|
|
@ -32,6 +33,14 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
for obj in objects:
|
||||
api.remove(obj.get(AVALON_PROPERTY))
|
||||
|
||||
def _remove_animation_instances(self, asset_group):
|
||||
instances = bpy.data.collections.get(AVALON_INSTANCES)
|
||||
if instances:
|
||||
for obj in list(asset_group.children):
|
||||
anim_collection = instances.children.get(obj.name+"_animation")
|
||||
if anim_collection:
|
||||
bpy.data.collections.remove(anim_collection)
|
||||
|
||||
def _get_loader(self, loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
|
|
@ -48,7 +57,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
|
||||
return None
|
||||
|
||||
def _process(self, libpath, asset_group, actions):
|
||||
def _process(self, libpath, asset, asset_group, actions):
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
with open(libpath, "r") as fp:
|
||||
|
|
@ -76,7 +85,9 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
options = {
|
||||
'parent': asset_group,
|
||||
'transform': element.get('transform'),
|
||||
'action': action
|
||||
'action': action,
|
||||
'create_animation': True if family == 'rig' else False,
|
||||
'animation_asset': asset
|
||||
}
|
||||
|
||||
# This should return the loaded asset, but the load call will be
|
||||
|
|
@ -121,7 +132,7 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
self._process(libpath, asset_group, None)
|
||||
self._process(libpath, asset, asset_group, None)
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
|
|
@ -206,11 +217,13 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
if not rig:
|
||||
raise Exception("No armature in the rig asset group.")
|
||||
if rig.animation_data and rig.animation_data.action:
|
||||
instance_name = obj_meta.get('instance_name')
|
||||
actions[instance_name] = rig.animation_data.action
|
||||
namespace = obj_meta.get('namespace')
|
||||
actions[namespace] = rig.animation_data.action
|
||||
|
||||
mat = asset_group.matrix_basis.copy()
|
||||
|
||||
self._remove_animation_instances(asset_group)
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
self._process(str(libpath), asset_group, actions)
|
||||
|
|
@ -236,6 +249,8 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
if not asset_group:
|
||||
return False
|
||||
|
||||
self._remove_animation_instances(asset_group)
|
||||
|
||||
self._remove(asset_group)
|
||||
|
||||
bpy.data.objects.remove(asset_group)
|
||||
|
|
|
|||
|
|
@ -137,8 +137,6 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
rotation = transform.get('rotation')
|
||||
scale = transform.get('scale')
|
||||
|
||||
# Y position is inverted in sign because Unreal and Blender have the
|
||||
# Y axis mirrored
|
||||
asset_group.location = (
|
||||
location.get('x'),
|
||||
location.get('y'),
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ 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 import lib
|
||||
from openpype.hosts.blender.api import plugin
|
||||
|
||||
|
||||
|
|
@ -164,18 +165,19 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
create_animation = False
|
||||
|
||||
if options is not None:
|
||||
parent = options.get('parent')
|
||||
transform = options.get('transform')
|
||||
action = options.get('action')
|
||||
create_animation = options.get('create_animation')
|
||||
|
||||
if parent and transform:
|
||||
location = transform.get('translation')
|
||||
rotation = transform.get('rotation')
|
||||
scale = transform.get('scale')
|
||||
|
||||
# Y position is inverted in sign because Unreal and Blender have the
|
||||
# Y axis mirrored
|
||||
asset_group.location = (
|
||||
location.get('x'),
|
||||
location.get('y'),
|
||||
|
|
@ -201,6 +203,27 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
|
||||
objects = self._process(libpath, asset_group, group_name, action)
|
||||
|
||||
if create_animation:
|
||||
creator_plugin = lib.get_creator_by_name("CreateAnimation")
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"CreateAnimation\" was "
|
||||
"not found.")
|
||||
|
||||
asset_group.select_set(True)
|
||||
|
||||
animation_asset = options.get('animation_asset')
|
||||
|
||||
api.create(
|
||||
creator_plugin,
|
||||
name=namespace+"_animation",
|
||||
# name=f"{unique_number}_{subset}_animation",
|
||||
asset=animation_asset,
|
||||
options={"useSelection": False, "asset_group": asset_group},
|
||||
data={"dependencies": str(context["representation"]["_id"])}
|
||||
)
|
||||
|
||||
bpy.ops.object.select_all(action='DESELECT')
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
|
|
|
|||
|
|
@ -29,9 +29,23 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
if avalon_prop.get('id') == 'pyblish.avalon.instance':
|
||||
yield obj
|
||||
|
||||
@staticmethod
|
||||
def get_collections() -> Generator:
|
||||
"""Return all 'model' collections.
|
||||
|
||||
Check if the family is 'model' and if it doesn't have the
|
||||
representation set. If the representation is set, it is a loaded model
|
||||
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('id') == 'pyblish.avalon.instance':
|
||||
yield collection
|
||||
|
||||
def process(self, context):
|
||||
"""Collect the models from the current Blender scene."""
|
||||
asset_groups = self.get_asset_groups()
|
||||
collections = self.get_collections()
|
||||
|
||||
for group in asset_groups:
|
||||
avalon_prop = group[AVALON_PROPERTY]
|
||||
|
|
@ -58,3 +72,31 @@ class CollectInstances(pyblish.api.ContextPlugin):
|
|||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
for obj in instance:
|
||||
self.log.debug(obj)
|
||||
|
||||
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)
|
||||
if family == "animation":
|
||||
for obj in collection.objects:
|
||||
if obj.type == 'EMPTY' and obj.get(AVALON_PROPERTY):
|
||||
for child in obj.children:
|
||||
if child.type == 'ARMATURE':
|
||||
members.append(child)
|
||||
members.append(collection)
|
||||
instance[:] = members
|
||||
self.log.debug(json.dumps(instance.data, indent=4))
|
||||
for obj in instance:
|
||||
self.log.debug(obj)
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class ExtractBlend(openpype.api.Extractor):
|
|||
|
||||
label = "Extract Blend"
|
||||
hosts = ["blender"]
|
||||
families = ["model", "camera", "rig", "action", "layout", "animation"]
|
||||
families = ["model", "camera", "rig", "action", "layout"]
|
||||
optional = True
|
||||
|
||||
def process(self, instance):
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
import os
|
||||
|
||||
import bpy
|
||||
|
||||
import openpype.api
|
||||
|
||||
|
||||
class ExtractBlendAnimation(openpype.api.Extractor):
|
||||
"""Extract a blend file."""
|
||||
|
||||
label = "Extract Blend"
|
||||
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..")
|
||||
|
||||
data_blocks = set()
|
||||
|
||||
for obj in instance:
|
||||
if isinstance(obj, bpy.types.Object) and obj.type == 'EMPTY':
|
||||
child = obj.children[0]
|
||||
if child and child.type == 'ARMATURE':
|
||||
if not obj.animation_data:
|
||||
obj.animation_data_create()
|
||||
obj.animation_data.action = child.animation_data.action
|
||||
obj.animation_data_clear()
|
||||
data_blocks.add(child.animation_data.action)
|
||||
data_blocks.add(obj)
|
||||
|
||||
bpy.data.libraries.write(filepath, data_blocks)
|
||||
|
||||
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)
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
import os
|
||||
import json
|
||||
|
||||
import openpype.api
|
||||
|
||||
import bpy
|
||||
import bpy_extras
|
||||
import bpy_extras.anim_utils
|
||||
|
||||
from openpype import api
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
|
||||
class ExtractAnimationFBX(openpype.api.Extractor):
|
||||
|
||||
class ExtractAnimationFBX(api.Extractor):
|
||||
"""Extract as animation."""
|
||||
|
||||
label = "Extract FBX"
|
||||
|
|
@ -20,33 +22,26 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
||||
context = bpy.context
|
||||
scene = context.scene
|
||||
|
||||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
collections = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection]
|
||||
# The first collection object in the instance is taken, as there
|
||||
# should be only one that contains the asset group.
|
||||
collection = [
|
||||
obj for obj in instance if type(obj) is bpy.types.Collection][0]
|
||||
|
||||
assert len(collections) == 1, "There should be one and only one " \
|
||||
"collection collected for this asset"
|
||||
# Again, the first object in the collection is taken , as there
|
||||
# should be only the asset group in the collection.
|
||||
asset_group = collection.objects[0]
|
||||
|
||||
old_scale = scene.unit_settings.scale_length
|
||||
armature = [
|
||||
obj for obj in asset_group.children if obj.type == 'ARMATURE'][0]
|
||||
|
||||
# We set the scale of the scene for the export
|
||||
scene.unit_settings.scale_length = 0.01
|
||||
|
||||
armatures = [
|
||||
obj for obj in collections[0].objects if obj.type == 'ARMATURE']
|
||||
|
||||
assert len(collections) == 1, "There should be one and only one " \
|
||||
"armature collected for this asset"
|
||||
|
||||
armature = armatures[0]
|
||||
asset_group_name = asset_group.name
|
||||
asset_group.name = asset_group.get(AVALON_PROPERTY).get("asset_name")
|
||||
|
||||
armature_name = armature.name
|
||||
original_name = armature_name.split(':')[0]
|
||||
original_name = armature_name.split(':')[1]
|
||||
armature.name = original_name
|
||||
|
||||
object_action_pairs = []
|
||||
|
|
@ -89,27 +84,29 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
for obj in bpy.data.objects:
|
||||
obj.select_set(False)
|
||||
|
||||
asset_group.select_set(True)
|
||||
armature.select_set(True)
|
||||
fbx_filename = f"{instance.name}_{armature.name}.fbx"
|
||||
filepath = os.path.join(stagingdir, fbx_filename)
|
||||
|
||||
override = bpy.context.copy()
|
||||
override['selected_objects'] = [armature]
|
||||
override = plugin.create_blender_context(
|
||||
active=asset_group, selected=[asset_group, armature])
|
||||
bpy.ops.export_scene.fbx(
|
||||
override,
|
||||
filepath=filepath,
|
||||
use_active_collection=False,
|
||||
use_selection=True,
|
||||
bake_anim_use_nla_strips=False,
|
||||
bake_anim_use_all_actions=False,
|
||||
add_leaf_bones=False,
|
||||
armature_nodetype='ROOT',
|
||||
object_types={'ARMATURE'}
|
||||
object_types={'EMPTY', 'ARMATURE'}
|
||||
)
|
||||
armature.name = armature_name
|
||||
asset_group.name = asset_group_name
|
||||
asset_group.select_set(False)
|
||||
armature.select_set(False)
|
||||
|
||||
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)):
|
||||
pair = object_action_pairs[i]
|
||||
|
|
@ -125,18 +122,20 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
json_filename = f"{instance.name}.json"
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
json_dict = {}
|
||||
json_dict = {
|
||||
"instance_name": asset_group.get(AVALON_PROPERTY).get("namespace")
|
||||
}
|
||||
|
||||
collection = instance.data.get("name")
|
||||
container = None
|
||||
for obj in bpy.data.collections[collection].objects:
|
||||
if obj.type == "ARMATURE":
|
||||
container_name = obj.get("avalon").get("container_name")
|
||||
container = bpy.data.collections[container_name]
|
||||
if container:
|
||||
json_dict = {
|
||||
"instance_name": container.get("avalon").get("instance_name")
|
||||
}
|
||||
# collection = instance.data.get("name")
|
||||
# container = None
|
||||
# for obj in bpy.data.collections[collection].objects:
|
||||
# if obj.type == "ARMATURE":
|
||||
# container_name = obj.get("avalon").get("container_name")
|
||||
# container = bpy.data.collections[container_name]
|
||||
# if container:
|
||||
# json_dict = {
|
||||
# "instance_name": container.get("avalon").get("instance_name")
|
||||
# }
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_dict, fp=file, indent=2)
|
||||
|
|
@ -159,6 +158,5 @@ class ExtractAnimationFBX(openpype.api.Extractor):
|
|||
instance.data["representations"].append(fbx_representation)
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
|
||||
self.log.info("Extracted instance '{}' to: {}".format(
|
||||
instance.name, fbx_representation))
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
"z": transform.translation.z
|
||||
},
|
||||
"rotation": {
|
||||
"x": math.radians(transform.rotation.euler().x + 90.0),
|
||||
"x": math.radians(transform.rotation.euler().x),
|
||||
"y": math.radians(transform.rotation.euler().y),
|
||||
"z": math.radians(180.0 - transform.rotation.euler().z)
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue