From fad41ba0b89be2995495031b723307dc14654ce6 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 25 Sep 2023 13:59:00 +0100 Subject: [PATCH] RenderController : Pass RenderOptions changes to Capsules --- Changes.md | 4 +- include/GafferScene/Private/RendererAlgo.h | 1 + include/GafferScene/RenderController.h | 1 + .../GafferSceneTest/RenderControllerTest.py | 99 +++++++++++++++++++ src/GafferScene/RenderController.cpp | 39 ++++++-- src/GafferScene/RendererAlgo.cpp | 7 ++ src/GafferSceneModule/RenderBinding.cpp | 1 + 7 files changed, 145 insertions(+), 7 deletions(-) diff --git a/Changes.md b/Changes.md index 1c1f525b096..7eaf4377772 100644 --- a/Changes.md +++ b/Changes.md @@ -13,7 +13,9 @@ Fixes ----- - GraphEditor : Removed dynamic raster-space sizing of focus icon, as it caused excessive overlap with other nodes at certain zoom levels and on certain high resolution displays (#5435). -- Encapsulate : Fixed bug where global attributes (from the point of encapsulation) were baked into the contents of the capsule instead of being inherited naturally (from the node being rendered). +- Encapsulate : + - Fixed bug where global attributes (from the point of encapsulation) were baked into the contents of the capsule instead of being inherited naturally (from the node being rendered). + - Fixed motion blur so that global settings are now taken from the downstream node being rendered, not from the input to the Encapsulate node. API --- diff --git a/include/GafferScene/Private/RendererAlgo.h b/include/GafferScene/Private/RendererAlgo.h index 34e9697c26b..11f7bf81baf 100644 --- a/include/GafferScene/Private/RendererAlgo.h +++ b/include/GafferScene/Private/RendererAlgo.h @@ -69,6 +69,7 @@ struct GAFFERSCENE_API RenderOptions RenderOptions( const ScenePlug *scene ); RenderOptions( const RenderOptions &other ) = default; RenderOptions& operator=( const RenderOptions &other ) = default; + bool operator==( const RenderOptions &other ) const; /// The globals from the scene. IECore::ConstCompoundObjectPtr globals; /// Convenient access to specific properties, taking into account default diff --git a/include/GafferScene/RenderController.h b/include/GafferScene/RenderController.h index 66b23656a3f..dc8e1cad1bb 100644 --- a/include/GafferScene/RenderController.h +++ b/include/GafferScene/RenderController.h @@ -122,6 +122,7 @@ class GAFFERSCENE_API RenderController : public Gaffer::Signals::Trackable DeformationBlurGlobalComponent = 32, CameraShutterGlobalComponent = 64, IncludedPurposesGlobalComponent = 128, + CapsuleAffectingGlobalComponents = TransformBlurGlobalComponent | DeformationBlurGlobalComponent | IncludedPurposesGlobalComponent, AllGlobalComponents = GlobalsGlobalComponent | SetsGlobalComponent | RenderSetsGlobalComponent | CameraOptionsGlobalComponent | TransformBlurGlobalComponent | DeformationBlurGlobalComponent | IncludedPurposesGlobalComponent }; diff --git a/python/GafferSceneTest/RenderControllerTest.py b/python/GafferSceneTest/RenderControllerTest.py index 9cdf17d963b..55e8329c512 100644 --- a/python/GafferSceneTest/RenderControllerTest.py +++ b/python/GafferSceneTest/RenderControllerTest.py @@ -1584,5 +1584,104 @@ def testIncludedPurposes( self ) : controller.update() self.assertIsNotNone( renderer.capturedObject( "/group/sphere" ) ) + def testCapsuleRenderOptions( self ) : + + rootFilter = GafferScene.PathFilter() + rootFilter["paths"].setValue( IECore.StringVectorData( [ "*" ] ) ) + + cube = GafferScene.Cube() + + encapsulate = GafferScene.Encapsulate() + encapsulate["in"].setInput( cube["out"] ) + encapsulate["filter"].setInput( rootFilter["out"] ) + + standardOptions = GafferScene.StandardOptions() + standardOptions["in"].setInput( encapsulate["out"] ) + standardOptions["options"]["includedPurposes"]["enabled"].setValue( True ) + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer() + controller = GafferScene.RenderController( standardOptions["out"], Gaffer.Context(), renderer ) + controller.setMinimumExpansionDepth( 2 ) + + def assertExpectedRenderOptions() : + + captured = renderer.capturedObject( "/cube" ) + self.assertIsNotNone( captured ) + self.assertEqual( len( captured.capturedSamples() ), 1 ) + self.assertIsInstance( captured.capturedSamples()[0], GafferScene.Capsule ) + self.assertEqual( + captured.capturedSamples()[0].getRenderOptions(), + GafferScene.Private.RendererAlgo.RenderOptions( standardOptions["out"] ) + ) + + # Check that a capsule has the initial RenderOptions we expect. + + self.assertTrue( controller.updateRequired() ) + controller.update() + assertExpectedRenderOptions() + + # Check that the capsule is updated when the RenderOptions change. + + standardOptions["options"]["includedPurposes"]["value"].setValue( IECore.StringVectorData( [ "default", "proxy" ] ) ) + self.assertTrue( controller.updateRequired() ) + controller.update() + assertExpectedRenderOptions() + + # Check that the capsule is not updated when the globals change + # but the RenderOptions that the capsule uses aren't affected. + + capture = renderer.capturedObject( "/cube" ) + standardOptions["options"]["performanceMonitor"]["enabled"].setValue( True ) + self.assertTrue( controller.updateRequired() ) + controller.update() + self.assertTrue( renderer.capturedObject( "/cube" ).isSame( capture ) ) + + # Change RenderOptions again, this time to the default, and check we + # get another update. + + del capture + standardOptions["options"]["includedPurposes"]["value"].setValue( IECore.StringVectorData( [ "default", "render", "proxy", "guide" ] ) ) + self.assertTrue( controller.updateRequired() ) + controller.update() + assertExpectedRenderOptions() + + # Remove `includedPurposes` option, so it's not in the globals. The + # fallback is the same as the previous value, so we should get no + # update. + + capture = renderer.capturedObject( "/cube" ) + standardOptions["options"]["includedPurposes"]["enabled"].setValue( False ) + self.assertTrue( controller.updateRequired() ) + controller.update() + self.assertTrue( renderer.capturedObject( "/cube" ).isSame( capture ) ) + + def testNoUnnecessaryObjectUpdatesOnPurposeChange( self ) : + + cube = GafferScene.Cube() + + standardOptions = GafferScene.StandardOptions() + standardOptions["in"].setInput( cube["out"] ) + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer() + controller = GafferScene.RenderController( standardOptions["out"], Gaffer.Context(), renderer ) + controller.setMinimumExpansionDepth( 2 ) + + # Check initial capture + + self.assertTrue( controller.updateRequired() ) + controller.update() + capture = renderer.capturedObject( "/cube" ) + self.assertIsNotNone( capture ) + + # Check that changing the purposes doesn't make an unnecessary edit for + # the object. It was included before and it is still included, so we + # want to reuse the old object. + + standardOptions["options"]["includedPurposes"]["enabled"].setValue( True ) + standardOptions["options"]["includedPurposes"]["value"].setValue( IECore.StringVectorData( [ "default", "proxy" ] ) ) + self.assertTrue( controller.updateRequired() ) + controller.update() + self.assertTrue( capture.isSame( renderer.capturedObject( "/cube" ) ) ) + if __name__ == "__main__": unittest.main() diff --git a/src/GafferScene/RenderController.cpp b/src/GafferScene/RenderController.cpp index 5fa551b6849..f5a32cba0bb 100644 --- a/src/GafferScene/RenderController.cpp +++ b/src/GafferScene/RenderController.cpp @@ -36,6 +36,7 @@ #include "GafferScene/RenderController.h" +#include "GafferScene/Capsule.h" #include "GafferScene/Private/IECoreScenePreview/Placeholder.h" #include "GafferScene/SceneAlgo.h" @@ -173,6 +174,7 @@ struct ObjectInterfaceHandle : public boost::noncopyable using RemovalCallback = std::function; ObjectInterfaceHandle() + : m_isCapsule( false ) { } @@ -189,7 +191,7 @@ struct ObjectInterfaceHandle : public boost::noncopyable assign( p, RemovalCallback() ); } - void assign( const IECoreScenePreview::Renderer::ObjectInterfacePtr &p, const RemovalCallback &removalCallback ) + void assign( const IECoreScenePreview::Renderer::ObjectInterfacePtr &p, const RemovalCallback &removalCallback, bool isCapsule = false ) { if( m_removalCallback ) { @@ -197,6 +199,7 @@ struct ObjectInterfaceHandle : public boost::noncopyable } m_objectInterface = p; m_removalCallback = removalCallback; + m_isCapsule = isCapsule; } IECoreScenePreview::Renderer::ObjectInterface *operator->() const @@ -214,10 +217,16 @@ struct ObjectInterfaceHandle : public boost::noncopyable return m_objectInterface.get(); } + bool isCapsule() const + { + return m_isCapsule; + } + private : IECoreScenePreview::Renderer::ObjectInterfacePtr m_objectInterface; RemovalCallback m_removalCallback; + bool m_isCapsule; }; @@ -471,7 +480,14 @@ class RenderController::SceneGraph // Object - if( ( m_dirtyComponents & ObjectComponent ) && updateObject( controller->m_scene->objectPlug(), type, controller->m_renderer.get(), controller->m_renderOptions.globals.get(), controller->m_scene.get(), controller->m_lightLinks.get() ) ) + if( m_objectInterface.isCapsule() && ( changedGlobals & CapsuleAffectingGlobalComponents ) ) + { + // Account for `Capsule::setRenderOptions()` being called in `updateObject()`. + m_dirtyComponents |= ObjectComponent; + m_objectHash = MurmurHash(); + } + + if( ( m_dirtyComponents & ObjectComponent ) && updateObject( controller->m_scene->objectPlug(), type, controller->m_renderer.get(), controller->m_renderOptions, controller->m_scene.get(), controller->m_lightLinks.get() ) ) { m_changedComponents |= ObjectComponent; } @@ -495,7 +511,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_renderOptions.globals.get(), controller->m_scene.get(), controller->m_lightLinks.get() ) ) + if( updateObject( controller->m_scene->objectPlug(), type, controller->m_renderer.get(), controller->m_renderOptions, controller->m_scene.get(), controller->m_lightLinks.get() ) ) { m_changedComponents |= ObjectComponent; controller->m_failedAttributeEdits++; @@ -787,7 +803,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 ) + bool updateObject( const ObjectPlug *objectPlug, Type type, IECoreScenePreview::Renderer *renderer, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const ScenePlug *scene, LightLinks *lightLinks ) { const bool hadObjectInterface = static_cast( m_objectInterface ); if( type == NoType || m_drawMode != VisibleSet::Visibility::Visible || !m_purposeIncluded ) @@ -905,7 +921,7 @@ class RenderController::SceneGraph if( auto cameraSample = runTimeCast( sample.get() ) ) { IECoreScene::CameraPtr cameraSampleCopy = cameraSample->copy(); - SceneAlgo::applyCameraGlobals( cameraSampleCopy.get(), globals, scene ); + SceneAlgo::applyCameraGlobals( cameraSampleCopy.get(), renderOptions.globals.get(), scene ); cameraSamples.push_back( cameraSampleCopy ); } } @@ -958,7 +974,18 @@ class RenderController::SceneGraph if( samples.size() == 1 ) { - m_objectInterface = renderer->object( name, samples[0].get(), attributesInterface( renderer ) ); + ConstObjectPtr sample = samples[0]; + if( auto capsule = runTimeCast( sample.get() ) ) + { + CapsulePtr capsuleCopy = capsule->copy(); + capsuleCopy->setRenderOptions( renderOptions ); + sample = capsuleCopy; + } + m_objectInterface.assign( + renderer->object( name, sample.get(), attributesInterface( renderer ) ), + ObjectInterfaceHandle::RemovalCallback(), + /* isCapsule = */ runTimeCast( sample.get() ) + ); } else { diff --git a/src/GafferScene/RendererAlgo.cpp b/src/GafferScene/RendererAlgo.cpp index 5f9b7549db4..ade0b58ed06 100644 --- a/src/GafferScene/RendererAlgo.cpp +++ b/src/GafferScene/RendererAlgo.cpp @@ -126,6 +126,13 @@ RenderOptions::RenderOptions( const ScenePlug *scene ) includedPurposes = includedPurposesData ? includedPurposesData : g_defaultIncludedPurposes; } +bool RenderOptions::operator==( const RenderOptions &other ) const +{ + // No need to test other fields because they are all derived directly + // from the globals. + return *globals == *other.globals && shutter == other.shutter; +} + bool RenderOptions::purposeIncluded( const CompoundObject *attributes ) const { const auto purposeData = attributes->member( g_purposeAttributeName ); diff --git a/src/GafferSceneModule/RenderBinding.cpp b/src/GafferSceneModule/RenderBinding.cpp index 52c7eefd13d..c2286b12d4e 100644 --- a/src/GafferSceneModule/RenderBinding.cpp +++ b/src/GafferSceneModule/RenderBinding.cpp @@ -451,6 +451,7 @@ void GafferSceneModule::bindRender() .def_readwrite( "deformationBlur", &GafferScene::Private::RendererAlgo::RenderOptions::deformationBlur ) .def_readwrite( "shutter", &GafferScene::Private::RendererAlgo::RenderOptions::shutter ) .def_readwrite( "includedPurposes", &GafferScene::Private::RendererAlgo::RenderOptions::includedPurposes ) + .def( self == self ) ; def( "objectSamples", &objectSamplesWrapper, ( arg( "objectPlug" ), arg( "sampleTimes" ), arg( "hash" ) = object(), arg( "_copy" ) = true ) );