mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-24 21:04:40 +01:00
Merge pull request #3719 from simonebarbieri/feature/maya-unreal-layout_existing_scene
Unreal: Layout Loader from Maya for Existing Scenes
This commit is contained in:
commit
de7a6dfcc7
6 changed files with 451 additions and 13 deletions
|
|
@ -34,14 +34,15 @@ class ExtractLayout(publish.Extractor):
|
|||
for asset in cmds.sets(str(instance), query=True):
|
||||
# Find the container
|
||||
grp_name = asset.split(':')[0]
|
||||
containers = cmds.ls(f"{grp_name}*_CON")
|
||||
containers = cmds.ls("{}*_CON".format(grp_name))
|
||||
|
||||
assert len(containers) == 1, \
|
||||
f"More than one container found for {asset}"
|
||||
"More than one container found for {}".format(asset)
|
||||
|
||||
container = containers[0]
|
||||
|
||||
representation_id = cmds.getAttr(f"{container}.representation")
|
||||
representation_id = cmds.getAttr(
|
||||
"{}.representation".format(container))
|
||||
|
||||
representation = get_representation_by_id(
|
||||
project_name,
|
||||
|
|
@ -56,7 +57,8 @@ class ExtractLayout(publish.Extractor):
|
|||
|
||||
json_element = {
|
||||
"family": family,
|
||||
"instance_name": cmds.getAttr(f"{container}.name"),
|
||||
"instance_name": cmds.getAttr(
|
||||
"{}.namespace".format(container)),
|
||||
"representation": str(representation_id),
|
||||
"version": str(version_id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,15 +20,11 @@ class StaticMeshAlembicLoader(plugin.Loader):
|
|||
icon = "cube"
|
||||
color = "orange"
|
||||
|
||||
def get_task(self, filename, asset_dir, asset_name, replace):
|
||||
@staticmethod
|
||||
def get_task(filename, asset_dir, asset_name, replace, default_conversion):
|
||||
task = unreal.AssetImportTask()
|
||||
options = unreal.AbcImportSettings()
|
||||
sm_settings = unreal.AbcStaticMeshSettings()
|
||||
conversion_settings = unreal.AbcConversionSettings(
|
||||
preset=unreal.AbcConversionPreset.CUSTOM,
|
||||
flip_u=False, flip_v=False,
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, 1.0, 1.0])
|
||||
|
||||
task.set_editor_property('filename', filename)
|
||||
task.set_editor_property('destination_path', asset_dir)
|
||||
|
|
@ -44,13 +40,20 @@ class StaticMeshAlembicLoader(plugin.Loader):
|
|||
|
||||
sm_settings.set_editor_property('merge_meshes', True)
|
||||
|
||||
if not default_conversion:
|
||||
conversion_settings = unreal.AbcConversionSettings(
|
||||
preset=unreal.AbcConversionPreset.CUSTOM,
|
||||
flip_u=False, flip_v=False,
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, 1.0, 1.0])
|
||||
options.conversion_settings = conversion_settings
|
||||
|
||||
options.static_mesh_settings = sm_settings
|
||||
options.conversion_settings = conversion_settings
|
||||
task.options = options
|
||||
|
||||
return task
|
||||
|
||||
def load(self, context, name, namespace, data):
|
||||
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
|
||||
|
|
@ -82,6 +85,10 @@ class StaticMeshAlembicLoader(plugin.Loader):
|
|||
asset_name = "{}".format(name)
|
||||
version = context.get('version').get('name')
|
||||
|
||||
default_conversion = False
|
||||
if options.get("default_conversion"):
|
||||
default_conversion = options.get("default_conversion")
|
||||
|
||||
tools = unreal.AssetToolsHelpers().get_asset_tools()
|
||||
asset_dir, container_name = tools.create_unique_asset_name(
|
||||
f"{root}/{asset}/{name}_v{version:03d}", suffix="")
|
||||
|
|
@ -91,7 +98,8 @@ class StaticMeshAlembicLoader(plugin.Loader):
|
|||
if not unreal.EditorAssetLibrary.does_directory_exist(asset_dir):
|
||||
unreal.EditorAssetLibrary.make_directory(asset_dir)
|
||||
|
||||
task = self.get_task(self.fname, asset_dir, asset_name, False)
|
||||
task = self.get_task(
|
||||
self.fname, asset_dir, asset_name, False, default_conversion)
|
||||
|
||||
unreal.AssetToolsHelpers.get_asset_tools().import_asset_tasks([task]) # noqa: E501
|
||||
|
||||
|
|
|
|||
418
openpype/hosts/unreal/plugins/load/load_layout_existing.py
Normal file
418
openpype/hosts/unreal/plugins/load/load_layout_existing.py
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import unreal
|
||||
from unreal import EditorLevelLibrary
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
|
||||
from openpype import pipeline
|
||||
from openpype.pipeline import (
|
||||
discover_loader_plugins,
|
||||
loaders_from_representation,
|
||||
load_container,
|
||||
get_representation_path,
|
||||
AVALON_CONTAINER_ID,
|
||||
legacy_io,
|
||||
)
|
||||
from openpype.api import get_current_project_settings
|
||||
from openpype.hosts.unreal.api import plugin
|
||||
from openpype.hosts.unreal.api import pipeline as upipeline
|
||||
|
||||
|
||||
class ExistingLayoutLoader(plugin.Loader):
|
||||
"""
|
||||
Load Layout for an existing scene, and match the existing assets.
|
||||
"""
|
||||
|
||||
families = ["layout"]
|
||||
representations = ["json"]
|
||||
|
||||
label = "Load Layout on Existing Scene"
|
||||
icon = "code-fork"
|
||||
color = "orange"
|
||||
ASSET_ROOT = "/Game/OpenPype"
|
||||
|
||||
@staticmethod
|
||||
def _create_container(
|
||||
asset_name, asset_dir, asset, representation, parent, family
|
||||
):
|
||||
container_name = f"{asset_name}_CON"
|
||||
|
||||
container = None
|
||||
if not unreal.EditorAssetLibrary.does_asset_exist(
|
||||
f"{asset_dir}/{container_name}"
|
||||
):
|
||||
container = upipeline.create_container(container_name, asset_dir)
|
||||
else:
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
obj = ar.get_asset_by_object_path(
|
||||
f"{asset_dir}/{container_name}.{container_name}")
|
||||
container = obj.get_asset()
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"asset": asset,
|
||||
"namespace": asset_dir,
|
||||
"container_name": container_name,
|
||||
"asset_name": asset_name,
|
||||
# "loader": str(self.__class__.__name__),
|
||||
"representation": representation,
|
||||
"parent": parent,
|
||||
"family": family
|
||||
}
|
||||
|
||||
upipeline.imprint(
|
||||
"{}/{}".format(asset_dir, container_name), data)
|
||||
|
||||
return container.get_path_name()
|
||||
|
||||
@staticmethod
|
||||
def _get_current_level():
|
||||
ue_version = unreal.SystemLibrary.get_engine_version().split('.')
|
||||
ue_major = ue_version[0]
|
||||
|
||||
if ue_major == '4':
|
||||
return EditorLevelLibrary.get_editor_world()
|
||||
elif ue_major == '5':
|
||||
return unreal.LevelEditorSubsystem().get_current_level()
|
||||
|
||||
raise NotImplementedError(
|
||||
f"Unreal version {ue_major} not supported")
|
||||
|
||||
def _get_transform(self, ext, import_data, lasset):
|
||||
conversion = unreal.Matrix.IDENTITY.transform()
|
||||
fbx_tuning = unreal.Matrix.IDENTITY.transform()
|
||||
|
||||
basis = unreal.Matrix(
|
||||
lasset.get('basis')[0],
|
||||
lasset.get('basis')[1],
|
||||
lasset.get('basis')[2],
|
||||
lasset.get('basis')[3]
|
||||
).transform()
|
||||
transform = unreal.Matrix(
|
||||
lasset.get('transform_matrix')[0],
|
||||
lasset.get('transform_matrix')[1],
|
||||
lasset.get('transform_matrix')[2],
|
||||
lasset.get('transform_matrix')[3]
|
||||
).transform()
|
||||
|
||||
# Check for the conversion settings. We cannot access
|
||||
# the alembic conversion settings, so we assume that
|
||||
# the maya ones have been applied.
|
||||
if ext == '.fbx':
|
||||
loc = import_data.import_translation
|
||||
rot = import_data.import_rotation.to_vector()
|
||||
scale = import_data.import_uniform_scale
|
||||
conversion = unreal.Transform(
|
||||
location=[loc.x, loc.y, loc.z],
|
||||
rotation=[rot.x, rot.y, rot.z],
|
||||
scale=[-scale, scale, scale]
|
||||
)
|
||||
fbx_tuning = unreal.Transform(
|
||||
rotation=[180.0, 0.0, 90.0],
|
||||
scale=[1.0, 1.0, 1.0]
|
||||
)
|
||||
elif ext == '.abc':
|
||||
# This is the standard conversion settings for
|
||||
# alembic files from Maya.
|
||||
conversion = unreal.Transform(
|
||||
location=[0.0, 0.0, 0.0],
|
||||
rotation=[0.0, 0.0, 0.0],
|
||||
scale=[1.0, -1.0, 1.0]
|
||||
)
|
||||
|
||||
new_transform = (basis.inverse() * transform * basis)
|
||||
return fbx_tuning * conversion.inverse() * new_transform
|
||||
|
||||
def _spawn_actor(self, obj, lasset):
|
||||
actor = EditorLevelLibrary.spawn_actor_from_object(
|
||||
obj, unreal.Vector(0.0, 0.0, 0.0)
|
||||
)
|
||||
|
||||
actor.set_actor_label(lasset.get('instance_name'))
|
||||
smc = actor.get_editor_property('static_mesh_component')
|
||||
mesh = smc.get_editor_property('static_mesh')
|
||||
import_data = mesh.get_editor_property('asset_import_data')
|
||||
filename = import_data.get_first_filename()
|
||||
path = Path(filename)
|
||||
|
||||
transform = self._get_transform(
|
||||
path.suffix, import_data, lasset)
|
||||
|
||||
actor.set_actor_transform(transform, False, True)
|
||||
|
||||
@staticmethod
|
||||
def _get_fbx_loader(loaders, family):
|
||||
name = ""
|
||||
if family == 'rig':
|
||||
name = "SkeletalMeshFBXLoader"
|
||||
elif family == 'model' or family == 'staticMesh':
|
||||
name = "StaticMeshFBXLoader"
|
||||
elif family == 'camera':
|
||||
name = "CameraLoader"
|
||||
|
||||
if name == "":
|
||||
return None
|
||||
|
||||
for loader in loaders:
|
||||
if loader.__name__ == name:
|
||||
return loader
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _get_abc_loader(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 _load_asset(self, representation, version, instance_name, family):
|
||||
valid_formats = ['fbx', 'abc']
|
||||
|
||||
repr_data = legacy_io.find_one({
|
||||
"type": "representation",
|
||||
"parent": ObjectId(version),
|
||||
"name": {"$in": valid_formats}
|
||||
})
|
||||
repr_format = repr_data.get('name')
|
||||
|
||||
all_loaders = discover_loader_plugins()
|
||||
loaders = loaders_from_representation(
|
||||
all_loaders, representation)
|
||||
|
||||
loader = None
|
||||
|
||||
if repr_format == 'fbx':
|
||||
loader = self._get_fbx_loader(loaders, family)
|
||||
elif repr_format == 'abc':
|
||||
loader = self._get_abc_loader(loaders, family)
|
||||
|
||||
if not loader:
|
||||
self.log.error(f"No valid loader found for {representation}")
|
||||
return []
|
||||
|
||||
# This option is necessary to avoid importing the assets with a
|
||||
# different conversion compared to the other assets. For ABC files,
|
||||
# it is in fact impossible to access the conversion settings. So,
|
||||
# we must assume that the Maya conversion settings have been applied.
|
||||
options = {
|
||||
"default_conversion": True
|
||||
}
|
||||
|
||||
assets = load_container(
|
||||
loader,
|
||||
representation,
|
||||
namespace=instance_name,
|
||||
options=options
|
||||
)
|
||||
|
||||
return assets
|
||||
|
||||
def _process(self, lib_path):
|
||||
data = get_current_project_settings()
|
||||
delete_unmatched = data["unreal"]["delete_unmatched_assets"]
|
||||
|
||||
ar = unreal.AssetRegistryHelpers.get_asset_registry()
|
||||
|
||||
actors = EditorLevelLibrary.get_all_level_actors()
|
||||
|
||||
with open(lib_path, "r") as fp:
|
||||
data = json.load(fp)
|
||||
|
||||
layout_data = []
|
||||
|
||||
# Get all the representations in the JSON from the database.
|
||||
for element in data:
|
||||
if element.get('representation'):
|
||||
layout_data.append((
|
||||
pipeline.legacy_io.find_one({
|
||||
"_id": ObjectId(element.get('representation'))
|
||||
}),
|
||||
element
|
||||
))
|
||||
|
||||
containers = []
|
||||
actors_matched = []
|
||||
|
||||
for (repr_data, lasset) in layout_data:
|
||||
if not repr_data:
|
||||
raise AssertionError("Representation not found")
|
||||
if not (repr_data.get('data') or
|
||||
repr_data.get('data').get('path')):
|
||||
raise AssertionError("Representation does not have path")
|
||||
if not repr_data.get('context'):
|
||||
raise AssertionError("Representation does not have context")
|
||||
|
||||
# For every actor in the scene, check if it has a representation in
|
||||
# those we got from the JSON. If so, create a container for it.
|
||||
# Otherwise, remove it from the scene.
|
||||
found = False
|
||||
|
||||
for actor in actors:
|
||||
if not actor.get_class().get_name() == 'StaticMeshActor':
|
||||
continue
|
||||
if actor in actors_matched:
|
||||
continue
|
||||
|
||||
# Get the original path of the file from which the asset has
|
||||
# been imported.
|
||||
smc = actor.get_editor_property('static_mesh_component')
|
||||
mesh = smc.get_editor_property('static_mesh')
|
||||
import_data = mesh.get_editor_property('asset_import_data')
|
||||
filename = import_data.get_first_filename()
|
||||
path = Path(filename)
|
||||
|
||||
if (not path.name or
|
||||
path.name not in repr_data.get('data').get('path')):
|
||||
continue
|
||||
|
||||
actor.set_actor_label(lasset.get('instance_name'))
|
||||
|
||||
mesh_path = Path(mesh.get_path_name()).parent.as_posix()
|
||||
|
||||
# Create the container for the asset.
|
||||
asset = repr_data.get('context').get('asset')
|
||||
subset = repr_data.get('context').get('subset')
|
||||
container = self._create_container(
|
||||
f"{asset}_{subset}", mesh_path, asset,
|
||||
repr_data.get('_id'), repr_data.get('parent'),
|
||||
repr_data.get('context').get('family')
|
||||
)
|
||||
containers.append(container)
|
||||
|
||||
# Set the transform for the actor.
|
||||
transform = self._get_transform(
|
||||
path.suffix, import_data, lasset)
|
||||
actor.set_actor_transform(transform, False, True)
|
||||
|
||||
actors_matched.append(actor)
|
||||
found = True
|
||||
break
|
||||
|
||||
# If an actor has not been found for this representation,
|
||||
# we check if it has been loaded already by checking all the
|
||||
# loaded containers. If so, we add it to the scene. Otherwise,
|
||||
# we load it.
|
||||
if found:
|
||||
continue
|
||||
|
||||
all_containers = upipeline.ls()
|
||||
|
||||
loaded = False
|
||||
|
||||
for container in all_containers:
|
||||
repr = container.get('representation')
|
||||
|
||||
if not repr == str(repr_data.get('_id')):
|
||||
continue
|
||||
|
||||
asset_dir = container.get('namespace')
|
||||
|
||||
filter = unreal.ARFilter(
|
||||
class_names=["StaticMesh"],
|
||||
package_paths=[asset_dir],
|
||||
recursive_paths=False)
|
||||
assets = ar.get_assets(filter)
|
||||
|
||||
for asset in assets:
|
||||
obj = asset.get_asset()
|
||||
self._spawn_actor(obj, lasset)
|
||||
|
||||
loaded = True
|
||||
break
|
||||
|
||||
# If the asset has not been loaded yet, we load it.
|
||||
if loaded:
|
||||
continue
|
||||
|
||||
assets = self._load_asset(
|
||||
lasset.get('representation'),
|
||||
lasset.get('version'),
|
||||
lasset.get('instance_name'),
|
||||
lasset.get('family')
|
||||
)
|
||||
|
||||
for asset in assets:
|
||||
obj = ar.get_asset_by_object_path(asset).get_asset()
|
||||
if not obj.get_class().get_name() == 'StaticMesh':
|
||||
continue
|
||||
self._spawn_actor(obj, lasset)
|
||||
|
||||
break
|
||||
|
||||
# Check if an actor was not matched to a representation.
|
||||
# If so, remove it from the scene.
|
||||
for actor in actors:
|
||||
if not actor.get_class().get_name() == 'StaticMeshActor':
|
||||
continue
|
||||
if actor not in actors_matched:
|
||||
self.log.warning(f"Actor {actor.get_name()} not matched.")
|
||||
if delete_unmatched:
|
||||
EditorLevelLibrary.destroy_actor(actor)
|
||||
|
||||
return containers
|
||||
|
||||
def load(self, context, name, namespace, options):
|
||||
print("Loading Layout and Match Assets")
|
||||
|
||||
asset = context.get('asset').get('name')
|
||||
asset_name = f"{asset}_{name}" if asset else name
|
||||
container_name = f"{asset}_{name}_CON"
|
||||
|
||||
curr_level = self._get_current_level()
|
||||
|
||||
if not curr_level:
|
||||
raise AssertionError("Current level not saved")
|
||||
|
||||
containers = self._process(self.fname)
|
||||
|
||||
curr_level_path = Path(
|
||||
curr_level.get_outer().get_path_name()).parent.as_posix()
|
||||
|
||||
if not unreal.EditorAssetLibrary.does_asset_exist(
|
||||
f"{curr_level_path}/{container_name}"
|
||||
):
|
||||
upipeline.create_container(
|
||||
container=container_name, path=curr_level_path)
|
||||
|
||||
data = {
|
||||
"schema": "openpype:container-2.0",
|
||||
"id": AVALON_CONTAINER_ID,
|
||||
"asset": asset,
|
||||
"namespace": curr_level_path,
|
||||
"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"],
|
||||
"loaded_assets": containers
|
||||
}
|
||||
upipeline.imprint(f"{curr_level_path}/{container_name}", data)
|
||||
|
||||
def update(self, container, representation):
|
||||
asset_dir = container.get('namespace')
|
||||
|
||||
source_path = get_representation_path(representation)
|
||||
containers = self._process(source_path)
|
||||
|
||||
data = {
|
||||
"representation": str(representation["_id"]),
|
||||
"parent": str(representation["parent"]),
|
||||
"loaded_assets": containers
|
||||
}
|
||||
upipeline.imprint(
|
||||
"{}/{}".format(asset_dir, container.get('container_name')), data)
|
||||
|
|
@ -265,6 +265,10 @@ def get_last_workfile_with_version(
|
|||
if not match:
|
||||
continue
|
||||
|
||||
if not match.groups():
|
||||
output_filenames.append(filename)
|
||||
continue
|
||||
|
||||
file_version = int(match.group(1))
|
||||
if version is None or file_version > version:
|
||||
output_filenames[:] = []
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"level_sequences_for_layouts": false,
|
||||
"delete_unmatched_assets": false,
|
||||
"project_setup": {
|
||||
"dev_mode": true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,11 @@
|
|||
"key": "level_sequences_for_layouts",
|
||||
"label": "Generate level sequences when loading layouts"
|
||||
},
|
||||
{
|
||||
"type": "boolean",
|
||||
"key": "delete_unmatched_assets",
|
||||
"label": "Delete assets that are not matched"
|
||||
},
|
||||
{
|
||||
"type": "dict",
|
||||
"collapsible": true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue