mirror of
https://github.com/ynput/ayon-core.git
synced 2026-01-02 00:44:52 +01:00
Layout now includes animations
This commit is contained in:
parent
11a576a0ed
commit
db23c995e2
3 changed files with 271 additions and 38 deletions
|
|
@ -110,6 +110,8 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
plugin.prepare_data(local_obj.data, group_name)
|
||||
|
||||
if action is not None:
|
||||
if local_obj.animation_data is None:
|
||||
local_obj.animation_data_create()
|
||||
local_obj.animation_data.action = action
|
||||
elif (local_obj.animation_data and
|
||||
local_obj.animation_data.action is not None):
|
||||
|
|
|
|||
|
|
@ -2,9 +2,12 @@ import os
|
|||
import json
|
||||
|
||||
import bpy
|
||||
import bpy_extras
|
||||
import bpy_extras.anim_utils
|
||||
|
||||
from avalon import io
|
||||
from avalon.blender.pipeline import AVALON_PROPERTY
|
||||
from openpype.hosts.blender.api import plugin
|
||||
import openpype.api
|
||||
|
||||
|
||||
|
|
@ -16,6 +19,99 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
families = ["layout"]
|
||||
optional = True
|
||||
|
||||
def _export_animation(self, asset, instance, stagingdir):
|
||||
file_names = []
|
||||
|
||||
for obj in asset.children:
|
||||
if obj.type != "ARMATURE":
|
||||
continue
|
||||
|
||||
asset_group_name = asset.name
|
||||
asset.name = asset.get(AVALON_PROPERTY).get("asset_name")
|
||||
|
||||
armature_name = obj.name
|
||||
original_name = armature_name.split(':')[1]
|
||||
obj.name = original_name
|
||||
|
||||
object_action_pairs = []
|
||||
original_actions = []
|
||||
|
||||
starting_frames = []
|
||||
ending_frames = []
|
||||
|
||||
# For each armature, we make a copy of the current action
|
||||
curr_action = None
|
||||
copy_action = None
|
||||
|
||||
if obj.animation_data and obj.animation_data.action:
|
||||
curr_action = obj.animation_data.action
|
||||
copy_action = curr_action.copy()
|
||||
|
||||
curr_frame_range = curr_action.frame_range
|
||||
|
||||
starting_frames.append(curr_frame_range[0])
|
||||
ending_frames.append(curr_frame_range[1])
|
||||
else:
|
||||
self.log.info("Object have no animation.")
|
||||
continue
|
||||
|
||||
object_action_pairs.append((obj, copy_action))
|
||||
original_actions.append(curr_action)
|
||||
|
||||
# We compute the starting and ending frames
|
||||
max_frame = min(starting_frames)
|
||||
min_frame = max(ending_frames)
|
||||
|
||||
# We bake the copy of the current action for each object
|
||||
bpy_extras.anim_utils.bake_action_objects(
|
||||
object_action_pairs,
|
||||
frames=range(int(min_frame), int(max_frame)),
|
||||
do_object=False,
|
||||
do_clean=False
|
||||
)
|
||||
|
||||
for o in bpy.data.objects:
|
||||
o.select_set(False)
|
||||
|
||||
asset.select_set(True)
|
||||
obj.select_set(True)
|
||||
fbx_filename = f"{instance.name}_{asset_group_name}.fbx"
|
||||
filepath = os.path.join(stagingdir, fbx_filename)
|
||||
|
||||
override = plugin.create_blender_context(
|
||||
active=asset, selected=[asset, obj])
|
||||
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={'EMPTY', 'ARMATURE'}
|
||||
)
|
||||
obj.name = armature_name
|
||||
asset.name = asset_group_name
|
||||
asset.select_set(False)
|
||||
obj.select_set(False)
|
||||
|
||||
# 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]
|
||||
action = original_actions[i]
|
||||
|
||||
if action:
|
||||
pair[0].animation_data.action = action
|
||||
|
||||
if pair[1]:
|
||||
pair[1].user_clear()
|
||||
bpy.data.actions.remove(pair[1])
|
||||
|
||||
file_names.append(fbx_filename)
|
||||
|
||||
return file_names
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
|
@ -23,7 +119,11 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
# Perform extraction
|
||||
self.log.info("Performing extraction..")
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
json_data = []
|
||||
fbx_files = []
|
||||
|
||||
asset_group = bpy.data.objects[str(instance)]
|
||||
|
||||
|
|
@ -78,22 +178,32 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
}
|
||||
json_data.append(json_element)
|
||||
|
||||
# Extract the animation as well
|
||||
if family == "rig":
|
||||
fbx_files.extend(
|
||||
self._export_animation(
|
||||
asset, instance, stagingdir))
|
||||
|
||||
json_filename = "{}.json".format(instance.name)
|
||||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
with open(json_path, "w+") as file:
|
||||
json.dump(json_data, fp=file, indent=2)
|
||||
|
||||
if "representations" not in instance.data:
|
||||
instance.data["representations"] = []
|
||||
|
||||
representation = {
|
||||
json_representation = {
|
||||
'name': 'json',
|
||||
'ext': 'json',
|
||||
'files': json_filename,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(representation)
|
||||
fbx_representation = {
|
||||
'name': 'fbx',
|
||||
'ext': 'fbx',
|
||||
'files': fbx_files,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(json_representation)
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
instance.name, json_representation)
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
import os
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
from unreal import EditorAssetLibrary
|
||||
from unreal import EditorLevelLibrary
|
||||
from unreal import AssetToolsHelpers
|
||||
from unreal import FBXImportType
|
||||
from unreal import MathLibrary as umath
|
||||
|
||||
from avalon import api, pipeline
|
||||
|
|
@ -58,6 +61,8 @@ class LayoutLoader(api.Loader):
|
|||
def _process_family(self, assets, classname, transform):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
actors = []
|
||||
|
||||
for asset in assets:
|
||||
obj = ar.get_asset_by_object_path(asset).get_asset()
|
||||
if obj.get_class().get_name() == classname:
|
||||
|
|
@ -75,7 +80,91 @@ class LayoutLoader(api.Loader):
|
|||
), False)
|
||||
actor.set_actor_scale3d(transform.get('scale'))
|
||||
|
||||
actors.append(actor)
|
||||
|
||||
return actors
|
||||
|
||||
def _import_animation(
|
||||
self, asset_dir, path, rig_count, instance_name, skeleton,
|
||||
actors_dict):
|
||||
anim_path = f"{asset_dir}/animations/{path.with_suffix('').name}_{rig_count:02d}"
|
||||
|
||||
# Import animation
|
||||
task = unreal.AssetImportTask()
|
||||
task.options = unreal.FbxImportUI()
|
||||
|
||||
task.set_editor_property(
|
||||
'filename', str(path.with_suffix(f".{rig_count:02d}.fbx")))
|
||||
task.set_editor_property('destination_path', anim_path)
|
||||
task.set_editor_property(
|
||||
'destination_name', f"{instance_name}_animation")
|
||||
task.set_editor_property('replace_existing', False)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', False)
|
||||
|
||||
# set import options here
|
||||
task.options.set_editor_property(
|
||||
'automated_import_should_detect_type', False)
|
||||
task.options.set_editor_property(
|
||||
'original_import_type', FBXImportType.FBXIT_SKELETAL_MESH)
|
||||
task.options.set_editor_property(
|
||||
'mesh_type_to_import', FBXImportType.FBXIT_ANIMATION)
|
||||
task.options.set_editor_property('import_mesh', False)
|
||||
task.options.set_editor_property('import_animations', True)
|
||||
task.options.set_editor_property('override_full_name', True)
|
||||
task.options.set_editor_property('skeleton', skeleton)
|
||||
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'animation_length',
|
||||
unreal.FBXAnimationLengthImportType.FBXALIT_EXPORTED_TIME
|
||||
)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_meshes_in_bone_hierarchy', False)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'use_default_sample_rate', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_custom_attribute', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'import_bone_tracks', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'remove_redundant_keys', True)
|
||||
task.options.anim_sequence_import_data.set_editor_property(
|
||||
'convert_scene', True)
|
||||
|
||||
AssetToolsHelpers.get_asset_tools().import_asset_tasks([task])
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
anim_path, recursive=False, include_folder=False
|
||||
)
|
||||
|
||||
animation = None
|
||||
|
||||
for a in asset_content:
|
||||
unreal.EditorAssetLibrary.save_asset(a)
|
||||
imported_asset_data = unreal.EditorAssetLibrary.find_asset_data(a)
|
||||
imported_asset = unreal.AssetRegistryHelpers.get_asset(
|
||||
imported_asset_data)
|
||||
if imported_asset.__class__ == unreal.AnimSequence:
|
||||
animation = imported_asset
|
||||
break
|
||||
|
||||
if animation:
|
||||
actor = None
|
||||
if actors_dict.get(instance_name):
|
||||
for a in actors_dict.get(instance_name):
|
||||
if a.get_class().get_name() == 'SkeletalMeshActor':
|
||||
actor = a
|
||||
break
|
||||
|
||||
animation.set_editor_property('enable_root_motion', True)
|
||||
actor.skeletal_mesh_component.set_editor_property(
|
||||
'animation_mode', unreal.AnimationMode.ANIMATION_SINGLE_NODE)
|
||||
actor.skeletal_mesh_component.animation_data.set_editor_property(
|
||||
'anim_to_play', animation)
|
||||
|
||||
def _process(self, libpath, asset_dir, loaded=None):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
with open(libpath, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
|
|
@ -84,46 +173,78 @@ class LayoutLoader(api.Loader):
|
|||
if not loaded:
|
||||
loaded = []
|
||||
|
||||
path = Path(libpath)
|
||||
rig_count = 0
|
||||
|
||||
skeleton_dict = {}
|
||||
actors_dict = {}
|
||||
|
||||
for element in data:
|
||||
reference = element.get('reference_fbx')
|
||||
|
||||
if reference in loaded:
|
||||
continue
|
||||
|
||||
loaded.append(reference)
|
||||
|
||||
family = element.get('family')
|
||||
loaders = api.loaders_from_representation(
|
||||
all_loaders, reference)
|
||||
loader = self._get_loader(loaders, family)
|
||||
|
||||
if not loader:
|
||||
continue
|
||||
|
||||
instance_name = element.get('instance_name')
|
||||
|
||||
options = {
|
||||
"asset_dir": asset_dir
|
||||
}
|
||||
skeleton = None
|
||||
|
||||
assets = api.load(
|
||||
loader,
|
||||
reference,
|
||||
namespace=instance_name,
|
||||
options=options
|
||||
)
|
||||
if reference not in loaded:
|
||||
loaded.append(reference)
|
||||
|
||||
instances = [
|
||||
item for item in data
|
||||
if item.get('reference_fbx') == reference]
|
||||
family = element.get('family')
|
||||
loaders = api.loaders_from_representation(
|
||||
all_loaders, reference)
|
||||
loader = self._get_loader(loaders, family)
|
||||
|
||||
for instance in instances:
|
||||
transform = instance.get('transform')
|
||||
if not loader:
|
||||
continue
|
||||
|
||||
if family == 'model':
|
||||
self._process_family(assets, 'StaticMesh', transform)
|
||||
elif family == 'rig':
|
||||
self._process_family(assets, 'SkeletalMesh', transform)
|
||||
options = {
|
||||
"asset_dir": asset_dir
|
||||
}
|
||||
|
||||
assets = api.load(
|
||||
loader,
|
||||
reference,
|
||||
namespace=instance_name,
|
||||
options=options
|
||||
)
|
||||
|
||||
instances = [
|
||||
item for item in data
|
||||
if item.get('reference_fbx') == reference]
|
||||
|
||||
for instance in instances:
|
||||
transform = instance.get('transform')
|
||||
inst = instance.get('instance_name')
|
||||
|
||||
actors = []
|
||||
|
||||
if family == 'model':
|
||||
actors = self._process_family(
|
||||
assets, 'StaticMesh', transform)
|
||||
elif family == 'rig':
|
||||
actors = self._process_family(
|
||||
assets, 'SkeletalMesh', transform)
|
||||
actors_dict[inst] = actors
|
||||
|
||||
if family == 'rig':
|
||||
# Finds skeleton among the imported assets
|
||||
for asset in assets:
|
||||
obj = ar.get_asset_by_object_path(asset).get_asset()
|
||||
if obj.get_class().get_name() == 'Skeleton':
|
||||
skeleton = obj
|
||||
if skeleton:
|
||||
break
|
||||
|
||||
if skeleton:
|
||||
skeleton_dict[reference] = skeleton
|
||||
else:
|
||||
skeleton = skeleton_dict.get(reference)
|
||||
|
||||
if skeleton:
|
||||
self._import_animation(
|
||||
asset_dir, path, rig_count, instance_name, skeleton,
|
||||
actors_dict)
|
||||
|
||||
rig_count += 1
|
||||
|
||||
def _remove_family(self, assets, components, classname, propname):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue