Importing a USD stage containing an animated UsdGeomMesh can fail to create both the UGeometryCache asset and UGeometryCacheUsdComponent when non-geometry prims exist alongside the animated geometry.
The animated mesh remains loaded and time-varying. The failure occurs because USDCore may select a UsdGeomScope as the Geometry Cache root, but Unreal does not register a Geometry Cache translator for UsdGeomScope.
Expected result
USDCore should select /root/render/geo/animatedGeometry, or another translatable UsdGeomXformable, as the Geometry Cache root.
Unreal should create a UGeometryCache asset and corresponding Geometry Cache component.
Cause
RecursiveCheckForGeometryCache correctly treats Material, Shader, and RenderVar prims as uncollapsible. This prevents /root and /root/render from becoming Geometry Cache roots.
FindValidGeometryCacheRoot then descends into /root/render/geo. USDCore allows a UsdGeomScope containing meshes to propagate the collapsible mesh state:
Engine/Plugins/Runtime/USDCore/Source/USDUtilities/Private/Objects/USDInfoCache.cpp
The relevant logic is around RecursiveCheckForGeometryCache, where UsdGeomScope is treated as an exception and inherits its children’s mesh state.
FindValidGeometryCacheRoot subsequently marks /root/render/geo as EGeometryCachePrimState::ValidRoot. It stops searching before reaching the child UsdGeomXform.
This selected root cannot be translated because Geometry Cache translation is registered only for UsdGeomMesh and UsdGeomXformable:
Engine/Plugins/Importers/USDImporter/Source/USDSchemas/Private/USDSchemasModule.cpp
The import geometry traversal also searches for UsdGeomXformable prims, not UsdGeomScope prims:
Engine/Plugins/Importers/USDImporter/Source/USDStageImporter/Private/USDStageImporter.cpp
This leaves the animated mesh present but without a translatable prim marked as the Geometry Cache root.
Actual result
No UGeometryCache asset is created.
No UUsdGeometryCacheComponent or UGeometryCacheUsdComponent is created in the imported level hierarchy.
The animated mesh and all contributing layers remain valid in the composed USD stage.
Proposed fix
diff --git a/Engine/Plugins/Runtime/USDCore/Source/USDUtilities/Private/Objects/USDInfoCache.cpp b/Engine/Plugins/Runtime/USDCore/Source/USDUtilities/Private/Objects/USDInfoCache.cpp
--- a/Engine/Plugins/Runtime/USDCore/Source/USDUtilities/Private/Objects/USDInfoCache.cpp
+++ b/Engine/Plugins/Runtime/USDCore/Source/USDUtilities/Private/Objects/USDInfoCache.cpp
#include "pxr/usd/usdSkel/root.h"
[Content removed]15 @@
{
FWriteScopeLock PrimLock = Impl.LockForWriting(*Info);
// A prim is considered a valid root if its subtree has no uncollapsible branch and a valid depth.
// A valid depth is positive, meaning it has an animated mesh, and doesn't exceed the limit.
bool bIsValidDepth = Info->GeometryCacheDepth > -1 && Info->GeometryCacheDepth <= GGeometryCacheMaxDepth;
- if (!EnumHasAnyFlags(Info->GeometryCacheState, EGeometryCachePrimState::Uncollapsible) && bIsValidDepth)
+
+ // Geometry Cache translators are registered for Mesh and Xformable prims, not Scope prims.
+ // Keep traversing through a Scope instead of selecting a root that cannot be translated.
+ const bool bCanTranslateGeometryCacheRoot = !!pxr::UsdGeomXformable(UsdPrim);
+
+ if (bCanTranslateGeometryCacheRoot
+ && !EnumHasAnyFlags(Info->GeometryCacheState, EGeometryCachePrimState::Uncollapsible) && bIsValidDepth)
{
OutState = EGeometryCachePrimState::ValidRoot;
Info->GeometryCacheState = EGeometryCachePrimState::ValidRoot;
return;