diff --git a/include/IECore/LinkedScene.h b/include/IECore/LinkedScene.h index 674f024f17..9cb3c29b30 100644 --- a/include/IECore/LinkedScene.h +++ b/include/IECore/LinkedScene.h @@ -36,6 +36,7 @@ #define IECORE_LINKEDSCENE_H #include "IECore/SampledSceneInterface.h" +#include "IECore/MurmurHash.h" namespace IECore { @@ -56,6 +57,9 @@ IE_CORE_FORWARDDECLARE( LinkedScene ); /// (1) calls to the function writeLink() or /// (2) calls to the function writeAttribute( LinkedScene::linkSceneAttribute, LinkedScene::linkAttributeData(), ... ). /// Note that the link can be animated, allowing for time remapped animations. +/// When reading, this object will return a dynamically computed attribute "SceneInterface:linkHash", which will be +/// a MurmurHash (as a StringData) that uniquely identify a given linked file (with any time remapping on parent links) and +/// could be used by readers for automatic instantiation. This attribute is never returned by attributeNames() function. class LinkedScene : public SampledSceneInterface { public : @@ -65,6 +69,9 @@ class LinkedScene : public SampledSceneInterface /// Equals to "SceneInterface:link" and it's the name given to the link attribute that is recognized // by this class when expanding linked scenes. static const Name &linkAttribute; + /// Equals to "SceneInterface:linkHash" and it's the name given to the attribute that uniquely identify + /// the target linked scene with any time remapping (recursivelly applied on all levels). + static const Name &linkHashAttribute; /// When the open mode is Read it expands the links and only the const methods may be used and the /// when the open mode is Write, only the non-const methods may be used and @@ -154,6 +161,10 @@ class LinkedScene : public SampledSceneInterface double remappedLinkTime( double time ) const; double remappedLinkTimeAtSample( size_t sampleIndex ) const; + // creates the hash that represents the link at the current location. + Object *computeLinkHashAttribute() const; + void computeLinkHash( MurmurHash &h ) const; + SceneInterfacePtr m_mainScene; ConstSceneInterfacePtr m_linkedScene; unsigned int m_rootLinkDepth; diff --git a/src/IECore/LinkedScene.cpp b/src/IECore/LinkedScene.cpp index 40d2fabf4c..3ba4167295 100644 --- a/src/IECore/LinkedScene.cpp +++ b/src/IECore/LinkedScene.cpp @@ -55,6 +55,7 @@ static IndexedIO::Description extensionDescription( ".lscc" ); static SceneInterface::FileFormatDescription registrar(".lscc", IndexedIO::Read | IndexedIO::Write); const SceneInterface::Name &LinkedScene::linkAttribute = InternedString( "sceneInterface:link" ); +const SceneInterface::Name &LinkedScene::linkHashAttribute = InternedString( "sceneInterface:linkHash" ); const InternedString LinkedScene::g_fileNameLinkAttribute("fileName"); const InternedString LinkedScene::g_rootLinkAttribute("root"); const InternedString LinkedScene::g_timeAttribute("time"); @@ -437,6 +438,19 @@ bool LinkedScene::hasAttribute( const Name &name ) const return false; } + if ( name == linkHashAttribute ) + { + if ( m_linkedScene ) + { + if ( m_atLink ) + { + return true; + } + return m_linkedScene->hasAttribute(name); + } + return false; + } + if ( m_linkedScene && !m_atLink ) { return m_linkedScene->hasAttribute(name); @@ -469,6 +483,11 @@ void LinkedScene::attributeNames( NameList &attrs ) const size_t LinkedScene::numAttributeSamples( const Name &name ) const { + if ( name == linkHashAttribute ) + { + return 1; + } + if (!m_sampled) { return 0; @@ -492,6 +511,11 @@ size_t LinkedScene::numAttributeSamples( const Name &name ) const double LinkedScene::attributeSampleTime( const Name &name, size_t sampleIndex ) const { + if ( name == linkHashAttribute ) + { + return 0; + } + if (!m_sampled) { throw Exception( "attributeSampleTime not supported: LinkedScene is pointing to a non-sampled scene!" ); @@ -515,6 +539,13 @@ double LinkedScene::attributeSampleTime( const Name &name, size_t sampleIndex ) double LinkedScene::attributeSampleInterval( const Name &name, double time, size_t &floorIndex, size_t &ceilIndex ) const { + if ( name == linkHashAttribute ) + { + floorIndex = 0; + ceilIndex = 0; + return 0; + } + if (!m_sampled) { throw Exception( "attributeSampleInterval not supported: LinkedScene is pointing to a non-sampled scene!" ); @@ -536,8 +567,56 @@ double LinkedScene::attributeSampleInterval( const Name &name, double time, size } } +void LinkedScene::computeLinkHash( MurmurHash &h ) const +{ + if ( m_linkedScene ) + { + if ( m_timeRemapped ) + { + /// \todo consider baking the time remap hash as an attribute at each link location + /// \todo consider checking for identity remapping and ignoring the hash in that case. + size_t linkAttributeSamples = numAttributeSamples( linkAttribute ); + const SampledSceneInterface *mainScene = static_cast(m_mainScene.get()); + for ( size_t s = 0; s < linkAttributeSamples; s++ ) + { + h.append( mainScene->attributeSampleTime( linkAttribute, s ) ); + h.append( remappedLinkTimeAtSample( s ) ); + } + } + + const LinkedScene *castedLinkedScene = runTimeCast< const LinkedScene >( m_linkedScene ); + if ( castedLinkedScene ) + { + castedLinkedScene->computeLinkHash( h ); + } + else + { + linkAttributeData( m_linkedScene )->hash( h ); + } + } + else + { + linkAttributeData( m_mainScene )->hash( h ); + } +} + +Object *LinkedScene::computeLinkHashAttribute() const +{ + MurmurHash hash; + computeLinkHash(hash); + + StringData *d = new StringData(); + d->writable() = hash.toString(); + return d; +} + ObjectPtr LinkedScene::readAttributeAtSample( const Name &name, size_t sampleIndex ) const { + if ( name == linkHashAttribute ) + { + return computeLinkHashAttribute(); + } + if (!m_sampled) { throw Exception( "readAttributeAtSample not supported: LinkedScene is pointing to a non-sampled scene!" ); @@ -561,6 +640,11 @@ ObjectPtr LinkedScene::readAttributeAtSample( const Name &name, size_t sampleInd ObjectPtr LinkedScene::readAttribute( const Name &name, double time ) const { + if ( name == linkHashAttribute ) + { + return computeLinkHashAttribute(); + } + if ( m_linkedScene && !m_atLink ) { if ( m_timeRemapped ) diff --git a/src/IECorePython/LinkedSceneBinding.cpp b/src/IECorePython/LinkedSceneBinding.cpp index 7f5c83755f..025ec0786f 100644 --- a/src/IECorePython/LinkedSceneBinding.cpp +++ b/src/IECorePython/LinkedSceneBinding.cpp @@ -67,6 +67,7 @@ void bindLinkedScene() .def( "linkAttributeData", linkAttributeData ) .def( "linkAttributeData", retimedLinkAttributeData ).staticmethod( "linkAttributeData" ) .def_readonly("linkAttribute", &LinkedScene::linkAttribute ) + .def_readonly("linkHashAttribute", &LinkedScene::linkHashAttribute ) ; } diff --git a/test/IECore/LinkedSceneTest.py b/test/IECore/LinkedSceneTest.py index fca37756cd..b0d407d070 100644 --- a/test/IECore/LinkedSceneTest.py +++ b/test/IECore/LinkedSceneTest.py @@ -263,6 +263,150 @@ def testTimeRemapping( self ): self.assertEqual( A2.readTransformAtSample(1), IECore.M44dData( IECore.M44d.createTranslated( IECore.V3d( 1.5, 0, 0 ) ) ) ) self.assertEqual( A2.readTransformAtSample(2), IECore.M44dData( IECore.M44d.createTranslated( IECore.V3d( 2, 0, 0 ) ) ) ) + def testLinkHash( self ): + + m = IECore.SceneCache( "test/IECore/data/sccFiles/animatedSpheres.scc", IECore.IndexedIO.OpenMode.Read ) + + l = IECore.LinkedScene( "/tmp/test.lscc", IECore.IndexedIO.OpenMode.Write ) + # save animated spheres with no time offset, but less samples + i0 = l.createChild("instance0") + i0.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 1.0 ), 1.0 ) + i0.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 2.0 ), 2.0 ) + # save animated spheres with same speed and no offset, same samples (time remapping is identity) + i1 = l.createChild("instance1") + i1.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 0.0 ), 0.0 ) + i1.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 1.0 ), 1.0 ) + i1.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 2.0 ), 2.0 ) + i1.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 3.0 ), 3.0 ) + # save animated spheres with same speed and with offset, same samples (time remapping is identity) + i2 = l.createChild("instance2") + i2.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 0.0 ), 1.0 ) + i2.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 1.0 ), 2.0 ) + i2.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 2.0 ), 3.0 ) + i2.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 3.0 ), 4.0 ) + # save non-animated link + i3 = l.createChild("instance3") + i3.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m ), 0.0 ) + # create a transform and have the same links as instance0 and instance3 + B = l.createChild("B") + i0b = B.createChild("instance0") + i0b.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 1.0 ), 1.0 ) + i0b.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m, 2.0 ), 2.0 ) + i3b = B.createChild("instance3") + i3b.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( m ), 0.0 ) + C = l.createChild("C") + + del i0, i1, i2, i3, i0b, i3b, B, C, l + + # open first linked scene for reading... + l = IECore.LinkedScene( "/tmp/test.lscc", IECore.IndexedIO.OpenMode.Read ) + i0 = l.child("instance0") + i1 = l.child("instance1") + i2 = l.child("instance2") + i3 = l.child("instance3") + B = l.child("B") + i0b = B.child("instance0") + i3b = B.child("instance3") + C = l.child("C") + + # create a second level of linked scene + l2 = IECore.LinkedScene( "/tmp/test2.lscc", IECore.IndexedIO.OpenMode.Write ) + # save animated spheres with no time offset, but less samples + j0 = l2.createChild("link0") + j0.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( l ), 0.0 ) + j1 = l2.createChild("link1") + j1.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( l, 1 ), 0.0 ) + j2 = l2.createChild("link2") + j2.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i0 ), 0.0 ) + j3 = l2.createChild("link3") + j3.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i0, 1 ), 0.0 ) + j4 = l2.createChild("link4") + j4.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i1 ), 0.0 ) + j5 = l2.createChild("link5") + j5.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i1, 1 ), 0.0 ) + j6 = l2.createChild("link6") + j6.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i2 ), 0.0 ) + j7 = l2.createChild("link7") + j7.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i2, 1 ), 0.0 ) + j8 = l2.createChild("link8") + j8.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i3 ), 0.0 ) + j9 = l2.createChild("link9") + j9.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i3, 1 ), 0.0 ) + j10 = l2.createChild("link10") + j10.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i0b ), 0.0 ) + j11 = l2.createChild("link11") + j11.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i0b, 1 ), 0.0 ) + j12 = l2.createChild("link12") + j12.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i3b ), 0.0 ) + j13 = l2.createChild("link13") + j13.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( i3b, 1 ), 0.0 ) + j14 = l2.createChild("link14") + j14.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( B ), 0.0 ) + j15 = l2.createChild("link15") + j15.writeAttribute( IECore.LinkedScene.linkAttribute, IECore.LinkedScene.linkAttributeData( C ), 0.0 ) + + del j0,j1,j2,j3,j4,j5,j6,j7,j8,j9,j10,j11,j12,j13,j14,j15,l2 + + # open second linked scene for reading.. + l2 = IECore.LinkedScene( "/tmp/test2.lscc", IECore.IndexedIO.OpenMode.Read ) + j0 = l2.child("link0") + j1 = l2.child("link1") + j2 = l2.child("link2") + j3 = l2.child("link3") + j4 = l2.child("link4") + j5 = l2.child("link5") + j6 = l2.child("link6") + j7 = l2.child("link7") + j8 = l2.child("link8") + j9 = l2.child("link9") + j10 = l2.child("link10") + j11 = l2.child("link11") + j12 = l2.child("link12") + j13 = l2.child("link13") + j14 = l2.child("link14") + j15 = l2.child("link15") + + self.assertTrue( j0.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + self.assertTrue( j13.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + self.assertTrue( j14.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + self.assertTrue( j15.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + self.assertFalse( l.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + self.assertFalse( l2.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + self.assertFalse( B.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + self.assertFalse( C.hasAttribute( IECore.LinkedScene.linkHashAttribute ) ) + + hashes = map( lambda s: s.readAttribute( IECore.LinkedScene.linkHashAttribute, 0 ).value, [ i0, i1, i2, i3, i0b, i3b, j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15 ] ) + + matchingIndices = [ + [ 0, 4, 8, 16 ], # instance0 indices + [ 1, 10 ], # instance1 indices + [ 2, 12 ], # instance2 indices + [ 3, 5, 14, 18 ], # instance3 indices + [ 9, 17 ], # time remapped instance0 + [ 15, 19 ], # time remapped instance3 + ] + + for indices in enumerate( matchingIndices ) : + h = hashes[indices[0]] + matchingHashes = map( lambda (i,h): h, filter( lambda (i,h): i in indices, enumerate(hashes) ) ) + mismatchingHashes = set(hashes).difference( matchingHashes ) + self.assertTrue( h in matchingHashes ) + self.assertFalse( h in mismatchingHashes ) + + # test all the other hashes + otherIndices = list() + for i in xrange(0,len(hashes)): + for mid in xrange(0,len(matchingIndices)): + if i in matchingIndices[mid] : + break + else : + otherIndices.append(i) + + for i in otherIndices : + h = hashes[i] + mismatchingHashes = list(hashes) + del mismatchingHashes[i] + self.assertFalse( h in mismatchingHashes ) def testReading( self ):