Skip to content

Commit

Permalink
Merge pull request #5470 from johnhaddon/renderPurposePR1
Browse files Browse the repository at this point in the history
Add `render:includedPurposes` option
  • Loading branch information
johnhaddon authored Sep 25, 2023
2 parents d29eda3 + bf593aa commit 1332d5e
Show file tree
Hide file tree
Showing 13 changed files with 506 additions and 30 deletions.
12 changes: 12 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
1.3.x.x (relative to 1.3.3.0)
=======

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

- Viewer : Added per-purpose control over which locations are shown in the viewer, according to their `usd:purpose` attribute.
- The Drawing Mode dropdown menu can be used to to choose the visible purposes.
- The default purposes can be specified in a startup file using `Gaffer.Metadata.registerValue( GafferSceneUI.SceneView, "drawingMode.includedPurposes.value", "userDefault", IECore.StringVectorData( [ "default", "proxy" ] ) )`.
- StandardOptions : Added `includedPurposes` plug, to control which locations are included in a render based on the value of their `usd:purpose` attribute.

API
---

- RenderController : Added missing `updateRequired()` Python binding.

1.3.3.0 (relative to 1.3.2.0)
=======
Expand Down
3 changes: 2 additions & 1 deletion include/GafferScene/RenderController.h
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,8 @@ class GAFFERSCENE_API RenderController : public Gaffer::Signals::Trackable
TransformBlurGlobalComponent = 16,
DeformationBlurGlobalComponent = 32,
CameraShutterGlobalComponent = 64,
AllGlobalComponents = GlobalsGlobalComponent | SetsGlobalComponent | RenderSetsGlobalComponent | CameraOptionsGlobalComponent | TransformBlurGlobalComponent | DeformationBlurGlobalComponent
IncludedPurposesGlobalComponent = 128,
AllGlobalComponents = GlobalsGlobalComponent | SetsGlobalComponent | RenderSetsGlobalComponent | CameraOptionsGlobalComponent | TransformBlurGlobalComponent | DeformationBlurGlobalComponent | IncludedPurposesGlobalComponent
};

struct MotionBlurOptions
Expand Down
77 changes: 77 additions & 0 deletions python/GafferSceneTest/RenderControllerTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -1507,5 +1507,82 @@ def testDescendantVisibilityChangeDoesntUpdateObject( self ) :
self.assertEqual( monitor.plugStatistics( sphere["out"]["object"] ).hashCount, 0 )
self.assertEqual( monitor.plugStatistics( sphere["out"]["object"] ).computeCount, 0 )

def testIncludedPurposes( self ) :

rootFilter = GafferScene.PathFilter()
rootFilter["paths"].setValue( IECore.StringVectorData( [ "*" ] ) )

sphere = GafferScene.Sphere()
sphereAttributes = GafferScene.CustomAttributes()
sphereAttributes["in"].setInput( sphere["out"] )
sphereAttributes["filter"].setInput( rootFilter["out"] )

group = GafferScene.Group()
group["in"][0].setInput( sphereAttributes["out"] )
groupAttributes = GafferScene.CustomAttributes()
groupAttributes["in"].setInput( group["out"] )
groupAttributes["filter"].setInput( rootFilter["out"] )

standardOptions = GafferScene.StandardOptions()
standardOptions["in"].setInput( groupAttributes["out"] )

renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer()
controller = GafferScene.RenderController( standardOptions["out"], Gaffer.Context(), renderer )
controller.setMinimumExpansionDepth( 2 )
controller.update()

# Should be visible by default - we haven't used any purpose attributes or options.

self.assertIsNotNone( renderer.capturedObject( "/group/sphere" ) )

# Should still be visible when we add a purpose attribute, because we haven't
# specified the `render:includedPurposes` option.

sphereAttributes["attributes"].addChild( Gaffer.NameValuePlug( "usd:purpose", "proxy", defaultEnabled = True ) )
self.assertTrue( controller.updateRequired() )
controller.update()
self.assertIsNotNone( renderer.capturedObject( "/group/sphere" ) )

# But should be hidden when we add `render:includedPurposes` to exclude it.

standardOptions["options"]["includedPurposes"]["enabled"].setValue( True )
self.assertEqual(
standardOptions["options"]["includedPurposes"]["value"].getValue(),
IECore.StringVectorData( [ "default", "render" ] ),
)
self.assertTrue( controller.updateRequired() )
controller.update()
self.assertIsNone( renderer.capturedObject( "/group/sphere" ) )

# Should be shown again if we change purpose to one that is included.

sphereAttributes["attributes"][0]["value"].setValue( "render" )
self.assertTrue( controller.updateRequired() )
controller.update()
self.assertIsNotNone( renderer.capturedObject( "/group/sphere" ) )

# Shouldn't matter if parent has a purpose which is excluded, because local
# purpose will override that.

groupAttributes["attributes"].addChild( Gaffer.NameValuePlug( "usd:purpose", "proxy", defaultEnabled = True ) )
self.assertTrue( controller.updateRequired() )
controller.update()
self.assertIsNotNone( renderer.capturedObject( "/group/sphere" ) )

# Unless there is no local purpose, in which case we inherit the parent
# purpose and will get hidden.

sphereAttributes["attributes"][0]["enabled"].setValue( False )
self.assertTrue( controller.updateRequired() )
controller.update()
self.assertIsNone( renderer.capturedObject( "/group/sphere" ) )

# Reverting to no `includedPurposes` option should revert to showing everything.

standardOptions["options"]["includedPurposes"]["enabled"].setValue( False )
self.assertTrue( controller.updateRequired() )
controller.update()
self.assertIsNotNone( renderer.capturedObject( "/group/sphere" ) )

if __name__ == "__main__":
unittest.main()
159 changes: 159 additions & 0 deletions python/GafferSceneTest/RendererAlgoTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -589,5 +589,164 @@ def testTransformSamplesCancellation( self ) :
self.assertEqual( [ s.translation().x for s in samples ], [ 0.0 ] )
self.assertNotEqual( h, IECore.MurmurHash() )

def testPurposes( self ) :

# /group
# /innerGroup1 (default)
# /cube
# /sphere (render)
# /innerGroup2
# /cube (proxy)
# /sphere

def purposeAttribute( purpose ) :

result = GafferScene.CustomAttributes()
result["attributes"].addChild( Gaffer.NameValuePlug( "usd:purpose", purpose ) )
return result

rootFilter = GafferScene.PathFilter()
rootFilter["paths"].setValue( IECore.StringVectorData( [ "*" ] ) )

cube = GafferScene.Cube()

renderSphere = GafferScene.Sphere()
renderSphereAttributes = purposeAttribute( "render" )
renderSphereAttributes["in"].setInput( renderSphere["out"] )
renderSphereAttributes["filter"].setInput( rootFilter["out"] )

innerGroup1 = GafferScene.Group()
innerGroup1["name"].setValue( "innerGroup1" )
innerGroup1["in"][0].setInput( cube["out"] )
innerGroup1["in"][1].setInput( renderSphereAttributes["out"] )

innerGroup1Attributes = purposeAttribute( "default" )
innerGroup1Attributes["in"].setInput( innerGroup1["out"] )
innerGroup1Attributes["filter"].setInput( rootFilter["out"] )

proxyCube = GafferScene.Cube()

proxyCubeAttributes = purposeAttribute( "proxy" )
proxyCubeAttributes["in"].setInput( proxyCube["out"] )
proxyCubeAttributes["filter"].setInput( rootFilter["out"] )

sphere = GafferScene.Sphere()

innerGroup2 = GafferScene.Group()
innerGroup2["name"].setValue( "innerGroup2" )
innerGroup2["in"][0].setInput( proxyCubeAttributes["out"] )
innerGroup2["in"][1].setInput( sphere["out"] )

group = GafferScene.Group()
group["in"][0].setInput( innerGroup1Attributes["out"] )
group["in"][1].setInput( innerGroup2["out"] )

def assertIncludedObjects( scene, includedPurposes, paths ) :

globals = IECore.CompoundObject()
if includedPurposes :
globals["option:render:includedPurposes"] = IECore.StringVectorData( includedPurposes )

renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer(
GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch
)
GafferScene.Private.RendererAlgo.outputObjects(
group["out"], globals, GafferScene.Private.RendererAlgo.RenderSets( scene ), GafferScene.Private.RendererAlgo.LightLinks(),
renderer
)

allPaths = {
"/group/innerGroup1/cube",
"/group/innerGroup1/sphere",
"/group/innerGroup2/cube",
"/group/innerGroup2/sphere",
}

self.assertTrue( paths.issubset( allPaths ) )
for path in allPaths :
if path in paths :
self.assertIsNotNone( renderer.capturedObject( path ) )
else :
self.assertIsNone( renderer.capturedObject( path ) )

# If we don't specify a purpose, then we should get everything.

assertIncludedObjects(
group["out"], None,
{
"/group/innerGroup1/cube",
"/group/innerGroup1/sphere",
"/group/innerGroup2/cube",
"/group/innerGroup2/sphere",
}
)

# The default purpose should pick objects without any purpose attribute,
# and those that explicitly have a value of "default".

assertIncludedObjects(
group["out"], [ "default" ],
{
"/group/innerGroup1/cube",
"/group/innerGroup2/sphere",
}
)

# Purpose-based visibility isn't pruning, so we can see a child location
# with the right purpose even if it is parented below a location with the
# wrong purpose.

assertIncludedObjects(
group["out"], [ "render" ],
{
"/group/innerGroup1/sphere",
}
)

assertIncludedObjects(
group["out"], [ "proxy" ],
{
"/group/innerGroup2/cube",
}
)

# Multiple purposes can be rendered at once.

assertIncludedObjects(
group["out"], [ "render", "default" ],
{
"/group/innerGroup1/cube",
"/group/innerGroup1/sphere",
"/group/innerGroup2/sphere",
}
)

assertIncludedObjects(
group["out"], [ "proxy", "default" ],
{
"/group/innerGroup1/cube",
"/group/innerGroup2/cube",
"/group/innerGroup2/sphere",
}
)

assertIncludedObjects(
group["out"], [ "render", "proxy", "default" ],
{
"/group/innerGroup1/cube",
"/group/innerGroup1/sphere",
"/group/innerGroup2/cube",
"/group/innerGroup2/sphere",
}
)

assertIncludedObjects(
group["out"], [ "proxy", "render" ],
{
"/group/innerGroup1/sphere",
"/group/innerGroup2/cube",
}
)

if __name__ == "__main__":
unittest.main()
28 changes: 28 additions & 0 deletions python/GafferSceneUI/SceneViewUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,34 @@ def __menuDefinition( self ) :

m.append( "/ComponentsDivider", { "divider" : True } )

includedPurposes = self.getPlug()["includedPurposes"]["value"].getValue()
includedPurposesEnabled = self.getPlug()["includedPurposes"]["enabled"].getValue()
allPurposes = [ "default", "render", "proxy", "guide" ]
for purpose in allPurposes :
newPurposes = IECore.StringVectorData( [
p for p in allPurposes
if
( p != purpose and p in includedPurposes ) or ( p == purpose and p not in includedPurposes )
] )
m.append(
"/Purposes/{}".format( purpose.capitalize() ),
{
"checkBox" : purpose in includedPurposes,
"active" : includedPurposesEnabled,
"command" : functools.partial( self.getPlug()["includedPurposes"]["value"].setValue, newPurposes ),
}
)
m.append( "/Purposes/SceneDivider", { "divider" : True } )
m.append(
"/Purposes/From Scene",
{
"checkBox" : not includedPurposesEnabled,
"command" : lambda checked : self.getPlug()["includedPurposes"]["enabled"].setValue( not checked ),
}
)

m.append( "/PurposesDivider", { "divider" : True } )

lightDrawingModePlug = self.getPlug()["light"]["drawingMode"]
for mode in ( "wireframe", "color", "texture" ) :
m.append(
Expand Down
Loading

0 comments on commit 1332d5e

Please sign in to comment.