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

Add render:includedPurposes option #5470

Merged
merged 8 commits into from
Sep 25, 2023
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
Loading