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 = +) +{ +}