Skip to content

USDScene : Fix loading of instanced skinning with unique animation #1471

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

Merged
merged 1 commit into from
Jun 10, 2025
Merged
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
5 changes: 5 additions & 0 deletions Changes
Original file line number Diff line number Diff line change
Expand Up @@ -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)
=========

Expand Down
12 changes: 12 additions & 0 deletions contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) :

Expand Down
129 changes: 129 additions & 0 deletions contrib/IECoreUSD/test/IECoreUSD/data/instancedSkinning.usda
Original file line number Diff line number Diff line change
@@ -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 = </Prototypes/SkeletonRoot/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 = </Instance1/InlineAnim>

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 = </Prototypes/SkeletonRoot>
)
{
}
}

# 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 = </SeparateAnim>

def Xform "Instance2"
{
over "SkeletonRoot" (
instanceable = true
prepend references = </Prototypes/SkeletonRoot>
)
{
}
}
}

# A third instance, this time sharing the animation with the second instance.

def Xform "Instance3" (
prepend apiSchemas = ["SkelBindingAPI"]
)
{
append rel skel:animationSource = </SeparateAnim>
over "SkeletonRoot" (
instanceable = true
prepend references = </Prototypes/SkeletonRoot>
)
{
}
}

# And now an instanceable reference to the third instance.

def Xform "Instance4" (
instanceable = true
prepend references = </Instance3>
)
{
}