From 646a878fed605be3531dc0d69b51837261f9936e Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 15 Sep 2023 15:58:56 +0100 Subject: [PATCH 1/8] RenderBinding : Bind `outputObjects()` --- src/GafferSceneModule/RenderBinding.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/GafferSceneModule/RenderBinding.cpp b/src/GafferSceneModule/RenderBinding.cpp index ab6a13ca6d4..c74207961c3 100644 --- a/src/GafferSceneModule/RenderBinding.cpp +++ b/src/GafferSceneModule/RenderBinding.cpp @@ -401,6 +401,11 @@ void outputLightsWrapper( const ScenePlug &scene, const IECore::CompoundObject & GafferScene::Private::RendererAlgo::outputLights( &scene, &globals, renderSets, &lightLinks, &renderer ); } +void outputObjectsWrapper( const ScenePlug &scene, const IECore::CompoundObject &globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, GafferScene::Private::RendererAlgo::LightLinks &lightLinks, IECoreScenePreview::Renderer &renderer, const ScenePlug::ScenePath &root ) +{ + IECorePython::ScopedGILRelease gilRelease; + GafferScene::Private::RendererAlgo::outputObjects( &scene, &globals, renderSets, &lightLinks, &renderer, root ); +} } // namespace @@ -451,6 +456,7 @@ void GafferSceneModule::bindRender() def( "outputCameras", &outputCamerasWrapper ); def( "outputLights", &outputLightsWrapper ); + def( "outputObjects", &outputObjectsWrapper, ( arg( "scene" ), arg( "globals" ), arg( "renderSets" ), arg( "lightLinks" ), arg( "renderer" ), arg( "root" ) = "/" ) ); } object ieCoreScenePreviewModule( borrowed( PyImport_AddModule( "GafferScene.Private.IECoreScenePreview" ) ) ); From 34e7fb43e21ae1307b9c9b2ba04d2e0b7dd60a7e Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 18 Sep 2023 11:59:21 +0100 Subject: [PATCH 2/8] StandardOptions : Add `render:includedPurposes` option --- Changes.md | 4 + python/GafferSceneUI/StandardOptionsUI.py | 90 +++++++++++++++++++++++ src/GafferScene/StandardOptions.cpp | 4 + 3 files changed, 98 insertions(+) diff --git a/Changes.md b/Changes.md index 80665fde002..8bd6d5c9f43 100644 --- a/Changes.md +++ b/Changes.md @@ -1,6 +1,10 @@ 1.3.x.x (relative to 1.3.3.0) ======= +Improvements +------------ + +- StandardOptions : Added `includedPurposes` plug, to control which locations are included in a render based on the value of their `usd:purpose` attribute. 1.3.3.0 (relative to 1.3.2.0) ======= diff --git a/python/GafferSceneUI/StandardOptionsUI.py b/python/GafferSceneUI/StandardOptionsUI.py index aee069dd018..9d3f8c62d61 100644 --- a/python/GafferSceneUI/StandardOptionsUI.py +++ b/python/GafferSceneUI/StandardOptionsUI.py @@ -34,6 +34,8 @@ # ########################################################################## +import functools + import IECore import IECoreScene @@ -41,6 +43,8 @@ import GafferUI import GafferScene +from GafferUI.PlugValueWidget import sole + ########################################################################## # Metadata ########################################################################## @@ -73,6 +77,13 @@ def __cameraSummary( plug ) : return ", ".join( info ) +def __purposeSummary( plug ) : + + if plug["includedPurposes"]["enabled"].getValue() : + return ", ".join( [ p.capitalize() for p in plug["includedPurposes"]["value"].getValue() ] ) + + return "" + def __motionBlurSummary( plug ) : info = [] @@ -104,6 +115,7 @@ def __statisticsSummary( plug ) : "options" : [ "layout:section:Camera:summary", __cameraSummary, + "layout:section:Purpose:summary", __purposeSummary, "layout:section:Motion Blur:summary", __motionBlurSummary, "layout:section:Statistics:summary", __statisticsSummary, @@ -320,6 +332,28 @@ def __statisticsSummary( plug ) : "layout:section", "Camera", ], + # Purpose + + "options.includedPurposes" : [ + + "description", + """ + Limits the objects included in the render according to the values of their `usd:purpose` + attribute. + + > Tip : Use the USDAttributes node to assign the `usd:purpose` attribute. + """, + + "layout:section", "Purpose", + + ], + + "options.includedPurposes.value" : [ + + "plugValueWidget:type", "GafferSceneUI.StandardOptionsUI._IncludedPurposesPlugValueWidget", + + ], + # Motion blur plugs "options.transformBlur" : [ @@ -409,3 +443,59 @@ def __statisticsSummary( plug ) : plugs = plugsMetadata ) + +class _IncludedPurposesPlugValueWidget( GafferUI.PlugValueWidget ) : + + __allPurposes = [ "default", "render", "proxy", "guide" ] + + def __init__( self, plugs, **kw ) : + + self.__menuButton = GafferUI.MenuButton( "", menu = GafferUI.Menu( Gaffer.WeakMethod( self.__menuDefinition ) ) ) + GafferUI.PlugValueWidget.__init__( self, self.__menuButton, plugs, **kw ) + + self._addPopupMenu( self.__menuButton ) + + self.__currentValue = None + + def _updateFromValues( self, values, exception ) : + + self.__currentValue = sole( values ) + self.__menuButton.setText( ", ".join( [ p.capitalize() for p in self.__currentValue ] ) if self.__currentValue is not None else "---" ) + self.__menuButton.setErrored( exception is not None ) + + def _updateFromEditable( self ) : + + self.__menuButton.setEnabled( self._editable() ) + + def __menuDefinition( self ) : + + result = IECore.MenuDefinition() + + currentValue = self.__currentValue or [] + for purpose in self.__allPurposes : + + result.append( + "/{}".format( purpose.capitalize() ), + { + "checkBox" : purpose in currentValue, + "command" : functools.partial( Gaffer.WeakMethod( self.__togglePurpose ), purpose = purpose ) + } + ) + + + return result + + def __togglePurpose( self, checked, purpose ) : + + with self.getContext() : + with Gaffer.UndoScope( next( iter( self.getPlugs() ) ).ancestor( Gaffer.ScriptNode ) ) : + for plug in self.getPlugs() : + value = plug.getValue() + # Conform value so that only valid purposes are present, and they are + # always presented in the same order. + value = [ + p for p in self.__allPurposes + if + ( p != purpose and p in value ) or ( p == purpose and checked ) + ] + plug.setValue( IECore.StringVectorData( value ) ) diff --git a/src/GafferScene/StandardOptions.cpp b/src/GafferScene/StandardOptions.cpp index 59ab41f2414..c4835a3f39e 100644 --- a/src/GafferScene/StandardOptions.cpp +++ b/src/GafferScene/StandardOptions.cpp @@ -65,6 +65,10 @@ StandardOptions::StandardOptions( const std::string &name ) options->addChild( new Gaffer::NameValuePlug( "render:overscanRight", new FloatPlug( "value", Plug::In, 0.1f, 0.0f, 1.0f ), false, "overscanRight" ) ); options->addChild( new Gaffer::NameValuePlug( "render:depthOfField", new IECore::BoolData( false ), false, "depthOfField" ) ); + // Purpose + + options->addChild( new Gaffer::NameValuePlug( "render:includedPurposes", new IECore::StringVectorData( { "default", "render" } ), false, "includedPurposes" ) ); + // Motion blur options->addChild( new Gaffer::NameValuePlug( "render:transformBlur", new IECore::BoolData( false ), false, "transformBlur" ) ); From f537c2296b02fcfab3d9507ca9f16572941c748b Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 18 Sep 2023 11:59:33 +0100 Subject: [PATCH 3/8] RendererAlgo : Support `render:includedPurposes` option --- python/GafferSceneTest/RendererAlgoTest.py | 159 +++++++++++++++++++++ src/GafferScene/RendererAlgo.cpp | 28 +++- 2 files changed, 185 insertions(+), 2 deletions(-) diff --git a/python/GafferSceneTest/RendererAlgoTest.py b/python/GafferSceneTest/RendererAlgoTest.py index 62c8ccb6ee5..ef651635f21 100644 --- a/python/GafferSceneTest/RendererAlgoTest.py +++ b/python/GafferSceneTest/RendererAlgoTest.py @@ -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() diff --git a/src/GafferScene/RendererAlgo.cpp b/src/GafferScene/RendererAlgo.cpp index db08cb517b9..836614b6ca7 100644 --- a/src/GafferScene/RendererAlgo.cpp +++ b/src/GafferScene/RendererAlgo.cpp @@ -1009,12 +1009,15 @@ namespace { const std::string g_optionPrefix( "option:" ); +const std::string g_defaultPurpose( "default" ); const IECore::InternedString g_frameOptionName( "frame" ); const IECore::InternedString g_cameraOptionLegacyName( "option:render:camera" ); const InternedString g_transformBlurOptionName( "option:render:transformBlur" ); const InternedString g_deformationBlurOptionName( "option:render:deformationBlur" ); const InternedString g_shutterOptionName( "option:render:shutter" ); +const InternedString g_includedPurposesOptionName( "option:render:includedPurposes" ); +const InternedString g_purposeAttributeName( "usd:purpose" ); InternedString g_visibleAttributeName( "scene:visible" ); @@ -1044,6 +1047,8 @@ struct LocationOutput m_options.shutter = SceneAlgo::shutter( globals, scene ); + m_options.includedPurposes = globals->member( g_includedPurposesOptionName ); + m_transformSamples.push_back( M44f() ); } @@ -1072,6 +1077,19 @@ struct LocationOutput protected : + bool purposeIncluded() const + { + if( !m_options.includedPurposes ) + { + return true; + } + const auto purposeData = m_attributes->member( g_purposeAttributeName ); + const std::string &purpose = purposeData ? purposeData->readable() : g_defaultPurpose; + const vector &purposes = m_options.includedPurposes->readable(); + return std::find( purposes.begin(), purposes.end(), purpose ) != purposes.end(); + + } + std::string name( const ScenePlug::ScenePath &path ) const { if( m_root.size() == path.size() ) @@ -1227,6 +1245,7 @@ struct LocationOutput bool transformBlur; bool deformationBlur; Imath::V2f shutter; + ConstStringVectorDataPtr includedPurposes; }; Options m_options; @@ -1255,7 +1274,7 @@ struct CameraOutput : public LocationOutput } const size_t cameraMatch = m_cameraSet.match( path ); - if( cameraMatch & IECore::PathMatcher::ExactMatch ) + if( ( cameraMatch & IECore::PathMatcher::ExactMatch ) && purposeIncluded() ) { // Sample cameras and apply globals vector sampleTimes; @@ -1347,7 +1366,7 @@ struct LightOutput : public LocationOutput } const size_t lightMatch = m_lightSet.match( path ); - if( lightMatch & IECore::PathMatcher::ExactMatch ) + if( ( lightMatch & IECore::PathMatcher::ExactMatch ) && purposeIncluded() ) { IECore::ConstObjectPtr object = scene->objectPlug()->getValue(); @@ -1443,6 +1462,11 @@ struct ObjectOutput : public LocationOutput return true; } + if( !purposeIncluded() ) + { + return true; + } + vector sampleTimes; deformationMotionTimes( sampleTimes ); From ef11c268707fdcdd71ef9ad5491072245e66aa5e Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 18 Sep 2023 14:54:54 +0100 Subject: [PATCH 4/8] RenderControllerBinding : Bind `updateRequired()` --- Changes.md | 5 +++++ src/GafferSceneModule/RenderControllerBinding.cpp | 1 + 2 files changed, 6 insertions(+) diff --git a/Changes.md b/Changes.md index 8bd6d5c9f43..067a76a92b3 100644 --- a/Changes.md +++ b/Changes.md @@ -6,6 +6,11 @@ Improvements - 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) ======= diff --git a/src/GafferSceneModule/RenderControllerBinding.cpp b/src/GafferSceneModule/RenderControllerBinding.cpp index f99dc889f52..3c67d347aec 100644 --- a/src/GafferSceneModule/RenderControllerBinding.cpp +++ b/src/GafferSceneModule/RenderControllerBinding.cpp @@ -163,6 +163,7 @@ void GafferSceneModule::bindRenderController() .def( "setMinimumExpansionDepth", &setMinimumExpansionDepth ) .def( "getMinimumExpansionDepth", &RenderController::getMinimumExpansionDepth ) .def( "updateRequiredSignal", &RenderController::updateRequiredSignal, return_internal_reference<1>() ) + .def( "updateRequired", &RenderController::updateRequired ) .def( "update", &update, ( arg( "callback" ) = object() ) ) .def( "updateMatchingPaths", &updateMatchingPaths, ( arg( "pathsToUpdate" ), arg( "callback" ) = object() ) ) .def( "updateInBackground", &updateInBackground, ( arg( "callback" ) = object(), arg( "priorityPaths" ) = IECore::PathMatcher() ) ) From 0e881a3296475433737b7c4f079a244fe4cbe8bb Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 21 Sep 2023 11:45:05 +0100 Subject: [PATCH 5/8] RenderController : Remove unused function arguments --- src/GafferScene/RenderController.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/GafferScene/RenderController.cpp b/src/GafferScene/RenderController.cpp index f504eb94ac3..9ab04db7ef3 100644 --- a/src/GafferScene/RenderController.cpp +++ b/src/GafferScene/RenderController.cpp @@ -429,7 +429,7 @@ class RenderController::SceneGraph const bool parentTransformChanged = m_parent && ( m_parent->m_changedComponents & TransformComponent ); if( ( m_dirtyComponents & TransformComponent ) || parentTransformChanged ) { - if( updateTransform( controller->m_scene->transformPlug(), parentTransformChanged, controller->m_motionBlurOptions ) ) + if( updateTransform( controller->m_scene->transformPlug(), parentTransformChanged ) ) { m_changedComponents |= TransformComponent; } @@ -453,7 +453,7 @@ class RenderController::SceneGraph // Object - if( ( m_dirtyComponents & ObjectComponent ) && updateObject( controller->m_scene->objectPlug(), type, controller->m_renderer.get(), controller->m_globals.get(), controller->m_scene.get(), controller->m_lightLinks.get(), controller->m_motionBlurOptions ) ) + if( ( m_dirtyComponents & ObjectComponent ) && updateObject( controller->m_scene->objectPlug(), type, controller->m_renderer.get(), controller->m_globals.get(), controller->m_scene.get(), controller->m_lightLinks.get() ) ) { m_changedComponents |= ObjectComponent; } @@ -477,7 +477,7 @@ class RenderController::SceneGraph { // Failed to apply attributes - must replace entire object. m_objectHash = MurmurHash(); - if( updateObject( controller->m_scene->objectPlug(), type, controller->m_renderer.get(), controller->m_globals.get(), controller->m_scene.get(), controller->m_lightLinks.get(), controller->m_motionBlurOptions ) ) + if( updateObject( controller->m_scene->objectPlug(), type, controller->m_renderer.get(), controller->m_globals.get(), controller->m_scene.get(), controller->m_lightLinks.get() ) ) { m_changedComponents |= ObjectComponent; controller->m_failedAttributeEdits++; @@ -722,7 +722,7 @@ class RenderController::SceneGraph } // Returns true if the transform changed. - bool updateTransform( const M44fPlug *transformPlug, bool parentTransformChanged, const MotionBlurOptions &motionBlurOptions ) + bool updateTransform( const M44fPlug *transformPlug, bool parentTransformChanged ) { if( parentTransformChanged ) { @@ -769,7 +769,7 @@ class RenderController::SceneGraph } // Returns true if the object changed. - bool updateObject( const ObjectPlug *objectPlug, Type type, IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const ScenePlug *scene, LightLinks *lightLinks, const MotionBlurOptions &motionBlurOptions ) + bool updateObject( const ObjectPlug *objectPlug, Type type, IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const ScenePlug *scene, LightLinks *lightLinks ) { const bool hadObjectInterface = static_cast( m_objectInterface ); if( type == NoType || m_drawMode != VisibleSet::Visibility::Visible ) From 888d6c0bd81286e89002778e4a8347347c45df0b Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 18 Sep 2023 14:57:41 +0100 Subject: [PATCH 6/8] RenderController : Support `render:includedPurposes` option --- include/GafferScene/RenderController.h | 3 +- .../GafferSceneTest/RenderControllerTest.py | 77 +++++++++++++++++++ src/GafferScene/RenderController.cpp | 57 +++++++++++++- 3 files changed, 133 insertions(+), 4 deletions(-) diff --git a/include/GafferScene/RenderController.h b/include/GafferScene/RenderController.h index 9d7468f13a2..ecbed4a5bf8 100644 --- a/include/GafferScene/RenderController.h +++ b/include/GafferScene/RenderController.h @@ -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 diff --git a/python/GafferSceneTest/RenderControllerTest.py b/python/GafferSceneTest/RenderControllerTest.py index e6f3b4b37e8..9cdf17d963b 100644 --- a/python/GafferSceneTest/RenderControllerTest.py +++ b/python/GafferSceneTest/RenderControllerTest.py @@ -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() diff --git a/src/GafferScene/RenderController.cpp b/src/GafferScene/RenderController.cpp index 9ab04db7ef3..9b2a89fd5d0 100644 --- a/src/GafferScene/RenderController.cpp +++ b/src/GafferScene/RenderController.cpp @@ -82,10 +82,14 @@ const InternedString g_compoundRendererName( "Compound" ); const InternedString g_cameraGlobalName( "option:render:camera" ); const InternedString g_transformBlurOptionName( "option:render:transformBlur" ); const InternedString g_deformationBlurOptionName( "option:render:deformationBlur" ); +const InternedString g_includedPurposesOptionName( "option:render:includedPurposes" ); +const InternedString g_purposeAttributeName( "usd:purpose" ); const InternedString g_visibleAttributeName( "scene:visible" ); const InternedString g_rendererContextName( "scene:renderer" ); +const std::string g_defaultPurpose( "default" ); + bool visible( const CompoundObject *attributes ) { const IECore::BoolData *d = attributes->member( g_visibleAttributeName ); @@ -106,6 +110,34 @@ bool cameraGlobalsChanged( const CompoundObject *globals, const CompoundObject * return *camera1 != *camera2; } +const IECore::ConstStringVectorDataPtr g_defaultIncludedPurposes = new StringVectorData( { "default", "render", "proxy", "guide" } ); + +bool includedPurposesChanged( const CompoundObject *globals, const CompoundObject *previousGlobals ) +{ + if( !previousGlobals ) + { + return true; + } + + auto *v1 = globals->member( g_includedPurposesOptionName ); + v1 = v1 ? v1 : g_defaultIncludedPurposes.get(); + + auto *v2 = previousGlobals->member( g_includedPurposesOptionName ); + v2 = v2 ? v2 : g_defaultIncludedPurposes.get(); + + return *v1 != *v2; +} + +bool purposeIncluded( const CompoundObject *attributes, const CompoundObject *globals ) +{ + const auto purposeData = attributes->member( g_purposeAttributeName ); + const std::string &purpose = purposeData ? purposeData->readable() : g_defaultPurpose; + + const auto includedPurposesData = globals->member( g_includedPurposesOptionName ); + const vector &includedPurposes = includedPurposesData ? includedPurposesData->readable() : g_defaultIncludedPurposes->readable(); + return std::find( includedPurposes.begin(), includedPurposes.end(), purpose ) != includedPurposes.end(); +} + // This is for the very specific case of determining change for global // attributes, where we need to avoid comparisons of certain synthetic members // that are only present in previousFullAttributes. @@ -326,7 +358,7 @@ class RenderController::SceneGraph // Constructs the root of the scene graph. // Children are constructed using updateChildren(). SceneGraph() - : m_parent( nullptr ), m_fullAttributes( new CompoundObject ), m_dirtyComponents( AllComponents ), m_changedComponents( NoComponent ) + : m_parent( nullptr ), m_fullAttributes( new CompoundObject ), m_purposeIncluded( true ), m_dirtyComponents( AllComponents ), m_changedComponents( NoComponent ) { clear(); } @@ -424,6 +456,20 @@ class RenderController::SceneGraph clean( AttributesComponent ); + // Purpose + + if( ( m_changedComponents & AttributesComponent ) || ( changedGlobals & IncludedPurposesGlobalComponent ) ) + { + const bool purposeIncludedPreviously = m_purposeIncluded; + m_purposeIncluded = purposeIncluded( m_fullAttributes.get(), controller->m_globals.get() ); + if( m_purposeIncluded != purposeIncludedPreviously ) + { + // We'll need to hide or show the object by considering `m_purposeIncluded` in + // `updateObject().` + m_dirtyComponents |= ObjectComponent; + } + } + // Transform const bool parentTransformChanged = m_parent && ( m_parent->m_changedComponents & TransformComponent ); @@ -647,7 +693,7 @@ class RenderController::SceneGraph private : SceneGraph( const InternedString &name, const SceneGraph *parent ) - : m_name( name ), m_parent( parent ), m_fullAttributes( new CompoundObject ) + : m_name( name ), m_parent( parent ), m_fullAttributes( new CompoundObject ), m_purposeIncluded( true ) { clear(); } @@ -772,7 +818,7 @@ class RenderController::SceneGraph bool updateObject( const ObjectPlug *objectPlug, Type type, IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const ScenePlug *scene, LightLinks *lightLinks ) { const bool hadObjectInterface = static_cast( m_objectInterface ); - if( type == NoType || m_drawMode != VisibleSet::Visibility::Visible ) + if( type == NoType || m_drawMode != VisibleSet::Visibility::Visible || !m_purposeIncluded ) { clearObject(); return hadObjectInterface; @@ -1103,6 +1149,7 @@ class RenderController::SceneGraph IECore::CompoundObjectPtr m_fullAttributes; IECoreScenePreview::Renderer::AttributesInterfacePtr m_attributesInterface; IECore::MurmurHash m_lightLinksHash; + bool m_purposeIncluded; IECore::MurmurHash m_transformHash; std::vector m_fullTransform; @@ -1598,6 +1645,10 @@ void RenderController::updateInternal( const ProgressCallback &callback, const I { m_changedGlobalComponents |= CameraOptionsGlobalComponent; } + if( includedPurposesChanged( globals.get(), m_globals.get() ) ) + { + m_changedGlobalComponents |= IncludedPurposesGlobalComponent; + } m_globals = globals; } From e018cd8bfbc5e46e9cab5047305186b5a5152158 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 18 Sep 2023 16:55:13 +0100 Subject: [PATCH 7/8] SceneView : Add included purposes to the `drawingMode` menu --- Changes.md | 3 ++ python/GafferSceneUI/SceneViewUI.py | 28 +++++++++++++++++ src/GafferSceneUI/SceneView.cpp | 49 ++++++++++++++++++----------- startup/gui/viewer.py | 2 ++ 4 files changed, 63 insertions(+), 19 deletions(-) diff --git a/Changes.md b/Changes.md index 067a76a92b3..182b43ea912 100644 --- a/Changes.md +++ b/Changes.md @@ -4,6 +4,9 @@ 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 diff --git a/python/GafferSceneUI/SceneViewUI.py b/python/GafferSceneUI/SceneViewUI.py index 21619e94052..485700c7f0e 100644 --- a/python/GafferSceneUI/SceneViewUI.py +++ b/python/GafferSceneUI/SceneViewUI.py @@ -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( diff --git a/src/GafferSceneUI/SceneView.cpp b/src/GafferSceneUI/SceneView.cpp index 60d869f9404..e4fba02038e 100644 --- a/src/GafferSceneUI/SceneView.cpp +++ b/src/GafferSceneUI/SceneView.cpp @@ -289,23 +289,9 @@ class SceneView::DrawingMode : public Signals::Trackable DrawingMode( SceneView *view ) : m_view( view ) { - // We can implement many drawing mode controls via render options. - // They simply modify the state used to render existing - // renderables. Visualisers however, may generate different - // renderables all together and so we need to modify the in-scene - // attribute values rather than any renderer option, which will - // cause the visualisers to be re-evaluated. We use a general - // purpose CustomAttributes preprocessor to set globals attributes - // with the desired values. This allows them to be overridden at - // specific locations in the user's graph if desired. - // Global attributes preprocessor - - m_preprocessor = new CustomAttributes(); - m_preprocessor->globalPlug()->setValue( true ); - CompoundDataPlug *attr = m_preprocessor->attributesPlug(); - - // View plugs controlling renderer options + // Plugs controlling OpenGL render options. We use these to + // drive `SceneGadget::setOpenGLOptions()` directly. ValuePlugPtr drawingMode = new ValuePlug( "drawingMode" ); m_view->addChild( drawingMode ); @@ -323,9 +309,34 @@ class SceneView::DrawingMode : public Signals::Trackable drawingMode->addChild( points ); points->addChild( new BoolPlug( "useGLPoints", Plug::In, true ) ); - // View plugs controlling attribute values + // A preprocessor which modifies the scene before it is displayed by + // the SceneGadget. We use this for drawing settings that aren't + // simple OpenGL options. + + m_preprocessor = new SceneProcessor(); + + CustomAttributesPtr customAttributes = new CustomAttributes(); + m_preprocessor->addChild( customAttributes ); + customAttributes->inPlug()->setInput( m_preprocessor->inPlug() ); + customAttributes->globalPlug()->setValue( true ); + CompoundDataPlug *attr = customAttributes->attributesPlug(); - // General : + StandardOptionsPtr standardOptions = new StandardOptions(); + m_preprocessor->addChild( standardOptions ); + standardOptions->inPlug()->setInput( customAttributes->outPlug() ); + m_preprocessor->outPlug()->setInput( standardOptions->outPlug() ); + + // Included purposes + + auto *includedPurposesPlug = standardOptions->optionsPlug()->getChild( "includedPurposes" ); + auto viewIncludedPurposesPlug = boost::static_pointer_cast( + includedPurposesPlug->createCounterpart( "includedPurposes", Plug::In ) + ); + viewIncludedPurposesPlug->enabledPlug()->setValue( true ); + drawingMode->addChild( viewIncludedPurposesPlug ); + includedPurposesPlug->setInput( viewIncludedPurposesPlug ); + + // Visualiser settings. ValuePlugPtr visualiser = new ValuePlug( "visualiser" ); drawingMode->addChild( visualiser ); @@ -431,7 +442,7 @@ class SceneView::DrawingMode : public Signals::Trackable sceneGadget()->setOpenGLOptions( options.get() ); } - CustomAttributesPtr m_preprocessor; + SceneProcessorPtr m_preprocessor; SceneView *m_view; diff --git a/startup/gui/viewer.py b/startup/gui/viewer.py index 72233a1698f..7639548175d 100644 --- a/startup/gui/viewer.py +++ b/startup/gui/viewer.py @@ -69,6 +69,8 @@ def __sceneView( plug ) : GafferUI.View.registerView( GafferScene.ScenePlug.staticTypeId(), __sceneView ) +Gaffer.Metadata.registerValue( GafferSceneUI.SceneView, "drawingMode.includedPurposes.value", "userDefault", IECore.StringVectorData( [ "default", "proxy" ] ) ) + # Add items to the viewer's right click menu def __viewContextMenu( viewer, view, menuDefinition ) : From bf593aa9de398f06c19887edac0d66887bf4bc96 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 22 Sep 2023 10:05:38 +0100 Subject: [PATCH 8/8] fixup! StandardOptions : Add `render:includedPurposes` option --- python/GafferSceneUI/StandardOptionsUI.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/python/GafferSceneUI/StandardOptionsUI.py b/python/GafferSceneUI/StandardOptionsUI.py index 9d3f8c62d61..16fbdf77ea3 100644 --- a/python/GafferSceneUI/StandardOptionsUI.py +++ b/python/GafferSceneUI/StandardOptionsUI.py @@ -80,7 +80,11 @@ def __cameraSummary( plug ) : def __purposeSummary( plug ) : if plug["includedPurposes"]["enabled"].getValue() : - return ", ".join( [ p.capitalize() for p in plug["includedPurposes"]["value"].getValue() ] ) + purposes = plug["includedPurposes"]["value"].getValue() + if purposes : + return ", ".join( [ p.capitalize() for p in purposes ] ) + else : + return "None" return "" @@ -339,7 +343,8 @@ def __statisticsSummary( plug ) : "description", """ Limits the objects included in the render according to the values of their `usd:purpose` - attribute. + attribute. The "Default" purpose includes all objects which have no `usd:purpose` attribute; + other than for debugging, there is probably no good reason to omit it. > Tip : Use the USDAttributes node to assign the `usd:purpose` attribute. """, @@ -460,7 +465,12 @@ def __init__( self, plugs, **kw ) : def _updateFromValues( self, values, exception ) : self.__currentValue = sole( values ) - self.__menuButton.setText( ", ".join( [ p.capitalize() for p in self.__currentValue ] ) if self.__currentValue is not None else "---" ) + if self.__currentValue : + self.__menuButton.setText( ", ".join( [ p.capitalize() for p in self.__currentValue ] ) ) + else : + # A value of `None` means we have multiple different values (from different plugs), + # and a value of `[]` means the user has disabled all purposes. + self.__menuButton.setText( "---" if self.__currentValue is None else "None" ) self.__menuButton.setErrored( exception is not None ) def _updateFromEditable( self ) :