diff --git a/Changes.md b/Changes.md index cdf1d052d9e..2f17d930673 100644 --- a/Changes.md +++ b/Changes.md @@ -1,7 +1,17 @@ 1.3.x.x (relative to 1.3.6.1) ======= +Improvements +------------ + +- Instancer : + - Improved scene generation for encapsulated instancers significantly, with some production scenes now generating 5-7x faster. + - Added `omitDuplicateIds` plug, to determine whether points with duplicate IDs are ignored or should trigger an error. + +API +--- +- Capsule : Added protected `renderOptions()` and `throwIfNoScene()` methods. 1.3.6.1 (relative to 1.3.6.0) ======= diff --git a/include/GafferScene/Capsule.h b/include/GafferScene/Capsule.h index 4856bfbc4e7..6e43bd3d4c6 100644 --- a/include/GafferScene/Capsule.h +++ b/include/GafferScene/Capsule.h @@ -96,10 +96,16 @@ class GAFFERSCENE_API Capsule : public IECoreScenePreview::Procedural void setRenderOptions( const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions ); std::optional getRenderOptions() const; - private : + protected : + + // Returns the current render options - this will be the override if setRenderOptions has been called, + // otherwise it will construct render options based on the `scene()`. + GafferScene::Private::RendererAlgo::RenderOptions renderOptions() const; void throwIfNoScene() const; + private : + IECore::MurmurHash m_hash; Imath::Box3f m_bound; // We don't own a reference to `m_scene` because it could cause its deletion diff --git a/include/GafferScene/Instancer.h b/include/GafferScene/Instancer.h index 31512c232d5..027337fac78 100644 --- a/include/GafferScene/Instancer.h +++ b/include/GafferScene/Instancer.h @@ -39,6 +39,15 @@ #include "GafferScene/Export.h" #include "GafferScene/BranchCreator.h" +#include "GafferScene/Capsule.h" + +namespace GafferSceneModule +{ + +// Forward declaration to enable friend declaration. +void bindHierarchy(); + +} // namespace GafferSceneModule namespace GafferScene { @@ -117,6 +126,9 @@ class GAFFERSCENE_API Instancer : public BranchCreator Gaffer::StringPlug *idPlug(); const Gaffer::StringPlug *idPlug() const; + Gaffer::BoolPlug *omitDuplicateIdsPlug(); + const Gaffer::BoolPlug *omitDuplicateIdsPlug() const; + Gaffer::StringPlug *positionPlug(); const Gaffer::StringPlug *positionPlug() const; @@ -211,13 +223,11 @@ class GAFFERSCENE_API Instancer : public BranchCreator private : IE_CORE_FORWARDDECLARE( EngineData ); + IE_CORE_FORWARDDECLARE( InstancerCapsule ); Gaffer::ObjectPlug *enginePlug(); const Gaffer::ObjectPlug *enginePlug() const; - Gaffer::AtomicCompoundDataPlug *prototypeChildNamesPlug(); - const Gaffer::AtomicCompoundDataPlug *prototypeChildNamesPlug() const; - GafferScene::ScenePlug *capsuleScenePlug(); const GafferScene::ScenePlug *capsuleScenePlug() const; @@ -230,9 +240,6 @@ class GAFFERSCENE_API Instancer : public BranchCreator ConstEngineDataPtr engine( const ScenePath &sourcePath, const Gaffer::Context *context ) const; void engineHash( const ScenePath &sourcePath, const Gaffer::Context *context, IECore::MurmurHash &h ) const; - IECore::ConstCompoundDataPtr prototypeChildNames( const ScenePath &sourcePath, const Gaffer::Context *context ) const; - void prototypeChildNamesHash( const ScenePath &sourcePath, const Gaffer::Context *context, IECore::MurmurHash &h ) const; - struct PrototypeScope : public Gaffer::Context::EditableScope { PrototypeScope( const Gaffer::ObjectPlug *enginePlug, const Gaffer::Context *context, const ScenePath *parentPath, const ScenePath *branchPath ); @@ -247,6 +254,10 @@ class GAFFERSCENE_API Instancer : public BranchCreator static size_t g_firstPlugIndex; + // For bindings + friend void GafferSceneModule::bindHierarchy(); + static const std::type_info &instancerCapsuleTypeInfo(); + }; IE_CORE_DECLAREPTR( Instancer ) diff --git a/include/GafferScene/TypeIds.h b/include/GafferScene/TypeIds.h index 7826449213c..c7e841055df 100644 --- a/include/GafferScene/TypeIds.h +++ b/include/GafferScene/TypeIds.h @@ -177,6 +177,7 @@ enum TypeId FramingConstraintTypeId = 110633, MeshNormalsTypeId = 110634, ImageScatterTypeId = 110635, + InstancerCapsuleTypeId = 110636, PreviewPlaceholderTypeId = 110647, PreviewGeometryTypeId = 110648, diff --git a/python/GafferArnoldTest/ArnoldRenderTest.py b/python/GafferArnoldTest/ArnoldRenderTest.py index 918d1f0580d..d4b05aceaf5 100644 --- a/python/GafferArnoldTest/ArnoldRenderTest.py +++ b/python/GafferArnoldTest/ArnoldRenderTest.py @@ -1256,7 +1256,7 @@ def __color4fAtUV( self, image, uv ) : def __arrayToSet( self, a ) : result = set() - for i in range( 0, arnold.AiArrayGetNumElements( a.contents ) ) : + for i in range( 0, arnold.AiArrayGetNumElements( a.contents ) ) : if arnold.AiArrayGetType( a.contents ) == arnold.AI_TYPE_STRING : result.add( arnold.AiArrayGetStr( a, i ) ) else : @@ -1478,5 +1478,271 @@ def testCoordinateSystem( self ) : # node to have been created for ours. self.assertIsNone( arnold.AiNodeLookUpByName( universe, "/coordinateSystem" ) ) + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 1 ) + def testInstancerPerf( self ) : + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["sphere"] = GafferScene.Sphere() + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["plane"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphere"]["out"] ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 5 ) + def testInstancerEncapsulatePerf( self ) : + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["sphere"] = GafferScene.Sphere() + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["plane"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphere"]["out"] ) + + s["instancer"]["encapsulateInstanceGroups"].setValue( True ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 1 ) + def testInstancerManyPrototypesPerf( self ) : + # Having a context variable set without anything in the prototype being affected by that + # context variable is mostly just going to add stress to the hash cache. This test exists + # mostly for comparison with the encapsulated case below. + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["sphere"] = GafferScene.Sphere() + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["plane"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphere"]["out"] ) + + s["instancer"]["contextVariables"].addChild( GafferScene.Instancer.ContextVariablePlug( "context" ) ) + s["instancer"]["contextVariables"][0]["name"].setValue( "P" ) + s["instancer"]["contextVariables"][0]["quantize"].setValue( 0 ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 1 ) + def testInstancerManyPrototypesEncapsulatePerf( self ) : + # Having a context variable set ( even without anything in the prototype reading it ), will force + # the encapsulate code path to allocate a bunch of separate prototypes, even if they all end up the same. + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["sphere"] = GafferScene.Sphere() + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["plane"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphere"]["out"] ) + + s["instancer"]["contextVariables"].addChild( GafferScene.Instancer.ContextVariablePlug( "context" ) ) + s["instancer"]["contextVariables"][0]["name"].setValue( "P" ) + s["instancer"]["contextVariables"][0]["quantize"].setValue( 0 ) + + s["instancer"]["encapsulateInstanceGroups"].setValue( True ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 1 ) + def testInstancerFewPrototypesPerf( self ) : + + # A slightly weird test, but it tests one extreme: there is a context variable, but quantize is + # set so high that all the contexts end up the same, and only one prototype is needed. + # This case is particularly bad for the unencapsulated code path, but quite good for the + # encapsulated path. + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["sphere"] = GafferScene.Sphere() + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["plane"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphere"]["out"] ) + + s["instancer"]["contextVariables"].addChild( GafferScene.Instancer.ContextVariablePlug( "context" ) ) + s["instancer"]["contextVariables"][0]["name"].setValue( "P" ) + s["instancer"]["contextVariables"][0]["quantize"].setValue( 100000 ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 1 ) + def testInstancerFewPrototypesEncapsulatePerf( self ) : + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["sphere"] = GafferScene.Sphere() + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["plane"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphere"]["out"] ) + + s["instancer"]["contextVariables"].addChild( GafferScene.Instancer.ContextVariablePlug( "context" ) ) + s["instancer"]["contextVariables"][0]["name"].setValue( "P" ) + s["instancer"]["contextVariables"][0]["quantize"].setValue( 1000000 ) + + s["instancer"]["encapsulateInstanceGroups"].setValue( True ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 1 ) + def testInstancerWithAttributesPerf( self ) : + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["shuffle"] = GafferScene.ShufflePrimitiveVariables() + s["shuffle"]["in"].setInput( s["plane"]["out"] ) + s["shuffle"]["filter"].setInput( s["pathFilter"]["out"] ) + for v in [ "A", "B", "C", "D", "E", "F", "G", "H" ]: + s["shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "P", v ) ) + + s["sphere"] = GafferScene.Sphere() + + s["sphereAttrs"] = GafferScene.CustomAttributes() + s["sphereAttrs"]["in"].setInput( s["sphere"]["out"] ) + for v in [ "I", "J", "K", "L", "M", "N", "O", "P" ]: + s["sphereAttrs"]["attributes"].addChild( Gaffer.NameValuePlug( v, Gaffer.IntPlug( "value", defaultValue = 7 ) ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["shuffle"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphereAttrs"]["out"] ) + s["instancer"]["attributes"].setValue( "P N uv A B C D E F G H" ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + + @GafferTest.TestRunner.PerformanceTestMethod( repeat = 5 ) + def testInstancerWithAttributesEncapsulatePerf( self ) : + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["plane"]["divisions"].setValue( imath.V2i( 500 ) ) + + s["pathFilter"] = GafferScene.PathFilter() + s["pathFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + s["shuffle"] = GafferScene.ShufflePrimitiveVariables() + s["shuffle"]["in"].setInput( s["plane"]["out"] ) + s["shuffle"]["filter"].setInput( s["pathFilter"]["out"] ) + for v in [ "A", "B", "C", "D", "E", "F", "G", "H" ]: + s["shuffle"]["shuffles"].addChild( Gaffer.ShufflePlug( "P", v ) ) + + s["sphere"] = GafferScene.Sphere() + + s["sphereAttrs"] = GafferScene.CustomAttributes() + s["sphereAttrs"]["in"].setInput( s["sphere"]["out"] ) + for v in [ "I", "J", "K", "L", "M", "N", "O", "P" ]: + s["sphereAttrs"]["attributes"].addChild( Gaffer.NameValuePlug( v, Gaffer.IntPlug( "value", defaultValue = 7 ) ) ) + + s["instancer"] = GafferScene.Instancer() + s["instancer"]["in"].setInput( s["shuffle"]["out"] ) + s["instancer"]["filter"].setInput( s["pathFilter"]["out"] ) + s["instancer"]["prototypes"].setInput( s["sphereAttrs"]["out"] ) + s["instancer"]["attributes"].setValue( "P N uv A B C D E F G H" ) + + s["instancer"]["encapsulateInstanceGroups"].setValue( True ) + + s["render"] = GafferArnold.ArnoldRender() + s["render"]["in"].setInput( s["instancer"]["out"] ) + + with Gaffer.Context() as c : + c["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + with GafferTest.TestRunner.PerformanceScope() : + s["render"]["task"].execute() + if __name__ == "__main__": unittest.main() diff --git a/python/GafferSceneTest/EncapsulateTest.py b/python/GafferSceneTest/EncapsulateTest.py index b52fcfbf680..500b54ced4b 100644 --- a/python/GafferSceneTest/EncapsulateTest.py +++ b/python/GafferSceneTest/EncapsulateTest.py @@ -278,5 +278,39 @@ def testSignalThreadSafety( self ) : # will throw if the subprocess crashes. subprocess.check_output( [ str( Gaffer.executablePath() ), "stats", script["fileName"].getValue(), "-scene", "collect" ] ) + @unittest.expectedFailure + def testInheritTransformSamples( self ): + # This turns out to be just an illustration of the known issue where you can't set motion blur + # attributes from outside the Capsule. In this case, it feels like the attributes should be set inside + # the Capsule, because they are set on '/group' before the Encapsulate, but attributes on the root + # location are stored outside the Capsule, so it hits the same problem. + + sphere = GafferScene.Sphere() + sphere["expression"] = Gaffer.Expression() + sphere["expression"].setExpression( 'parent["transform"]["translate"]["x"] = context.getFrame()' ) + + group = GafferScene.Group() + group["in"][0].setInput( sphere["out"] ) + + pathFilter = GafferScene.PathFilter() + pathFilter["paths"].setValue( IECore.StringVectorData( [ '/group' ] ) ) + + standardAttributes = GafferScene.StandardAttributes() + standardAttributes["in"].setInput( group["out"] ) + standardAttributes["filter"].setInput( pathFilter["out"] ) + standardAttributes["attributes"]["transformBlurSegments"]["value"].setValue( 4 ) + standardAttributes["attributes"]["transformBlurSegments"]["enabled"].setValue( True ) + + standardOptions = GafferScene.StandardOptions() + standardOptions["in"].setInput( standardAttributes["out"] ) + standardOptions["options"]["transformBlur"]["value"].setValue( True ) + standardOptions["options"]["transformBlur"]["enabled"].setValue( True ) + + encapsulate = GafferScene.Encapsulate() + encapsulate["in"].setInput( standardOptions["out"] ) + encapsulate["filter"].setInput( pathFilter["out"] ) + + self.assertScenesRenderSame( encapsulate["in"], encapsulate["out"], expandProcedurals = True, ignoreLinks = True ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferSceneTest/IECoreScenePreviewTest/CapturingRendererTest.py b/python/GafferSceneTest/IECoreScenePreviewTest/CapturingRendererTest.py index 840f71280a4..b6a1c8f7359 100644 --- a/python/GafferSceneTest/IECoreScenePreviewTest/CapturingRendererTest.py +++ b/python/GafferSceneTest/IECoreScenePreviewTest/CapturingRendererTest.py @@ -226,7 +226,9 @@ def __expandCapturingRenderer( capturingRenderer, expandProcedurals = False ): for subName, subObject in procExpanded.items(): - if subName.startswith( "/" ): + if subName == "" or subName == "/": + newName = objectName + elif subName.startswith( "/" ): newName = objectName + subName else: newName = objectName + "/" + subName diff --git a/python/GafferSceneTest/InstancerTest.py b/python/GafferSceneTest/InstancerTest.py index c260cd0f6fc..f165af2332e 100644 --- a/python/GafferSceneTest/InstancerTest.py +++ b/python/GafferSceneTest/InstancerTest.py @@ -53,6 +53,27 @@ class InstancerTest( GafferSceneTest.SceneTestCase ) : + def assertEncapsulatedRendersSame( self, instancer ): + + encapInstancer = GafferScene.Instancer() + for i in instancer["contextVariables"]: + encapInstancer["contextVariables"].addChild( GafferScene.Instancer.ContextVariablePlug( i.getName() ) ) + + for i in Gaffer.ValuePlug.RecursiveInputRange( instancer ): + if not i.isSetToDefault(): + corresponding = encapInstancer.descendant( i.relativeName( instancer ) ) + if i.getInput(): + corresponding.setInput( i.getInput() ) + else: + if not hasattr( i, "getValue" ): + # Probably a compound plug, can't setValue, but children will get transferred + continue + corresponding.setValue( i.getValue() ) + + encapInstancer["encapsulateInstanceGroups"].setValue( True ) + + self.assertScenesRenderSame( instancer["out"], encapInstancer["out"], expandProcedurals = True, ignoreLinks = True ) + def test( self ) : sphere = IECoreScene.SpherePrimitive() @@ -152,6 +173,13 @@ def test( self ) : encapInstancer["name"].setValue( "instances" ) encapInstancer["encapsulateInstanceGroups"].setValue( True ) + # Test an edge case, and make sure while we're at it that we're actually getting an InstancerCapsule + # ( Because it's private, it's bound to Python in a sorta weird way that means its typeName() will + # report Capsule, not InstancerCapsule, but this error is a quick test that we are actually dealing with + # the right thing. ) + with self.assertRaisesRegex( RuntimeError, "Null renderer passed to InstancerCapsule" ) : + encapInstancer["out"].object( "/seeds/instances/sphere" ).render( None ) + # Check that the capsule expands during rendering to render the same as the unencapsulated scene. # ( Except for the light links, which aren't output by the Capsule currently ) self.assertScenesRenderSame( instancer["out"], encapInstancer["out"], expandProcedurals = True, ignoreLinks = True ) @@ -272,6 +300,7 @@ def testEmptyName( self ) : deleteObject["filter"].setInput( f["out"] ) self.assertScenesEqual( instancer["out"], deleteObject["out"] ) + self.assertEncapsulatedRendersSame( instancer ) def testEmptyParent( self ) : @@ -286,6 +315,7 @@ def testEmptyParent( self ) : self.assertScenesEqual( instancer["out"], plane["out"] ) self.assertSceneHashesEqual( instancer["out"], plane["out"] ) + self.assertEncapsulatedRendersSame( instancer ) def testSeedsAffectBound( self ) : @@ -651,6 +681,7 @@ def testTransform( self ) : instancer["parent"].setValue( "/object" ) self.assertEqual( instancer["out"].transform( "/object/instances/sphere/0" ), imath.M44f().translate( imath.V3f( 4, 0, 0 ) ) ) + self.assertEncapsulatedRendersSame( instancer ) instancer["orientation"].setValue( "orientation" ) self.assertTrue( @@ -659,6 +690,7 @@ def testTransform( self ) : 0.00001 ) ) + self.assertEncapsulatedRendersSame( instancer ) instancer["scale"].setValue( "scale" ) self.assertTrue( @@ -667,6 +699,7 @@ def testTransform( self ) : 0.00001 ) ) + self.assertEncapsulatedRendersSame( instancer ) instancer["scale"].setValue( "uniformScale" ) self.assertTrue( @@ -675,6 +708,168 @@ def testTransform( self ) : 0.00001 ) ) + self.assertEncapsulatedRendersSame( instancer ) + + def testAnimation( self ) : + + pointA = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ + imath.V3f( 4, 0, 0 ), imath.V3f( 6, 0, 0 ), imath.V3f( 8, 0, 0 ) + ] ) ) + objectToSceneA = GafferScene.ObjectToScene() + objectToSceneA["object"].setValue( pointA ) + + pointB = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ + imath.V3f( 7, 0, 0 ), imath.V3f( 9, 0, 0 ), imath.V3f( 11, 0, 0 ) + ] ) ) + objectToSceneB = GafferScene.ObjectToScene() + objectToSceneB["object"].setValue( pointB ) + + switch = Gaffer.Switch() + switch.setup( objectToSceneA["out"] ) + switch["expression"] = Gaffer.Expression() + switch["expression"].setExpression( 'parent["index"] = context.getFrame() > 0', "python" ) + switch["in"][0].setInput( objectToSceneA["out"] ) + switch["in"][1].setInput( objectToSceneB["out"] ) + + allFilter = GafferScene.PathFilter() + # It feels weird that we need to explicitly include /group/sphere here, when you + # should just be able to set attributes at the root locations and have them inherited, + # but we're working around the issue where attributes cannot be inherited from "outside" + # a Capsule, and attributes at the root of a Capsule are treated as outside it + allFilter["paths"].setValue( IECore.StringVectorData( [ "/*", "/group/sphere" ] ) ) + + pointsAttributes = GafferScene.StandardAttributes() + pointsAttributes["in"].setInput( switch["out"] ) + pointsAttributes["attributes"]["transformBlurSegments"]["value"].setValue( 4 ) + pointsAttributes["attributes"]["transformBlurSegments"]["enabled"].setValue( True ) + pointsAttributes["attributes"]["deformationBlurSegments"]["value"].setValue( 3 ) + pointsAttributes["attributes"]["deformationBlurSegments"]["enabled"].setValue( True ) + pointsAttributes["filter"].setInput( allFilter["out"] ) + + pointsOptions = GafferScene.StandardOptions() + pointsOptions["in"].setInput( pointsAttributes["out"] ) + pointsOptions["options"]["transformBlur"]["value"].setValue( True ) + pointsOptions["options"]["transformBlur"]["enabled"].setValue( True ) + pointsOptions["options"]["deformationBlur"]["value"].setValue( True ) + pointsOptions["options"]["deformationBlur"]["enabled"].setValue( True ) + + sphere = GafferScene.Sphere() + sphere["type"].setValue( GafferScene.Sphere.Type.Primitive ) + sphere["expression"] = Gaffer.Expression() + sphere["expression"].setExpression( """ +parent["transform"]["translate"] = context.getFrame() * imath.V3f( 0, 0, 5 ) +parent["radius"] = ( 2 + context.getFrame() ) * 15 +""" ) + + group = GafferScene.Group() + group["in"][0].setInput( sphere["out"] ) + group["expression"] = Gaffer.Expression() + group["expression"].setExpression( 'parent["transform"]["translate"] = context.getFrame() * imath.V3f( 0, 7, 0 )' ) + + prototypeAttributes = GafferScene.StandardAttributes() + prototypeAttributes["in"].setInput( group["out"] ) + prototypeAttributes["attributes"]["transformBlurSegments"]["value"].setValue( 4 ) + prototypeAttributes["attributes"]["transformBlurSegments"]["enabled"].setValue( True ) + prototypeAttributes["attributes"]["deformationBlurSegments"]["value"].setValue( 3 ) + prototypeAttributes["attributes"]["deformationBlurSegments"]["enabled"].setValue( True ) + prototypeAttributes["filter"].setInput( allFilter["out"] ) + + instancer = GafferScene.Instancer() + instancer["in"].setInput( pointsOptions["out"] ) + instancer["prototypes"].setInput( prototypeAttributes["out"] ) + instancer["parent"].setValue( "/object" ) + + testContext = Gaffer.Context() + testContext.setFrame( 0 ) + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) + controller = GafferScene.RenderController( instancer["out"], testContext, renderer ) + controller.setMinimumExpansionDepth( 1024 ) + controller.update() + + co = renderer.capturedObject( "/object/instances/group/1/sphere" ) + self.assertEqual( + [ i.translation() for i in co.capturedTransforms() ], + [ + imath.V3f(6, -1.75, -1.25), + imath.V3f(6, -0.875, -0.625), + imath.V3f(6, 0, 0), + imath.V3f(9, 0.875, 0.625), + imath.V3f(9, 1.75, 1.25) + ] + ) + self.assertEqual( [ i.radius() for i in co.capturedSamples() ], [26.25, 28.75, 31.25, 33.75] ) + + with testContext: + + self.assertEncapsulatedRendersSame( instancer ) + + # Throw a bit of rotation in the mix to make sure we're composing in the correct order when encapsulating + group["transform"]["rotate"].setValue( imath.V3f( 30, 30, 0 ) ) + + self.assertEncapsulatedRendersSame( instancer ) + + group["transform"]["rotate"].setValue( imath.V3f( 0 ) ) + + + # Remove the hierarchy from the prototype - this doesn't make things any harder for the non-encapsulated + # case, but it enables a special case in the encapsulated case for protypes without hierarchy + prototypeAttributes["in"].setInput( sphere["out"] ) + with testContext: + self.assertEncapsulatedRendersSame( instancer ) + # Restore hierarchy + prototypeAttributes["in"].setInput( group["out"] ) + + + # Now test the nastiest case: the assignment of ids to points changes during the shutter. + # It would quite possibly be better to just prohibit this ... but it would be a bit tricky to detect + # it and raise an exception in the non-encapsulated case, and we generally want parity between + # encapsulated and non-encapsulated. I think I probably want to remove this functionality in + # the name of simpler code, but we'll see what John thinks + pointB["instanceId"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.IntVectorData( [ 1, 2, 0 ] ) ) + objectToSceneB["object"].setValue( pointB ) + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) + controller = GafferScene.RenderController( instancer["out"], testContext, renderer ) + controller.setMinimumExpansionDepth( 1024 ) + controller.update() + + co = renderer.capturedObject( "/object/instances/group/1/sphere" ) + self.assertEqual( + [ i.translation() for i in co.capturedTransforms() ], + [ + imath.V3f(6, -1.75, -1.25), + imath.V3f(6, -0.875, -0.625), + imath.V3f(6, 0, 0), + imath.V3f(7, 0.875, 0.625), + imath.V3f(7, 1.75, 1.25) + ] + ) + + with testContext: + self.assertEncapsulatedRendersSame( instancer ) + + # Try a different frame, just in case + testContext.setFrame( 100 ) + with testContext: + self.assertEncapsulatedRendersSame( instancer ) + testContext.setFrame( 0 ) + + # Finally, the case where things must fail: if the point counts don't match across the shutter, + # we must get an exception, whether encapsulating or not. + + pointB = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( 0 ) ] * 2 ) ) + objectToSceneB["object"].setValue( pointB ) + + with testContext: + with self.assertRaisesRegex( RuntimeError, 'Instancer.out.transform : Instance id "2" is invalid, instancer produces only 2 children. Topology may have changed during shutter.' ): + self.assertScenesRenderSame( instancer["out"], instancer["out"], expandProcedurals = True, ignoreLinks = True ) + + instancer["encapsulateInstanceGroups"].setValue( True ) + + with testContext: + with self.assertRaisesRegex( RuntimeError, 'Instance id "2" is invalid, instancer produces only 2 children. Topology may have changed during shutter.' ): + self.assertScenesRenderSame( instancer["out"], instancer["out"], expandProcedurals = True, ignoreLinks = True ) def testIndexedRootsListWithEmptyList( self ) : @@ -717,6 +912,7 @@ def testIndexedRootsListWithEmptyList( self ) : self.assertEqual( instancer["out"].object( "/object/instances/cube/2" ), cube["out"].object( "/cube" ) ) self.assertSceneValid( instancer["out"] ) + self.assertEncapsulatedRendersSame( instancer ) def buildPrototypeRootsScript( self ) : @@ -764,6 +960,12 @@ def buildPrototypeRootsScript( self ) : script["group2"]["name"].setValue( "foo" ) script["group2"]["in"][0].setInput( script["group"]["out"] ) + # /foo/baseSphere - this won't get attributes assigned in testPrototypeAttributes, allowing us to + # test rendering with the attributes inherited from the base of the prototype + script["baseSphere"] = GafferScene.Sphere() + script["baseSphere"]["name"].setValue( "baseSphere" ) + script["group2"]["in"][1].setInput( script["baseSphere"]["out"] ) + # /bar/baz/cube script["cube"] = GafferScene.Cube() script["group3"] = GafferScene.Group() @@ -798,7 +1000,7 @@ def assertRootsMatchPrototypeSceneChildren( self, script ) : for i in [ "0", "3" ] : - self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}".format( i=i ) ), IECore.InternedStringVectorData( [ "bar" ] ) ) + self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}".format( i=i ) ), IECore.InternedStringVectorData( [ "bar", "baseSphere" ] ) ) self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}/bar".format( i=i ) ), IECore.InternedStringVectorData( [ "sphere" ] ) ) self.assertEqual( script["instancer"]["out"].object( "/object/instances/foo/{i}".format( i=i ) ), IECore.NullObject.defaultNullObject() ) @@ -828,7 +1030,7 @@ def assertSingleRoot( self, script ) : for i in [ "0", "1", "2", "3" ] : - self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}".format( i=i ) ), IECore.InternedStringVectorData( [ "bar" ] ) ) + self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}".format( i=i ) ), IECore.InternedStringVectorData( [ "bar", "baseSphere" ] ) ) self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}/bar".format( i=i ) ), IECore.InternedStringVectorData( [ "sphere" ] ) ) self.assertEqual( script["instancer"]["out"].object( "/object/instances/foo/{i}".format( i=i ) ), IECore.NullObject.defaultNullObject() ) @@ -887,7 +1089,7 @@ def assertSwappedRoots( self, script ) : for i in [ "1", "2" ] : - self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}".format( i=i ) ), IECore.InternedStringVectorData( [ "bar" ] ) ) + self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}".format( i=i ) ), IECore.InternedStringVectorData( [ "bar", "baseSphere" ] ) ) self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/foo/{i}/bar".format( i=i ) ), IECore.InternedStringVectorData( [ "sphere" ] ) ) self.assertEqual( script["instancer"]["out"].object( "/object/instances/foo/{i}".format( i=i ) ), IECore.NullObject.defaultNullObject() ) @@ -950,7 +1152,7 @@ def assertRootsToRoot( self, script ) : for i in [ "0", "1", "2", "3" ] : self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/root/{i}".format( i=i ) ), IECore.InternedStringVectorData( [ "foo", "bar" ] ) ) - self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/root/{i}/foo".format( i=i ) ), IECore.InternedStringVectorData( [ "bar" ] ) ) + self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/root/{i}/foo".format( i=i ) ), IECore.InternedStringVectorData( [ "bar", "baseSphere" ] ) ) self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/root/{i}/foo/bar".format( i=i ) ), IECore.InternedStringVectorData( [ "sphere" ] ) ) self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/root/{i}/bar".format( i=i ) ), IECore.InternedStringVectorData( [ "baz" ] ) ) self.assertEqual( script["instancer"]["out"].childNames( "/object/instances/root/{i}/bar/baz".format( i=i ) ), IECore.InternedStringVectorData( [ "cube" ] ) ) @@ -970,35 +1172,44 @@ def testIndexedRootsList( self ) : script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [] ) ) self.assertRootsMatchPrototypeSceneChildren( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "", ] ) ) self.assertUnderspecifiedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "/foo", ] ) ) self.assertSingleRoot( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # roots list matching the prototype root children # we expect the same results as without a roots list script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "/foo", "/bar" ] ) ) self.assertRootsMatchPrototypeSceneChildren( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "/foo/bar", "/bar" ] ) ) self.assertConflictingRootNames( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # opposite order to the prototype root children script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "/bar", "/foo" ] ) ) self.assertSwappedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "", "/bar" ] ) ) self.assertSkippedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # roots all the way to the leaf level of the prototype scene script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "/foo/bar/sphere", "/bar/baz/cube" ] ) ) self.assertRootsToLeaves( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # we can specify the root of the prototype scene script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "/" ] ) ) self.assertRootsToRoot( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["instancer"]["prototypeRootsList"].setValue( IECore.StringVectorData( [ "/foo", "/does/not/exist" ] ) ) self.assertRaisesRegex( @@ -1019,32 +1230,40 @@ def testIndexedRootsVariable( self ) : script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "", ] ) ) self.assertUnderspecifiedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "/foo", ] ) ) self.assertSingleRoot( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # roots list matching the prototype root children # we expect the same results as without a roots list script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "/foo", "/bar" ] ) ) self.assertRootsMatchPrototypeSceneChildren( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "/foo/bar", "/bar" ] ) ) self.assertConflictingRootNames( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # opposite order to the prototype root children script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "/bar", "/foo" ] ) ) self.assertSwappedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "", "/bar" ] ) ) self.assertSkippedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # roots all the way to the leaf level of the prototype scene script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "/foo/bar/sphere", "/bar/baz/cube" ] ) ) self.assertRootsToLeaves( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # we can specify the root of the prototype scene script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "/" ] ) ) self.assertRootsToRoot( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) script["variables"]["primitiveVariables"]["prototypeRoots"]["value"].setValue( IECore.StringVectorData( [ "/foo", "/does/not/exist" ] ) ) self.assertRaisesRegex( @@ -1080,33 +1299,42 @@ def updateRoots( roots, indices ) : updateRoots( IECore.StringVectorData( [ "", ] ), IECore.IntVectorData( [ 0, 0, 0, 0 ] ) ) self.assertUnderspecifiedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) updateRoots( IECore.StringVectorData( [ "/foo", ] ), IECore.IntVectorData( [ 0, 0, 0, 0 ] ) ) self.assertSingleRoot( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # roots list matching the prototype root children # we expect the same results as without a roots list updateRoots( IECore.StringVectorData( [ "/foo", "/bar" ] ), IECore.IntVectorData( [ 0, 1, 1, 0 ] ) ) self.assertRootsMatchPrototypeSceneChildren( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) updateRoots( IECore.StringVectorData( [ "/foo/bar", "/bar" ] ), IECore.IntVectorData( [ 0, 1, 1, 0 ] ) ) self.assertConflictingRootNames( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # opposite order to the prototype root children updateRoots( IECore.StringVectorData( [ "/bar", "/foo" ] ), IECore.IntVectorData( [ 0, 1, 1, 0 ] ) ) self.assertSwappedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) updateRoots( IECore.StringVectorData( [ "", "/bar" ] ), IECore.IntVectorData( [ 0, 1, 1, 0 ] ) ) self.assertSkippedRoots( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # roots all the way to the leaf level of the prototype scene updateRoots( IECore.StringVectorData( [ "/foo/bar/sphere", "/bar/baz/cube" ] ), IECore.IntVectorData( [ 0, 1, 1, 0 ] ) ) self.assertRootsToLeaves( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) # we can specify the root of the prototype scene updateRoots( IECore.StringVectorData( [ "/", ] ), IECore.IntVectorData( [ 0, 0, 0, 0 ] ) ) self.assertRootsToRoot( script ) + self.assertEncapsulatedRendersSame( script["instancer"] ) + updateRoots( IECore.StringVectorData( [ "/foo", "/does/not/exist" ] ), IECore.IntVectorData( [ 0, 1, 1, 0 ] ) ) self.assertRaisesRegex( Gaffer.ProcessException, '.*Prototype root "/does/not/exist" does not exist.*', @@ -1310,6 +1538,8 @@ def testIds( self ) : self.assertSceneValid( instancer["out"] ) + self.assertEncapsulatedRendersSame( instancer ) + def testNegativeIdsAndIndices( self ) : points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 0, 2 ) ] ) ) @@ -1353,33 +1583,75 @@ def testNegativeIdsAndIndices( self ) : self.assertSceneValid( instancer["out"] ) + self.assertEncapsulatedRendersSame( instancer ) + def testDuplicateIds( self ) : points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 6 ) ] ) ) points["id"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, - IECore.IntVectorData( [ 0, 0, 2, 2, 4, 4 ] ), + IECore.IntVectorData( [ 0, 1, 2, 2, 3, 4 ] ), ) objectToScene = GafferScene.ObjectToScene() objectToScene["object"].setValue( points ) sphere = GafferScene.Sphere() + parent = GafferScene.Parent() + parent["parent"].setValue( "/" ) + parent["in"].setInput( sphere["out"] ) + parent["children"][0].setInput( sphere["out"] ) instancer = GafferScene.Instancer() instancer["in"].setInput( objectToScene["out"] ) - instancer["prototypes"].setInput( sphere["out"] ) + instancer["prototypes"].setInput( parent["out"] ) instancer["parent"].setValue( "/object" ) instancer["id"].setValue( "id" ) + instancer["omitDuplicateIds"].setValue( False ) + + with self.assertRaisesRegex( RuntimeError, 'Instancer.__engine : Instance id "2" is duplicated at index 2 and 3. This probably indicates invalid source data, if you want to hack around it, you can set "omitDuplicateIds"' ) : + self.assertSceneValid( instancer["out"] ) + + instancer["omitDuplicateIds"].setValue( True ) self.assertSceneValid( instancer["out"] ) - self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "2", "4" ] ) ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "1", "3", "4" ] ) ) self.assertEqual( instancer["out"].transform( "/object/instances/sphere/0" ), imath.M44f().translate( imath.V3f( 0, 0, 0 ) ) ) - self.assertEqual( instancer["out"].transform( "/object/instances/sphere/2" ), imath.M44f().translate( imath.V3f( 2, 0, 0 ) ) ) - self.assertEqual( instancer["out"].transform( "/object/instances/sphere/4" ), imath.M44f().translate( imath.V3f( 4, 0, 0 ) ) ) + self.assertEqual( instancer["out"].transform( "/object/instances/sphere/1" ), imath.M44f().translate( imath.V3f( 1, 0, 0 ) ) ) + self.assertEqual( instancer["out"].transform( "/object/instances/sphere/3" ), imath.M44f().translate( imath.V3f( 4, 0, 0 ) ) ) + self.assertEqual( instancer["out"].transform( "/object/instances/sphere/4" ), imath.M44f().translate( imath.V3f( 5, 0, 0 ) ) ) + + self.assertEncapsulatedRendersSame( instancer ) + + instancer["prototypeIndex"].setValue( "prototypeIndex" ) + + # Test duplicate ids between different prototypes - the handling of this is now pretty consistent, but + # it used to be treated quite differently + points["prototypeIndex"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1, 0, 1, 0, 1 ] ), + ) + objectToScene["object"].setValue( points ) + + instancer["omitDuplicateIds"].setValue( False ) + + with self.assertRaisesRegex( RuntimeError, 'Instancer.__engine : Instance id "2" is duplicated at index 2 and 3. This probably indicates invalid source data, if you want to hack around it, you can set "omitDuplicateIds"' ) : + self.assertSceneValid( instancer["out"] ) + + instancer["omitDuplicateIds"].setValue( True ) + + self.assertEqual( instancer["out"].childNames( "/object/instances" ), IECore.InternedStringVectorData( [ "sphere", "sphere1" ] ) ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere" ), IECore.InternedStringVectorData( [ "0", "3" ] ) ) + self.assertEqual( instancer["out"].childNames( "/object/instances/sphere1" ), IECore.InternedStringVectorData( [ "1", "4" ] ) ) + + self.assertEqual( instancer["out"].transform( "/object/instances/sphere/0" ), imath.M44f().translate( imath.V3f( 0, 0, 0 ) ) ) + self.assertEqual( instancer["out"].transform( "/object/instances/sphere/3" ), imath.M44f().translate( imath.V3f( 4, 0, 0 ) ) ) + + self.assertEqual( instancer["out"].transform( "/object/instances/sphere1/1" ), imath.M44f().translate( imath.V3f( 1, 0, 0 ) ) ) + self.assertEqual( instancer["out"].transform( "/object/instances/sphere1/4" ), imath.M44f().translate( imath.V3f( 5, 0, 0 ) ) ) def testAttributes( self ) : @@ -1399,15 +1671,38 @@ def testAttributes( self ) : IECore.GeometricData.Interpretation.Point ), ) + points["prototypeAttr"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.FloatVectorData( [ 12, 13 ] ), + ) objectToScene = GafferScene.ObjectToScene() objectToScene["object"].setValue( points ) + pointsFilter = GafferScene.PathFilter() + pointsFilter["paths"].setValue( IECore.StringVectorData( [ '/object' ] ) ) + + pointsAttrs = GafferScene.CustomAttributes() + pointsAttrs["filter"].setInput( pointsFilter["out"] ) + pointsAttrs["in"].setInput( objectToScene["out"] ) + pointsAttrs["attributes"].addChild( Gaffer.NameValuePlug( "inheritedAttr", Gaffer.FloatPlug( "value", defaultValue = 7.0 ), True ) ) + pointsAttrs["attributes"].addChild( Gaffer.NameValuePlug( "testFloat", Gaffer.FloatPlug( "value", defaultValue = 7.0 ), True ) ) + pointsAttrs["attributes"].addChild( Gaffer.NameValuePlug( "prototypeAttr", Gaffer.FloatPlug( "value", defaultValue = -1 ), True ) ) + sphere = GafferScene.Sphere() + sphereFilter = GafferScene.PathFilter() + sphereFilter["paths"].setValue( IECore.StringVectorData( [ '/sphere' ] ) ) + + sphereAttributes = GafferScene.CustomAttributes() + sphereAttributes["in"].setInput( sphere["out"] ) + sphereAttributes["filter"].setInput( sphereFilter["out"] ) + sphereAttributes["attributes"].addChild( Gaffer.NameValuePlug( "prototypeAttr", Gaffer.FloatPlug( "value", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ), True ) ) + sphereAttributes["attributes"][0]["value"].setValue( 42.0 ) + instancer = GafferScene.Instancer() - instancer["in"].setInput( objectToScene["out"] ) - instancer["prototypes"].setInput( sphere["out"] ) + instancer["in"].setInput( pointsAttrs["out"] ) + instancer["prototypes"].setInput( sphereAttributes["out"] ) instancer["parent"].setValue( "/object" ) self.assertEqual( @@ -1422,9 +1717,11 @@ def testAttributes( self ) : self.assertEqual( instancer["out"].attributes( "/object/instances/sphere/0" ), - IECore.CompoundObject() + IECore.CompoundObject( { 'prototypeAttr' : IECore.FloatData( 42 ) } ) ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["attributes"].setValue( "testFloat testColor testPoint" ) self.assertEqual( @@ -1435,7 +1732,8 @@ def testAttributes( self ) : "testPoint" : IECore.V3fData( imath.V3f( 0 ), IECore.GeometricData.Interpretation.Point - ) + ), + 'prototypeAttr' : IECore.FloatData( 42 ) } ) ) @@ -1447,10 +1745,13 @@ def testAttributes( self ) : "testPoint" : IECore.V3fData( imath.V3f( 1 ), IECore.GeometricData.Interpretation.Point - ) + ), + 'prototypeAttr' : IECore.FloatData( 42 ) } ) ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["attributePrefix"].setValue( "user:" ) self.assertEqual( @@ -1461,7 +1762,8 @@ def testAttributes( self ) : "user:testPoint" : IECore.V3fData( imath.V3f( 0 ), IECore.GeometricData.Interpretation.Point - ) + ), + 'prototypeAttr' : IECore.FloatData( 42 ) } ) ) @@ -1473,10 +1775,13 @@ def testAttributes( self ) : "user:testPoint" : IECore.V3fData( imath.V3f( 1 ), IECore.GeometricData.Interpretation.Point - ) + ), + 'prototypeAttr' : IECore.FloatData( 42 ) } ) ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["attributePrefix"].setValue( "foo:" ) self.assertEqual( @@ -1487,7 +1792,8 @@ def testAttributes( self ) : "foo:testPoint" : IECore.V3fData( imath.V3f( 0 ), IECore.GeometricData.Interpretation.Point - ) + ), + 'prototypeAttr' : IECore.FloatData( 42 ) } ) ) @@ -1499,10 +1805,32 @@ def testAttributes( self ) : "foo:testPoint" : IECore.V3fData( imath.V3f( 1 ), IECore.GeometricData.Interpretation.Point - ) + ), + 'prototypeAttr' : IECore.FloatData( 42 ) + } ) + ) + + self.assertEncapsulatedRendersSame( instancer ) + + # Test that point attributes can override prototype attributes + instancer["attributePrefix"].setValue( "" ) + instancer["attributes"].setValue( "testFloat testColor testPoint prototypeAttr" ) + + self.assertEqual( + instancer["out"].attributes( "/object/instances/sphere/0" ), + IECore.CompoundObject( { + "testFloat" : IECore.FloatData( 0.0 ), + "testColor" : IECore.Color3fData( imath.Color3f( 1, 0, 0 ) ), + "testPoint" : IECore.V3fData( + imath.V3f( 0 ), + IECore.GeometricData.Interpretation.Point + ), + 'prototypeAttr' : IECore.FloatData( 12 ) } ) ) + self.assertEncapsulatedRendersSame( instancer ) + def testEmptyAttributesHaveConstantHash( self ) : points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 0, 2 ) ] ) ) @@ -1580,6 +1908,8 @@ def testEditAttributes( self ) : } ) ) + self.assertEncapsulatedRendersSame( instancer ) + def testPrototypeAttributes( self ) : script = self.buildPrototypeRootsScript() @@ -1625,6 +1955,8 @@ def testPrototypeAttributes( self ) : self.assertSceneValid( script["instancer"]["out"] ) + self.assertEncapsulatedRendersSame( script["instancer"] ) + def testUnconnectedInstanceInput( self ) : plane = GafferScene.Plane() @@ -1698,10 +2030,10 @@ def testSetPassThroughs( self ) : def testContexts( self ): points = IECoreScene.PointsPrimitive( - IECore.V3fVectorData( - [ imath.V3f( i, 0, 0 ) for i in range( 100 ) ] - ) - ) + IECore.V3fVectorData( + [ imath.V3f( i, 0, 0 ) for i in range( 100 ) ] + ) + ) points["floatVar"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.FloatVectorData( [ 2 * math.sin( i ) for i in range( 100 ) ] @@ -1756,6 +2088,10 @@ def testContexts( self ): customAttributes["attributes"].addChild( Gaffer.NameValuePlug( "seedAttr", Gaffer.IntPlug( "value", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), True, "member8" ) ) customAttributes["attributes"].addChild( Gaffer.NameValuePlug( "frameAttr", Gaffer.FloatPlug( "value", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), True, "member9" ) ) + + # NOTE : It kinda feels like we ought to be able to omit the default values, and just use + # context["floatVar"] - but this fails because for some evaluations ( like deciding the bounding box + # of the whole output group ), we use the bound of the prototype with the context variable unset customAttributes["ReadContextExpression"] = Gaffer.Expression() customAttributes["ReadContextExpression"].setExpression( inspect.cleandoc( """ @@ -1838,6 +2174,8 @@ def quant( x, q ): self.assertEqual( childNameStrings( "points/instances/plane" ), [] ) self.assertEqual( childNameStrings( "points/instances/sphere" ), [] ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["prototypeMode"].setValue( GafferScene.Instancer.PrototypeMode.RootPerVertex ) instancer["prototypeRoots"].setValue( "indexedRoots" ) self.assertEqual( uniqueCounts(), { "" : 3 } ) @@ -1845,6 +2183,8 @@ def quant( x, q ): self.assertEqual( childNameStrings( "points/instances/plane" ), [ str(i) for i in range( 34, 68 ) ] ) self.assertEqual( childNameStrings( "points/instances/sphere" ), [ str(i) for i in range( 68, 100 ) ] ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["prototypeRoots"].setValue( "unindexedRoots" ) """ # How things should work @@ -1859,6 +2199,8 @@ def quant( x, q ): self.assertEqual( childNameStrings( "points/instances/plane" ), [] ) self.assertEqual( childNameStrings( "points/instances/sphere" ), [] ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["prototypeMode"].setValue( GafferScene.Instancer.PrototypeMode.IndexedRootsList ) instancer["prototypeIndex"].setValue( 'intVar' ) @@ -1868,6 +2210,8 @@ def quant( x, q ): self.assertEqual( childNameStrings( "points/instances/plane" ), [ str(i) for i in range( 2, 100, 4 ) ] ) self.assertEqual( childNameStrings( "points/instances/sphere" ), [ str(i) for i in range( 3, 100, 4 ) ] ) + self.assertEncapsulatedRendersSame( instancer ) + # No context overrides yet testAttributes( frameAttr = [ 1 ] * 25 ) @@ -1880,11 +2224,15 @@ def quant( x, q ): # Check both the global unique count, and the per-context variable unique counts self.assertEqual( uniqueCounts(), { "" : 100, "floatVar" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + # With massive quantization, all values collapse instancer["contextVariables"][0]["quantize"].setValue( 100 ) testAttributes( frameAttr = [ 1 ] * 25, floatAttr = [ 0 for i in range(0, 100, 4) ] ) self.assertEqual( uniqueCounts(), { "" : 4, "floatVar" : 1 } ) + self.assertEncapsulatedRendersSame( instancer ) + # With moderate quantization, we can see how different prototypes combine with the contexts to produce # more unique values instancer["contextVariables"][0]["quantize"].setValue( 1 ) @@ -1892,14 +2240,20 @@ def quant( x, q ): testAttributes( frameAttr = [ 1 ] * 25, floatAttr = floatExpected ) self.assertEqual( uniqueCounts(), { "" : 20, "floatVar" : 5 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["prototypeRootsList"].setValue( IECore.StringVectorData( [ "withAttrs", "cube", "plane", "sphere" ] ) ) testAttributes( frameAttr = [ 1 ] * 25, floatAttr = floatExpected ) self.assertEqual( uniqueCounts(), { "" : 20, "floatVar" : 5 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Test an empty root instancer["prototypeRootsList"].setValue( IECore.StringVectorData( [ "withAttrs", "", "plane", "sphere" ] ) ) self.assertEqual( uniqueCounts(), { "" : 15, "floatVar" : 5 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Now lets just focus on context variation instancer["prototypeRootsList"].setValue( IECore.StringVectorData( [] ) ) instancer["prototypeIndex"].setValue( '' ) @@ -1907,6 +2261,8 @@ def quant( x, q ): testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected ) self.assertEqual( uniqueCounts(), { "" : 5, "floatVar" : 5 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Add a second context variation instancer["contextVariables"].addChild( GafferScene.Instancer.ContextVariablePlug( "context" ) ) instancer["contextVariables"][1]["name"].setValue( "vectorVar" ) @@ -1917,12 +2273,16 @@ def quant( x, q ): ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "vectorVar" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["quantize"].setValue( 10 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, vectorAttr = [ imath.V3f( quant( i + 2, 10 ), quant( i + 3, 10 ), quant( i + 4, 10 ) ) for i in range(0, 100) ] ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "vectorVar" : 31, "" : 64 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Try all the different types instancer["contextVariables"][1]["name"].setValue( "uvVar" ) instancer["contextVariables"][1]["quantize"].setValue( 0 ) @@ -1932,12 +2292,16 @@ def quant( x, q ): ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "uvVar" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["quantize"].setValue( 1 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, uvAttr = [ imath.V2f( compatRound( i * 0.01 ), compatRound( i * 0.02 ) ) for i in range(0, 100) ] ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "uvVar" : 4, "" : 20 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["name"].setValue( "intVar" ) instancer["contextVariables"][1]["quantize"].setValue( 0 ) @@ -1947,12 +2311,16 @@ def quant( x, q ): ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "intVar" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["quantize"].setValue( 10 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, intAttr = [ quant( i, 10 ) for i in range(0, 100) ] ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "intVar" : 11, "" : 48 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["name"].setValue( "stringVar" ) instancer["contextVariables"][1]["quantize"].setValue( 0 ) @@ -1962,13 +2330,15 @@ def quant( x, q ): ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "stringVar" : 3, "" : 15 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["quantize"].setValue( 10 ) self.assertRaisesRegex( - Gaffer.ProcessException, 'Instancer.out.attributes : Context variable "0" : cannot quantize variable of type StringVectorData', + Gaffer.ProcessException, 'Instancer.out.attributes : Context variable "stringVar" : cannot quantize variable of type StringVectorData', instancer['out'].attributes, "points/instances/withAttrs/0/sphere" ) self.assertRaisesRegex( - Gaffer.ProcessException, 'Instancer.variations : Context variable "0" : cannot quantize variable of type StringVectorData', + Gaffer.ProcessException, 'Instancer.variations : Context variable "stringVar" : cannot quantize variable of type StringVectorData', uniqueCounts ) @@ -1981,12 +2351,16 @@ def quant( x, q ): ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "colorVar" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["quantize"].setValue( 1 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, colorAttr = [ imath.Color3f( compatRound( i * 0.1 + 2 ), compatRound( i * 0.1 + 3 ), compatRound( i * 0.1 + 4 ) ) for i in range(0, 100) ] ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "colorVar" : 11, "" : 48 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["name"].setValue( "color4fVar" ) instancer["contextVariables"][1]["quantize"].setValue( 0 ) @@ -1995,29 +2369,39 @@ def quant( x, q ): ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][1]["quantize"].setValue( 1 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, color4fAttr = [ imath.Color4f( compatRound( i * 0.1 + 2 ), compatRound( i * 0.1 + 3 ), compatRound( i * 0.1 + 4 ), compatRound( i * 0.1 + 5 ) ) for i in range(0, 100) ] ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 11, "" : 48 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Set a high quantize so we can see how these variations interact with other types of variations instancer["contextVariables"][1]["quantize"].setValue( 10 ) color4fExpected = [ imath.Color4f( quant( i * 0.1 + 2, 10 ), quant( i * 0.1 + 3, 10 ), quant( i * 0.1 + 4, 10 ), quant( i * 0.1 + 5, 10 ) ) for i in range(0, 100) ] testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, color4fAttr = color4fExpected ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "" : 20 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["seedEnabled"].setValue( True ) instancer["rawSeed"].setValue( True ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, color4fAttr = color4fExpected, seedAttr = list( range( 100 ) ) ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["rawSeed"].setValue( False ) instancer["seeds"].setValue( 10 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, color4fAttr = color4fExpected, seedAttr_seedCount = 10 ) initialFirstVal = instancer['out'].attributes( '/points/instances/withAttrs/0/sphere' )["seedAttr"] self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 10, "" : 67 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Changing the seed changes individual values, but not the overall behaviour instancer["seedPermutation"].setValue( 1 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, color4fAttr = color4fExpected, seedAttr_seedCount = 10 ) @@ -2025,11 +2409,15 @@ def quant( x, q ): # Total variation count is a bit different because the different variation sources line up differently self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 10, "" : 69 } ) + self.assertEncapsulatedRendersSame( instancer ) + # If we generate 100 seeds from 100 ids, we will get many collisions, and only 67 unique values instancer["seeds"].setValue( 100 ) testAttributes( frameAttr = [ 1 ] * 100, floatAttr = floatExpected, color4fAttr = color4fExpected, seedAttr_seedCount = 67 ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 67, "" : 94 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Now turn on time offset as well and play with everything together instancer["seeds"].setValue( 10 ) instancer["timeOffset"]["enabled"].setValue( True ) @@ -2038,6 +2426,8 @@ def quant( x, q ): testAttributes( frameAttr = [ 1 + 2 * math.sin( i ) for i in range(0, 100) ], floatAttr = floatExpected, color4fAttr = color4fExpected, seedAttr_seedCount = 10 ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 10, "frame" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["timeOffset"]["quantize"].setValue( 0.5 ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 10, "frame" : 9, "" : 82 } ) @@ -2049,45 +2439,55 @@ def quant( x, q ): with c: testAttributes( frameAttr = [ i + 42 for i in floatExpected ], floatAttr = floatExpected, color4fAttr = color4fExpected, seedAttr_seedCount = 10 ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "seed" : 10, "frame" : 5, "" : 69 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Now reduce back down the variations to test different cumulative combinations instancer["seedEnabled"].setValue( False ) testAttributes( frameAttr = [ i + 1 for i in floatExpected ], floatAttr = floatExpected, color4fAttr = color4fExpected ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "color4fVar" : 4, "frame" : 5, "" : 20 } ) + self.assertEncapsulatedRendersSame( instancer ) + # With just one context var, driven by the same prim var as frame, with the same quantization, # the variations don't multiply del instancer["contextVariables"][1] testAttributes( frameAttr = [ i + 1 for i in floatExpected ], floatAttr = floatExpected ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "frame" : 5, "" : 5 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Using a different source primVar means the variations will multiply instancer["timeOffset"]["name"].setValue( 'intVar' ) instancer["timeOffset"]["quantize"].setValue( 0 ) testAttributes( frameAttr = [ i + 1 for i in range(100) ], floatAttr = floatExpected ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "frame" : 100, "" : 100 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["timeOffset"]["quantize"].setValue( 20 ) testAttributes( frameAttr = [ ((i+10)//20)*20 + 1 for i in range(100) ], floatAttr = floatExpected ) self.assertEqual( uniqueCounts(), { "floatVar" : 5, "frame" : 6, "" : 30 } ) + self.assertEncapsulatedRendersSame( instancer ) - # Test with multiple point sources - pointsMerge = GafferScene.Parent() - pointsMerge["parent"].setValue( '/' ) + # Test with multiple point sources pointSources = [] for j in range( 3 ): points = IECoreScene.PointsPrimitive( - IECore.V3fVectorData( - [ imath.V3f( i, 0, 0 ) for i in range( 10 ) ] - ) - ) + IECore.V3fVectorData( + [ imath.V3f( i, 0, 0 ) for i in range( 10 ) ] + ) + ) - points["floatVar"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.FloatVectorData( - [ i * 0.1 + j for i in range( 10 ) ] - ) ) + points["floatVar"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.FloatVectorData( + [ i * 0.1 + j for i in range( 10 ) ] + ) + ) pointSources.append( GafferScene.ObjectToScene() ) pointSources[-1]["name"].setValue( "points" ) pointSources[-1]["object"].setValue( points ) @@ -2102,12 +2502,16 @@ def quant( x, q ): self.assertAlmostEqual( instancer['out'].attributes( "points2/instances/withAttrs/5/sphere" )["floatAttr"].value, 2.5 ) self.assertEqual( uniqueCounts(), { "floatVar" : 30, "" : 30 } ) + self.assertEncapsulatedRendersSame( instancer ) + instancer["contextVariables"][0]["quantize"].setValue( 0.2001 ) self.assertAlmostEqual( instancer['out'].attributes( "points/instances/withAttrs/2/sphere" )["floatAttr"].value, 0.2001, places = 6 ) self.assertAlmostEqual( instancer['out'].attributes( "points1/instances/withAttrs/3/sphere" )["floatAttr"].value, 1.2006, places = 6 ) self.assertAlmostEqual( instancer['out'].attributes( "points2/instances/withAttrs/5/sphere" )["floatAttr"].value, 2.4012, places = 6 ) self.assertEqual( uniqueCounts(), { "floatVar" : 15, "" : 15 } ) + self.assertEncapsulatedRendersSame( instancer ) + # Test invalid location for func in [ instancer["out"].object, instancer["out"].childNames, instancer["out"].bound, instancer["out"].transform ]: @@ -2229,6 +2633,58 @@ def testRootPerVertexWithEmptyPoints( self ) : self.assertEqual( instancer["out"].childNames( "/points/instances" ), IECore.InternedStringVectorData() ) self.assertSceneValid( instancer["out"] ) + def testPurpose( self ): + + points = IECoreScene.PointsPrimitive( + IECore.V3fVectorData( + [ imath.V3f( i, 0, 0 ) for i in range( 100 ) ] + ) + ) + + points["intVar"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( + [ i for i in range( 100 ) ] + ) + ) + pointsSource = GafferScene.ObjectToScene() + pointsSource["name"].setValue( "points" ) + pointsSource["object"].setValue( points ) + + purposeOption = GafferScene.StandardOptions() + purposeOption["in"].setInput( pointsSource["out"] ) + purposeOption['options']['includedPurposes']["value"].setValue( IECore.StringVectorData( [ "default" ] ) ) + purposeOption['options']['includedPurposes']["enabled"].setValue( True ) + + sphere = GafferScene.Sphere() + + sphereFilter = GafferScene.PathFilter() + sphereFilter["paths"].setValue( IECore.StringVectorData( [ '/*' ] ) ) + + purposeAttr = GafferScene.CustomAttributes() + purposeAttr["in"].setInput( sphere["out"] ) + purposeAttr["filter"].setInput( sphereFilter["out"] ) + purposeAttr["attributes"].addChild( Gaffer.NameValuePlug( "usd:purpose", "default" ) ) + purposeAttr["expression"] = Gaffer.Expression() + purposeAttr["expression"].setExpression( 'parent["attributes"]["NameValuePlug"]["value"] = "default" if context.get("intVar", 1 ) % 2 else "proxy"', "python" ) + + pointsFilter = GafferScene.PathFilter() + pointsFilter["paths"].setValue( IECore.StringVectorData( [ '/points' ] ) ) + + instancer = GafferScene.Instancer() + instancer["in"].setInput( purposeOption["out"] ) + instancer["filter"].setInput( pointsFilter["out"] ) + instancer["prototypes"].setInput( purposeAttr["out"] ) + + instancer["contextVariables"].addChild( GafferScene.Instancer.ContextVariablePlug( "context" ) ) + instancer["contextVariables"][0]["name"].setValue( "intVar" ) + instancer["contextVariables"][0]["quantize"].setValue( 0 ) + + # We don't need to do anything special with purpose if not encapsulated - it just uses the standard + # renderer code. But when encapsulated, we have to handle it ourselves, so we just check that the + # encapsulated matches the non-encapsulated + self.assertEncapsulatedRendersSame( instancer ) + def testNoScenePathInPrototypeSetContext( self ) : plane = GafferScene.Plane() @@ -2420,25 +2876,127 @@ def testPrototypePropertiesAffectCapsule( self ) : ) @unittest.skipIf( GafferTest.inCI(), "Performance not relevant on CI platform" ) + @GafferTest.TestRunner.CategorisedTestMethod( { "expensivePerformance" } ) @GafferTest.TestRunner.PerformanceTestMethod() def testContextSetPerfNoVariationsSingleEvaluate( self ): self.runTestContextSetPerf( False, False ) @unittest.skipIf( GafferTest.inCI(), "Performance not relevant on CI platform" ) + @GafferTest.TestRunner.CategorisedTestMethod( { "expensivePerformance" } ) @GafferTest.TestRunner.PerformanceTestMethod() def testContextSetPerfNoVariationsParallelEvaluate( self ): self.runTestContextSetPerf( False, True ) @unittest.skipIf( GafferTest.inCI(), "Performance not relevant on CI platform" ) + @GafferTest.TestRunner.CategorisedTestMethod( { "expensivePerformance" } ) @GafferTest.TestRunner.PerformanceTestMethod() def testContextSetPerfWithVariationsSingleEvaluate( self ): self.runTestContextSetPerf( True, False ) @unittest.skipIf( GafferTest.inCI(), "Performance not relevant on CI platform" ) + @GafferTest.TestRunner.CategorisedTestMethod( { "expensivePerformance" } ) @GafferTest.TestRunner.PerformanceTestMethod() def testContextSetPerfWithVariationsParallelEvaluate( self ): self.runTestContextSetPerf( True, True ) + def initSimpleInstancer( self, withPrototypes = False, withIds = False ): + mesh = IECoreScene.MeshPrimitive.createPlane( + imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ), + imath.V2i( 2499 ) + ) + + if withPrototypes: + mesh["index"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1 ] * 3125000 ), + ) + + if withIds: + mesh["instanceId"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ i for i in range( 6250000 ) ] ), + ) + + nodes = {} + nodes["meshSource"] = GafferScene.ObjectToScene() + nodes["meshSource"]["name"].setValue( "plane" ) + nodes["meshSource"]["object"].setValue( mesh ) + + # Pre-evaluate the big object, so we just measure the instancer time + nodes["meshSource"]["out"].object( "/plane" ) + + nodes["sphere"] = GafferScene.Sphere() + nodes["cube"] = GafferScene.Cube() + + nodes["parent"] = GafferScene.Parent() + nodes["parent"]["parent"].setValue( '/' ) + nodes["parent"]["in"].setInput( nodes["sphere"]["out"] ) + nodes["parent"]["children"][0].setInput( nodes["cube"]["out"] ) + + + # Instancer + nodes["instancerFilter"] = GafferScene.PathFilter() + nodes["instancerFilter"]["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) ) + + nodes["instancer"] = GafferScene.Instancer() + nodes["instancer"]["in"].setInput( nodes["meshSource"]["out"] ) + nodes["instancer"]["filter"].setInput( nodes["instancerFilter"]["out"] ) + nodes["instancer"]["prototypes"].setInput( nodes["parent"]["out"] ) + if withPrototypes: + nodes["instancer"]["prototypeIndex"].setValue( "index" ) + if withIds: + nodes["instancer"]["id"].setValue( "instanceId" ) + + return nodes + + @GafferTest.TestRunner.PerformanceTestMethod() + def testEngineDataPerf( self ): + Gaffer.ValuePlug.clearCache() + Gaffer.ValuePlug.clearHashCache( True ) + nodes = self.initSimpleInstancer() + with GafferTest.TestRunner.PerformanceScope() : + self.assertEqual( nodes["instancer"]["out"].childNames( "/plane/instances" ), IECore.InternedStringVectorData( [ "sphere", "cube" ] ) ) + + + @GafferTest.TestRunner.PerformanceTestMethod() + def testChildNamesPerf( self ): + nodes = self.initSimpleInstancer() + with GafferTest.TestRunner.PerformanceScope() : + nodes["instancer"]["out"].childNames( "/plane/instances/sphere" ) + nodes["instancer"]["out"].childNames( "/plane/instances/cube" ) + + @GafferTest.TestRunner.PerformanceTestMethod() + def testEngineDataPerfWithPrototypes( self ): + Gaffer.ValuePlug.clearCache() + Gaffer.ValuePlug.clearHashCache( True ) + nodes = self.initSimpleInstancer( withPrototypes = True ) + with GafferTest.TestRunner.PerformanceScope() : + self.assertEqual( nodes["instancer"]["out"].childNames( "/plane/instances" ), IECore.InternedStringVectorData( [ "sphere", "cube" ] ) ) + + + @GafferTest.TestRunner.PerformanceTestMethod() + def testChildNamesPerfWithPrototypes( self ): + nodes = self.initSimpleInstancer( withPrototypes = True ) + with GafferTest.TestRunner.PerformanceScope() : + nodes["instancer"]["out"].childNames( "/plane/instances/sphere" ) + nodes["instancer"]["out"].childNames( "/plane/instances/cube" ) + + @GafferTest.TestRunner.PerformanceTestMethod() + def testEngineDataPerfWithPrototypesAndIds( self ): + Gaffer.ValuePlug.clearCache() + Gaffer.ValuePlug.clearHashCache( True ) + nodes = self.initSimpleInstancer( withPrototypes = True, withIds = True ) + with GafferTest.TestRunner.PerformanceScope() : + self.assertEqual( nodes["instancer"]["out"].childNames( "/plane/instances" ), IECore.InternedStringVectorData( [ "sphere", "cube" ] ) ) + + + @GafferTest.TestRunner.PerformanceTestMethod() + def testChildNamesPerfWithPrototypesAndIds( self ): + nodes = self.initSimpleInstancer( withPrototypes = True, withIds = True ) + with GafferTest.TestRunner.PerformanceScope() : + nodes["instancer"]["out"].childNames( "/plane/instances/sphere" ) + nodes["instancer"]["out"].childNames( "/plane/instances/cube" ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferSceneTest/SceneTestCase.py b/python/GafferSceneTest/SceneTestCase.py index d7939a8b266..366b890aea1 100644 --- a/python/GafferSceneTest/SceneTestCase.py +++ b/python/GafferSceneTest/SceneTestCase.py @@ -413,12 +413,12 @@ def assertScenesRenderSame( plugA, plugB, expandProcedurals = False, ignoreLinks # which should have the same effect, but is a separate code path. rendererA = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) - controllerA = GafferScene.RenderController( plugA, Gaffer.Context(), rendererA ) + controllerA = GafferScene.RenderController( plugA, Gaffer.Context.current(), rendererA ) controllerA.setMinimumExpansionDepth( 1024 ) controllerA.update() rendererB = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) - controllerB = GafferScene.RenderController( plugB, Gaffer.Context(), rendererB ) + controllerB = GafferScene.RenderController( plugB, Gaffer.Context.current(), rendererB ) controllerB.setMinimumExpansionDepth( 1024 ) controllerB.update() diff --git a/python/GafferSceneUI/InstancerUI.py b/python/GafferSceneUI/InstancerUI.py index 8efb5de64bc..b9a3c27abdf 100644 --- a/python/GafferSceneUI/InstancerUI.py +++ b/python/GafferSceneUI/InstancerUI.py @@ -286,50 +286,50 @@ def __init__( self, headings, toolTipOverride = "" ) : "layout:customWidget:seedColumnHeadings:widgetType", "GafferSceneUI.InstancerUI._SeedColumnHeadings", "layout:customWidget:seedColumnHeadings:section", "Context Variations", - "layout:customWidget:seedColumnHeadings:index", 18, + "layout:customWidget:seedColumnHeadings:index", 19, "layout:customWidget:idContextCountSpacer:widgetType", "GafferSceneUI.InstancerUI._SeedCountSpacer", "layout:customWidget:idContextCountSpacer:section", "Context Variations", - "layout:customWidget:idContextCountSpacer:index", 19, + "layout:customWidget:idContextCountSpacer:index", 20, "layout:customWidget:idContextCountSpacer:accessory", True, "layout:customWidget:idContextCount:widgetType", "GafferSceneUI.InstancerUI._SeedCountWidget", "layout:customWidget:idContextCount:section", "Context Variations", - "layout:customWidget:idContextCount:index", 19, + "layout:customWidget:idContextCount:index", 20, "layout:customWidget:idContextCount:accessory", True, "layout:customWidget:seedVariableSpacer:widgetType", "GafferSceneUI.InstancerUI._VariationSpacer", "layout:customWidget:seedVariableSpacer:section", "Context Variations", - "layout:customWidget:seedVariableSpacer:index", 20, + "layout:customWidget:seedVariableSpacer:index", 21, "layout:customWidget:seedVariableSpacer:accessory", True, "layout:customWidget:seedsSpacer:widgetType", "GafferSceneUI.InstancerUI._VariationSpacer", "layout:customWidget:seedsSpacer:section", "Context Variations", - "layout:customWidget:seedsSpacer:index", 21, + "layout:customWidget:seedsSpacer:index", 22, "layout:customWidget:seedsSpacer:accessory", True, "layout:customWidget:seedPermutationSpacer:widgetType", "GafferSceneUI.InstancerUI._VariationSpacer", "layout:customWidget:seedPermutationSpacer:section", "Context Variations", - "layout:customWidget:seedPermutationSpacer:index", 22, + "layout:customWidget:seedPermutationSpacer:index", 23, "layout:customWidget:seedPermutationSpacer:accessory", True, "layout:customWidget:seedSpacer:widgetType", "GafferSceneUI.InstancerUI._SectionSpacer", "layout:customWidget:seedSpacer:section", "Context Variations", - "layout:customWidget:seedSpacer:index", 23, + "layout:customWidget:seedSpacer:index", 24, "layout:customWidget:timeOffsetHeadings:widgetType", "GafferSceneUI.InstancerUI._TimeOffsetColumnHeadings", "layout:customWidget:timeOffsetHeadings:section", "Context Variations", - "layout:customWidget:timeOffsetHeadings:index", 24, + "layout:customWidget:timeOffsetHeadings:index", 25, "layout:customWidget:timeOffsetHeadings:description", "Testing description", "layout:customWidget:timeOffsetSpacer:widgetType", "GafferSceneUI.InstancerUI._SectionSpacer", "layout:customWidget:timeOffsetSpacer:section", "Context Variations", - "layout:customWidget:timeOffsetSpacer:index", 25, + "layout:customWidget:timeOffsetSpacer:index", 26, "layout:customWidget:timeOffsetSpacer:divider", True, "layout:customWidget:totalSpacer:widgetType", "GafferSceneUI.InstancerUI._SectionSpacer", "layout:customWidget:totalSpacer:section", "Context Variations", - "layout:customWidget:totalSpacer:index", 26, + "layout:customWidget:totalSpacer:index", 27, plugs = { @@ -483,6 +483,21 @@ def __init__( self, headings, toolTipOverride = "" ) : ], + "omitDuplicateIds" : [ + + "description", + """ + When off, having the same ids on multiple points is considered + an error. Setting on will allow a render to proceed, with all + instances that share an id being omitted. + """, + + "layout:section", "Settings.General", + + "userDefault", False, + + ], + "position" : [ "description", diff --git a/python/GafferSceneUI/SceneViewUI.py b/python/GafferSceneUI/SceneViewUI.py index 485700c7f0e..04a5834d69f 100644 --- a/python/GafferSceneUI/SceneViewUI.py +++ b/python/GafferSceneUI/SceneViewUI.py @@ -604,6 +604,15 @@ def _leafTypes( typeId ) : typeId = IECore.RunTimeTyped.typeIdFromTypeName( typeId ) derivedTypes = IECore.RunTimeTyped.derivedTypeIds( typeId ) + + # By "leaf" we really mean "derived enough to appear in the Selection Mask + # menu". So we must pretend that the private InstancerCapsule subclass of + # Capsule doesn't exist. + ## \todo No doubt this could be expressed more naturally somehow, perhaps + # just with a set union of `derivedTypes` and `typesWeUseInTheMenu`. + instancerCapsuleTypeId = IECore.RunTimeTyped.typeIdFromTypeName( "InstancerCapsule" ) + derivedTypes = [ t for t in derivedTypes if t != instancerCapsuleTypeId ] + if derivedTypes : return set().union( *[ _leafTypes( t ) for t in derivedTypes ] ) else : diff --git a/src/GafferScene/Capsule.cpp b/src/GafferScene/Capsule.cpp index 8ef665ed3e3..e6a7116a71b 100644 --- a/src/GafferScene/Capsule.cpp +++ b/src/GafferScene/Capsule.cpp @@ -172,13 +172,9 @@ void Capsule::render( IECoreScenePreview::Renderer *renderer ) const { throwIfNoScene(); ScenePlug::GlobalScope scope( m_context.get() ); - std::optional renderOptions = getRenderOptions(); - if( !renderOptions ) - { - renderOptions = GafferScene::Private::RendererAlgo::RenderOptions( m_scene ); - } + const GafferScene::Private::RendererAlgo::RenderOptions renderOpts = renderOptions(); GafferScene::Private::RendererAlgo::RenderSets renderSets( m_scene ); - GafferScene::Private::RendererAlgo::outputObjects( m_scene, *renderOptions, renderSets, /* lightLinks = */ nullptr, renderer, m_root ); + GafferScene::Private::RendererAlgo::outputObjects( m_scene, renderOpts, renderSets, /* lightLinks = */ nullptr, renderer, m_root ); } const ScenePlug *Capsule::scene() const @@ -219,6 +215,19 @@ std::optional Capsule::getRen return std::nullopt; } +GafferScene::Private::RendererAlgo::RenderOptions Capsule::renderOptions() const +{ + std::optional renderOptions = getRenderOptions(); + if( renderOptions ) + { + return *renderOptions; + } + else + { + return GafferScene::Private::RendererAlgo::RenderOptions( m_scene ); + } +} + void Capsule::throwIfNoScene() const { if( !m_scene ) diff --git a/src/GafferScene/Instancer.cpp b/src/GafferScene/Instancer.cpp index 8698da4d0c5..0303464cb1e 100644 --- a/src/GafferScene/Instancer.cpp +++ b/src/GafferScene/Instancer.cpp @@ -41,9 +41,12 @@ #include "GafferScene/SceneAlgo.h" #include "GafferScene/Private/ChildNamesMap.h" +#include "GafferScene/Private/RendererAlgo.h" +#include "GafferScene/Private/IECoreScenePreview/Renderer.h" #include "Gaffer/Context.h" #include "Gaffer/StringPlug.h" +#include "Gaffer/Private/IECorePreview/LRUCache.h" #include "IECoreScene/Primitive.h" @@ -303,11 +306,12 @@ class Instancer::EngineData : public Data EngineData( ConstPrimitivePtr primitive, PrototypeMode mode, - const std::string &index, + const std::string &prototypeIndexName, const std::string &rootsVariable, const StringVectorData *rootsList, const ScenePlug *prototypes, - const std::string &id, + const std::string &idName, + bool omitDuplicateIds, const std::string &position, const std::string &orientation, const std::string &scale, @@ -318,7 +322,7 @@ class Instancer::EngineData : public Data : m_primitive( primitive ), m_numPrototypes( 0 ), m_numValidPrototypes( 0 ), - m_indices( nullptr ), + m_prototypeIndices( nullptr ), m_ids( nullptr ), m_positions( nullptr ), m_orientations( nullptr ), @@ -331,14 +335,14 @@ class Instancer::EngineData : public Data return; } - initPrototypes( mode, index, rootsVariable, rootsList, prototypes ); + initPrototypes( mode, prototypeIndexName, rootsVariable, rootsList, prototypes ); - if( const IntVectorData *ids = m_primitive->variableData( id ) ) + if( const IntVectorData *ids = m_primitive->variableData( idName ) ) { m_ids = &ids->readable(); if( m_ids->size() != numPoints() ) { - throw IECore::Exception( fmt::format( "Id primitive variable \"{}\" has incorrect size", id ) ); + throw IECore::Exception( fmt::format( "Id primitive variable \"{}\" has incorrect size", idName ) ); } } @@ -377,13 +381,26 @@ class Instancer::EngineData : public Data } } + bool hasDuplicates = false; if( m_ids ) { - for( size_t i = 0; isecond = std::numeric_limits::max(); + hasDuplicates = true; + } } } @@ -403,6 +420,91 @@ class Instancer::EngineData : public Data throw IECore::Exception( fmt::format( "Context primitive variable for \"{}\" is not a correctly sized Vertex primitive variable", v.name.string() ) ); } } + + if( !m_numValidPrototypes ) + { + // We don't need to build m_pointIndicesForPrototype if we're not outputting any prototypes + return; + } + + int constantPrototypeIndex = -1; + if( !m_prototypeIndices ) + { + constantPrototypeIndex = m_prototypeIndexRemap[ 0 ]; + if( constantPrototypeIndex == -1 ) + { + // If we have no indices to specify other prototypes, and the first prototype is + // invalid, we're not going to output anything, and can early exit + return; + } + } + + // We need a list of which point indices belong to each prototype + std::vector< std::vector > pointIndicesForPrototypeIndex( m_numPrototypes ); + // Pre allocate if there's just one prototype, since we know the length will just be every point + if( constantPrototypeIndex != -1 ) + { + pointIndicesForPrototypeIndex[ constantPrototypeIndex ].reserve( numPoints() ); + } + + if( constantPrototypeIndex != -1 && !hasDuplicates ) + { + // If there's a single prototype, and no indices are being omitted because they are duplicates, + // then the list of point indices for the prototype is just an identity map of all integers + // from 0 .. N - 1. + // + // It's pretty wasteful to store this, but it avoids special cases throughout this code to skip + // using pointIndicesForPrototypeIndex when it isn't needed + for( size_t i = 0, e = numPoints(); i < e; ++i ) + { + pointIndicesForPrototypeIndex[ constantPrototypeIndex ].push_back( i ); + } + } + else + { + // The assignment of point indices to prototypes is non-trivial, so we actually have to do + // a bit of work + for( size_t i = 0, e = numPoints(); i < e; ++i ) + { + // If there are duplicates in the id list, then some point indices will be omitted - we + // need to check each point index to see if it got assigned an id correctly + if( hasDuplicates ) + { + int id = (*m_ids)[i]; + + if( m_idsToPointIndices[id] != i ) + { + continue; + } + } + + // Add this point index to the list for its prototype + + int prototypeIndex = constantPrototypeIndex != -1 ? + constantPrototypeIndex : + m_prototypeIndexRemap[ (*m_prototypeIndices)[ i ] % m_numPrototypes ]; + + if( prototypeIndex != -1 ) + { + pointIndicesForPrototypeIndex[ prototypeIndex ].push_back( i ); + } + } + } + + // We've populated instancerPrototypeIndex with a list of point indices for each prototype index. + // When we need this, however, we need it indexed by name, so we move the vectors we've just built + // to m_pointIndicesForPrototype which is indexed by name. + const std::vector< InternedString > &outputChildNames = m_names->outputChildNames()->readable(); + for( unsigned int i = 0; i < m_numPrototypes; i++ ) + { + int prototypeIndex = m_prototypeIndexRemap[ i ]; + if( prototypeIndex == -1 ) + { + continue; + } + + m_pointIndicesForPrototype.emplace( IECore::InternedString( outputChildNames[prototypeIndex] ), std::move( pointIndicesForPrototypeIndex[prototypeIndex] ) ); + } } size_t numPoints() const @@ -415,14 +517,13 @@ class Instancer::EngineData : public Data return m_ids ? (*m_ids)[pointIndex] : pointIndex; } - size_t pointIndex( const InternedString &name ) const + size_t pointIndex( size_t i ) const { - const size_t i = boost::lexical_cast( name ); if( !m_ids ) { if( i >= numPoints() ) { - throw IECore::Exception( fmt::format( "Instance id \"{}\" is invalid, instancer produces only {} children. Topology may have changed during shutter.", name.string(), numPoints() ) ); + throw IECore::Exception( fmt::format( "Instance id \"{}\" is invalid, instancer produces only {} children. Topology may have changed during shutter.", i, numPoints() ) ); } return i; } @@ -430,12 +531,17 @@ class Instancer::EngineData : public Data IdsToPointIndices::const_iterator it = m_idsToPointIndices.find( i ); if( it == m_idsToPointIndices.end() ) { - throw IECore::Exception( fmt::format( "Instance id \"{}\" is invalid. Topology may have changed during shutter.", name.string() ) ); + throw IECore::Exception( fmt::format( "Instance id \"{}\" is invalid. Topology may have changed during shutter.", i ) ); } return it->second; } + size_t pointIndex( const InternedString &name ) const + { + return pointIndex( boost::lexical_cast( name ) ); + } + size_t numValidPrototypes() const { return m_numValidPrototypes; @@ -445,7 +551,7 @@ class Instancer::EngineData : public Data { if( m_numPrototypes ) { - return m_prototypeIndexRemap[ ( m_indices ? (*m_indices)[pointIndex] : 0 ) % m_numPrototypes ]; + return m_prototypeIndexRemap[ ( m_prototypeIndices ? (*m_prototypeIndices)[pointIndex] : 0 ) % m_numPrototypes ]; } else { @@ -560,9 +666,9 @@ class Instancer::EngineData : public Data return m_prototypeContextVariables.size() != 0; } - // Set the context variables in the context for this index, based on the m_prototypeContextVariables + // Set the context variables in the context for this point index, based on the m_prototypeContextVariables // set up for this EngineData - void setPrototypeContextVariables( int index, Context::EditableScope &scope ) const + void setPrototypeContextVariables( int pointIndex, Context::EditableScope &scope ) const { for( unsigned int i = 0; i < m_prototypeContextVariables.size(); i++ ) { @@ -570,7 +676,7 @@ class Instancer::EngineData : public Data if( v.seedMode ) { - scope.setAllocated( v.name, seedForPoint( index, v.primVar, v.numSeeds, v.seedScramble ) ); + scope.setAllocated( v.name, seedForPoint( pointIndex, v.primVar, v.numSeeds, v.seedScramble ) ); continue; } @@ -581,24 +687,29 @@ class Instancer::EngineData : public Data try { - IECore::dispatch( v.primVar->data.get(), AccessPrototypeContextVariable(), v, index, scope ); + IECore::dispatch( v.primVar->data.get(), AccessPrototypeContextVariable(), v, pointIndex, scope ); } catch( QuantizeException & ) { - throw IECore::Exception( fmt::format( "Context variable \"{}\" : cannot quantize variable of type {}", index, v.primVar->data->typeName() ) ); + throw IECore::Exception( fmt::format( "Context variable \"{}\" : cannot quantize variable of type {}", v.name.string(), v.primVar->data->typeName() ) ); } } } + const std::vector & pointIndicesForPrototype( const IECore::InternedString &prototypeName ) const + { + return m_pointIndicesForPrototype.at( prototypeName ); + } + protected : // Needs to match setPrototypeContextVariables above, except that it operates on one // PrototypeContextVariable at a time instead of iterating through them - void hashPrototypeContextVariable( int index, const PrototypeContextVariable &v, IECore::MurmurHash &result ) const + void hashPrototypeContextVariable( int pointIndex, const PrototypeContextVariable &v, IECore::MurmurHash &result ) const { if( v.seedMode ) { - result.append( seedForPoint( index, v.primVar, v.numSeeds, v.seedScramble ) ); + result.append( seedForPoint( pointIndex, v.primVar, v.numSeeds, v.seedScramble ) ); return; } @@ -609,11 +720,11 @@ class Instancer::EngineData : public Data try { - IECore::dispatch( v.primVar->data.get(), UniqueHashPrototypeContextVariable(), v, index, result ); + IECore::dispatch( v.primVar->data.get(), UniqueHashPrototypeContextVariable(), v, pointIndex, result ); } catch( QuantizeException & ) { - throw IECore::Exception( fmt::format( "Context variable \"{}\" : cannot quantize variable of type {}", index, v.primVar->data->typeName() ) ); + throw IECore::Exception( fmt::format( "Context variable \"{}\" : cannot quantize variable of type {}", v.name.string(), v.primVar->data->typeName() ) ); } } @@ -697,7 +808,7 @@ class Instancer::EngineData : public Data } } - void initPrototypes( PrototypeMode mode, const std::string &index, const std::string &rootsVariable, const StringVectorData *rootsList, const ScenePlug *prototypes ) + void initPrototypes( PrototypeMode mode, const std::string &prototypeIndex, const std::string &rootsVariable, const StringVectorData *rootsList, const ScenePlug *prototypes ) { const std::vector *rootStrings = nullptr; @@ -705,12 +816,12 @@ class Instancer::EngineData : public Data { case PrototypeMode::IndexedRootsList : { - if( const auto *indices = m_primitive->variableData( index ) ) + if( const auto *prototypeIndices = m_primitive->variableData( prototypeIndex ) ) { - m_indices = &indices->readable(); - if( m_indices->size() != numPoints() ) + m_prototypeIndices = &prototypeIndices->readable(); + if( m_prototypeIndices->size() != numPoints() ) { - throw IECore::Exception( fmt::format( "prototypeIndex primitive variable \"{}\" has incorrect size", index ) ); + throw IECore::Exception( fmt::format( "prototypeIndex primitive variable \"{}\" has incorrect size", prototypeIndex ) ); } } @@ -720,12 +831,12 @@ class Instancer::EngineData : public Data } case PrototypeMode::IndexedRootsVariable : { - if( const auto *indices = m_primitive->variableData( index ) ) + if( const auto *prototypeIndices = m_primitive->variableData( prototypeIndex ) ) { - m_indices = &indices->readable(); - if( m_indices->size() != numPoints() ) + m_prototypeIndices = &prototypeIndices->readable(); + if( m_prototypeIndices->size() != numPoints() ) { - throw IECore::Exception( fmt::format( "prototypeIndex primitive variable \"{}\" has incorrect size", index ) ); + throw IECore::Exception( fmt::format( "prototypeIndex primitive variable \"{}\" has incorrect size", prototypeIndex ) ); } } @@ -761,7 +872,7 @@ class Instancer::EngineData : public Data throw IECore::Exception( message ); } - m_indices = view->indices(); + m_prototypeIndices = view->indices(); rootStrings = &view->data(); break; } @@ -804,8 +915,11 @@ class Instancer::EngineData : public Data } m_names = new Private::ChildNamesMap( inputNames ); + + const std::vector< InternedString > outputChildNames = m_names->outputChildNames()->readable(); m_numPrototypes = m_prototypeIndexRemap.size(); - m_numValidPrototypes = m_names->outputChildNames()->readable().size(); + m_numValidPrototypes = outputChildNames.size(); + } IECoreScene::ConstPrimitivePtr m_primitive; @@ -814,7 +928,7 @@ class Instancer::EngineData : public Data Private::ChildNamesMapPtr m_names; std::vector m_roots; std::vector m_prototypeIndexRemap; - const std::vector *m_indices; + const std::vector *m_prototypeIndices; const std::vector *m_ids; const std::vector *m_positions; const std::vector *m_orientations; @@ -828,8 +942,99 @@ class Instancer::EngineData : public Data MurmurHash m_attributesHash; const std::vector< PrototypeContextVariable > m_prototypeContextVariables; + + std::unordered_map< InternedString, std::vector > m_pointIndicesForPrototype; }; + +////////////////////////////////////////////////////////////////////////// +// InstancerCapsule +////////////////////////////////////////////////////////////////////////// + +// We can achieve better performance using a special capsule that understands EngineData instead of +// using a generic Capsule that only understands generic ScenePlugs +class Instancer::InstancerCapsule : public Capsule +{ + + public : + + InstancerCapsule() + : m_instancer( nullptr ) + { + } + + InstancerCapsule( + const Instancer *instancer, + const ScenePlug::ScenePath &root, + const Gaffer::Context &context, + const IECore::MurmurHash &hash, + const Imath::Box3f &bound + ) + : Capsule( instancer->capsuleScenePlug(), root, context, hash, bound ), + m_instancer( instancer ) + { + } + + ~InstancerCapsule() override + { + } + + IE_CORE_DECLAREEXTENSIONOBJECT( InstancerCapsule, GafferScene::InstancerCapsuleTypeId, GafferScene::Capsule ); + + // Defined at the bottom of this file, where it makes more sense + void render( IECoreScenePreview::Renderer *renderer ) const override; + + private : + + const Instancer *m_instancer; +}; + +IE_CORE_DEFINEOBJECTTYPEDESCRIPTION( Instancer::InstancerCapsule ); + +bool Instancer::InstancerCapsule::isEqualTo( const IECore::Object *other ) const +{ + return Capsule::isEqualTo( other ); +} + +void Instancer::InstancerCapsule::hash( IECore::MurmurHash &h ) const +{ + Capsule::hash( h ); +} + +void Instancer::InstancerCapsule::copyFrom( const IECore::Object *other, IECore::Object::CopyContext *context ) +{ + Capsule::copyFrom( other, context ); + + const InstancerCapsule *instancerCapsule = static_cast( other ); + + m_instancer = instancerCapsule->m_instancer; +} + +void Instancer::InstancerCapsule::save( IECore::Object::SaveContext *context ) const +{ + // Parent class just takes care of printing warning about not being supported + Capsule::save( context ); +} + +void Instancer::InstancerCapsule::load( IECore::Object::LoadContextPtr context ) +{ + // Parent class just takes care of printing warning about not being supported + Capsule::load( context ); +} + +void Instancer::InstancerCapsule::memoryUsage( IECore::Object::MemoryAccumulator &accumulator ) const +{ + Capsule::memoryUsage( accumulator ); + + // The size of the base class is already included, so no need to duplicate that + accumulator.accumulate( sizeof( InstancerCapsule ) - sizeof( Capsule ) ); + +} + +// Implementation of InstancerCapsule::render() +// is defined at the bottom of this file, where it makes more sense + + ////////////////////////////////////////////////////////////////////////// // Instancer ////////////////////////////////////////////////////////////////////////// @@ -837,7 +1042,7 @@ class Instancer::EngineData : public Data GAFFER_PLUG_DEFINE_TYPE( Instancer::ContextVariablePlug ); Instancer::ContextVariablePlug::ContextVariablePlug( const std::string &name, Direction direction, bool defaultEnable, unsigned flags ) - : ValuePlug( name, direction, flags ) + : ValuePlug( name, direction, flags ) { addChild( new BoolPlug( "enabled", direction, defaultEnable ) ); addChild( new StringPlug( "name", direction, "" ) ); @@ -903,6 +1108,7 @@ Instancer::Instancer( const std::string &name ) addChild( new StringPlug( "prototypeRoots", Plug::In, "prototypeRoots" ) ); addChild( new StringVectorDataPlug( "prototypeRootsList", Plug::In, new StringVectorData ) ); addChild( new StringPlug( "id", Plug::In, "instanceId" ) ); + addChild( new BoolPlug( "omitDuplicateIds", Plug::In, true ) ); addChild( new StringPlug( "position", Plug::In, "P" ) ); addChild( new StringPlug( "orientation", Plug::In ) ); addChild( new StringPlug( "scale", Plug::In ) ); @@ -918,7 +1124,6 @@ Instancer::Instancer( const std::string &name ) addChild( new ContextVariablePlug( "timeOffset", Plug::In, false, Plug::Flags::Default ) ); addChild( new AtomicCompoundDataPlug( "variations", Plug::Out, new CompoundData() ) ); addChild( new ObjectPlug( "__engine", Plug::Out, NullObject::defaultNullObject() ) ); - addChild( new AtomicCompoundDataPlug( "__prototypeChildNames", Plug::Out, new CompoundData ) ); addChild( new ScenePlug( "__capsuleScene", Plug::Out ) ); addChild( new PathMatcherDataPlug( "__setCollaborate", Plug::Out, new IECore::PathMatcherData() ) ); @@ -1007,164 +1212,164 @@ const Gaffer::StringPlug *Instancer::idPlug() const return getChild( g_firstPlugIndex + 6 ); } +Gaffer::BoolPlug *Instancer::omitDuplicateIdsPlug() +{ + return getChild( g_firstPlugIndex + 7 ); +} + +const Gaffer::BoolPlug *Instancer::omitDuplicateIdsPlug() const +{ + return getChild( g_firstPlugIndex + 7 ); +} + Gaffer::StringPlug *Instancer::positionPlug() { - return getChild( g_firstPlugIndex + 7 ); + return getChild( g_firstPlugIndex + 8 ); } const Gaffer::StringPlug *Instancer::positionPlug() const { - return getChild( g_firstPlugIndex + 7 ); + return getChild( g_firstPlugIndex + 8 ); } Gaffer::StringPlug *Instancer::orientationPlug() { - return getChild( g_firstPlugIndex + 8 ); + return getChild( g_firstPlugIndex + 9 ); } const Gaffer::StringPlug *Instancer::orientationPlug() const { - return getChild( g_firstPlugIndex + 8 ); + return getChild( g_firstPlugIndex + 9 ); } Gaffer::StringPlug *Instancer::scalePlug() { - return getChild( g_firstPlugIndex + 9 ); + return getChild( g_firstPlugIndex + 10 ); } const Gaffer::StringPlug *Instancer::scalePlug() const { - return getChild( g_firstPlugIndex + 9 ); + return getChild( g_firstPlugIndex + 10 ); } Gaffer::StringPlug *Instancer::attributesPlug() { - return getChild( g_firstPlugIndex + 10 ); + return getChild( g_firstPlugIndex + 11 ); } const Gaffer::StringPlug *Instancer::attributesPlug() const { - return getChild( g_firstPlugIndex + 10 ); + return getChild( g_firstPlugIndex + 11 ); } Gaffer::StringPlug *Instancer::attributePrefixPlug() { - return getChild( g_firstPlugIndex + 11 ); + return getChild( g_firstPlugIndex + 12 ); } const Gaffer::StringPlug *Instancer::attributePrefixPlug() const { - return getChild( g_firstPlugIndex + 11 ); + return getChild( g_firstPlugIndex + 12 ); } Gaffer::BoolPlug *Instancer::encapsulateInstanceGroupsPlug() { - return getChild( g_firstPlugIndex + 12 ); + return getChild( g_firstPlugIndex + 13 ); } const Gaffer::BoolPlug *Instancer::encapsulateInstanceGroupsPlug() const { - return getChild( g_firstPlugIndex + 12 ); + return getChild( g_firstPlugIndex + 13 ); } Gaffer::BoolPlug *Instancer::seedEnabledPlug() { - return getChild( g_firstPlugIndex + 13 ); + return getChild( g_firstPlugIndex + 14 ); } const Gaffer::BoolPlug *Instancer::seedEnabledPlug() const { - return getChild( g_firstPlugIndex + 13 ); + return getChild( g_firstPlugIndex + 14 ); } Gaffer::StringPlug *Instancer::seedVariablePlug() { - return getChild( g_firstPlugIndex + 14 ); + return getChild( g_firstPlugIndex + 15 ); } const Gaffer::StringPlug *Instancer::seedVariablePlug() const { - return getChild( g_firstPlugIndex + 14 ); + return getChild( g_firstPlugIndex + 15 ); } Gaffer::IntPlug *Instancer::seedsPlug() { - return getChild( g_firstPlugIndex + 15 ); + return getChild( g_firstPlugIndex + 16 ); } const Gaffer::IntPlug *Instancer::seedsPlug() const { - return getChild( g_firstPlugIndex + 15 ); + return getChild( g_firstPlugIndex + 16 ); } Gaffer::IntPlug *Instancer::seedPermutationPlug() { - return getChild( g_firstPlugIndex + 16 ); + return getChild( g_firstPlugIndex + 17 ); } const Gaffer::IntPlug *Instancer::seedPermutationPlug() const { - return getChild( g_firstPlugIndex + 16 ); + return getChild( g_firstPlugIndex + 17 ); } Gaffer::BoolPlug *Instancer::rawSeedPlug() { - return getChild( g_firstPlugIndex + 17 ); + return getChild( g_firstPlugIndex + 18 ); } const Gaffer::BoolPlug *Instancer::rawSeedPlug() const { - return getChild( g_firstPlugIndex + 17 ); + return getChild( g_firstPlugIndex + 18 ); } Gaffer::ValuePlug *Instancer::contextVariablesPlug() { - return getChild( g_firstPlugIndex + 18 ); + return getChild( g_firstPlugIndex + 19 ); } const Gaffer::ValuePlug *Instancer::contextVariablesPlug() const { - return getChild( g_firstPlugIndex + 18 ); + return getChild( g_firstPlugIndex + 19 ); } GafferScene::Instancer::ContextVariablePlug *Instancer::timeOffsetPlug() { - return getChild( g_firstPlugIndex + 19 ); + return getChild( g_firstPlugIndex + 20 ); } const GafferScene::Instancer::ContextVariablePlug *Instancer::timeOffsetPlug() const { - return getChild( g_firstPlugIndex + 19 ); + return getChild( g_firstPlugIndex + 20 ); } Gaffer::AtomicCompoundDataPlug *Instancer::variationsPlug() { - return getChild( g_firstPlugIndex + 20 ); + return getChild( g_firstPlugIndex + 21 ); } const Gaffer::AtomicCompoundDataPlug *Instancer::variationsPlug() const { - return getChild( g_firstPlugIndex + 20 ); + return getChild( g_firstPlugIndex + 21 ); } Gaffer::ObjectPlug *Instancer::enginePlug() { - return getChild( g_firstPlugIndex + 21 ); + return getChild( g_firstPlugIndex + 22 ); } const Gaffer::ObjectPlug *Instancer::enginePlug() const { - return getChild( g_firstPlugIndex + 21 ); -} - -Gaffer::AtomicCompoundDataPlug *Instancer::prototypeChildNamesPlug() -{ - return getChild( g_firstPlugIndex + 22 ); -} - -const Gaffer::AtomicCompoundDataPlug *Instancer::prototypeChildNamesPlug() const -{ - return getChild( g_firstPlugIndex + 22 ); + return getChild( g_firstPlugIndex + 22 ); } GafferScene::ScenePlug *Instancer::capsuleScenePlug() @@ -1200,6 +1405,7 @@ void Instancer::affects( const Plug *input, AffectedPlugsContainer &outputs ) co input == prototypesPlug()->childNamesPlug() || input == prototypesPlug()->existsPlug() || input == idPlug() || + input == omitDuplicateIdsPlug() || input == positionPlug() || input == orientationPlug() || input == scalePlug() || @@ -1217,11 +1423,6 @@ void Instancer::affects( const Plug *input, AffectedPlugsContainer &outputs ) co outputs.push_back( enginePlug() ); } - if( input == enginePlug() ) - { - outputs.push_back( prototypeChildNamesPlug() ); - } - // For the affects of our output plug, we can mostly rely on BranchCreator's mechanism driven // by affectsBranchObject etc., but for these 3 plugs, we have an overridden hash/compute // which in addition to everything that BranchCreator handles, are also affected by @@ -1270,7 +1471,6 @@ void Instancer::affects( const Plug *input, AffectedPlugsContainer &outputs ) co if( input == enginePlug() || input == prototypesPlug()->setPlug() || - input == prototypeChildNamesPlug() || input == namePlug() ) { @@ -1293,6 +1493,7 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co h.append( prototypesPlug()->childNamesHash( ScenePath() ) ); idPlug()->hash( h ); + omitDuplicateIdsPlug()->hash( h ); positionPlug()->hash( h ); orientationPlug()->hash( h ); scalePlug()->hash( h ); @@ -1321,10 +1522,6 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co timeOffsetPlug()->quantizePlug()->hash( h ); } } - else if( output == prototypeChildNamesPlug() ) - { - enginePlug()->hash( h ); - } else if( output == variationsPlug() ) { // The sum of the variations across different engines depends on all the engines, but @@ -1361,7 +1558,6 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co // accurate hash when we do actually have context variables: the slower hash won't change // if point locations change, unlike the engineHash which includes all changes engineHash( sourcePath, context, h ); - prototypeChildNamesHash( sourcePath, context, h ); Context::EditableScope scope( context ); scope.remove( ScenePlug::scenePathContextName ); prototypesPlug()->setPlug()->hash( h ); @@ -1369,17 +1565,15 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co return; } - IECore::ConstCompoundDataPtr prototypeChildNames = this->prototypeChildNames( sourcePath, context ); - tbb::task_group_context taskGroupContext( tbb::task_group_context::isolated ); for( const auto &prototypeName : engine->prototypeNames()->readable() ) { - const vector &childNames = prototypeChildNames->member( prototypeName )->readable(); + const std::vector &pointIndicesForPrototype = engine->pointIndicesForPrototype( prototypeName ); std::atomic h1Accum( 0 ), h2Accum( 0 ); const ThreadState &threadState = ThreadState::current(); - tbb::parallel_for( tbb::blocked_range( 0, childNames.size() ), [&]( const tbb::blocked_range &r ) + tbb::parallel_for( tbb::blocked_range( 0, pointIndicesForPrototype.size() ), [&]( const tbb::blocked_range &r ) { Context::EditableScope scope( threadState ); // As part of the setCollaborate plug machinery, we put the sourcePath in the context. @@ -1387,10 +1581,11 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co scope.remove( ScenePlug::scenePathContextName ); for( size_t i = r.begin(); i != r.end(); ++i ) { - const size_t pointIndex = engine->pointIndex( childNames[i] ); + const size_t pointIndex = pointIndicesForPrototype[i]; + size_t instanceId = engine->instanceId( pointIndex ); engine->setPrototypeContextVariables( pointIndex, scope ); IECore::MurmurHash instanceH; - instanceH.append( childNames[i] ); + instanceH.append( instanceId ); prototypesPlug()->setPlug()->hash( instanceH ); h1Accum += instanceH.h1(); h2Accum += instanceH.h2(); @@ -1409,8 +1604,7 @@ void Instancer::hash( const Gaffer::ValuePlug *output, const Gaffer::Context *co void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *context ) const { - // Both the enginePlug and prototypeChildNamesPlug are evaluated - // in a context in which scene:path holds the parent path for a + // EnginePlug is evaluated in a context in which scene:path holds the parent path for a // branch. if( output == enginePlug() ) { @@ -1522,6 +1716,7 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte prototypeRootsList.get(), prototypesPlug(), idPlug()->getValue(), + omitDuplicateIdsPlug()->getValue(), positionPlug()->getValue(), orientationPlug()->getValue(), scalePlug()->getValue(), @@ -1532,52 +1727,6 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte ); return; } - else if( output == prototypeChildNamesPlug() ) - { - // Here we compute and cache the child names for all of - // the /instances/ locations at once. We - // could instead compute them one at a time in - // computeBranchChildNames() but that would require N - // passes over the input points, where N is the number - // of prototypes. - ConstEngineDataPtr engine = boost::static_pointer_cast( enginePlug()->getValue() ); - const auto &prototypeNames = engine->prototypeNames()->readable(); - - vector> indexedPrototypeChildIds; - - size_t numPrototypes = engine->numValidPrototypes(); - if( numPrototypes ) - { - indexedPrototypeChildIds.resize( numPrototypes ); - for( size_t i = 0, e = engine->numPoints(); i < e; ++i ) - { - int prototypeIndex = engine->prototypeIndex( i ); - if( prototypeIndex != -1 ) - { - indexedPrototypeChildIds[prototypeIndex].push_back( engine->instanceId( i ) ); - } - } - } - - CompoundDataPtr result = new CompoundData; - for( size_t i = 0; i < numPrototypes; ++i ) - { - // Sort and uniquify ids before converting to string - std::sort( indexedPrototypeChildIds[i].begin(), indexedPrototypeChildIds[i].end() ); - auto last = std::unique( indexedPrototypeChildIds[i].begin(), indexedPrototypeChildIds[i].end() ); - indexedPrototypeChildIds[i].erase( last, indexedPrototypeChildIds[i].end() ); - - InternedStringVectorDataPtr prototypeChildNames = new InternedStringVectorData; - for( size_t id : indexedPrototypeChildIds[i] ) - { - prototypeChildNames->writable().emplace_back( id ); - } - result->writable()[prototypeNames[i]] = prototypeChildNames; - } - - static_cast( output )->setValue( result ); - return; - } else if( output == variationsPlug() ) { // Compute the number of variations by accumulating massive lists of unique hashes from all EngineDatas @@ -1648,8 +1797,6 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte ConstEngineDataPtr engine = this->engine( sourcePath, context ); - IECore::ConstCompoundDataPtr prototypeChildNames = this->prototypeChildNames( sourcePath, context ); - PathMatcherDataPtr outputSetData = new PathMatcherData; PathMatcher &outputSet = outputSetData->writable(); @@ -1663,12 +1810,12 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte branchPath.back() = prototypeName; const ScenePlug::ScenePath *prototypeRoot = engine->prototypeRoot( prototypeName ); - const vector &childNames = prototypeChildNames->member( prototypeName )->readable(); + const std::vector &pointIndicesForPrototype = engine->pointIndicesForPrototype( prototypeName ); tbb::spin_mutex instanceMutex; branchPath.emplace_back( InternedString() ); const ThreadState &threadState = ThreadState::current(); - tbb::parallel_for( tbb::blocked_range( 0, childNames.size() ), [&]( const tbb::blocked_range &r ) + tbb::parallel_for( tbb::blocked_range( 0, pointIndicesForPrototype.size() ), [&]( const tbb::blocked_range &r ) { Context::EditableScope scope( threadState ); // As part of the setCollaborate plug machinery, we put the sourcePath in the context. @@ -1677,13 +1824,14 @@ void Instancer::compute( Gaffer::ValuePlug *output, const Gaffer::Context *conte for( size_t i = r.begin(); i != r.end(); ++i ) { - const size_t pointIndex = engine->pointIndex( childNames[i] ); + const size_t pointIndex = pointIndicesForPrototype[i]; + size_t instanceId = engine->instanceId( pointIndex ); engine->setPrototypeContextVariables( pointIndex, scope ); ConstPathMatcherDataPtr instanceSet = prototypesPlug()->setPlug()->getValue(); PathMatcher pointInstanceSet = instanceSet->readable().subTree( *prototypeRoot ); tbb::spin_mutex::scoped_lock lock( instanceMutex ); - branchPath.back() = childNames[i]; + branchPath.back() = instanceId; outputSet.addPaths( pointInstanceSet, branchPath ); } }, @@ -1731,7 +1879,6 @@ bool Instancer::affectsBranchBound( const Gaffer::Plug *input ) const input == namePlug() || input == prototypesPlug()->boundPlug() || input == prototypesPlug()->transformPlug() || - input == prototypeChildNamesPlug() || input == outPlug()->childBoundsPlug() ; } @@ -1755,11 +1902,11 @@ void Instancer::hashBranchBound( const ScenePath &sourcePath, const ScenePath &b BranchCreator::hashBranchBound( sourcePath, branchPath, context, h ); engineHash( sourcePath, context, h ); - prototypeChildNamesHash( sourcePath, context, h ); h.append( branchPath.back() ); { PrototypeScope scope( enginePlug(), context, &sourcePath, &branchPath ); + prototypesPlug()->transformPlug()->hash( h ); prototypesPlug()->boundPlug()->hash( h ); } @@ -1794,8 +1941,6 @@ Imath::Box3f Instancer::computeBranchBound( const ScenePath &sourcePath, const S // more efficiently than `ScenePlug::childBounds()`. ConstEngineDataPtr e = engine( sourcePath, context ); - ConstCompoundDataPtr ic = prototypeChildNames( sourcePath, context ); - const vector &childNames = ic->member( branchPath.back() )->readable(); M44f childTransform; Box3f childBound; @@ -1805,17 +1950,20 @@ Imath::Box3f Instancer::computeBranchBound( const ScenePath &sourcePath, const S childBound = prototypesPlug()->boundPlug()->getValue(); } - using ISIterator = vector::const_iterator; - using ISRange = blocked_range; + const std::vector &pointIndicesForPrototype = e->pointIndicesForPrototype( branchPath.back() ); + // TODO - might be worth using a looser approximation - expand point cloud bound by largest diagonal of + // prototype bound x largest scale. Especially since this isn't fully accurate anyway: we are getting a + // single bound for the prototype with no context variables set, which may have nothing to do with actual + // prototype we get once the context variables are set. task_group_context taskGroupContext( task_group_context::isolated ); return parallel_reduce( - ISRange( childNames.begin(), childNames.end() ), + tbb::blocked_range( 0, pointIndicesForPrototype.size() ), Box3f(), - [ &e, &childBound, &childTransform ] ( const ISRange &r, Box3f u ) { - for( ISIterator i = r.begin(); i != r.end(); ++i ) + [ pointIndicesForPrototype, &e, &childBound, &childTransform ] ( const tbb::blocked_range &r, Box3f u ) { + for( size_t i = r.begin(); i != r.end(); ++i ) { - const size_t pointIndex = e->pointIndex( *i ); + const size_t pointIndex = pointIndicesForPrototype[i]; const M44f m = childTransform * e->instanceTransform( pointIndex ); const Box3f b = transform( childBound, m ); u.extendBy( b ); @@ -2056,14 +2204,13 @@ IECore::ConstObjectPtr Instancer::computeObject( const ScenePath &path, const Ga parentAndBranchPaths( path, sourcePath, branchPath ); if( branchPath.size() == 2 ) { - return new Capsule( - capsuleScenePlug(), + return new InstancerCapsule( + this, context->get( ScenePlug::scenePathContextName ) , *context, outPlug()->objectPlug()->hash(), outPlug()->boundPlug()->getValue() ); - } } @@ -2075,7 +2222,6 @@ bool Instancer::affectsBranchChildNames( const Gaffer::Plug *input ) const { return input == namePlug() || - input == prototypeChildNamesPlug() || input == enginePlug() ; } @@ -2098,7 +2244,7 @@ void Instancer::hashBranchChildNames( const ScenePath &sourcePath, const ScenePa { // "/instances/" BranchCreator::hashBranchChildNames( sourcePath, branchPath, context, h ); - prototypeChildNamesHash( sourcePath, context, h ); + engineHash( sourcePath, context, h ); h.append( branchPath.back() ); } else @@ -2131,8 +2277,37 @@ IECore::ConstInternedStringVectorDataPtr Instancer::computeBranchChildNames( con else if( branchPath.size() == 2 ) { // "/instances/" - IECore::ConstCompoundDataPtr ic = prototypeChildNames( sourcePath, context ); - return ic->member( branchPath.back() ); + + ConstEngineDataPtr engineData = engine( sourcePath, context ); + + const std::vector &pointIndicesForPrototype = engineData->pointIndicesForPrototype( branchPath.back() ); + + // The children of the prototypeName are all the instances which use this prototype, + // which we can query from the engine - however the names we output under use + // the ids, not the point indices, and must be sorted. So we need to allocate a + // temp buffer of integer ids, before converting to strings. + + std::vector ids; + ids.reserve( pointIndicesForPrototype.size() ); + + for( int q : pointIndicesForPrototype ) + { + ids.push_back( engineData->instanceId( q ) ); + } + + // Sort ids before converting to string ( they have already been uniquified but not sorted by + // the EngineData which uses a hash table ) + std::sort( ids.begin(), ids.end() ); + + InternedStringVectorDataPtr childNamesData = new InternedStringVectorData; + std::vector &childNames = childNamesData->writable(); + childNames.reserve( ids.size() ); + for( size_t id : ids ) + { + childNames.emplace_back( id ); + } + + return childNamesData; } else { @@ -2195,7 +2370,6 @@ bool Instancer::affectsBranchSet( const Gaffer::Plug *input ) const return input == enginePlug() || input == prototypesPlug()->setPlug() || - input == prototypeChildNamesPlug() || input == namePlug() || input == setCollaboratePlug() ; @@ -2232,7 +2406,6 @@ void Instancer::hashBranchSet( const ScenePath &sourcePath, const IECore::Intern else { engineHash( sourcePath, context, h ); - prototypeChildNamesHash( sourcePath, context, h ); prototypesPlug()->setPlug()->hash( h ); namePlug()->hash( h ); } @@ -2253,27 +2426,30 @@ IECore::ConstPathMatcherDataPtr Instancer::computeBranchSet( const ScenePath &so return setCollaboratePlug()->getValue(); } - IECore::ConstCompoundDataPtr prototypeChildNames = this->prototypeChildNames( sourcePath, context ); ConstPathMatcherDataPtr inputSet = prototypesPlug()->setPlug()->getValue(); PathMatcherDataPtr outputSetData = new PathMatcherData; PathMatcher &outputSet = outputSetData->writable(); - vector branchPath( { namePlug()->getValue() } ); + vector branchPath( { namePlug()->getValue(), InternedString(), InternedString() } ); + + vector outputPrototypePath( sourcePath.size() + 2 ); + outputPrototypePath = sourcePath; + outputPrototypePath.push_back( namePlug()->getValue() ); + outputPrototypePath.push_back( InternedString() ); for( const auto &prototypeName : engine->prototypeNames()->readable() ) { - branchPath.resize( 2 ); - branchPath.back() = prototypeName; - PathMatcher instanceSet = inputSet->readable().subTree( *engine->prototypeRoot( prototypeName ) ); + branchPath[1] = prototypeName; + + outputPrototypePath.back() = prototypeName; - const vector &childNames = prototypeChildNames->member( prototypeName )->readable(); + ConstInternedStringVectorDataPtr childNamesData = capsuleScenePlug()->childNames( outputPrototypePath ); - branchPath.emplace_back( InternedString() ); - for( const auto &childName : childNames ) + for( const auto &childName : childNamesData->readable() ) { - branchPath.back() = childName; + branchPath[2] = childName; outputSet.addPaths( instanceSet, branchPath ); } } @@ -2314,16 +2490,9 @@ void Instancer::engineHash( const ScenePath &sourcePath, const Gaffer::Context * enginePlug()->hash( h ); } -IECore::ConstCompoundDataPtr Instancer::prototypeChildNames( const ScenePath &sourcePath, const Gaffer::Context *context ) const -{ - ScenePlug::PathScope scope( context, &sourcePath ); - return prototypeChildNamesPlug()->getValue(); -} - -void Instancer::prototypeChildNamesHash( const ScenePath &sourcePath, const Gaffer::Context *context, IECore::MurmurHash &h ) const +const std::type_info &Instancer::instancerCapsuleTypeInfo() { - ScenePlug::PathScope scope( context, &sourcePath ); - prototypeChildNamesPlug()->hash( h ); + return typeid( InstancerCapsule ); } Instancer::PrototypeScope::PrototypeScope( const Gaffer::ObjectPlug *enginePlug, const Gaffer::Context *context, const ScenePath *sourcePath, const ScenePath *branchPath ) @@ -2367,3 +2536,328 @@ void Instancer::PrototypeScope::setPrototype( const EngineData *engine, const Sc set( ScenePlug::scenePathContextName, prototypeRoot ); } } + +namespace +{ + +// It shouldn't be necessary for this to be refcounted - but LRUCache is set up to make it impossible +// to get a pointer to the internal storage, since things could be evicted. We are disabling evictions, +// but we're still stuck with needing a shared pointer of some sort. +struct Prototype : public IECore::RefCounted +{ + Prototype( + const ScenePlug *prototypesPlug, const ScenePlug::ScenePath *prototypeRoot, + const std::vector &sampleTimes, const IECore::MurmurHash &hash, + const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, + const Context *prototypeContext, + IECoreScenePreview::Renderer *renderer, + bool prepareRendererAttributes + ) + { + const float onFrameTime = prototypeContext->getFrame(); + + Context::EditableScope scope( prototypeContext ); + + scope.set( ScenePlug::scenePathContextName, prototypeRoot ); + + m_attributes = prototypesPlug->attributesPlug()->getValue(); + if( prepareRendererAttributes ) + { + m_rendererAttributes = renderer->attributes( m_attributes.get() ); + } + + for( unsigned int i = 0; i < sampleTimes.size(); i++ ) + { + scope.setFrame( sampleTimes[i] ); + m_transforms.push_back( prototypesPlug->transformPlug()->getValue() ); + } + + IECore::MurmurHash h = hash; + h.append( prototypeContext->hash() ); + + // We find the capsules using the engine at shutter open, but the time used to construct the capsules + // must be the on-frame time, since the capsules will add their own shutter + scope.setFrame( onFrameTime ); + + if( prototypesPlug->childNamesPlug()->getValue()->readable().size() == 0 ) + { + if( !renderOptions.purposeIncluded( m_attributes.get() ) ) + { + // This prototype is not included. Leave m_object empty, which means this prototype will be skipped. + return; + } + + GafferScene::Private::RendererAlgo::deformationMotionTimes( renderOptions, m_attributes.get(), m_objectSampleTimes ); + GafferScene::Private::RendererAlgo::objectSamples( prototypesPlug->objectPlug(), m_objectSampleTimes, m_object ); + + m_objectPointers.reserve( m_object.size() ); + for( ConstObjectPtr &i : m_object ) + { + m_objectPointers.push_back( i.get() ); + } + } + else + { + // \todo - are there situations where this will be slow, and the renderer doesn't use it? + const Box3f bound = prototypesPlug->boundPlug()->getValue(); + + CapsulePtr newCapsule = new Capsule( + prototypesPlug, + *prototypeRoot, + *prototypeContext, + h, + bound + ); + + // Pass through our render options to the sub-capsules + newCapsule->setRenderOptions( renderOptions ); + m_object.push_back( std::move( newCapsule ) ); + } + } + + std::vector m_object; + + // Rather awkwardly, we need to store the objects as raw pointers as well, because Renderer::object + // requires a vector of pointers for the animated case. + std::vector m_objectPointers; + std::vector m_objectSampleTimes; + ConstCompoundObjectPtr m_attributes; + IECoreScenePreview::Renderer::AttributesInterfacePtr m_rendererAttributes; + std::vector m_transforms; +}; + +typedef boost::intrusive_ptr< const Prototype > ConstPrototypePtr; + +struct PrototypeCacheGetterKey +{ + + PrototypeCacheGetterKey( const Context *context ) + : context( context ) + { + } + + operator IECore::MurmurHash () const + { + return context->hash(); + } + + const Context *context; +}; + +} // namespace + +void Instancer::InstancerCapsule::render( IECoreScenePreview::Renderer *renderer ) const +{ + if( !renderer ) + { + throw IECore::Exception( "Null renderer passed to InstancerCapsule" ); + } + throwIfNoScene(); + + // ============================================================================ + // Prepare context for scene evaluation + // ============================================================================ + + const float onFrameTime = context()->getFrame(); + Context::EditableScope scope( context() ); + + const ScenePlug::ScenePath enginePath( root().begin(), root().begin() + root().size() - 2 ); + + const GafferScene::Private::RendererAlgo::RenderOptions renderOpts = renderOptions(); + + // This is a bit of a weird convention for using a const variable with an initialization that doesn't + // fit in one line ... not sure how I feel about it. In this case, it's crucial that sampleTimes is + // const, because it is used from multiple threads simultaneously. + const vector sampleTimes = [this, &enginePath, &renderOpts]() + { + vector result; + const ConstCompoundObjectPtr sceneAttributes = m_instancer->inPlug()->fullAttributes( enginePath ); + GafferScene::Private::RendererAlgo::transformMotionTimes( renderOpts, sceneAttributes.get(), result ); + + if( result.size() == 0 ) + { + result.push_back( context()->getFrame() ); + } + + return result; + }(); + + // ============================================================================ + // Get the Engines + // ============================================================================ + + std::vector< ConstEngineDataPtr > engines( sampleTimes.size() ); + for( unsigned int i = 0; i < sampleTimes.size(); i++ ) + { + scope.setFrame( sampleTimes[i] ); + engines[i] = m_instancer->engine( enginePath, scope.context() ); + } + + scope.setFrame( onFrameTime ); + + // ============================================================================ + // Get a constant prototype ( or set up cache that will be used to find + // prototypes if the protoype is not constant ) + // ============================================================================ + const ScenePlug::ScenePath *prototypeRoot = engines[0]->prototypeRoot( root().back() ); + + const ScenePlug *prototypesPlug = m_instancer->prototypesPlug(); + + const IECore::MurmurHash outerCapsuleHash = Object::hash(); + + const bool hasAttributes = engines[0]->numInstanceAttributes() > 0; + + // If constantPrototype is set, then every instance will use the same prototype. + ConstPrototypePtr constantPrototype; + if( !engines[0]->hasContextVariables() ) + { + constantPrototype = new Prototype( + prototypesPlug, prototypeRoot, sampleTimes, outerCapsuleHash, renderOpts, + scope.context(), renderer, + // If we don't have instance attributes, we can prepare renderer attributes ahead of time + !hasAttributes + ); + } + + // If constantPrototype is not set, we will put prototypes in this cache whenever we first encounter + // a prototype using a given context. + IECorePreview::LRUCache prototypeCache( + [ + &prototypesPlug, &prototypeRoot, &sampleTimes, &outerCapsuleHash, &renderOpts, + &renderer, &hasAttributes + ] + ( const PrototypeCacheGetterKey &key, size_t &cost, const IECore::Canceller *canceller ) -> ConstPrototypePtr + { + cost = 1; + return new Prototype( + prototypesPlug, prototypeRoot, sampleTimes, outerCapsuleHash, renderOpts, + key.context, renderer, + // If we don't have instance attributes, we can prepare renderer attributes ahead of time + !hasAttributes + ); + }, + std::numeric_limits::max() // Never evict, even if prototypes are all unique + ); + + // ============================================================================ + // Output the instances + // ============================================================================ + + const std::vector &pointIndicesForPrototype = engines[0]->pointIndicesForPrototype( root().back() ); + + // We've found problems with performance when running too many iterations in parallel, which appear + // to be related with hitting AiNode too hard in parallel ( perhaps related to threads spread between + // separate processors ). To partially solve this, we set the grain size so that we shouldn't use more + // than 32 threads, which appears to help some in testing. + size_t grainSize = std::max( (size_t)1, pointIndicesForPrototype.size() / 32 ); + task_group_context taskGroupContext( task_group_context::isolated ); + + const ThreadState &threadState = ThreadState::current(); + tbb::parallel_for( tbb::blocked_range( 0, pointIndicesForPrototype.size(), grainSize ), + [&]( const tbb::blocked_range &r ) + { + Context::EditableScope prototypeScope( threadState ); + + vector pointTransforms( sampleTimes.size() ); + std::string name; + IECoreScenePreview::Renderer::AttributesInterfacePtr attribsStorage; + + + for( size_t idx = r.begin(); idx != r.end(); ++idx ) + { + int pointIndex = pointIndicesForPrototype[idx]; + + const Prototype *proto; + if( constantPrototype ) + { + proto = constantPrototype.get(); + } + else + { + // The prototype depends on the context, so we need to find the prototype context for + // this instance. + + + // We find the capsules using the engine at shutter open, but the time used to construct the capsules + // must be the on-frame time, since the capsules will add their own shutter ( and we also handle + // the shutter ourselves for transform matrices ) + // + // For most context variables, we are overwriting them for each prototype anyway, so + // we can reuse the context. But timeOffset is relative, so it's important that we reset the + // time before we do setPrototypeContextVariables for the next element. ( Should this be more + // general instead of assuming that frame is the only variable for which offsetMode may be set? ) + prototypeScope.setFrame( onFrameTime ); + + engines[0]->setPrototypeContextVariables( pointIndex, prototypeScope ); + + proto = prototypeCache.get( PrototypeCacheGetterKey( prototypeScope.context() ) ).get(); + } + + if( !proto->m_object.size() ) + { + // No object to render. This could happen if the protype didn't meet the + // RenderOptions::purposeIncluded test. + continue; + } + + IECoreScenePreview::Renderer::AttributesInterface *attribs; + if( hasAttributes ) + { + CompoundObjectPtr currentAttributes = new CompoundObject(); + + // Since we're not going to modify any existing members (only add new ones), + // and our result is only read in this function, and never written, we can + // directly reference the input members in our result without copying. Be + // careful not to modify them though! + currentAttributes->members() = proto->m_attributes->members(); + + engines[0]->instanceAttributes( pointIndex, *currentAttributes ); + attribsStorage = renderer->attributes( currentAttributes.get() ); + attribs = attribsStorage.get(); + } + else + { + attribs = proto->m_rendererAttributes.get(); + } + + int instanceId = engines[0]->instanceId( pointIndex ); + + // We are running inside a procedural, so we don't need globally unique name. We are making a whole + // lot of these names for instances, so we make these names as absolutely minimal as possible. + name.resize( std::numeric_limits< int >::digits10 + 1 ); + name.resize( std::to_chars( &name[0], &(*name.end()), instanceId ).ptr - &name[0] ); + + IECoreScenePreview::Renderer::ObjectInterfacePtr objectInterface; + if( proto->m_objectSampleTimes.size() ) + { + objectInterface = renderer->object( + name, proto->m_objectPointers, proto->m_objectSampleTimes, attribs + ); + } + else + { + objectInterface = renderer->object( + name, proto->m_object[0].get(), attribs + ); + } + + if( sampleTimes.size() == 1 ) + { + objectInterface->transform( proto->m_transforms[0] * engines[0]->instanceTransform( pointIndex ) ); + } + else + { + for( unsigned int i = 0; i < engines.size(); i++ ) + { + int curPointIndex = i == 0 ? pointIndex : engines[i]->pointIndex( instanceId ); + pointTransforms[i] = proto->m_transforms[i] * engines[i]->instanceTransform( curPointIndex ); + } + + objectInterface->transform( pointTransforms, sampleTimes ); + } + + } + }, + taskGroupContext + ); +} diff --git a/src/GafferScene/RendererAlgo.cpp b/src/GafferScene/RendererAlgo.cpp index fb838d4b01d..3d3dd733d9b 100644 --- a/src/GafferScene/RendererAlgo.cpp +++ b/src/GafferScene/RendererAlgo.cpp @@ -442,11 +442,15 @@ bool objectSamples( const ObjectPlug *objectPlug, const std::vector &samp Context::Scope frameScope( frameContext ); std::vector tempTimes = {}; + // \todo - this is quite bad for the case of any Capsules, which use a naive hash + // that always varies with the context. This should be investigated soon as a follow + // up. + // // This is a pretty weird case - we would have taken an earlier branch if the hashes // had all matched, so it looks like this object is actual animated, despite not supporting // animation. // The most correct thing to do here is reset the hash, since we may not have included the - // on frame in the samples we hashed, and in theory, the on frame value could vary indepndently + // on frame in the samples we hashed, and in theory, the on frame value could vary independently // of shutter open and close. This means that an animated non-animateable object will never have // a matching hash, and will be updated every pass. May be a performance hazard, but probably // preferable to incorrect behaviour? Just means people need to be careful to make sure their diff --git a/src/GafferSceneModule/HierarchyBinding.cpp b/src/GafferSceneModule/HierarchyBinding.cpp index 9b7a51284ff..bf9e780a9da 100644 --- a/src/GafferSceneModule/HierarchyBinding.cpp +++ b/src/GafferSceneModule/HierarchyBinding.cpp @@ -162,6 +162,18 @@ void GafferSceneModule::bindHierarchy() .attr( "__qualname__" ) = "Instancer.ContextVariablePlug" ; + // Expose InstancerCapsules as if they were plain Capsules. We don't + // want to bind them fully because then we'd be exposing a private class, but + // we need to register them so that they can be returned to Python + // successfully when testing expansion in Python + // + // See "Boost.Python and slightly more tricky inheritance" at + // http://lists.boost.org/Archives/boost/2005/09/93017.php for more details. + + boost::python::objects::copy_class_object( + type_id(), Instancer::instancerCapsuleTypeInfo() + ); + } {