From 39e837d922f200ce290b44674b196a5b5d03cf31 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Fri, 18 Oct 2024 15:28:28 +0200 Subject: [PATCH 1/4] Allow to process USD files to use relative paths when published via plug-in `USDOutputProcessorRemapToRelativePaths` you can enable. --- ...ate_usd_output_processor_remap_relative.py | 133 ++++++++++++++++++ server/settings/main.py | 6 + server/settings/publish_plugins.py | 31 ++++ 3 files changed, 170 insertions(+) create mode 100644 client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py create mode 100644 server/settings/publish_plugins.py diff --git a/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py new file mode 100644 index 0000000..fdacc80 --- /dev/null +++ b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py @@ -0,0 +1,133 @@ +import os + +import pyblish.api + +from ayon_core.pipeline import OptionalPyblishPluginMixin +from ayon_core.pipeline.publish.lib import get_instance_expected_output_path + +# Avoid USD imports turning into errors when running in a host that does +# not support the USD libs. +try: + from pxr import Sdf, UsdUtils + HAS_USD_LIBS = True +except ImportError: + HAS_USD_LIBS = False + + +RELATIVE_ANCHOR_PREFIXES = ("./", "../", ".\\", "..\\") + + +def get_drive(path) -> str: + """Return disk drive from path""" + return os.path.splitdrive(path)[0] + + +class USDOutputProcessorRemapToRelativePaths(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Remap all paths in a USD Layer to be relative to its published path""" + + label = "Process USD files to use relative paths" + families = ["usd"] + + # Run just before the Integrator + order = pyblish.api.IntegratorOrder - 0.01 + + def process(self, instance): + if not self.is_active(instance.data): + return + + # Skip instance if not marked for integration + if not instance.data.get("integrate", True): + return + + # Some hosts may not have USD libs available but can publish USD data. + # For those we'll log a warning. + if not HAS_USD_LIBS: + self.log.warning( + "Unable to process USD files to relative paths because " + "`pxr` USD libraries could not be imported.") + return + + # For each USD representation, process the file. + for representation in instance.data.get("representations", []): + representation: dict + + if representation.get("name") != "usd": + continue + + # Get expected publish path + published_path = get_instance_expected_output_path( + instance, + representation_name=representation["name"], + ext=representation.get("ext") + ) + published_path_root = os.path.dirname(published_path) + + # Process all files of the representation + staging_dir: str = representation.get( + "stagingDir", instance.data.get("stagingDir")) + + # Process single file or sequence of the representation + if isinstance(representation["files"], str): + # Single file + fname: str = representation["files"] + path = os.path.join(staging_dir, fname) + self.process_usd(path, start=published_path_root) + else: + # Sequence + for fname in representation["files"]: + path = os.path.join(staging_dir, fname) + self.process_usd(path, start=published_path_root) + + # Some instance may have additional transferred files which + # themselves are not a representation. For those we need to look in + # the `instance.data["transfers"]` + for src, dest in instance.data.get("transfers", []): + if not dest.endswith(".usd"): + continue + + # Process USD file at `src` and turn all paths relative to + # the `dest` path the file will end up at. + dest_root = os.path.dirname(dest) + self.process_usd(src, start=dest_root) + + def process_usd(self, usd_path, start): + """Process a USD layer making all paths relative to `start`""" + self.log.debug(f"Processing '{usd_path}'") + layer = Sdf.Layer.FindOrOpen(usd_path) + + def modify_fn(asset_path: str): + """Make all absolute non-anchored paths relative to `start`""" + self.log.debug(f"Processing asset path: {asset_path}") + + # Do not touch paths already anchored paths + if not os.path.isabs(asset_path): + return asset_path + + # Do not touch paths already anchored paths + if asset_path.startswith(RELATIVE_ANCHOR_PREFIXES): + # Already anchored + return asset_path + + # Do not touch what we know are AYON URIs + if asset_path.startswith(("ayon://", "ayon+entity://")): + return asset_path + + # Consider only files on the same drive, because otherwise no + # 'relative' path exists for the file. + if get_drive(start) != get_drive(asset_path): + # Log a warning if different drive + self.log.warning( + f"USD Asset Path '{asset_path}' can not be made relative" + f" to '{start}' because they are not on the same drive.") + return asset_path + + anchored_path = "./" + os.path.relpath(asset_path, start) + self.log.debug(f"Anchored path: {anchored_path}") + return anchored_path + + # Get all "asset path" specs, sublayer paths and references/payloads. + # Make all the paths relative. + UsdUtils.ModifyAssetPaths(layer, modify_fn) + if layer.dirty: + layer.Save() diff --git a/server/settings/main.py b/server/settings/main.py index 552ca06..39c328b 100644 --- a/server/settings/main.py +++ b/server/settings/main.py @@ -2,6 +2,7 @@ from ayon_server.settings import BaseSettingsModel, SettingsField +from .publish_plugins import PublishPluginsModel, DEFAULT_PUBLISH_VALUES def platform_enum(): """Return enumerator for supported platforms.""" @@ -298,3 +299,8 @@ class USDSettings(BaseSettingsModel): ) usd: UsdSettings = SettingsField(default_factory=UsdSettings, title="UsdLib Config") + + publish: PublishPluginsModel = SettingsField( + title="Publish plugins", + default=DEFAULT_PUBLISH_VALUES + ) \ No newline at end of file diff --git a/server/settings/publish_plugins.py b/server/settings/publish_plugins.py new file mode 100644 index 0000000..e584e2a --- /dev/null +++ b/server/settings/publish_plugins.py @@ -0,0 +1,31 @@ +from ayon_server.settings import ( + BaseSettingsModel, + SettingsField, +) + + +class EnabledBaseModel(BaseSettingsModel): + _isGroup = True + enabled: bool = SettingsField(True) + optional: bool = SettingsField(True, title="Optional") + active: bool = SettingsField(True, title="Active") + + +class PublishPluginsModel(BaseSettingsModel): + USDOutputProcessorRemapToRelativePaths: EnabledBaseModel = SettingsField( + default_factory=EnabledBaseModel, + title="Process USD files to use relative paths", + description=( + "When enabled, published USD layers will anchor the asset paths to" + " the published filepath. " + ) + ) + + +DEFAULT_PUBLISH_VALUES = { + "USDOutputProcessorRemapToRelativePaths": { + "enabled": False, + "optional": False, + "active": True, + }, +} From e69ae9229197e594000cf66056ac8e01311de719 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Oct 2024 00:48:19 +0100 Subject: [PATCH 2/4] Refactor logic --- ...grate_usd_output_processor_remap_relative.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py index fdacc80..8ceba39 100644 --- a/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py +++ b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py @@ -68,16 +68,15 @@ def process(self, instance): "stagingDir", instance.data.get("stagingDir")) # Process single file or sequence of the representation - if isinstance(representation["files"], str): - # Single file - fname: str = representation["files"] - path = os.path.join(staging_dir, fname) + filenames = representation["files"] + if isinstance(filenames, str): + # Single file is stored as `str` in `instance.data["files"]` + filenames = [filenames] + + filenames: "list[str]" + for filename in filenames: + path = os.path.join(staging_dir, filename) self.process_usd(path, start=published_path_root) - else: - # Sequence - for fname in representation["files"]: - path = os.path.join(staging_dir, fname) - self.process_usd(path, start=published_path_root) # Some instance may have additional transferred files which # themselves are not a representation. For those we need to look in From ea04ebe0c7f428c6e419beb55eed4de996213244 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Oct 2024 00:51:41 +0100 Subject: [PATCH 3/4] Log path we're making paths relative to --- .../publish/integrate_usd_output_processor_remap_relative.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py index 8ceba39..5aba6d8 100644 --- a/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py +++ b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py @@ -62,6 +62,8 @@ def process(self, instance): ext=representation.get("ext") ) published_path_root = os.path.dirname(published_path) + self.log.debug( + f"Making USD paths relative to {published_path_root}") # Process all files of the representation staging_dir: str = representation.get( @@ -72,7 +74,7 @@ def process(self, instance): if isinstance(filenames, str): # Single file is stored as `str` in `instance.data["files"]` filenames = [filenames] - + filenames: "list[str]" for filename in filenames: path = os.path.join(staging_dir, filename) From 15b2a1e31fd2f73d3e8e507411d47e95a4a065c0 Mon Sep 17 00:00:00 2001 From: Roy Nieterau Date: Tue, 29 Oct 2024 10:21:58 +0100 Subject: [PATCH 4/4] Fix settings actually being applied correctly (so plug-in is disabled by default) --- .../publish/integrate_usd_output_processor_remap_relative.py | 1 + 1 file changed, 1 insertion(+) diff --git a/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py index 5aba6d8..d5ca696 100644 --- a/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py +++ b/client/ayon_usd/plugins/publish/integrate_usd_output_processor_remap_relative.py @@ -28,6 +28,7 @@ class USDOutputProcessorRemapToRelativePaths(pyblish.api.InstancePlugin, label = "Process USD files to use relative paths" families = ["usd"] + settings_category = "usd" # Run just before the Integrator order = pyblish.api.IntegratorOrder - 0.01