Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to process USD files to use relative paths when published #70

Merged
merged 5 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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)
self.log.debug(
f"Making USD paths relative to {published_path_root}")

# Process all files of the representation
staging_dir: str = representation.get(
"stagingDir", instance.data.get("stagingDir"))

# Process single file or sequence of the representation
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)

# 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()
6 changes: 6 additions & 0 deletions server/settings/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -296,3 +297,8 @@ class USDSettings(BaseSettingsModel):

usd: UsdLibConfigSettings = SettingsField(
default_factory=UsdLibConfigSettings, title="USD Library Config")

publish: PublishPluginsModel = SettingsField(
title="Publish plugins",
default=DEFAULT_PUBLISH_VALUES
)
31 changes: 31 additions & 0 deletions server/settings/publish_plugins.py
Original file line number Diff line number Diff line change
@@ -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,
},
}