mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #2066 from simonebarbieri/feature/unreal-load_layout
Unreal: JSON Layout Loading support
This commit is contained in:
commit
c6eb14ab62
15 changed files with 928 additions and 69 deletions
|
|
@ -49,10 +49,13 @@ def get_unique_number(
|
|||
return f"{count:0>2}"
|
||||
|
||||
|
||||
def prepare_data(data, container_name):
|
||||
def prepare_data(data, container_name=None):
|
||||
name = data.name
|
||||
local_data = data.make_local()
|
||||
local_data.name = f"{container_name}:{name}"
|
||||
if container_name:
|
||||
local_data.name = f"{container_name}:{name}"
|
||||
else:
|
||||
local_data.name = f"{name}"
|
||||
return local_data
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from typing import Dict, List, Optional
|
|||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from openpype import lib
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
AVALON_CONTAINERS,
|
||||
|
|
@ -61,7 +62,9 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
library = bpy.data.libraries.get(bpy.path.basename(libpath))
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
def _process(self, libpath, asset_group, group_name, actions):
|
||||
def _process(
|
||||
self, libpath, asset_group, group_name, asset, representation, actions
|
||||
):
|
||||
with bpy.data.libraries.load(
|
||||
libpath, link=True, relative=False
|
||||
) as (data_from, data_to):
|
||||
|
|
@ -74,7 +77,8 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
container = None
|
||||
|
||||
for empty in empties:
|
||||
if empty.get(AVALON_PROPERTY):
|
||||
if (empty.get(AVALON_PROPERTY) and
|
||||
empty.get(AVALON_PROPERTY).get('family') == 'layout'):
|
||||
container = empty
|
||||
break
|
||||
|
||||
|
|
@ -85,12 +89,16 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
objects = []
|
||||
nodes = list(container.children)
|
||||
|
||||
for obj in nodes:
|
||||
obj.parent = asset_group
|
||||
allowed_types = ['ARMATURE', 'MESH', 'EMPTY']
|
||||
|
||||
for obj in nodes:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
if obj.type in allowed_types:
|
||||
obj.parent = asset_group
|
||||
|
||||
for obj in nodes:
|
||||
if obj.type in allowed_types:
|
||||
objects.append(obj)
|
||||
nodes.extend(list(obj.children))
|
||||
|
||||
objects.reverse()
|
||||
|
||||
|
|
@ -108,7 +116,7 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
parent.objects.link(obj)
|
||||
|
||||
for obj in objects:
|
||||
local_obj = plugin.prepare_data(obj, group_name)
|
||||
local_obj = plugin.prepare_data(obj)
|
||||
|
||||
action = None
|
||||
|
||||
|
|
@ -116,7 +124,7 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
action = actions.get(local_obj.name, None)
|
||||
|
||||
if local_obj.type == 'MESH':
|
||||
plugin.prepare_data(local_obj.data, group_name)
|
||||
plugin.prepare_data(local_obj.data)
|
||||
|
||||
if obj != local_obj:
|
||||
for constraint in constraints:
|
||||
|
|
@ -125,15 +133,18 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
|
||||
for material_slot in local_obj.material_slots:
|
||||
if material_slot.material:
|
||||
plugin.prepare_data(material_slot.material, group_name)
|
||||
plugin.prepare_data(material_slot.material)
|
||||
elif local_obj.type == 'ARMATURE':
|
||||
plugin.prepare_data(local_obj.data, group_name)
|
||||
plugin.prepare_data(local_obj.data)
|
||||
|
||||
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.action is not None:
|
||||
elif (local_obj.animation_data and
|
||||
local_obj.animation_data.action is not None):
|
||||
plugin.prepare_data(
|
||||
local_obj.animation_data.action, group_name)
|
||||
local_obj.animation_data.action)
|
||||
|
||||
# Set link the drivers to the local object
|
||||
if local_obj.data.animation_data:
|
||||
|
|
@ -142,6 +153,21 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
for t in v.targets:
|
||||
t.id = local_obj
|
||||
|
||||
elif local_obj.type == 'EMPTY':
|
||||
creator_plugin = lib.get_creator_by_name("CreateAnimation")
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"CreateAnimation\" was "
|
||||
"not found.")
|
||||
|
||||
api.create(
|
||||
creator_plugin,
|
||||
name=local_obj.name.split(':')[-1] + "_animation",
|
||||
asset=asset,
|
||||
options={"useSelection": False,
|
||||
"asset_group": local_obj},
|
||||
data={"dependencies": representation}
|
||||
)
|
||||
|
||||
if not local_obj.get(AVALON_PROPERTY):
|
||||
local_obj[AVALON_PROPERTY] = dict()
|
||||
|
||||
|
|
@ -150,7 +176,63 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
|
||||
objects.reverse()
|
||||
|
||||
bpy.data.orphans_purge(do_local_ids=False)
|
||||
armatures = [
|
||||
obj for obj in bpy.data.objects
|
||||
if obj.type == 'ARMATURE' and obj.library is None]
|
||||
arm_act = {}
|
||||
|
||||
# The armatures with an animation need to be at the center of the
|
||||
# scene to be hooked correctly by the curves modifiers.
|
||||
for armature in armatures:
|
||||
if armature.animation_data and armature.animation_data.action:
|
||||
arm_act[armature] = armature.animation_data.action
|
||||
armature.animation_data.action = None
|
||||
armature.location = (0.0, 0.0, 0.0)
|
||||
for bone in armature.pose.bones:
|
||||
bone.location = (0.0, 0.0, 0.0)
|
||||
bone.rotation_euler = (0.0, 0.0, 0.0)
|
||||
|
||||
curves = [obj for obj in data_to.objects if obj.type == 'CURVE']
|
||||
|
||||
for curve in curves:
|
||||
curve_name = curve.name.split(':')[0]
|
||||
curve_obj = bpy.data.objects.get(curve_name)
|
||||
|
||||
local_obj = plugin.prepare_data(curve)
|
||||
plugin.prepare_data(local_obj.data)
|
||||
|
||||
# Curves need to reset the hook, but to do that they need to be
|
||||
# in the view layer.
|
||||
parent.objects.link(local_obj)
|
||||
plugin.deselect_all()
|
||||
local_obj.select_set(True)
|
||||
bpy.context.view_layer.objects.active = local_obj
|
||||
if local_obj.library is None:
|
||||
bpy.ops.object.mode_set(mode='EDIT')
|
||||
bpy.ops.object.hook_reset()
|
||||
bpy.ops.object.mode_set(mode='OBJECT')
|
||||
parent.objects.unlink(local_obj)
|
||||
|
||||
local_obj.use_fake_user = True
|
||||
|
||||
for mod in local_obj.modifiers:
|
||||
mod.object = bpy.data.objects.get(f"{mod.object.name}")
|
||||
|
||||
if not local_obj.get(AVALON_PROPERTY):
|
||||
local_obj[AVALON_PROPERTY] = dict()
|
||||
|
||||
avalon_info = local_obj[AVALON_PROPERTY]
|
||||
avalon_info.update({"container_name": group_name})
|
||||
|
||||
local_obj.parent = curve_obj
|
||||
objects.append(local_obj)
|
||||
|
||||
for armature in armatures:
|
||||
if arm_act.get(armature):
|
||||
armature.animation_data.action = arm_act[armature]
|
||||
|
||||
while bpy.data.orphans_purge(do_local_ids=False):
|
||||
pass
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
|
|
@ -170,6 +252,7 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
libpath = self.fname
|
||||
asset = context["asset"]["name"]
|
||||
subset = context["subset"]["name"]
|
||||
representation = str(context["representation"]["_id"])
|
||||
|
||||
asset_name = plugin.asset_name(asset, subset)
|
||||
unique_number = plugin.get_unique_number(asset, subset)
|
||||
|
|
@ -185,7 +268,8 @@ class BlendLayoutLoader(plugin.AssetLoader):
|
|||
asset_group.empty_display_type = 'SINGLE_ARROW'
|
||||
avalon_container.objects.link(asset_group)
|
||||
|
||||
objects = self._process(libpath, asset_group, group_name, None)
|
||||
objects = self._process(
|
||||
libpath, asset_group, group_name, asset, representation, None)
|
||||
|
||||
for child in asset_group.children:
|
||||
if child.get(AVALON_PROPERTY):
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
'animation_asset': asset
|
||||
}
|
||||
|
||||
if element.get('animation'):
|
||||
options['animation_file'] = str(Path(libpath).with_suffix(
|
||||
'')) + "." + element.get('animation')
|
||||
|
||||
# This should return the loaded asset, but the load call will be
|
||||
# added to the queue to run in the Blender main thread, so
|
||||
# at this time it will not return anything. The assets will be
|
||||
|
|
@ -106,20 +110,22 @@ class JsonLayoutLoader(plugin.AssetLoader):
|
|||
options=options
|
||||
)
|
||||
|
||||
# Create the camera asset and the camera instance
|
||||
creator_plugin = lib.get_creator_by_name("CreateCamera")
|
||||
if not creator_plugin:
|
||||
raise ValueError("Creator plugin \"CreateCamera\" was "
|
||||
"not found.")
|
||||
# Camera creation when loading a layout is not necessary for now,
|
||||
# but the code is worth keeping in case we need it in the future.
|
||||
# # Create the camera asset and the camera instance
|
||||
# creator_plugin = lib.get_creator_by_name("CreateCamera")
|
||||
# if not creator_plugin:
|
||||
# raise ValueError("Creator plugin \"CreateCamera\" was "
|
||||
# "not found.")
|
||||
|
||||
api.create(
|
||||
creator_plugin,
|
||||
name="camera",
|
||||
# name=f"{unique_number}_{subset}_animation",
|
||||
asset=asset,
|
||||
options={"useSelection": False}
|
||||
# data={"dependencies": str(context["representation"]["_id"])}
|
||||
)
|
||||
# api.create(
|
||||
# creator_plugin,
|
||||
# name="camera",
|
||||
# # name=f"{unique_number}_{subset}_animation",
|
||||
# asset=asset,
|
||||
# options={"useSelection": False}
|
||||
# # data={"dependencies": str(context["representation"]["_id"])}
|
||||
# )
|
||||
|
||||
def process_asset(self,
|
||||
context: dict,
|
||||
|
|
|
|||
|
|
@ -83,7 +83,8 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
plugin.prepare_data(local_obj.data, group_name)
|
||||
|
||||
for material_slot in local_obj.material_slots:
|
||||
plugin.prepare_data(material_slot.material, group_name)
|
||||
if material_slot.material:
|
||||
plugin.prepare_data(material_slot.material, group_name)
|
||||
|
||||
if not local_obj.get(AVALON_PROPERTY):
|
||||
local_obj[AVALON_PROPERTY] = dict()
|
||||
|
|
@ -247,7 +248,8 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
# 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)
|
||||
if library:
|
||||
bpy.data.libraries.remove(library)
|
||||
|
||||
self._process(str(libpath), asset_group, object_name)
|
||||
|
||||
|
|
@ -255,6 +257,7 @@ class BlendModelLoader(plugin.AssetLoader):
|
|||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
metadata["parent"] = str(representation["parent"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing container from a Blender scene.
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ from typing import Dict, List, Optional
|
|||
import bpy
|
||||
|
||||
from avalon import api
|
||||
from avalon.blender import lib as avalon_lib
|
||||
from openpype import lib
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import (
|
||||
|
|
@ -112,6 +113,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):
|
||||
|
|
@ -196,12 +199,14 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
plugin.deselect_all()
|
||||
|
||||
create_animation = False
|
||||
anim_file = None
|
||||
|
||||
if options is not None:
|
||||
parent = options.get('parent')
|
||||
transform = options.get('transform')
|
||||
action = options.get('action')
|
||||
create_animation = options.get('create_animation')
|
||||
anim_file = options.get('animation_file')
|
||||
|
||||
if parent and transform:
|
||||
location = transform.get('translation')
|
||||
|
|
@ -254,6 +259,26 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
|
||||
plugin.deselect_all()
|
||||
|
||||
if anim_file:
|
||||
bpy.ops.import_scene.fbx(filepath=anim_file, anim_offset=0.0)
|
||||
|
||||
imported = avalon_lib.get_selection()
|
||||
|
||||
armature = [
|
||||
o for o in asset_group.children if o.type == 'ARMATURE'][0]
|
||||
|
||||
imported_group = [
|
||||
o for o in imported if o.type == 'EMPTY'][0]
|
||||
|
||||
for obj in imported:
|
||||
if obj.type == 'ARMATURE':
|
||||
if not armature.animation_data:
|
||||
armature.animation_data_create()
|
||||
armature.animation_data.action = obj.animation_data.action
|
||||
|
||||
self._remove(imported_group)
|
||||
bpy.data.objects.remove(imported_group)
|
||||
|
||||
bpy.context.scene.collection.objects.link(asset_group)
|
||||
|
||||
asset_group[AVALON_PROPERTY] = {
|
||||
|
|
@ -350,6 +375,7 @@ class BlendRigLoader(plugin.AssetLoader):
|
|||
|
||||
metadata["libpath"] = str(libpath)
|
||||
metadata["representation"] = str(representation["_id"])
|
||||
metadata["parent"] = str(representation["parent"])
|
||||
|
||||
def exec_remove(self, container: Dict) -> bool:
|
||||
"""Remove an existing asset group from a Blender scene.
|
||||
|
|
|
|||
|
|
@ -29,12 +29,13 @@ class ExtractBlendAnimation(openpype.api.Extractor):
|
|||
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)
|
||||
if child.animation_data and child.animation_data.action:
|
||||
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)
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ class ExtractFBX(api.Extractor):
|
|||
new_materials.append(mat)
|
||||
new_materials_objs.append(obj)
|
||||
|
||||
scale_length = bpy.context.scene.unit_settings.scale_length
|
||||
bpy.context.scene.unit_settings.scale_length = 0.01
|
||||
|
||||
# We export the fbx
|
||||
bpy.ops.export_scene.fbx(
|
||||
context,
|
||||
|
|
@ -60,6 +63,8 @@ class ExtractFBX(api.Extractor):
|
|||
add_leaf_bones=False
|
||||
)
|
||||
|
||||
bpy.context.scene.unit_settings.scale_length = scale_length
|
||||
|
||||
plugin.deselect_all()
|
||||
|
||||
for mat in new_materials:
|
||||
|
|
|
|||
|
|
@ -37,13 +37,6 @@ class ExtractAnimationFBX(api.Extractor):
|
|||
armature = [
|
||||
obj for obj in asset_group.children if obj.type == 'ARMATURE'][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(':')[1]
|
||||
armature.name = original_name
|
||||
|
||||
object_action_pairs = []
|
||||
original_actions = []
|
||||
|
||||
|
|
@ -66,6 +59,13 @@ class ExtractAnimationFBX(api.Extractor):
|
|||
self.log.info("Object have no animation.")
|
||||
return
|
||||
|
||||
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(':')[1]
|
||||
armature.name = original_name
|
||||
|
||||
object_action_pairs.append((armature, copy_action))
|
||||
original_actions.append(curr_action)
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ class ExtractAnimationFBX(api.Extractor):
|
|||
json_path = os.path.join(stagingdir, json_filename)
|
||||
|
||||
json_dict = {
|
||||
"instance_name": asset_group.get(AVALON_PROPERTY).get("namespace")
|
||||
"instance_name": asset_group.get(AVALON_PROPERTY).get("objectName")
|
||||
}
|
||||
|
||||
# collection = instance.data.get("name")
|
||||
|
|
|
|||
|
|
@ -2,8 +2,11 @@ import os
|
|||
import json
|
||||
|
||||
import bpy
|
||||
import bpy_extras
|
||||
import bpy_extras.anim_utils
|
||||
|
||||
from avalon import io
|
||||
from openpype.hosts.blender.api import plugin
|
||||
from openpype.hosts.blender.api.pipeline import AVALON_PROPERTY
|
||||
import openpype.api
|
||||
|
||||
|
|
@ -16,6 +19,99 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
families = ["layout"]
|
||||
optional = True
|
||||
|
||||
def _export_animation(self, asset, instance, stagingdir, fbx_count):
|
||||
n = fbx_count
|
||||
|
||||
for obj in asset.children:
|
||||
if obj.type != "ARMATURE":
|
||||
continue
|
||||
|
||||
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
|
||||
|
||||
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.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"{n:03d}.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])
|
||||
|
||||
return fbx_filename, n + 1
|
||||
|
||||
return None, n
|
||||
|
||||
def process(self, instance):
|
||||
# Define extract output file path
|
||||
stagingdir = self.staging_dir(instance)
|
||||
|
|
@ -23,10 +119,16 @@ 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)]
|
||||
|
||||
fbx_count = 0
|
||||
|
||||
for asset in asset_group.children:
|
||||
metadata = asset.get(AVALON_PROPERTY)
|
||||
|
||||
|
|
@ -34,6 +136,7 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
family = metadata["family"]
|
||||
|
||||
self.log.debug("Parent: {}".format(parent))
|
||||
# Get blend reference
|
||||
blend = io.find_one(
|
||||
{
|
||||
"type": "representation",
|
||||
|
|
@ -41,10 +144,39 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
"name": "blend"
|
||||
},
|
||||
projection={"_id": True})
|
||||
blend_id = blend["_id"]
|
||||
blend_id = None
|
||||
if blend:
|
||||
blend_id = blend["_id"]
|
||||
# Get fbx reference
|
||||
fbx = io.find_one(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": io.ObjectId(parent),
|
||||
"name": "fbx"
|
||||
},
|
||||
projection={"_id": True})
|
||||
fbx_id = None
|
||||
if fbx:
|
||||
fbx_id = fbx["_id"]
|
||||
# Get abc reference
|
||||
abc = io.find_one(
|
||||
{
|
||||
"type": "representation",
|
||||
"parent": io.ObjectId(parent),
|
||||
"name": "abc"
|
||||
},
|
||||
projection={"_id": True})
|
||||
abc_id = None
|
||||
if abc:
|
||||
abc_id = abc["_id"]
|
||||
|
||||
json_element = {}
|
||||
json_element["reference"] = str(blend_id)
|
||||
if blend_id:
|
||||
json_element["reference"] = str(blend_id)
|
||||
if fbx_id:
|
||||
json_element["reference_fbx"] = str(fbx_id)
|
||||
if abc_id:
|
||||
json_element["reference_abc"] = str(abc_id)
|
||||
json_element["family"] = family
|
||||
json_element["instance_name"] = asset.name
|
||||
json_element["asset_name"] = metadata["asset_name"]
|
||||
|
|
@ -67,6 +199,16 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
"z": asset.scale.z
|
||||
}
|
||||
}
|
||||
|
||||
# Extract the animation as well
|
||||
if family == "rig":
|
||||
f, n = self._export_animation(
|
||||
asset, instance, stagingdir, fbx_count)
|
||||
if f:
|
||||
fbx_files.append(f)
|
||||
json_element["animation"] = f
|
||||
fbx_count = n
|
||||
|
||||
json_data.append(json_element)
|
||||
|
||||
json_filename = "{}.json".format(instance.name)
|
||||
|
|
@ -75,16 +217,32 @@ class ExtractLayout(openpype.api.Extractor):
|
|||
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)
|
||||
instance.data["representations"].append(json_representation)
|
||||
|
||||
self.log.debug(fbx_files)
|
||||
|
||||
if len(fbx_files) == 1:
|
||||
fbx_representation = {
|
||||
'name': 'fbx',
|
||||
'ext': '000.fbx',
|
||||
'files': fbx_files[0],
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
elif len(fbx_files) > 1:
|
||||
fbx_representation = {
|
||||
'name': 'fbx',
|
||||
'ext': 'fbx',
|
||||
'files': fbx_files,
|
||||
"stagingDir": stagingdir,
|
||||
}
|
||||
instance.data["representations"].append(fbx_representation)
|
||||
|
||||
self.log.info("Extracted instance '%s' to: %s",
|
||||
instance.name, representation)
|
||||
instance.name, json_representation)
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class IncrementWorkfileVersion(pyblish.api.ContextPlugin):
|
|||
label = "Increment Workfile Version"
|
||||
optional = True
|
||||
hosts = ["blender"]
|
||||
families = ["animation", "model", "rig", "action"]
|
||||
families = ["animation", "model", "rig", "action", "layout"]
|
||||
|
||||
def process(self, context):
|
||||
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ import openpype.hosts.blender.api.action
|
|||
|
||||
|
||||
class ValidateObjectIsInObjectMode(pyblish.api.InstancePlugin):
|
||||
"""Validate that the current object is in Object Mode."""
|
||||
"""Validate that the objects in the instance are in Object Mode."""
|
||||
|
||||
order = pyblish.api.ValidatorOrder - 0.01
|
||||
hosts = ["blender"]
|
||||
families = ["model", "rig"]
|
||||
families = ["model", "rig", "layout"]
|
||||
category = "geometry"
|
||||
label = "Object is in Object Mode"
|
||||
label = "Validate Object Mode"
|
||||
actions = [openpype.hosts.blender.api.action.SelectInvalidAction]
|
||||
optional = True
|
||||
optional = False
|
||||
|
||||
@classmethod
|
||||
def get_invalid(cls, instance) -> List:
|
||||
|
|
|
|||
|
|
@ -71,8 +71,18 @@ class AnimationFBXLoader(api.Loader):
|
|||
|
||||
if instance_name:
|
||||
automated = True
|
||||
actor_name = 'PersistentLevel.' + instance_name
|
||||
actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)
|
||||
# Old method to get the actor
|
||||
# actor_name = 'PersistentLevel.' + instance_name
|
||||
# actor = unreal.EditorLevelLibrary.get_actor_reference(actor_name)
|
||||
actors = unreal.EditorLevelLibrary.get_all_level_actors()
|
||||
for a in actors:
|
||||
if a.get_class().get_name() != "SkeletalMeshActor":
|
||||
continue
|
||||
if a.get_actor_label() == instance_name:
|
||||
actor = a
|
||||
break
|
||||
if not actor:
|
||||
raise Exception(f"Could not find actor {instance_name}")
|
||||
skeleton = actor.skeletal_mesh_component.skeletal_mesh.skeleton
|
||||
task.options.set_editor_property('skeleton', skeleton)
|
||||
|
||||
|
|
@ -173,20 +183,35 @@ class AnimationFBXLoader(api.Loader):
|
|||
task.set_editor_property('destination_name', name)
|
||||
task.set_editor_property('replace_existing', True)
|
||||
task.set_editor_property('automated', True)
|
||||
task.set_editor_property('save', False)
|
||||
task.set_editor_property('save', True)
|
||||
|
||||
# set import options here
|
||||
task.options.set_editor_property(
|
||||
'automated_import_should_detect_type', True)
|
||||
'automated_import_should_detect_type', False)
|
||||
task.options.set_editor_property(
|
||||
'original_import_type', unreal.FBXImportType.FBXIT_ANIMATION)
|
||||
'original_import_type', unreal.FBXImportType.FBXIT_SKELETAL_MESH)
|
||||
task.options.set_editor_property(
|
||||
'mesh_type_to_import', unreal.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.skeletal_mesh_import_data.set_editor_property(
|
||||
'import_content_type',
|
||||
unreal.FBXImportContentType.FBXICT_SKINNING_WEIGHTS
|
||||
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)
|
||||
|
||||
skeletal_mesh = unreal.EditorAssetLibrary.load_asset(
|
||||
container.get('namespace') + "/" + container.get('asset_name'))
|
||||
|
|
@ -219,7 +244,7 @@ class AnimationFBXLoader(api.Loader):
|
|||
unreal.EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = unreal.EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False
|
||||
parent_path, recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
|
|
|
|||
544
openpype/hosts/unreal/plugins/load/load_layout.py
Normal file
544
openpype/hosts/unreal/plugins/load/load_layout.py
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
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
|
||||
from avalon.unreal import lib
|
||||
from avalon.unreal import pipeline as unreal_pipeline
|
||||
|
||||
|
||||
class LayoutLoader(api.Loader):
|
||||
"""Load Layout from a JSON file"""
|
||||
|
||||
families = ["layout"]
|
||||
representations = ["json"]
|
||||
|
||||
label = "Load Layout"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
|
||||
def _get_asset_containers(self, path):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
path, recursive=True)
|
||||
|
||||
asset_containers = []
|
||||
|
||||
# Get all the asset containers
|
||||
for a in asset_content:
|
||||
obj = ar.get_asset_by_object_path(a)
|
||||
if obj.get_asset().get_class().get_name() == 'AssetContainer':
|
||||
asset_containers.append(obj)
|
||||
|
||||
return asset_containers
|
||||
|
||||
def _get_fbx_loader(self, loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "SkeletalMeshFBXLoader"
|
||||
elif family == 'model':
|
||||
name = "StaticMeshFBXLoader"
|
||||
elif family == 'camera':
|
||||
name = "CameraLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
def _get_abc_loader(self, loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "SkeletalMeshAlembicLoader"
|
||||
elif family == 'model':
|
||||
name = "StaticMeshAlembicLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
def _process_family(self, assets, classname, transform, inst_name=None):
|
||||
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:
|
||||
actor = EditorLevelLibrary.spawn_actor_from_object(
|
||||
obj,
|
||||
transform.get('translation')
|
||||
)
|
||||
if inst_name:
|
||||
try:
|
||||
# Rename method leads to crash
|
||||
# actor.rename(name=inst_name)
|
||||
|
||||
# The label works, although it make it slightly more
|
||||
# complicated to check for the names, as we need to
|
||||
# loop through all the actors in the level
|
||||
actor.set_actor_label(inst_name)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
actor.set_actor_rotation(unreal.Rotator(
|
||||
umath.radians_to_degrees(
|
||||
transform.get('rotation').get('x')),
|
||||
-umath.radians_to_degrees(
|
||||
transform.get('rotation').get('y')),
|
||||
umath.radians_to_degrees(
|
||||
transform.get('rotation').get('z')),
|
||||
), False)
|
||||
actor.set_actor_scale3d(transform.get('scale'))
|
||||
|
||||
actors.append(actor)
|
||||
|
||||
return actors
|
||||
|
||||
def _import_animation(
|
||||
self, asset_dir, path, instance_name, skeleton, actors_dict,
|
||||
animation_file):
|
||||
anim_file = Path(animation_file)
|
||||
anim_file_name = anim_file.with_suffix('')
|
||||
|
||||
anim_path = f"{asset_dir}/animations/{anim_file_name}"
|
||||
|
||||
# Import animation
|
||||
task = unreal.AssetImportTask()
|
||||
task.options = unreal.FbxImportUI()
|
||||
|
||||
task.set_editor_property(
|
||||
'filename', str(path.with_suffix(f".{animation_file}")))
|
||||
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)
|
||||
|
||||
all_loaders = api.discover(api.Loader)
|
||||
|
||||
if not loaded:
|
||||
loaded = []
|
||||
|
||||
path = Path(libpath)
|
||||
|
||||
skeleton_dict = {}
|
||||
actors_dict = {}
|
||||
|
||||
for element in data:
|
||||
reference = None
|
||||
if element.get('reference_fbx'):
|
||||
reference = element.get('reference_fbx')
|
||||
elif element.get('reference_abc'):
|
||||
reference = element.get('reference_abc')
|
||||
|
||||
# If reference is None, this element is skipped, as it cannot be
|
||||
# imported in Unreal
|
||||
if not reference:
|
||||
continue
|
||||
|
||||
instance_name = element.get('instance_name')
|
||||
|
||||
skeleton = None
|
||||
|
||||
if reference not in loaded:
|
||||
loaded.append(reference)
|
||||
|
||||
family = element.get('family')
|
||||
loaders = api.loaders_from_representation(
|
||||
all_loaders, reference)
|
||||
|
||||
loader = None
|
||||
|
||||
if reference == element.get('reference_fbx'):
|
||||
loader = self._get_fbx_loader(loaders, family)
|
||||
elif reference == element.get('reference_abc'):
|
||||
loader = self._get_abc_loader(loaders, family)
|
||||
|
||||
if not loader:
|
||||
continue
|
||||
|
||||
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 or
|
||||
item.get('reference_abc') == 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, inst)
|
||||
elif family == 'rig':
|
||||
actors = self._process_family(
|
||||
assets, 'SkeletalMesh', transform, inst)
|
||||
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)
|
||||
|
||||
animation_file = element.get('animation')
|
||||
|
||||
if animation_file and skeleton:
|
||||
self._import_animation(
|
||||
asset_dir, path, instance_name, skeleton,
|
||||
actors_dict, animation_file)
|
||||
|
||||
def _remove_family(self, assets, components, classname, propname):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
objects = []
|
||||
for a in assets:
|
||||
obj = ar.get_asset_by_object_path(a)
|
||||
if obj.get_asset().get_class().get_name() == classname:
|
||||
objects.append(obj)
|
||||
for obj in objects:
|
||||
for comp in components:
|
||||
if comp.get_editor_property(propname) == obj.get_asset():
|
||||
comp.get_owner().destroy_actor()
|
||||
|
||||
def _remove_actors(self, path):
|
||||
asset_containers = self._get_asset_containers(path)
|
||||
|
||||
# Get all the static and skeletal meshes components in the level
|
||||
components = EditorLevelLibrary.get_all_level_actors_components()
|
||||
static_meshes_comp = [
|
||||
c for c in components
|
||||
if c.get_class().get_name() == 'StaticMeshComponent']
|
||||
skel_meshes_comp = [
|
||||
c for c in components
|
||||
if c.get_class().get_name() == 'SkeletalMeshComponent']
|
||||
|
||||
# For all the asset containers, get the static and skeletal meshes.
|
||||
# Then, check the components in the level and destroy the matching
|
||||
# actors.
|
||||
for asset_container in asset_containers:
|
||||
package_path = asset_container.get_editor_property('package_path')
|
||||
family = EditorAssetLibrary.get_metadata_tag(
|
||||
asset_container.get_asset(), 'family')
|
||||
assets = EditorAssetLibrary.list_assets(
|
||||
str(package_path), recursive=False)
|
||||
if family == 'model':
|
||||
self._remove_family(
|
||||
assets, static_meshes_comp, 'StaticMesh', 'static_mesh')
|
||||
elif family == 'rig':
|
||||
self._remove_family(
|
||||
assets, skel_meshes_comp, 'SkeletalMesh', 'skeletal_mesh')
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
"""
|
||||
Load and containerise representation into Content Browser.
|
||||
|
||||
This is two step process. First, import FBX to temporary path and
|
||||
then call `containerise()` on it - this moves all content to new
|
||||
directory and then it will create AssetContainer there and imprint it
|
||||
with metadata. This will mark this path as container.
|
||||
|
||||
Args:
|
||||
context (dict): application context
|
||||
name (str): subset name
|
||||
namespace (str): in Unreal this is basically path to container.
|
||||
This is not passed here, so namespace is set
|
||||
by `containerise()` because only then we know
|
||||
real path.
|
||||
data (dict): Those would be data to be imprinted. This is not used
|
||||
now, data are imprinted by `containerise()`.
|
||||
|
||||
Returns:
|
||||
list(str): list of container content
|
||||
"""
|
||||
# Create directory for asset and avalon container
|
||||
root = "/Game/Avalon/Assets"
|
||||
asset = context.get('asset').get('name')
|
||||
suffix = "_CON"
|
||||
if asset:
|
||||
asset_name = "{}_{}".format(asset, name)
|
||||
else:
|
||||
asset_name = "{}".format(name)
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
"{}/{}/{}".format(root, asset, name), suffix="")
|
||||
|
||||
container_name += suffix
|
||||
|
||||
EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
self._process(self.fname, asset_dir)
|
||||
|
||||
# Create Asset Container
|
||||
lib.create_avalon_container(
|
||||
container=container_name, path=asset_dir)
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": pipeline.AVALON_CONTAINER_ID,
|
||||
"asset": asset,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
"loader": str(self.__class__.__name__),
|
||||
"representation": context["representation"]["_id"],
|
||||
"parent": context["representation"]["parent"],
|
||||
"family": context["representation"]["context"]["family"]
|
||||
}
|
||||
unreal_pipeline.imprint(
|
||||
"{}/{}".format(asset_dir, container_name), data)
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
asset_dir, recursive=True, include_folder=False)
|
||||
|
||||
for a in asset_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
return asset_content
|
||||
|
||||
def update(self, container, representation):
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
source_path = api.get_representation_path(representation)
|
||||
destination_path = container["namespace"]
|
||||
libpath = Path(api.get_representation_path(representation))
|
||||
|
||||
self._remove_actors(destination_path)
|
||||
|
||||
# Delete old animations
|
||||
anim_path = f"{destination_path}/animations/"
|
||||
EditorAssetLibrary.delete_directory(anim_path)
|
||||
|
||||
with open(source_path, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
references = [e.get('reference_fbx') for e in data]
|
||||
asset_containers = self._get_asset_containers(destination_path)
|
||||
loaded = []
|
||||
|
||||
# Delete all the assets imported with the previous version of the
|
||||
# layout, if they're not in the new layout.
|
||||
for asset_container in asset_containers:
|
||||
if asset_container.get_editor_property(
|
||||
'asset_name') == container["objectName"]:
|
||||
continue
|
||||
ref = EditorAssetLibrary.get_metadata_tag(
|
||||
asset_container.get_asset(), 'representation')
|
||||
ppath = asset_container.get_editor_property('package_path')
|
||||
|
||||
if ref not in references:
|
||||
# If the asset is not in the new layout, delete it.
|
||||
# Also check if the parent directory is empty, and delete that
|
||||
# as well, if it is.
|
||||
EditorAssetLibrary.delete_directory(ppath)
|
||||
|
||||
parent = os.path.dirname(str(ppath))
|
||||
parent_content = EditorAssetLibrary.list_assets(
|
||||
parent, recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
if len(parent_content) == 0:
|
||||
EditorAssetLibrary.delete_directory(parent)
|
||||
else:
|
||||
# If the asset is in the new layout, search the instances in
|
||||
# the JSON file, and create actors for them.
|
||||
|
||||
actors_dict = {}
|
||||
skeleton_dict = {}
|
||||
|
||||
for element in data:
|
||||
reference = element.get('reference_fbx')
|
||||
instance_name = element.get('instance_name')
|
||||
|
||||
skeleton = None
|
||||
|
||||
if reference == ref and ref not in loaded:
|
||||
loaded.append(ref)
|
||||
|
||||
family = element.get('family')
|
||||
|
||||
assets = EditorAssetLibrary.list_assets(
|
||||
ppath, recursive=True, include_folder=False)
|
||||
|
||||
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, inst)
|
||||
elif family == 'rig':
|
||||
actors = self._process_family(
|
||||
assets, 'SkeletalMesh', transform, inst)
|
||||
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)
|
||||
|
||||
animation_file = element.get('animation')
|
||||
|
||||
if animation_file and skeleton:
|
||||
self._import_animation(
|
||||
destination_path, libpath,
|
||||
instance_name, skeleton,
|
||||
actors_dict, animation_file)
|
||||
|
||||
self._process(source_path, destination_path, loaded)
|
||||
|
||||
container_path = "{}/{}".format(container["namespace"],
|
||||
container["objectName"])
|
||||
# update metadata
|
||||
unreal_pipeline.imprint(
|
||||
container_path,
|
||||
{
|
||||
"representation": str(representation["_id"]),
|
||||
"parent": str(representation["parent"])
|
||||
})
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
destination_path, recursive=True, include_folder=False)
|
||||
|
||||
for a in asset_content:
|
||||
EditorAssetLibrary.save_asset(a)
|
||||
|
||||
def remove(self, container):
|
||||
"""
|
||||
First, destroy all actors of the assets to be removed. Then, deletes
|
||||
the asset's directory.
|
||||
"""
|
||||
path = container["namespace"]
|
||||
parent_path = os.path.dirname(path)
|
||||
|
||||
self._remove_actors(path)
|
||||
|
||||
EditorAssetLibrary.delete_directory(path)
|
||||
|
||||
asset_content = EditorAssetLibrary.list_assets(
|
||||
parent_path, recursive=False, include_folder=True
|
||||
)
|
||||
|
||||
if len(asset_content) == 0:
|
||||
EditorAssetLibrary.delete_directory(parent_path)
|
||||
|
|
@ -15,7 +15,7 @@ class SkeletalMeshFBXLoader(api.Loader):
|
|||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
def load(self, context, name, namespace, options):
|
||||
"""
|
||||
Load and containerise representation into Content Browser.
|
||||
|
||||
|
|
@ -40,6 +40,8 @@ class SkeletalMeshFBXLoader(api.Loader):
|
|||
|
||||
# Create directory for asset and avalon container
|
||||
root = "/Game/Avalon/Assets"
|
||||
if options and options.get("asset_dir"):
|
||||
root = options["asset_dir"]
|
||||
asset = context.get('asset').get('name')
|
||||
suffix = "_CON"
|
||||
if asset:
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class StaticMeshFBXLoader(api.Loader):
|
|||
|
||||
return task
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
def load(self, context, name, namespace, options):
|
||||
"""
|
||||
Load and containerise representation into Content Browser.
|
||||
|
||||
|
|
@ -65,6 +65,8 @@ class StaticMeshFBXLoader(api.Loader):
|
|||
|
||||
# Create directory for asset and avalon container
|
||||
root = "/Game/Avalon/Assets"
|
||||
if options and options.get("asset_dir"):
|
||||
root = options["asset_dir"]
|
||||
asset = context.get('asset').get('name')
|
||||
suffix = "_CON"
|
||||
if asset:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue