Skip to content

Commit

Permalink
Instancer : Don't output separate capsules for prototypes
Browse files Browse the repository at this point in the history
  • Loading branch information
danieldresser-ie authored and johnhaddon committed Sep 26, 2024
1 parent 796d790 commit 36fed89
Show file tree
Hide file tree
Showing 6 changed files with 336 additions and 195 deletions.
1 change: 1 addition & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ Breaking Changes
----------------

- IECoreArnold : Added `messageContext` argument to `NodeAlgo::Converter` and `NodeAlgo::MotionConverter`.
- Instancer : `encapsulate` now produces a single Capsule at the `.../instances` location, instead of the previous `encapsulateInstanceGroups` plug which produced separate capsules at each `.../instances/<prototypeName>` location. Outputting a single capsule is slightly less flexible for users, but is much faster to render in Arnold ( Arnold is currently unable to deal effectively with large numbers of fully overlapping procedurals ). This change also allowed us to simplify the code slightly, reducing the time spent in Gaffer when rendering an encapsulated Instancer by around 20%.

[^1]: To be omitted from 1.5.0.0 release notes.

Expand Down
12 changes: 10 additions & 2 deletions include/GafferScene/Instancer.h
Original file line number Diff line number Diff line change
Expand Up @@ -144,8 +144,8 @@ class GAFFERSCENE_API Instancer : public BranchCreator
Gaffer::StringPlug *attributePrefixPlug();
const Gaffer::StringPlug *attributePrefixPlug() const;

Gaffer::BoolPlug *encapsulateInstanceGroupsPlug();
const Gaffer::BoolPlug *encapsulateInstanceGroupsPlug() const;
Gaffer::BoolPlug *encapsulatePlug();
const Gaffer::BoolPlug *encapsulatePlug() const;

Gaffer::BoolPlug *seedEnabledPlug();
const Gaffer::BoolPlug *seedEnabledPlug() const;
Expand Down Expand Up @@ -223,11 +223,16 @@ class GAFFERSCENE_API Instancer : public BranchCreator
private :

IE_CORE_FORWARDDECLARE( EngineData );
IE_CORE_FORWARDDECLARE( EngineSplitPrototypesData );
IE_CORE_FORWARDDECLARE( InstancerCapsule );

Gaffer::ObjectPlug *enginePlug();
const Gaffer::ObjectPlug *enginePlug() const;

Gaffer::ObjectPlug *engineSplitPrototypesPlug();
const Gaffer::ObjectPlug *engineSplitPrototypesPlug() const;


GafferScene::ScenePlug *capsuleScenePlug();
const GafferScene::ScenePlug *capsuleScenePlug() const;

Expand All @@ -240,6 +245,9 @@ 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;

ConstEngineSplitPrototypesDataPtr engineSplitPrototypes( const ScenePath &sourcePath, const Gaffer::Context *context ) const;
void engineSplitPrototypesHash( 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 );
Expand Down
35 changes: 17 additions & 18 deletions python/GafferSceneTest/InstancerTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def assertEncapsulatedRendersSame( self, instancer ):
continue
corresponding.setValue( i.getValue() )

encapInstancer["encapsulateInstanceGroups"].setValue( True )
encapInstancer["encapsulate"].setValue( True )

self.assertScenesRenderSame( instancer["out"], encapInstancer["out"], expandProcedurals = True, ignoreLinks = True )

Expand Down Expand Up @@ -171,14 +171,14 @@ def test( self ) :
encapInstancer["prototypes"].setInput( instanceInput["out"] )
encapInstancer["parent"].setValue( "/seeds" )
encapInstancer["name"].setValue( "instances" )
encapInstancer["encapsulateInstanceGroups"].setValue( True )
encapInstancer["encapsulate"].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 )
encapInstancer["out"].object( "/seeds/instances" ).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 )
Expand All @@ -191,8 +191,8 @@ def test( self ) :
unencap["in"].setInput( encapInstancer["out"] )
unencap["filter"].setInput( unencapFilter["out"] )

self.assertTrue( isinstance( encapInstancer["out"].object( "/seeds/instances/sphere/" ), GafferScene.Capsule ) )
self.assertEqual( encapInstancer["out"].childNames( "/seeds/instances/sphere/" ), IECore.InternedStringVectorData() )
self.assertTrue( isinstance( encapInstancer["out"].object( "/seeds/instances" ), GafferScene.Capsule ) )
self.assertEqual( encapInstancer["out"].childNames( "/seeds/instances" ), IECore.InternedStringVectorData() )
self.assertScenesEqual( unencap["out"], instancer["out"] )

# Edit seeds object
Expand Down Expand Up @@ -880,7 +880,7 @@ def testAnimation( self ) :
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 )
instancer["encapsulate"].setValue( True )

with testContext:
with self.assertRaisesRegex( RuntimeError, 'Instance id "2" is invalid, instancer produces only 2 children. Topology may have changed during shutter.' ):
Expand Down Expand Up @@ -1432,7 +1432,7 @@ def testSets( self ) :
encapInstancer["prototypes"].setInput( instances["out"] )
encapInstancer["parent"].setValue( "/object" )
encapInstancer["prototypeIndex"].setValue( "index" )
encapInstancer["encapsulateInstanceGroups"].setValue( True )
encapInstancer["encapsulate"].setValue( True )

unencapFilter = GafferScene.PathFilter()
unencapFilter["paths"].setValue( IECore.StringVectorData( [ "/..." ] ) )
Expand Down Expand Up @@ -2594,7 +2594,7 @@ def testContextSet( self ):
# When encapsulating, we shouldn't pay any time cost for evaluating the set, even with a huge
# number of instances
plane["divisions"].setValue( imath.V2i( 1000 ) )
instancer["encapsulateInstanceGroups"].setValue( True )
instancer["encapsulate"].setValue( True )
t = time.time()
instancer["out"].set( "set" )
totalTime = time.time() - t
Expand Down Expand Up @@ -2836,33 +2836,33 @@ def testPrototypePropertiesAffectCapsule( self ) :

with self.subTest( encapsulate = encapsulate ) :

instancer["encapsulateInstanceGroups"].setValue( encapsulate )
instancer["encapsulate"].setValue( encapsulate )

# Prototype properties used by the capsule should be reflected
# in the object hash. For the hash to be updated successfully,
# the `object` plug must also be dirtied (because we cache
# hashes).

hashes = set()
hashes.add( instancer["out"].objectHash( "/plane/instances/sphere" ) )
hashes.add( instancer["out"].objectHash( "/plane/instances" ) )
self.assertEqual( len( hashes ), 1 )

sphere["radius"].setValue( 2 + int( encapsulate ) )
hashes.add( instancer["out"].objectHash( "/plane/instances/sphere" ) )
hashes.add( instancer["out"].objectHash( "/plane/instances" ) )
self.assertEqual( len( hashes ), 2 if encapsulate else 1 )

dirtiedPlugs = GafferTest.CapturingSlot( instancer.plugDirtiedSignal() )

sphere["transform"]["translate"]["x"].setValue( 2 + int( encapsulate ) )
hashes.add( instancer["out"].objectHash( "/plane/instances/sphere" ) )
hashes.add( instancer["out"].objectHash( "/plane/instances" ) )
self.assertEqual( len( hashes ), 3 if encapsulate else 1 )

sphereAttributes["attributes"]["test"] = Gaffer.NameValuePlug( "test", encapsulate )
hashes.add( instancer["out"].objectHash( "/plane/instances/sphere" ) )
hashes.add( instancer["out"].objectHash( "/plane/instances" ) )
self.assertEqual( len( hashes ), 4 if encapsulate else 1 )

sphere["sets"].setValue( "testSet{}".format( encapsulate ) )
hashes.add( instancer["out"].objectHash( "/plane/instances/sphere" ) )
hashes.add( instancer["out"].objectHash( "/plane/instances" ) )
self.assertEqual( len( hashes ), 5 if encapsulate else 1 )

# When not encapsulating, there should be no unnecessary
Expand All @@ -2880,7 +2880,7 @@ def testPrototypePropertiesAffectCapsule( self ) :
del dirtiedPlugs[:]

sphereOptions["options"]["test"] = Gaffer.NameValuePlug( "test", encapsulate )
hashes.add( instancer["out"].objectHash( "/plane/instances/sphere" ) )
hashes.add( instancer["out"].objectHash( "/plane/instances" ) )
self.assertEqual( len( hashes ), 5 if encapsulate else 1 )
self.assertNotIn(
instancer["out"]["object"],
Expand Down Expand Up @@ -3115,13 +3115,12 @@ def testChildNamesPerfWithPrototypesAndIds( self ):
@GafferTest.TestRunner.PerformanceTestMethod()
def testEncapsulatedRenderPerf( self ):
nodes = self.initSimpleInstancer( withPrototypes = True, withIds = False )
nodes["instancer"]["encapsulateInstanceGroups"].setValue( True )
nodes["instancer"]["encapsulate"].setValue( True )

renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch )

with GafferTest.TestRunner.PerformanceScope() :
nodes["instancer"]["out"].object( "/plane/instances/sphere" ).render( renderer )
nodes["instancer"]["out"].object( "/plane/instances/cube" ).render( renderer )
nodes["instancer"]["out"].object( "/plane/instances" ).render( renderer )

if __name__ == "__main__":
unittest.main()
9 changes: 5 additions & 4 deletions python/GafferSceneUI/InstancerUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -576,22 +576,23 @@ def __init__( self, headings, toolTipOverride = "" ) :

],

"encapsulateInstanceGroups" : [
"encapsulate" : [

"description",
"""
Converts each group of instances into a capsule, which won't
Converts instances into a capsule, which won't
be expanded until you Unencapsulate or render. When keeping
these locations encapsulated, downstream nodes can't see the
instance locations, which prevents editing but improves
performance. This option should be preferred to a downstream
Encapsulate node because it has the following benefits :
- Substantially improved performance when the prototypes
define sets.
define sets.
- Fewer unnecessary updates during interactive rendering.
- Faster performance in renderer backends with special
instancer capsule support ( ie. Arnold )
""",
"label", "Instance Groups",

"layout:section", "Settings.Encapsulation",

Expand Down
Loading

0 comments on commit 36fed89

Please sign in to comment.