From fc10b26ea0610dc5d32ed7c2ce285dac8fdef9eb Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 8 Dec 2022 11:20:24 +0000 Subject: [PATCH 01/10] Implemented creator and extractor for uasset --- .../unreal/plugins/create/create_uasset.py | 61 +++++++++++++++++++ .../unreal/plugins/publish/extract_uasset.py | 45 ++++++++++++++ openpype/plugins/publish/integrate.py | 3 +- 3 files changed, 108 insertions(+), 1 deletion(-) create mode 100644 openpype/hosts/unreal/plugins/create/create_uasset.py create mode 100644 openpype/hosts/unreal/plugins/publish/extract_uasset.py diff --git a/openpype/hosts/unreal/plugins/create/create_uasset.py b/openpype/hosts/unreal/plugins/create/create_uasset.py new file mode 100644 index 00000000000..ee584ac00c7 --- /dev/null +++ b/openpype/hosts/unreal/plugins/create/create_uasset.py @@ -0,0 +1,61 @@ +"""Create UAsset.""" +from pathlib import Path + +import unreal + +from openpype.hosts.unreal.api import pipeline +from openpype.pipeline import LegacyCreator + + +class CreateUAsset(LegacyCreator): + """UAsset.""" + + name = "UAsset" + label = "UAsset" + family = "uasset" + icon = "cube" + + root = "/Game/OpenPype" + suffix = "_INS" + + def __init__(self, *args, **kwargs): + super(CreateUAsset, self).__init__(*args, **kwargs) + + def process(self): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + subset = self.data["subset"] + path = f"{self.root}/PublishInstances/" + + unreal.EditorAssetLibrary.make_directory(path) + + selection = [] + if (self.options or {}).get("useSelection"): + sel_objects = unreal.EditorUtilityLibrary.get_selected_assets() + selection = [a.get_path_name() for a in sel_objects] + + if len(selection) != 1: + raise RuntimeError("Please select only one object.") + + obj = selection[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + + if not sys_path: + raise RuntimeError( + f"{Path(obj).name} is not on the disk. Likely it needs to" + "be saved first.") + + if Path(sys_path).suffix != ".uasset": + raise RuntimeError(f"{Path(sys_path).name} is not a UAsset.") + + unreal.log("selection: {}".format(selection)) + container_name = f"{subset}{self.suffix}" + pipeline.create_publish_instance( + instance=container_name, path=path) + + data = self.data.copy() + data["members"] = selection + + pipeline.imprint(f"{path}/{container_name}", data) diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py new file mode 100644 index 00000000000..99279e38a15 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -0,0 +1,45 @@ +from pathlib import Path +import shutil + +import unreal +from unreal import EditorLevelLibrary as ell +from unreal import EditorAssetLibrary as eal + +from openpype.client import get_representation_by_name +from openpype.pipeline import legacy_io, publish + + +class ExtractUAsset(publish.Extractor): + """Extract a UAsset.""" + + label = "Extract UAsset" + hosts = ["unreal"] + families = ["uasset"] + optional = True + + def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + self.log.info("Performing extraction..") + + staging_dir = self.staging_dir(instance) + filename = "{}.uasset".format(instance.name) + + obj = instance[0] + + asset = ar.get_asset_by_object_path(obj).get_asset() + sys_path = unreal.SystemLibrary.get_system_path(asset) + filename = Path(sys_path).name + + shutil.copy(sys_path, staging_dir) + + if "representations" not in instance.data: + instance.data["representations"] = [] + + representation = { + 'name': 'uasset', + 'ext': 'uasset', + 'files': filename, + "stagingDir": staging_dir, + } + instance.data["representations"].append(representation) diff --git a/openpype/plugins/publish/integrate.py b/openpype/plugins/publish/integrate.py index 6a85a87129e..6efff8440c5 100644 --- a/openpype/plugins/publish/integrate.py +++ b/openpype/plugins/publish/integrate.py @@ -130,7 +130,8 @@ class IntegrateAsset(pyblish.api.InstancePlugin): "mvUsdComposition", "mvUsdOverride", "simpleUnrealTexture", - "online" + "online", + "uasset" ] default_template_name = "publish" From 2b566bb594229d452dde7cb871239e07fac9366c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 8 Dec 2022 11:20:47 +0000 Subject: [PATCH 02/10] Implemented balidator to check if the uasset has any dependency --- .../publish/validate_no_dependencies.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py diff --git a/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py b/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py new file mode 100644 index 00000000000..b7f42a772b6 --- /dev/null +++ b/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py @@ -0,0 +1,40 @@ +import unreal + +import pyblish.api + + +class ValidateNoDependencies(pyblish.api.InstancePlugin): + """Ensure that the uasset has no dependencies + + The uasset is checked for dependencies. If there are any, the instance + cannot be published. + """ + + order = pyblish.api.ValidatorOrder + label = "Check no dependencies" + families = ["uasset"] + hosts = ["unreal"] + optional = True + + def process(self, instance): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + all_dependencies = [] + + for obj in instance[:]: + asset = ar.get_asset_by_object_path(obj) + dependencies = ar.get_dependencies( + asset.package_name, + unreal.AssetRegistryDependencyOptions( + include_soft_package_references=True, + include_hard_package_references=True, + include_searchable_names=False, + include_soft_management_references=False, + include_hard_management_references=False + )) + if dependencies: + for dep in dependencies: + all_dependencies.append(str(dep)) + + if all_dependencies: + raise RuntimeError( + f"Dependencies found: {all_dependencies}") From ee3d88756cbb75deb966fab37c8369d8a4d9df6d Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 8 Dec 2022 13:00:31 +0000 Subject: [PATCH 03/10] Implemented loading --- .../hosts/unreal/plugins/load/load_uasset.py | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 openpype/hosts/unreal/plugins/load/load_uasset.py diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py new file mode 100644 index 00000000000..e3f967c43d1 --- /dev/null +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +"""Load UAsset.""" +from pathlib import Path +import shutil + +from openpype.pipeline import ( + get_representation_path, + AVALON_CONTAINER_ID +) +from openpype.hosts.unreal.api import plugin +from openpype.hosts.unreal.api import pipeline as unreal_pipeline +import unreal # noqa + + +class UAssetLoader(plugin.Loader): + """Load UAsset.""" + + families = ["uasset"] + label = "Load UAsset" + representations = ["uasset"] + icon = "cube" + color = "orange" + + def load(self, context, name, namespace, options): + """Load and containerise representation into Content Browser. + + 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. + options (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 OpenPype container + root = "/Game/OpenPype/Assets" + if options and options.get("asset_dir"): + root = options["asset_dir"] + 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 + + unreal.EditorAssetLibrary.make_directory(asset_dir) + + # Create Asset Container + container = unreal_pipeline.create_container( + container=container_name, path=asset_dir) + + container_path = unreal.SystemLibrary.get_system_path(container) + destination_path = Path(container_path).parent.as_posix() + + shutil.copy(self.fname, destination_path) + + 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": context["representation"]["_id"], + "parent": context["representation"]["parent"], + "family": context["representation"]["context"]["family"] + } + unreal_pipeline.imprint( + "{}/{}".format(asset_dir, container_name), data) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + return asset_content From a847626aac962d0f954b14a8de0e341793092d69 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 8 Dec 2022 16:12:48 +0000 Subject: [PATCH 04/10] Don't use container path to get destination folder --- .../hosts/unreal/plugins/load/load_uasset.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index e3f967c43d1..76c4de1fbe7 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -40,8 +40,6 @@ def load(self, context, name, namespace, options): # Create directory for asset and OpenPype container root = "/Game/OpenPype/Assets" - if options and options.get("asset_dir"): - root = options["asset_dir"] asset = context.get('asset').get('name') suffix = "_CON" if asset: @@ -57,15 +55,17 @@ def load(self, context, name, namespace, options): unreal.EditorAssetLibrary.make_directory(asset_dir) - # Create Asset Container - container = unreal_pipeline.create_container( - container=container_name, path=asset_dir) - - container_path = unreal.SystemLibrary.get_system_path(container) - destination_path = Path(container_path).parent.as_posix() + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) shutil.copy(self.fname, destination_path) + # Create Asset Container + unreal_pipeline.create_container( + container=container_name, path=asset_dir) + data = { "schema": "openpype:container-2.0", "id": AVALON_CONTAINER_ID, From bc574e5e5e8174c69528e3834465de677b857115 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 8 Dec 2022 17:23:10 +0000 Subject: [PATCH 05/10] Implemented update and remove --- .../hosts/unreal/plugins/load/load_uasset.py | 56 ++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/load/load_uasset.py b/openpype/hosts/unreal/plugins/load/load_uasset.py index 76c4de1fbe7..eccfc7b445d 100644 --- a/openpype/hosts/unreal/plugins/load/load_uasset.py +++ b/openpype/hosts/unreal/plugins/load/load_uasset.py @@ -60,7 +60,7 @@ def load(self, context, name, namespace, options): Path(unreal.Paths.project_content_dir()).as_posix(), 1) - shutil.copy(self.fname, destination_path) + shutil.copy(self.fname, f"{destination_path}/{name}.uasset") # Create Asset Container unreal_pipeline.create_container( @@ -89,3 +89,57 @@ def load(self, context, name, namespace, options): unreal.EditorAssetLibrary.save_asset(a) return asset_content + + def update(self, container, representation): + ar = unreal.AssetRegistryHelpers.get_asset_registry() + + asset_dir = container["namespace"] + name = representation["context"]["subset"] + + destination_path = asset_dir.replace( + "/Game", + Path(unreal.Paths.project_content_dir()).as_posix(), + 1) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=False, include_folder=True + ) + + for asset in asset_content: + obj = ar.get_asset_by_object_path(asset).get_asset() + if not obj.get_class().get_name() == 'AssetContainer': + unreal.EditorAssetLibrary.delete_asset(asset) + + update_filepath = get_representation_path(representation) + + shutil.copy(update_filepath, f"{destination_path}/{name}.uasset") + + container_path = "{}/{}".format(container["namespace"], + container["objectName"]) + # update metadata + unreal_pipeline.imprint( + container_path, + { + "representation": str(representation["_id"]), + "parent": str(representation["parent"]) + }) + + asset_content = unreal.EditorAssetLibrary.list_assets( + asset_dir, recursive=True, include_folder=True + ) + + for a in asset_content: + unreal.EditorAssetLibrary.save_asset(a) + + def remove(self, container): + path = container["namespace"] + parent_path = Path(path).parent.as_posix() + + unreal.EditorAssetLibrary.delete_directory(path) + + asset_content = unreal.EditorAssetLibrary.list_assets( + parent_path, recursive=False + ) + + if len(asset_content) == 0: + unreal.EditorAssetLibrary.delete_directory(parent_path) From 976ba9b6ce5396da651d08b032fab052856fd3f4 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Thu, 8 Dec 2022 17:28:26 +0000 Subject: [PATCH 06/10] Hound fixes --- openpype/hosts/unreal/plugins/publish/extract_uasset.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/extract_uasset.py b/openpype/hosts/unreal/plugins/publish/extract_uasset.py index 99279e38a15..89d779d3681 100644 --- a/openpype/hosts/unreal/plugins/publish/extract_uasset.py +++ b/openpype/hosts/unreal/plugins/publish/extract_uasset.py @@ -2,11 +2,8 @@ import shutil import unreal -from unreal import EditorLevelLibrary as ell -from unreal import EditorAssetLibrary as eal -from openpype.client import get_representation_by_name -from openpype.pipeline import legacy_io, publish +from openpype.pipeline import publish class ExtractUAsset(publish.Extractor): From d9e6bedf3b7fbf3c30825df4aff91a4965bd7d7c Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 13 Dec 2022 10:35:15 +0000 Subject: [PATCH 07/10] Do not check soft references --- .../hosts/unreal/plugins/publish/validate_no_dependencies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py b/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py index b7f42a772b6..79d54306c43 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py +++ b/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py @@ -25,7 +25,7 @@ def process(self, instance): dependencies = ar.get_dependencies( asset.package_name, unreal.AssetRegistryDependencyOptions( - include_soft_package_references=True, + include_soft_package_references=False, include_hard_package_references=True, include_searchable_names=False, include_soft_management_references=False, From bf10a77fb6f1dcde764020de3588e54391df9f4a Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Tue, 13 Dec 2022 10:50:38 +0000 Subject: [PATCH 08/10] Check only dependencies that are in the Content folder We ignore native dependencies and dependencies from plugins --- .../hosts/unreal/plugins/publish/validate_no_dependencies.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py b/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py index 79d54306c43..c7601295502 100644 --- a/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py +++ b/openpype/hosts/unreal/plugins/publish/validate_no_dependencies.py @@ -33,7 +33,8 @@ def process(self, instance): )) if dependencies: for dep in dependencies: - all_dependencies.append(str(dep)) + if str(dep).startswith("/Game/"): + all_dependencies.append(str(dep)) if all_dependencies: raise RuntimeError( From 59f051d06596f63cbb861938b1a7c26d2d992e05 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 9 Jan 2023 18:30:10 +0100 Subject: [PATCH 09/10] :bug: fix instance collection --- openpype/hosts/unreal/plugins/publish/collect_instances.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instances.py b/openpype/hosts/unreal/plugins/publish/collect_instances.py index 1f25cbde7d6..6696eacb6a6 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instances.py @@ -26,8 +26,8 @@ def process(self, context): ar = unreal.AssetRegistryHelpers.get_asset_registry() class_name = ["/Script/OpenPype", - "AssetContainer"] if UNREAL_VERSION.major == 5 and \ - UNREAL_VERSION.minor > 0 else "OpenPypePublishInstance" # noqa + "OpenPypePublishInstance"] if UNREAL_VERSION.major == 5 and \ + UNREAL_VERSION.minor > 0 else "OpenPypePublishInstance" # noqa instance_containers = ar.get_assets_by_class(class_name, True) for container_data in instance_containers: From ea65afdbc176e3daf4ffcf1fb95939d39ed1e0b5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 9 Jan 2023 18:36:14 +0100 Subject: [PATCH 10/10] :rotating_light: hound fix --- .../hosts/unreal/plugins/publish/collect_instances.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/openpype/hosts/unreal/plugins/publish/collect_instances.py b/openpype/hosts/unreal/plugins/publish/collect_instances.py index 6696eacb6a6..27b711cad6e 100644 --- a/openpype/hosts/unreal/plugins/publish/collect_instances.py +++ b/openpype/hosts/unreal/plugins/publish/collect_instances.py @@ -25,9 +25,13 @@ class CollectInstances(pyblish.api.ContextPlugin): def process(self, context): ar = unreal.AssetRegistryHelpers.get_asset_registry() - class_name = ["/Script/OpenPype", - "OpenPypePublishInstance"] if UNREAL_VERSION.major == 5 and \ - UNREAL_VERSION.minor > 0 else "OpenPypePublishInstance" # noqa + class_name = [ + "/Script/OpenPype", + "OpenPypePublishInstance" + ] if ( + UNREAL_VERSION.major == 5 + and UNREAL_VERSION.minor > 0 + ) else "OpenPypePublishInstance" # noqa instance_containers = ar.get_assets_by_class(class_name, True) for container_data in instance_containers: