From b717e5dd20205e432256bc7ba114a8ebd1531acd Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 9 Jun 2025 14:33:21 +0100 Subject: [PATCH] USDScene : Fix loading of instanced skinning with unique animation The `skel:animationSource` can be inherited from ancestor prims, allowing instanced skeletons to receive unique animation. We need to account for that when generating the object hash, otherwise objects with distinct animation will falsely share a hash. In the comment I've mentioned that ideally ObjectAlgo would be in charge of this hashing, but I'm not 100% sure that is the case. I have a prototype which does more accurate hashing using SdfPrimSpecs, and in that USDScene tracks an inherited hash to account for value clips. It may be that it would be better to track an inherited animation hash as well, which would be hard to delegate out to ObjectAlgo. --- Changes | 5 + contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp | 12 ++ .../IECoreUSD/test/IECoreUSD/USDSceneTest.py | 43 ++++++ .../IECoreUSD/data/instancedSkinning.usda | 129 ++++++++++++++++++ 4 files changed, 189 insertions(+) create mode 100644 contrib/IECoreUSD/test/IECoreUSD/data/instancedSkinning.usda diff --git a/Changes b/Changes index 24730ad212..4c22125fac 100644 --- a/Changes +++ b/Changes @@ -6,6 +6,11 @@ Improvements - USDScene : Added loading of ArnoldAlembic, ArnoldUsd and ArnoldProceduralCustom prims as Cortex ExternalProcedural objects. +Fixes +----- + +- USDScene : Fixed loading of instanced UsdSkel geometry with unique animation applied. + 10.5.14.1 (relative to 10.5.14.0) ========= diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp index 40dec351ae..2e676dad50 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp @@ -71,6 +71,7 @@ IECORE_PUSH_DEFAULT_VISIBILITY #include "pxr/usd/usdShade/material.h" #include "pxr/usd/usdShade/materialBindingAPI.h" #include "pxr/usd/usdShade/connectableAPI.h" +#include "pxr/usd/usdSkel/bindingAPI.h" #include "pxr/usd/usdUtils/stageCache.h" #ifdef IECOREUSD_WITH_OPENVDB #include "pxr/usd/usdVol/fieldBase.h" @@ -1764,6 +1765,17 @@ void USDScene::objectHash( double time, IECore::MurmurHash &h ) const { h.append( time ); } + // Account for the skinning applied by PrimitiveAlgo. Ideally this + // responsibility would be taken on by PrimitiveAlgo itself, but that + // would require modifying the ObjectAlgo API, which we don't want to + // do right now. + if( auto skelBindingAPI = pxr::UsdSkelBindingAPI( m_location->prim ) ) + { + if( auto animationSource = skelBindingAPI.GetInheritedAnimationSource() ) + { + appendPrimOrMasterPath( animationSource, h ); + } + } } } void USDScene::childNamesHash( double time, IECore::MurmurHash &h ) const diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index c9a52115cf..b3f1328108 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -2588,6 +2588,49 @@ def testSkinnedFaceVaryingNormals( self ) : for referenceNormal, normal in zip( referenceNormals.data, cubeMesh["N"].data ) : self.assertTrue( normal.equalWithAbsError( referenceNormal, 0.000001 ) ) + def testInstancedSkinning( self ) : + + # Skinned meshes can be instanced, but with each instance inheriting different + # skeleton animation. Make sure we account for that. + + root = IECoreScene.SceneInterface.create( os.path.dirname( __file__ ) + "/data/instancedSkinning.usda", IECore.IndexedIO.OpenMode.Read ) + + # Check that the skinned meshes come out with the expected skinning. + + cube1 = root.scene( [ "Instance1", "SkeletonRoot", "SkinnedCube" ] ) + self.assertEqual( cube1.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, 0.5 ), imath.V3f( 0.5, 0.5, 1.5 ) ) ) + + cube2 = root.scene( [ "Group", "Instance2", "SkeletonRoot", "SkinnedCube" ] ) + self.assertEqual( cube2.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -1.5 ), imath.V3f( 0.5, 0.5, -0.5 ) ) ) + + cube3 = root.scene( [ "Instance3", "SkeletonRoot", "SkinnedCube" ] ) + self.assertEqual( cube2.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -1.5 ), imath.V3f( 0.5, 0.5, -0.5 ) ) ) + + cube4 = root.scene( [ "Instance4", "SkeletonRoot", "SkinnedCube" ] ) + self.assertEqual( cube2.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -1.5 ), imath.V3f( 0.5, 0.5, -0.5 ) ) ) + + # And check that their object hashes match the results above. + + ObjectHash = IECoreScene.SceneInterface.HashType.ObjectHash + self.assertNotEqual( cube1.hash( ObjectHash, 0 ), cube2.hash( ObjectHash, 0 ) ) # Different animation + self.assertEqual( cube2.hash( ObjectHash, 0 ), cube3.hash( ObjectHash, 0 ) ) # Same animation + self.assertEqual( cube2.hash( ObjectHash, 0 ), cube4.hash( ObjectHash, 0 ) ) # Same animation + + # All the unskinned meshes should be the same. + + unskinnedHashes = set() + for path in [ + [ "Instance1", "SkeletonRoot", "UnskinnedCube" ], + [ "Group", "Instance2", "SkeletonRoot", "UnskinnedCube" ], + [ "Instance3", "SkeletonRoot", "UnskinnedCube" ], + [ "Instance4", "SkeletonRoot", "UnskinnedCube" ], + ] : + cube = root.scene( path ) + self.assertEqual( cube.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -0.5 ), imath.V3f( 0.5, 0.5, 0.5 ) ) ) + unskinnedHashes.add( cube.hash( ObjectHash, 0 ) ) + + self.assertEqual( len( unskinnedHashes ), 1 ) + @unittest.skipIf( ( IECore.TestUtil.inMacCI() or IECore.TestUtil.inWindowsCI() ), "Mac and Windows CI are too slow for reliable timing" ) def testCancel ( self ) : diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/instancedSkinning.usda b/contrib/IECoreUSD/test/IECoreUSD/data/instancedSkinning.usda new file mode 100644 index 0000000000..183f1b09eb --- /dev/null +++ b/contrib/IECoreUSD/test/IECoreUSD/data/instancedSkinning.usda @@ -0,0 +1,129 @@ +#usda 1.0 + +# A prototype containing a skeleton and a couple of cubes, one of them skinned. + +def Scope "Prototypes" +{ + + uniform token visibility = "invisible" + + def SkelRoot "SkeletonRoot" ( + prepend apiSchemas = ["SkelBindingAPI"] + ) + { + def Skeleton "Skeleton" ( + prepend apiSchemas = ["SkelBindingAPI"] + ) + { + uniform matrix4d[] bindTransforms = [( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )] + uniform token[] joints = ["Joint1"] + uniform matrix4d[] restTransforms = [( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )] + } + + def Mesh "SkinnedCube" ( + prepend apiSchemas = ["SkelBindingAPI"] + ) + { + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4] + uniform token subdivisionScheme = "none" + point3f[] points = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)] + matrix4d primvars:skel:geomBindTransform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) ) + int[] primvars:skel:jointIndices = [0, 0, 0, 0, 0, 0, 0, 0] ( + elementSize = 1 + interpolation = "vertex" + ) + float[] primvars:skel:jointWeights = [1, 1, 1, 1, 1, 1, 1, 1] ( + elementSize = 1 + interpolation = "vertex" + ) + rel skel:skeleton = + } + + # Just regular geometry. Even though it's inside a SkelRoot, it + # shouldn't be affected by SkelAnimation at all. + def Mesh "UnskinnedCube" + { + int[] faceVertexCounts = [4, 4, 4, 4, 4, 4] + int[] faceVertexIndices = [0, 1, 3, 2, 2, 3, 5, 4, 4, 5, 7, 6, 6, 7, 1, 0, 1, 7, 5, 3, 6, 0, 2, 4] + uniform token subdivisionScheme = "none" + point3f[] points = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (-0.5, 0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5)] + } + } + +} + +# Instance of the prototype, with an animation inherited onto it. + +def Xform "Instance1" ( + prepend apiSchemas = ["SkelBindingAPI"] +) +{ + append rel skel:animationSource = + + def SkelAnimation "InlineAnim" + { + uniform token[] joints = ["Joint1"] + quatf[] rotations = [(1, 0, 0, 0)] + half3[] scales = [(1, 1, 1)] + float3[] translations = [(0, 0, 1)] + } + + over "SkeletonRoot" ( + instanceable = true + prepend references = + ) + { + } +} + +# Another instance of the prototype, with a different animation inherited onto it. + +def SkelAnimation "SeparateAnim" +{ + uniform token[] joints = ["Joint1"] + quatf[] rotations = [(1, 0, 0, 0)] + half3[] scales = [(1, 1, 1)] + float3[] translations = [(0, 0, -1)] +} + +def Xform "Group" ( + prepend apiSchemas = ["SkelBindingAPI"] +) +{ + append rel skel:animationSource = + + def Xform "Instance2" + { + over "SkeletonRoot" ( + instanceable = true + prepend references = + ) + { + } + } +} + +# A third instance, this time sharing the animation with the second instance. + +def Xform "Instance3" ( + prepend apiSchemas = ["SkelBindingAPI"] +) +{ + append rel skel:animationSource = + over "SkeletonRoot" ( + instanceable = true + prepend references = + ) + { + } +} + +# And now an instanceable reference to the third instance. + +def Xform "Instance4" ( + instanceable = true + prepend references = +) +{ +}