Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle USD PointInstancer - Rendering and Point Promotion #6221

Merged
merged 9 commits into from
Jan 30, 2025
9 changes: 9 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
1.5.x.x (relative to 1.5.4.0)
=======

Features
--------

- Parent/Duplicate/Scatter ( Nodes derived from BranchCreator ) : Added `copySourceAttributes` plug, to preserve attributes when using the `destination` plug to change where in the hierarchy branches are added.
- GafferUSD :
- Added render adaptor which automatically expands USD PointInstancers at render time. Can be controlled with the Viewer menu "Expansion > Expand USD Instancers". Defaults on for all renderers besides OpenGL. Can be set manually with the bool attribute `gafferUSD:pointInstancerAdaptor:enabled`. If you want the resulting instances to have some of the point cloud primitive variables promoted to user attributes, you can set the attribute `gafferUSD:pointInstancerAdaptor:attributes`.
- Added PromotePointInstances node, for workflows which use USD point instancers, but want to selectively convert some points to expanded geometry before rendering.
- PrimitiveVariableTweaks : Added `invertSelection` plug.

Improvements
------------

Expand Down
3 changes: 3 additions & 0 deletions include/GafferScene/BranchCreator.h
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ class GAFFERSCENE_API BranchCreator : public FilteredSceneProcessor
Gaffer::StringPlug *destinationPlug();
const Gaffer::StringPlug *destinationPlug() const;

Gaffer::BoolPlug *copySourceAttributesPlug();
const Gaffer::BoolPlug *copySourceAttributesPlug() const;

void affects( const Gaffer::Plug *input, AffectedPlugsContainer &outputs ) const override;

protected :
Expand Down
3 changes: 3 additions & 0 deletions include/GafferScene/PrimitiveVariableTweaks.h
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ class GAFFERSCENE_API PrimitiveVariableTweaks : public Deformer
Gaffer::StringPlug *maskVariablePlug();
const Gaffer::StringPlug *maskVariablePlug() const;

Gaffer::BoolPlug *invertSelectionPlug();
const Gaffer::BoolPlug *invertSelectionPlug() const;

Gaffer::BoolPlug *ignoreMissingPlug();
const Gaffer::BoolPlug *ignoreMissingPlug() const;

Expand Down
120 changes: 120 additions & 0 deletions python/GafferSceneTest/InstancerTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3158,6 +3158,126 @@ def testDestinationBug( self ) :

self.assertSceneValid( instancer["out"] )

def testRecursive( self ):

# The Instancer node doesn't expose the ability to do recursive instancing ( expanding a scene containing
# instancer where some of the prototypes are also instancers that need expanding ). But we do require this
# to work in the render adaptor for rendering USD PointInstancers, so we test here that this works if we
# do some naughty rewiring of things.

# Create a test scene with the following structure
# /plane/prototypes/cube/prototypes/sphere
# ... where both the plane and the cube are treated as instancers, resulting in 32 leaf instance spheres
# ( each of the 4 vertices of the plane gets a cube of 8 spheres ).

sphere = GafferScene.Sphere()

sphereFilter = GafferScene.PathFilter()
sphereFilter["paths"].setValue( IECore.StringVectorData( [ '/sphere' ] ) )

testSet = GafferScene.Set()
testSet["in"].setInput( sphere["out"] )
testSet["filter"].setInput( sphereFilter["out"] )
testSet["name"].setValue( 'testSet' )

cubePrototypes = GafferScene.Group()
cubePrototypes["in"][0].setInput( testSet["out"] )
cubePrototypes["name"].setValue( 'prototypes' )

cube = GafferScene.Cube()

cubeFilter = GafferScene.PathFilter()
cubeFilter["paths"].setValue( IECore.StringVectorData( [ '/cube' ] ) )

cubePrototypeVars = GafferScene.PrimitiveVariableTweaks()
cubePrototypeVars["tweaks"].addChild( Gaffer.TweakPlug( Gaffer.StringVectorDataPlug( "value", defaultValue = IECore.StringVectorData( [ ] ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "tweak1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) )
cubePrototypeVars["in"].setInput( cube["out"] )
cubePrototypeVars["filter"].setInput( cubeFilter["out"] )
cubePrototypeVars["interpolation"].setValue( 1 )
cubePrototypeVars["tweaks"]["tweak1"]["name"].setValue( 'prototypeRoots' )
cubePrototypeVars["tweaks"]["tweak1"]["mode"].setValue( 5 )
cubePrototypeVars["tweaks"]["tweak1"]["value"].setValue( IECore.StringVectorData( [ '/plane/prototypes/cube/prototypes/sphere' ] ) )

mergeCubePrototypes = GafferScene.Parent()
mergeCubePrototypes["in"].setInput( cubePrototypeVars["out"] )
mergeCubePrototypes["parent"].setValue( '/cube' )
mergeCubePrototypes["children"][0].setInput( cubePrototypes["out"] )

plane = GafferScene.Plane()
plane["dimensions"].setValue( imath.V2f( 10, 10 ) )

planeFilter = GafferScene.PathFilter()
planeFilter["paths"].setValue( IECore.StringVectorData( [ '/plane' ] ) )

planePrototypeVars = GafferScene.PrimitiveVariableTweaks()
planePrototypeVars["tweaks"].addChild( Gaffer.TweakPlug( Gaffer.StringVectorDataPlug( "value", defaultValue = IECore.StringVectorData( [ ] ), flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ), "tweak1", flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic, ) )
planePrototypeVars["in"].setInput( plane["out"] )
planePrototypeVars["filter"].setInput( planeFilter["out"] )
planePrototypeVars["interpolation"].setValue( 1 )
planePrototypeVars["tweaks"]["tweak1"]["name"].setValue( 'prototypeRoots' )
planePrototypeVars["tweaks"]["tweak1"]["mode"].setValue( 5 )
planePrototypeVars["tweaks"]["tweak1"]["value"].setValue( IECore.StringVectorData( [ '/plane/prototypes/cube' ] ) )

planePrototypes = GafferScene.Group()
planePrototypes["in"][0].setInput( mergeCubePrototypes["out"] )
planePrototypes["name"].setValue( 'prototypes' )

mergePlanePrototypes = GafferScene.Parent()
mergePlanePrototypes["in"].setInput( planePrototypeVars["out"] )
mergePlanePrototypes["parent"].setValue( '/plane' )
mergePlanePrototypes["children"][0].setInput( planePrototypes["out"] )

instancersFilter = GafferScene.PathFilter()
instancersFilter["paths"].setValue( IECore.StringVectorData( [ '/plane', '/plane/prototypes/cube' ] ) )

instancer = GafferScene.Instancer()
encapInstancer = GafferScene.Instancer()
for i in [ instancer, encapInstancer ]:
i["in"].setInput( mergePlanePrototypes["out"] )
i["filter"].setInput( instancersFilter["out"] )
i['prototypeMode'].setValue( GafferScene.Instancer.PrototypeMode.IndexedRootsVariable )
i["prototypeIndex"].setValue( 'prototypeIndex' )

# HACK TO MAKE THE INSTANCER RECURSIVE
# ====================================
for plug in Gaffer.Plug.Range( i["prototypes"] ) :
plug.setFlags( Gaffer.Plug.Flags.AcceptsDependencyCycles, True )
i["prototypes"].setInput( i["out"] )
i["out"]["setNames"].setInput( i["in"]["setNames"] )
i["out"]["set"].setInput( i["in"]["set"] )
# =====================================

encapInstancer["encapsulate"].setValue( True )

sphereGeo = sphere["out"].object( "sphere" )

# Test that all the instances are getting created
topLevelInstanceNames = IECore.InternedStringVectorData( [ "0", "1", "2", "3" ] )
subLevelInstanceNames = IECore.InternedStringVectorData( [ "0", "1", "2", "3", "4", "5", "6", "7" ] )

self.assertEqual( instancer["out"].childNames( '/plane/instances/cube' ), topLevelInstanceNames )
self.assertEqual( instancer["out"].childNames( '/plane/instances/cube/0/instances/sphere' ), subLevelInstanceNames )
self.assertEqual( instancer["out"].childNames( '/plane/instances/cube/1/instances/sphere' ), subLevelInstanceNames )
self.assertEqual( instancer["out"].childNames( '/plane/instances/cube/2/instances/sphere' ), subLevelInstanceNames )
self.assertEqual( instancer["out"].childNames( '/plane/instances/cube/3/instances/sphere' ), subLevelInstanceNames )

# Check some random instances to see that there are objects there
self.assertEqual( instancer["out"].object( '/plane/instances/cube/0/instances/sphere/7' ), sphereGeo )
self.assertEqual( instancer["out"].object( '/plane/instances/cube/2/instances/sphere/4' ), sphereGeo )
self.assertEqual( instancer["out"].object( '/plane/instances/cube/3/instances/sphere/0' ), sphereGeo )

# Make sure encapsulation works
self.assertScenesRenderSame( instancer["out"], encapInstancer["out"], expandProcedurals = True, ignoreLinks = True )

# Documenting the current behaviour with sets : we don't expand the sets at all ( even though this set
# actually should now be echoed throughout many instances ). This is fine in our currently intended use,
# as an adaptor that runs right before rendering. If we were officially exposing recursive instancing we
# would need to do something a lot smarter.
self.assertEqual(
instancer["out"].set( 'testSet' ),
IECore.PathMatcherData( IECore.PathMatcher( ['/plane/prototypes/cube/prototypes/sphere'] ) )
)

@GafferTest.TestRunner.PerformanceTestMethod( repeat = 10 )
def testBoundPerformance( self ) :

Expand Down
109 changes: 109 additions & 0 deletions python/GafferSceneTest/ParentTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1258,5 +1258,114 @@ def evaluateScene( scene ) :
parentCTask.wait()
parentA1Task.wait()

def testCopySourceAttributes( self ) :

groupA = GafferScene.Group()
groupA["name"].setValue( 'A' )

groupB = GafferScene.Group()
groupB["name"].setValue( 'B' )
groupB["in"][0].setInput( groupA["out"] )
groupB["in"][1].setInput( groupA["out"] )

groupC = GafferScene.Group()
groupC["name"].setValue( 'C' )
groupC["in"][0].setInput( groupB["out"] )
groupC["in"][1].setInput( groupB["out"] )

pathFilterAll = GafferScene.PathFilter()
pathFilterAll["paths"].setValue( IECore.StringVectorData( [ '/...' ] ) )

customAttributes = GafferScene.CustomAttributes()
customAttributes["attributes"].addChild( Gaffer.NameValuePlug( "attr:${scene:path}", Gaffer.StringPlug( "value", defaultValue = 'test' ), True, "member1" ) )
customAttributes["attributes"].addChild( Gaffer.NameValuePlug( "attr:override", Gaffer.StringPlug( "value", defaultValue = '${scene:path}' ), True, "member2" ) )
customAttributes["in"].setInput( groupC["out"] )
customAttributes["filter"].setInput( pathFilterAll["out"] )

sphere = GafferScene.Sphere()

sphereAttributes = GafferScene.CustomAttributes()
sphereAttributes["in"].setInput( sphere["out"] )
sphereAttributes["filter"].setInput( pathFilterAll["out"] )
sphereAttributes["attributes"].addChild( Gaffer.NameValuePlug( "attr:sphere", Gaffer.StringPlug( "value", defaultValue = 'blah' ), True, "member1" ) )

pathFilterTarget = GafferScene.PathFilter()
pathFilterTarget["paths"].setValue( IECore.StringVectorData( [ '/C/B1/A1' ] ) )

parent = GafferScene.Parent()
parent["in"].setInput( customAttributes["out"] )
parent["children"][0].setInput( sphereAttributes["out"] )
parent["filter"].setInput( pathFilterTarget["out"] )
parent["destination"].setValue( '/' )

def attrsAsDict( path ):
return { k : v.value for (k,v) in parent["out"].attributes( path ).items() }

self.assertEqual( attrsAsDict( "/sphere" ), { 'attr:sphere' : 'blah' } )

parent["copySourceAttributes"].setValue( True )

# We get all the attributes from the source location
self.assertEqual( attrsAsDict( "/sphere" ), {
'attr:sphere' : 'blah',
'attr:/C': 'test',
'attr:/C/B1': 'test',
'attr:/C/B1/A1': 'test',
'attr:override': '/C/B1/A1',
} )

# Branch attributes can override
sphereAttributes["attributes"].addChild( Gaffer.NameValuePlug( "attr:override", Gaffer.StringPlug( "value", defaultValue = 'branchOverride' ), True, "member2" ) )

self.assertEqual( attrsAsDict( "/sphere" ), {
'attr:sphere' : 'blah',
'attr:/C': 'test',
'attr:/C/B1': 'test',
'attr:/C/B1/A1': 'test',
'attr:override': 'branchOverride',
} )

del sphereAttributes["attributes"][-1]

# We only get the attributes that are part of the source hierachy and not the dest hierarchy
parent["destination"].setValue( '/C/B1' )

self.assertEqual( attrsAsDict( "/C/B1/sphere" ), {
'attr:sphere' : 'blah',
'attr:/C/B1/A1': 'test',
'attr:override': '/C/B1/A1',
} )

parent["destination"].setValue( '/C/B1/A1' )
self.assertEqual( attrsAsDict( "/C/B1/A1/sphere" ), {
'attr:sphere' : 'blah',
} )

parent["destination"].setValue( '/C/B/A' )
self.assertEqual( attrsAsDict( "/C/B/A/sphere" ), {
'attr:sphere' : 'blah',
'attr:/C/B1': 'test',
'attr:/C/B1/A1': 'test',
'attr:override': '/C/B1/A1',
} )

# Parent two different sources to the root, they each get their attributes
pathFilterTarget["paths"].setValue( IECore.StringVectorData( [ '/C/B/A', '/C/B1/A1' ] ) )
parent["destination"].setValue( '/' )
self.assertEqual( attrsAsDict( "/sphere" ), {
'attr:sphere' : 'blah',
'attr:/C': 'test',
'attr:/C/B': 'test',
'attr:/C/B/A': 'test',
'attr:override': '/C/B/A',
} )
self.assertEqual( attrsAsDict( "/sphere1" ), {
'attr:sphere' : 'blah',
'attr:/C': 'test',
'attr:/C/B1': 'test',
'attr:/C/B1/A1': 'test',
'attr:override': '/C/B1/A1',
} )

if __name__ == "__main__":
unittest.main()
14 changes: 14 additions & 0 deletions python/GafferSceneTest/PrimitiveVariableTweaksTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,10 +761,24 @@ def testUniformIdList( self ):
o = tweak["out"].object( "/plane" )
self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 0, 42, 49, 0 ] ) ) )

tweak["idList"].setValue( IECore.Int64VectorData( [ 2, 3 ] ) )
o = tweak["out"].object( "/plane" )
self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 0, 42, 49, 7 ] ) ) )

# Test inverting selection

tweak["invertSelection"].setValue( True )
o = tweak["out"].object( "/plane" )
self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7, 49, 42, 0 ] ) ) )

# Test selection mode

tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.All )
o = tweak["out"].object( "/plane" )
self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7, 49, 49, 7 ] ) ) )

# Test wrong interpolation

tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList )
tweak["id"].setValue( "vertexIds" )

Expand Down
56 changes: 56 additions & 0 deletions python/GafferSceneTest/usdFiles/recursiveInst.usda
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#usda 1.0
(
)

def PointInstancer "inst" (
kind = "group"
)
{
int64[] ids = [5, 6, 7, 8, 9, 0, 1, 2, 3, 4]

float[] primvars:testA (
interpolation = "vertex"
)
float[] primvars:testA.timeSamples = { 0 : [ 0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9 ] }

point3f[] positions = [(0, 0, -20), (0, 0, -16), (0, 0, -12), (0, 0, -8), (0, 0, -4), (0, 0, 0), (0, 0, 4), (0, 0, 8), (0, 0, 12), (0, 0, 16)]
int[] protoIndices = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1]
rel prototypes = [ </inst/Prototypes/sphere>, </inst/Prototypes/subInst> ]

def Scope "Prototypes" (
kind = "group"
)
{
def Sphere "sphere"
{
double radius = 1
}
def PointInstancer "subInst" (
kind = "group"
)
{
int64[] ids = [5, 6, 7, 8, 9, 0, 1, 2 ]

float[] primvars:testB (
interpolation = "vertex"
)
float[] primvars:testB.timeSamples = { 0 : [ 0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875 ] }

point3f[] positions = [
(-1, -1, -1), (1, -1, -1), (-1, 1, -1), (1, 1, -1),
(-1, -1, 1), (1, -1, 1), (-1, 1, 1), (1, 1, 1)
]
rel prototypes = [ </inst/Prototypes/subInst/Prototypes/sphere> ]

def Scope "Prototypes" (
kind = "group"
)
{
def Sphere "sphere"
{
double radius = 0.3
}
}
}
}
}
12 changes: 12 additions & 0 deletions python/GafferSceneUI/BranchCreatorUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@

"layout:activator:filterNotConnected", lambda node : node["filter"].getInput() is None,
"layout:activator:parentInUse", lambda node : node["parent"].getInput() is not None or node["parent"].getValue() != "",
"layout:activator:nonDefaultDestination", lambda node : not node["destination"].isSetToDefault(),

plugs = {

Expand All @@ -80,6 +81,17 @@

"plugValueWidget:type", "GafferSceneUI.ScenePathPlugValueWidget",
"ui:spreadsheet:selectorValue", "${scene:path}",
"layout:index", -2,

],

"copySourceAttributes" : [

"description",
"""
Copies attributes to newly created destination locations to match the attributes at the source location.
""",
"layout:activator", "nonDefaultDestination",
"layout:index", -1,

],
Expand Down
Loading
Loading