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

[UsdUtils] Add UDIM support for dependencies/usdz and switch material… #2133

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions pxr/usd/usdUtils/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ pxr_test_scripts(
testenv/testUsdUtilsStitch.py
testenv/testUsdUtilsStitchClips.py
testenv/testUsdUtilsTimeCodeRange.py
testenv/testUsdUtilsUdims.py
testenv/testUsdUtilsUpdateSchemaWithSdrNode.py
testenv/testUsdUtilsVarSelsSessionLayer.py
)
Expand Down Expand Up @@ -191,6 +192,11 @@ pxr_install_test_dir(
DEST testUsdUtilsStitchClips
)

pxr_install_test_dir(
SRC testenv/testUsdUtilsUdims
DEST testUsdUtilsUdims
)

pxr_install_test_dir(
SRC testenv/testUsdUtilsUpdateSchemaWithSdrNode
DEST testUsdUtilsUpdateSchemaWithSdrNode
Expand Down Expand Up @@ -475,6 +481,12 @@ pxr_register_test(testUsdUtilsStitchClips
EXPECTED_RETURN_CODE 0
)

pxr_register_test(testUsdUtilsUdims
PYTHON
COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdUtilsUdims"
EXPECTED_RETURN_CODE 0
)

pxr_register_test(testUsdUtilsUpdateSchemaWithSdrNode
PYTHON
COMMAND "${CMAKE_INSTALL_PREFIX}/tests/testUsdUtilsUpdateSchemaWithSdrNode"
Expand Down
184 changes: 177 additions & 7 deletions pxr/usd/usdUtils/dependencies.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ enum class _ReferenceTypesToInclude {
All
};

static const char UDIM_PATTERN[] = "<UDIM>";
static const int UDIM_START_TILE = 1001;
static const int UDIM_END_TILE = 1100;
static const std::string::size_type UDIM_TILE_NUMBER_LENGTH = 4;

class _FileAnalyzer {
public:
// The asset remapping function's signature.
Expand Down Expand Up @@ -692,8 +697,31 @@ class _AssetLocalizer {
auto &fileAnalyzer = destFilePathAndAnalyzer.second;

if (!fileAnalyzer.GetLayer()) {
_fileCopyMap.emplace_back(fileAnalyzer.GetFilePath(),
destFilePath);
const std::string srcFilePath = fileAnalyzer.GetFilePath();
if (UsdUtilsIsUdimIdentifier(srcFilePath)) {
// Since the source path should already be pre-resolved,
// a proper layer doesn't have to be provided
std::vector<std::string> udimPaths =
UsdUtilsGetUdimFiles(srcFilePath, SdfLayerHandle());

for (auto &udimPath : udimPaths) {
// Find the UDIM numbers to sub
const std::string::size_type right = udimPath.rfind(".");
const std::string::size_type left =
right - UDIM_TILE_NUMBER_LENGTH;

// Using assumption that UsdUtilsResolveUdimPath
// guarantees <UDIM> is in the resolved path
const std::string destUdimPath = TfStringReplace(
destFilePath, UDIM_PATTERN,
udimPath.substr(left, right-left));

_fileCopyMap.emplace_back(udimPath, destUdimPath);
}
} else {
_fileCopyMap.emplace_back(srcFilePath, destFilePath);
}

continue;
}

Expand All @@ -719,11 +747,18 @@ class _AssetLocalizer {
ref = ArSplitPackageRelativePathOuter(ref).first;
}

const std::string refAssetPath =
SdfComputeAssetPathRelativeToLayer(
fileAnalyzer.GetLayer(), ref);

std::string resolvedRefFilePath = resolver.Resolve(refAssetPath);
std::string refAssetPath =
SdfComputeAssetPathRelativeToLayer(
fileAnalyzer.GetLayer(), ref);
std::string resolvedRefFilePath;

// Specially handle UDIM paths
if (UsdUtilsIsUdimIdentifier(ref)) {
resolvedRefFilePath = UsdUtilsResolveUdimPath(
ref, fileAnalyzer.GetLayer());
} else {
resolvedRefFilePath = resolver.Resolve(refAssetPath);
}

if (resolvedRefFilePath.empty()) {
TF_WARN("Failed to resolve reference @%s@ with computed "
Expand Down Expand Up @@ -1346,4 +1381,139 @@ UsdUtilsModifyAssetPaths(
);
}

// Split a udim file path such as /someDir/myFile.<UDIM>.exr into a
// prefix (/someDir/myFile.) and suffix (.exr).
//
// We might support other patterns such as /someDir/myFile._MAPID_.exr
// in the future.
static
std::pair<std::string, std::string>
_SplitUdimPattern(const std::string &path)
{
static const std::vector<std::string> patterns = { UDIM_PATTERN };

for (const std::string &pattern : patterns) {
const std::string::size_type pos = path.find(pattern);
if (pos != std::string::npos) {
return { path.substr(0, pos), path.substr(pos + pattern.size()) };
}
}

return { std::string(), std::string() };
}

// Given the prefix (e.g., /someDir/myImage.) and suffix (e.g., .exr),
// add integer between them and try to resolve
static
std::vector<std::string>
_ResolveUdimPath(
const std::string &udimPath,
SdfLayerHandle const &layer,
bool stopAtFirst=false)
{
TRACE_FUNCTION();

std::vector<std::string> paths;

// Check for bookends, and exit early if it's not a UDIM path
const std::pair<std::string, std::string>
splitPath = _SplitUdimPattern(udimPath);
if (splitPath.first.empty() && splitPath.second.empty()) {
return paths;
}

ArResolver& resolver = ArGetResolver();

for (int i = UDIM_START_TILE; i < UDIM_END_TILE; i++) {
// Fill in integer
std::string path =
splitPath.first + std::to_string(i) + splitPath.second;
if (layer) {
// Deal with layer-relative paths.
path = SdfComputeAssetPathRelativeToLayer(layer, path);
}
// Resolve. Unlike the non-UDIM case, we do not resolve symlinks
// here to handle the case where the symlinks follow the UDIM
// naming pattern but the files that are linked do not. We'll
// let whoever consumes the pattern determine if they want to
// resolve symlinks themselves.
path = resolver.Resolve(path);
if (!path.empty()) {
paths.push_back(path);

if (stopAtFirst) {
return paths;
}
}
}
return paths;
}

bool
UsdUtilsIsUdimIdentifier(const std::string &identifier)
{
const std::pair<std::string, std::string>
splitPath = _SplitUdimPattern(identifier);
return !(splitPath.first.empty() && splitPath.second.empty());
}

std::vector<std::string>
UsdUtilsGetUdimFiles(
const std::string &udimPath,
const SdfLayerHandle& layer)
{
return _ResolveUdimPath(udimPath, layer);
}

std::string
UsdUtilsResolveUdimPath(
const std::string &udimPath,
const SdfLayerHandle& layer)
{
// Return empty if passed path is a non-UDIM path or just doesn't resolve as a UDIM
std::vector<std::string> udimPaths =
_ResolveUdimPath(udimPath, layer, true);
if (udimPaths.empty()) {
return std::string();
}

// Just need first tile to verify and then revert to <UDIM>
std::string firstTilePackage;
std::string firstTilePath = udimPaths[0];

// If the resolved path of the first tile is located in a packaged asset,
// like /foo/bar/baz.usdz[myImage.0001.exr], we need to separate the
// paths to restore the "<UDIM>" prefix to the image filename in the
// code below, then join the path back togther before we return.
if (ArIsPackageRelativePath(firstTilePath)) {
std::tie(firstTilePackage, firstTilePath) =
ArSplitPackageRelativePathInner(firstTilePath);
}

// Construct the file path /filePath/myImage.<UDIM>.exr by using
// the first part from the first resolved tile, "<UDIM>" and the
// suffix.
const std::string &suffix = "." + TfGetExtension(udimPath);

// Sanity check that the part after <UDIM> did not change.
if (!TfStringEndsWith(firstTilePath, suffix)) {
TF_WARN(
"Resolution of first udim tile gave ambigious result. "
"First tile for '%s' is '%s'.",
udimPath.c_str(), firstTilePath.c_str());
return std::string();
}

// Length of the part /filePath/myImage.<UDIM>.exr.
const std::string::size_type prefixLength =
firstTilePath.size() - suffix.size() - UDIM_TILE_NUMBER_LENGTH;

firstTilePath =
firstTilePath.substr(0, prefixLength) + UDIM_PATTERN + suffix;

return firstTilePackage.empty() ?
firstTilePath :
ArJoinPackageRelativePath(firstTilePackage, firstTilePath);
}

PXR_NAMESPACE_CLOSE_SCOPE
26 changes: 26 additions & 0 deletions pxr/usd/usdUtils/dependencies.h
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,32 @@ void UsdUtilsModifyAssetPaths(
const SdfLayerHandle& layer,
const UsdUtilsModifyAssetPathFn& modifyFn);

/// Checks if \p identifier contains a UDIM token. Currently only "<UDIM>" is
/// supported, but other patterns such as "_MAPID_" may be supported in the future.
USDUTILS_API
bool
UsdUtilsIsUdimIdentifier(const std::string &identifier);

/// Retrieves all UDIM tiles matching \p udimPath. The path is first anchored with
/// the passed \p layer if needed, then the function attempts to resolve all possible
/// UDIM numbers in the path, returning all successfully resolved paths.
USDUTILS_API
std::vector<std::string>
UsdUtilsGetUdimFiles(
const std::string &udimPath,
const SdfLayerHandle& layer);

/// Resolves a \p udimPath containing a UDIM token. The path is first
/// anchored with the passed \p layer if needed, then the function attempts to
/// resolve any possible UDIM tiles. If any exist, the resolved path is returned
/// with "<UDIM>" subsituted back in. If no resolves succeed or \p udimPath does
/// not contain a UDIM token, an empty string is returned.
USDUTILS_API
std::string
UsdUtilsResolveUdimPath(
const std::string &udimPath,
const SdfLayerHandle& layer);

PXR_NAMESPACE_CLOSE_SCOPE

#endif // PXR_USD_USD_UTILS_DEPENDENCIES_H
27 changes: 27 additions & 0 deletions pxr/usd/usdUtils/testenv/testUsdUtilsDependencies.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,5 +99,32 @@ def test_ComputeAllDependenciesInvalidClipTemplate(self):
stage.Save()
UsdUtils.ComputeAllDependencies(stage.GetRootLayer().identifier)

def test_ComputeAllDependenciesUdims(self):
"""Test for catching UDIMs with UsdUtils.ComputeAllDependencies.
Not included in the main test to keep it cleaner."""

rootLayer = "computeAllDependenciesUdims/layer.usda"
layers, assets, unresolved = \
UsdUtils.ComputeAllDependencies(rootLayer)

self.assertEqual(
set(layers),
set([Sdf.Layer.Find(rootLayer)]))

self.assertEqual(
set([os.path.normcase(f) for f in assets]),
set([os.path.normcase(
os.path.abspath("computeAllDependenciesUdims/" + f))
for f in ["image_a.1001.exr",
"image_b.1002.exr",
"image_c.1003.exr"]]))

self.assertEqual(
set([os.path.normcase(f) for f in unresolved]),
set([os.path.normcase(
os.path.abspath("computeAllDependenciesUdims/" + f))
for f in ["image_d.<UDIM>.exr",
"image_e.<UDIM>.exr"]]))

if __name__=="__main__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exr file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exr file
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Not a real exr file
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#usda 1.0

def "foo"
{
asset udim_attr = @./image_a.<UDIM>.exr@
asset[] udim_list = [
@./image_b.<UDIM>.exr@,
@./image_c.<UDIM>.exr@,
@./image_d.<UDIM>.exr@,
]
asset udim_missing = @./image_e.<UDIM>.exr@
}
Loading