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:
Ondřej Samohel 2022-10-13 12:35:56 +02:00 committed by GitHub
commit de7a6dfcc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 451 additions and 13 deletions

View file

@ -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)
}

View file

@ -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

View 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)

View file

@ -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[:] = []

View file

@ -1,5 +1,6 @@
{
"level_sequences_for_layouts": false,
"delete_unmatched_assets": false,
"project_setup": {
"dev_mode": true
}

View file

@ -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,