Skip to content

Commit b717e5d

Browse files
committed
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.
1 parent 2e9f8ef commit b717e5d

File tree

4 files changed

+189
-0
lines changed

4 files changed

+189
-0
lines changed

Changes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ Improvements
66

77
- USDScene : Added loading of ArnoldAlembic, ArnoldUsd and ArnoldProceduralCustom prims as Cortex ExternalProcedural objects.
88

9+
Fixes
10+
-----
11+
12+
- USDScene : Fixed loading of instanced UsdSkel geometry with unique animation applied.
13+
914
10.5.14.1 (relative to 10.5.14.0)
1015
=========
1116

contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@ IECORE_PUSH_DEFAULT_VISIBILITY
7171
#include "pxr/usd/usdShade/material.h"
7272
#include "pxr/usd/usdShade/materialBindingAPI.h"
7373
#include "pxr/usd/usdShade/connectableAPI.h"
74+
#include "pxr/usd/usdSkel/bindingAPI.h"
7475
#include "pxr/usd/usdUtils/stageCache.h"
7576
#ifdef IECOREUSD_WITH_OPENVDB
7677
#include "pxr/usd/usdVol/fieldBase.h"
@@ -1764,6 +1765,17 @@ void USDScene::objectHash( double time, IECore::MurmurHash &h ) const
17641765
{
17651766
h.append( time );
17661767
}
1768+
// Account for the skinning applied by PrimitiveAlgo. Ideally this
1769+
// responsibility would be taken on by PrimitiveAlgo itself, but that
1770+
// would require modifying the ObjectAlgo API, which we don't want to
1771+
// do right now.
1772+
if( auto skelBindingAPI = pxr::UsdSkelBindingAPI( m_location->prim ) )
1773+
{
1774+
if( auto animationSource = skelBindingAPI.GetInheritedAnimationSource() )
1775+
{
1776+
appendPrimOrMasterPath( animationSource, h );
1777+
}
1778+
}
17671779
}
17681780
}
17691781
void USDScene::childNamesHash( double time, IECore::MurmurHash &h ) const

contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2588,6 +2588,49 @@ def testSkinnedFaceVaryingNormals( self ) :
25882588
for referenceNormal, normal in zip( referenceNormals.data, cubeMesh["N"].data ) :
25892589
self.assertTrue( normal.equalWithAbsError( referenceNormal, 0.000001 ) )
25902590

2591+
def testInstancedSkinning( self ) :
2592+
2593+
# Skinned meshes can be instanced, but with each instance inheriting different
2594+
# skeleton animation. Make sure we account for that.
2595+
2596+
root = IECoreScene.SceneInterface.create( os.path.dirname( __file__ ) + "/data/instancedSkinning.usda", IECore.IndexedIO.OpenMode.Read )
2597+
2598+
# Check that the skinned meshes come out with the expected skinning.
2599+
2600+
cube1 = root.scene( [ "Instance1", "SkeletonRoot", "SkinnedCube" ] )
2601+
self.assertEqual( cube1.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, 0.5 ), imath.V3f( 0.5, 0.5, 1.5 ) ) )
2602+
2603+
cube2 = root.scene( [ "Group", "Instance2", "SkeletonRoot", "SkinnedCube" ] )
2604+
self.assertEqual( cube2.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -1.5 ), imath.V3f( 0.5, 0.5, -0.5 ) ) )
2605+
2606+
cube3 = root.scene( [ "Instance3", "SkeletonRoot", "SkinnedCube" ] )
2607+
self.assertEqual( cube2.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -1.5 ), imath.V3f( 0.5, 0.5, -0.5 ) ) )
2608+
2609+
cube4 = root.scene( [ "Instance4", "SkeletonRoot", "SkinnedCube" ] )
2610+
self.assertEqual( cube2.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -1.5 ), imath.V3f( 0.5, 0.5, -0.5 ) ) )
2611+
2612+
# And check that their object hashes match the results above.
2613+
2614+
ObjectHash = IECoreScene.SceneInterface.HashType.ObjectHash
2615+
self.assertNotEqual( cube1.hash( ObjectHash, 0 ), cube2.hash( ObjectHash, 0 ) ) # Different animation
2616+
self.assertEqual( cube2.hash( ObjectHash, 0 ), cube3.hash( ObjectHash, 0 ) ) # Same animation
2617+
self.assertEqual( cube2.hash( ObjectHash, 0 ), cube4.hash( ObjectHash, 0 ) ) # Same animation
2618+
2619+
# All the unskinned meshes should be the same.
2620+
2621+
unskinnedHashes = set()
2622+
for path in [
2623+
[ "Instance1", "SkeletonRoot", "UnskinnedCube" ],
2624+
[ "Group", "Instance2", "SkeletonRoot", "UnskinnedCube" ],
2625+
[ "Instance3", "SkeletonRoot", "UnskinnedCube" ],
2626+
[ "Instance4", "SkeletonRoot", "UnskinnedCube" ],
2627+
] :
2628+
cube = root.scene( path )
2629+
self.assertEqual( cube.readObject( 0 ).bound(), imath.Box3f( imath.V3f( -0.5, -0.5, -0.5 ), imath.V3f( 0.5, 0.5, 0.5 ) ) )
2630+
unskinnedHashes.add( cube.hash( ObjectHash, 0 ) )
2631+
2632+
self.assertEqual( len( unskinnedHashes ), 1 )
2633+
25912634
@unittest.skipIf( ( IECore.TestUtil.inMacCI() or IECore.TestUtil.inWindowsCI() ), "Mac and Windows CI are too slow for reliable timing" )
25922635
def testCancel ( self ) :
25932636

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
#usda 1.0
2+
3+
# A prototype containing a skeleton and a couple of cubes, one of them skinned.
4+
5+
def Scope "Prototypes"
6+
{
7+
8+
uniform token visibility = "invisible"
9+
10+
def SkelRoot "SkeletonRoot" (
11+
prepend apiSchemas = ["SkelBindingAPI"]
12+
)
13+
{
14+
def Skeleton "Skeleton" (
15+
prepend apiSchemas = ["SkelBindingAPI"]
16+
)
17+
{
18+
uniform matrix4d[] bindTransforms = [( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )]
19+
uniform token[] joints = ["Joint1"]
20+
uniform matrix4d[] restTransforms = [( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )]
21+
}
22+
23+
def Mesh "SkinnedCube" (
24+
prepend apiSchemas = ["SkelBindingAPI"]
25+
)
26+
{
27+
int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]
28+
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]
29+
uniform token subdivisionScheme = "none"
30+
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)]
31+
matrix4d primvars:skel:geomBindTransform = ( (1, 0, 0, 0), (0, 1, 0, 0), (0, 0, 1, 0), (0, 0, 0, 1) )
32+
int[] primvars:skel:jointIndices = [0, 0, 0, 0, 0, 0, 0, 0] (
33+
elementSize = 1
34+
interpolation = "vertex"
35+
)
36+
float[] primvars:skel:jointWeights = [1, 1, 1, 1, 1, 1, 1, 1] (
37+
elementSize = 1
38+
interpolation = "vertex"
39+
)
40+
rel skel:skeleton = </Prototypes/SkeletonRoot/Skeleton>
41+
}
42+
43+
# Just regular geometry. Even though it's inside a SkelRoot, it
44+
# shouldn't be affected by SkelAnimation at all.
45+
def Mesh "UnskinnedCube"
46+
{
47+
int[] faceVertexCounts = [4, 4, 4, 4, 4, 4]
48+
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]
49+
uniform token subdivisionScheme = "none"
50+
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)]
51+
}
52+
}
53+
54+
}
55+
56+
# Instance of the prototype, with an animation inherited onto it.
57+
58+
def Xform "Instance1" (
59+
prepend apiSchemas = ["SkelBindingAPI"]
60+
)
61+
{
62+
append rel skel:animationSource = </Instance1/InlineAnim>
63+
64+
def SkelAnimation "InlineAnim"
65+
{
66+
uniform token[] joints = ["Joint1"]
67+
quatf[] rotations = [(1, 0, 0, 0)]
68+
half3[] scales = [(1, 1, 1)]
69+
float3[] translations = [(0, 0, 1)]
70+
}
71+
72+
over "SkeletonRoot" (
73+
instanceable = true
74+
prepend references = </Prototypes/SkeletonRoot>
75+
)
76+
{
77+
}
78+
}
79+
80+
# Another instance of the prototype, with a different animation inherited onto it.
81+
82+
def SkelAnimation "SeparateAnim"
83+
{
84+
uniform token[] joints = ["Joint1"]
85+
quatf[] rotations = [(1, 0, 0, 0)]
86+
half3[] scales = [(1, 1, 1)]
87+
float3[] translations = [(0, 0, -1)]
88+
}
89+
90+
def Xform "Group" (
91+
prepend apiSchemas = ["SkelBindingAPI"]
92+
)
93+
{
94+
append rel skel:animationSource = </SeparateAnim>
95+
96+
def Xform "Instance2"
97+
{
98+
over "SkeletonRoot" (
99+
instanceable = true
100+
prepend references = </Prototypes/SkeletonRoot>
101+
)
102+
{
103+
}
104+
}
105+
}
106+
107+
# A third instance, this time sharing the animation with the second instance.
108+
109+
def Xform "Instance3" (
110+
prepend apiSchemas = ["SkelBindingAPI"]
111+
)
112+
{
113+
append rel skel:animationSource = </SeparateAnim>
114+
over "SkeletonRoot" (
115+
instanceable = true
116+
prepend references = </Prototypes/SkeletonRoot>
117+
)
118+
{
119+
}
120+
}
121+
122+
# And now an instanceable reference to the third instance.
123+
124+
def Xform "Instance4" (
125+
instanceable = true
126+
prepend references = </Instance3>
127+
)
128+
{
129+
}

0 commit comments

Comments
 (0)