diff --git a/Changes.md b/Changes.md index d57c39ebaae..f1815ea6369 100644 --- a/Changes.md +++ b/Changes.md @@ -17,6 +17,9 @@ Fixes - StringPlugValueWidget : Fixed bug handling Esc. - Arnold : Fixed unnecessary `opaque` attribute deprecation warnings. These are now only emitted in the case that `opaque` has been explicitly turned off. - ShaderUI : Fixed bug causing identical but independent shaders in a shader network from being included in the shader browser. +- 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/Capsule.h b/include/GafferScene/Capsule.h index 1cb625afbf5..4856bfbc4e7 100644 --- a/include/GafferScene/Capsule.h +++ b/include/GafferScene/Capsule.h @@ -37,6 +37,7 @@ #pragma once #include "GafferScene/Private/IECoreScenePreview/Procedural.h" +#include "GafferScene/Private/RendererAlgo.h" #include "GafferScene/ScenePlug.h" #include "Gaffer/Context.h" @@ -90,6 +91,11 @@ class GAFFERSCENE_API Capsule : public IECoreScenePreview::Procedural const ScenePlug::ScenePath &root() const; const Gaffer::Context *context() const; + /// Used to apply the correct render settings to the capsule before rendering it. + /// For internal use only. + void setRenderOptions( const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions ); + std::optional getRenderOptions() const; + private : void throwIfNoScene() const; diff --git a/include/GafferScene/Private/RendererAlgo.h b/include/GafferScene/Private/RendererAlgo.h index de0b599bb3b..11f7bf81baf 100644 --- a/include/GafferScene/Private/RendererAlgo.h +++ b/include/GafferScene/Private/RendererAlgo.h @@ -63,14 +63,34 @@ namespace Private namespace RendererAlgo { +struct GAFFERSCENE_API RenderOptions +{ + 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 + /// values if they have not been specified in the scene. + bool transformBlur; + bool deformationBlur; + Imath::V2f shutter; + IECore::ConstStringVectorDataPtr includedPurposes; + /// Returns true if `includedPurposes` includes the purpose defined by + /// `attributes`. + bool purposeIncluded( const IECore::CompoundObject *attributes ) const; +}; + /// Creates the directories necessary to receive the outputs defined in globals. GAFFERSCENE_API void createOutputDirectories( const IECore::CompoundObject *globals ); -/// Set the "times" to a list of times to sample the transform or deformation of a location at, based on the -/// "motionBlur" enable coming from the options, a shutter, and location attributes. Returns a boolean for -/// whether times has been altered ( returns false if times was already set correctly ). -GAFFERSCENE_API bool transformMotionTimes( bool motionBlur, const Imath::V2f &shutter, const IECore::CompoundObject *attributes, std::vector × ); -GAFFERSCENE_API bool deformationMotionTimes( bool motionBlur, const Imath::V2f &shutter, const IECore::CompoundObject *attributes, std::vector × ); +/// Sets `times` to a list of times to sample the transform or deformation of a +/// location at, based on the render options and location attributes. Returns `true` +/// if `times` was altered and `false` if it was already set correctly. +GAFFERSCENE_API bool transformMotionTimes( const RenderOptions &renderOptions, const IECore::CompoundObject *attributes, std::vector × ); +GAFFERSCENE_API bool deformationMotionTimes( const RenderOptions &renderOptions, const IECore::CompoundObject *attributes, std::vector × ); /// Samples the local transform from the current location in preparation for output to the renderer. /// "samples" will be set to contain one sample for each sampleTime, unless the samples are all identical, @@ -259,10 +279,10 @@ class GAFFERSCENE_API LightLinks : boost::noncopyable }; -GAFFERSCENE_API void outputCameras( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, IECoreScenePreview::Renderer *renderer ); -GAFFERSCENE_API void outputLightFilters( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ); -GAFFERSCENE_API void outputLights( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ); -GAFFERSCENE_API void outputObjects( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, const LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer, const ScenePlug::ScenePath &root = ScenePlug::ScenePath() ); +GAFFERSCENE_API void outputCameras( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, IECoreScenePreview::Renderer *renderer ); +GAFFERSCENE_API void outputLightFilters( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ); +GAFFERSCENE_API void outputLights( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ); +GAFFERSCENE_API void outputObjects( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, const LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer, const ScenePlug::ScenePath &root = ScenePlug::ScenePath() ); } // namespace RendererAlgo diff --git a/include/GafferScene/RenderController.h b/include/GafferScene/RenderController.h index ecbed4a5bf8..c257cb10088 100644 --- a/include/GafferScene/RenderController.h +++ b/include/GafferScene/RenderController.h @@ -122,14 +122,15 @@ 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 }; - struct MotionBlurOptions + struct Unused { - bool transformBlur = false; - bool deformationBlur = false; - Imath::V2f shutter = Imath::V2f( 0 ); + bool unused1; + bool unused2; + Imath::V2f unused3; }; void plugDirtied( const Gaffer::Plug *plug ); @@ -165,8 +166,8 @@ class GAFFERSCENE_API RenderController : public Gaffer::Signals::Trackable std::vector > m_sceneGraphs; unsigned m_dirtyGlobalComponents; unsigned m_changedGlobalComponents; - IECore::ConstCompoundObjectPtr m_globals; - MotionBlurOptions m_motionBlurOptions; + std::unique_ptr m_renderOptions; + Unused m_unused; Private::RendererAlgo::RenderSets m_renderSets; std::unique_ptr m_lightLinks; IECoreScenePreview::Renderer::ObjectInterfacePtr m_defaultCamera; diff --git a/python/GafferSceneTest/CapsuleTest.py b/python/GafferSceneTest/CapsuleTest.py index 96d05c41562..c99934be68f 100644 --- a/python/GafferSceneTest/CapsuleTest.py +++ b/python/GafferSceneTest/CapsuleTest.py @@ -34,7 +34,6 @@ # ########################################################################## -import inspect import unittest import IECore @@ -72,5 +71,69 @@ def test( self ) : self.assertEqual( capsuleCopy.bound(), sphere["out"].bound( "/" ) ) self.assertEqual( capsuleCopy.hash(), capsule.hash() ) + def testInheritedAttributesAreNotBakedIntoCapsuleContents( self ) : + + # Make a Capsule which inherits attributes from its parent + # and from the scene globals. + + sphere = GafferScene.Sphere() + group = GafferScene.Group() + group["in"][0].setInput( sphere["out"] ) + + groupFilter = GafferScene.PathFilter() + groupFilter["paths"].setValue( IECore.StringVectorData( [ "/group" ] ) ) + + groupAttributes = GafferScene.CustomAttributes() + groupAttributes["in"].setInput( group["out"] ) + groupAttributes["filter"].setInput( groupFilter["out"] ) + groupAttributes["attributes"].addChild( Gaffer.NameValuePlug( "groupAttribute", 10 ) ) + + globalAttributes = GafferScene.CustomAttributes() + globalAttributes["in"].setInput( groupAttributes["out"] ) + globalAttributes["global"].setValue( True ) + globalAttributes["attributes"].addChild( Gaffer.NameValuePlug( "globalAttribute", 20 ) ) + + encapsulate = GafferScene.Encapsulate() + encapsulate["in"].setInput( globalAttributes["out"] ) + encapsulate["filter"].setInput( groupFilter["out"] ) + + # Render it, and check that the capsule object had the inherited attributes applied to it. + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + GafferScene.Private.RendererAlgo.outputObjects( + encapsulate["out"], GafferScene.Private.RendererAlgo.RenderOptions( encapsulate["out"] ), GafferScene.Private.RendererAlgo.RenderSets( encapsulate["out"] ), GafferScene.Private.RendererAlgo.LightLinks(), + renderer + ) + + capturedGroup = renderer.capturedObject( "/group" ) + self.assertIsInstance( capturedGroup.capturedSamples()[0], GafferScene.Capsule ) + self.assertEqual( + capturedGroup.capturedAttributes().attributes(), + IECore.CompoundObject( { + "groupAttribute" : IECore.IntData( 10 ), + "globalAttribute" : IECore.IntData( 20 ), + "sets" : IECore.InternedStringVectorData(), + } ) + ) + + # Expand the capsule, and check that it didn't bake the inherited attributes onto + # its contents. It is the responsibity of the Renderer itself to take care of attribute + # inheritance, ideally doing it "live", so that changes to inherited attributes don't + # require re-expansion of the capsule. + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + capturedGroup.capturedSamples()[0].render( renderer ) + capturedSphere = renderer.capturedObject( "/sphere" ) + self.assertEqual( + capturedSphere.capturedAttributes().attributes(), + IECore.CompoundObject( { + "sets" : IECore.InternedStringVectorData(), + } ) + ) + if __name__ == "__main__": unittest.main() 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/python/GafferSceneTest/RendererAlgoTest.py b/python/GafferSceneTest/RendererAlgoTest.py index ef651635f21..6f7e799177e 100644 --- a/python/GafferSceneTest/RendererAlgoTest.py +++ b/python/GafferSceneTest/RendererAlgoTest.py @@ -36,6 +36,8 @@ import unittest +import imath + import IECore import Gaffer @@ -97,6 +99,7 @@ def testOutputCameras( self ) : options = GafferScene.StandardOptions() options["in"].setInput( camera["out"] ) + renderOptions = GafferScene.Private.RendererAlgo.RenderOptions( options["out"] ) renderSets = GafferScene.Private.RendererAlgo.RenderSets( options["out"] ) def expectedCamera( frame ) : @@ -105,7 +108,7 @@ def expectedCamera( frame ) : c.setFrame( frame ) camera = options["out"].object( "/camera" ) - GafferScene.SceneAlgo.applyCameraGlobals( camera, sceneGlobals, options["out"] ) + GafferScene.SceneAlgo.applyCameraGlobals( camera, options["out"].globals(), options["out"] ) return camera # Non-animated case @@ -113,8 +116,7 @@ def expectedCamera( frame ) : renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) - sceneGlobals = options["out"].globals() - GafferScene.Private.RendererAlgo.outputCameras( options["out"], sceneGlobals, renderSets, renderer ) + GafferScene.Private.RendererAlgo.outputCameras( options["out"], renderOptions, renderSets, renderer ) capturedCamera = renderer.capturedObject( "/camera" ) @@ -129,8 +131,8 @@ def expectedCamera( frame ) : renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) - sceneGlobals = options["out"].globals() - GafferScene.Private.RendererAlgo.outputCameras( options["out"], sceneGlobals, renderSets, renderer ) + renderOptions = GafferScene.Private.RendererAlgo.RenderOptions( options["out"] ) + GafferScene.Private.RendererAlgo.outputCameras( options["out"], renderOptions, renderSets, renderer ) capturedCamera = renderer.capturedObject( "/camera" ) self.assertEqual( capturedCamera.capturedSamples(), [ expectedCamera( 0.75 ), expectedCamera( 1.25 ) ] ) @@ -155,7 +157,8 @@ def testInvisibleCamera( self ) : ) with self.assertRaisesRegex( RuntimeError, "Camera \"/camera\" is hidden" ) : GafferScene.Private.RendererAlgo.outputCameras( - standardOptions["out"], standardOptions["out"].globals(), + standardOptions["out"], + GafferScene.Private.RendererAlgo.RenderOptions( standardOptions["out"] ), GafferScene.Private.RendererAlgo.RenderSets( standardOptions["out"] ), renderer ) @@ -317,14 +320,14 @@ def testLightSolo( self ) : # Output the lights to the renderer + renderOptions = GafferScene.Private.RendererAlgo.RenderOptions( soloSet["out"] ) renderSets = GafferScene.Private.RendererAlgo.RenderSets( soloSet["out"] ) lightLinks = GafferScene.Private.RendererAlgo.LightLinks() renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) - sceneGlobals = soloSet["out"].globals() - GafferScene.Private.RendererAlgo.outputLights( soloSet["out"], sceneGlobals, renderSets, lightLinks, renderer ) + GafferScene.Private.RendererAlgo.outputLights( soloSet["out"], renderOptions, renderSets, lightLinks, renderer ) # Check that the output is correct @@ -448,14 +451,14 @@ def testLightMute( self ) : # Output the lights to the renderer + renderOptions = GafferScene.Private.RendererAlgo.RenderOptions( muteAttributes["out"] ) renderSets = GafferScene.Private.RendererAlgo.RenderSets( muteAttributes["out"] ) lightLinks = GafferScene.Private.RendererAlgo.LightLinks() renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch ) - sceneGlobals = muteAttributes["out"].globals() - GafferScene.Private.RendererAlgo.outputLights( muteAttributes["out"], sceneGlobals, renderSets, lightLinks, renderer ) + GafferScene.Private.RendererAlgo.outputLights( muteAttributes["out"], renderOptions, renderSets, lightLinks, renderer ) # Check that the output is correct @@ -643,15 +646,15 @@ def purposeAttribute( purpose ) : def assertIncludedObjects( scene, includedPurposes, paths ) : - globals = IECore.CompoundObject() + renderOptions = GafferScene.Private.RendererAlgo.RenderOptions( scene ) if includedPurposes : - globals["option:render:includedPurposes"] = IECore.StringVectorData( includedPurposes ) + renderOptions.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(), + group["out"], renderOptions, GafferScene.Private.RendererAlgo.RenderSets( scene ), GafferScene.Private.RendererAlgo.LightLinks(), renderer ) @@ -748,5 +751,179 @@ def assertIncludedObjects( scene, includedPurposes, paths ) : } ) + def testCapsuleMotionBlur( self ) : + + sphere = GafferScene.Sphere() + sphere["type"].setValue( sphere.Type.Primitive ) + sphere["expression"] = Gaffer.Expression() + sphere["expression"].setExpression( + 'parent["radius"] = context.getFrame() + 1; parent["transform"]["translate"]["x"] = context.getFrame()' + ) + + group = GafferScene.Group() + group["in"][0].setInput( sphere["out"] ) + + groupFilter = GafferScene.PathFilter() + groupFilter["paths"].setValue( IECore.StringVectorData( [ "/group" ] ) ) + + encapsulate = GafferScene.Encapsulate() + encapsulate["in"].setInput( group["out"] ) + encapsulate["filter"].setInput( groupFilter["out"] ) + + camera = GafferScene.Camera() + camera["renderSettingOverrides"]["shutter"]["value"].setValue( imath.V2f( -0.5, 0.5 ) ) + + parent = GafferScene.Parent() + parent["in"].setInput( encapsulate["out"] ) + parent["parent"].setValue( "/" ) + parent["in"].setInput( encapsulate["out"] ) + parent["children"][0].setInput( camera["out"] ) + + standardOptions = GafferScene.StandardOptions() + standardOptions["in"].setInput( parent["out"] ) + standardOptions["options"]["transformBlur"]["enabled"].setValue( True ) + standardOptions["options"]["deformationBlur"]["enabled"].setValue( True ) + standardOptions["options"]["shutter"]["enabled"].setValue( True ) + standardOptions["options"]["renderCamera"]["enabled"].setValue( True ) + standardOptions["options"]["renderCamera"]["value"].setValue( "/camera" ) + + def assertExpectedMotion( scene ) : + + # Render to capture Capsule. This will always contain only a single + # motion sample because procedurals themselves can't have motion samples + # (although their contents can). + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + GafferScene.Private.RendererAlgo.outputObjects( + scene, GafferScene.Private.RendererAlgo.RenderOptions( scene ), + GafferScene.Private.RendererAlgo.RenderSets( scene ), GafferScene.Private.RendererAlgo.LightLinks(), + renderer + ) + + capsule = renderer.capturedObject( "/group" ) + self.assertEqual( len( capsule.capturedSamples() ), 1 ) + capsule = capsule.capturedSamples()[0] + self.assertIsInstance( capsule, GafferScene.Capsule ) + + # Render again to expand contents of Capsule. + + capsuleRenderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + capsule.render( capsuleRenderer ) + + # Check transform blur of contents matches what was requested by the scene globals. + + motionTimes = list( GafferScene.SceneAlgo.shutter( scene.globals(), scene ) ) + sphere = capsuleRenderer.capturedObject( "/sphere" ) + if scene.globals()["option:render:transformBlur"].value : + transformTimes = motionTimes + else : + transformTimes = [ Gaffer.Context.current().getFrame() ] + + self.assertEqual( len( sphere.capturedTransforms() ), len( transformTimes ) ) + self.assertEqual( sphere.capturedTransformTimes(), transformTimes if len( transformTimes ) > 1 else [] ) + for index, time in enumerate( transformTimes ) : + with Gaffer.Context() as context : + context.setFrame( time ) + self.assertEqual( sphere.capturedTransforms()[index], capsule.scene().transform( "/group/sphere" ) ) + + # Check deformation blur of contents matches what was requested by the scene globals. + + if scene.globals()["option:render:deformationBlur"].value : + objectTimes = motionTimes + else : + objectTimes = [ Gaffer.Context.current().getFrame() ] + + self.assertEqual( len( sphere.capturedSamples() ), len( objectTimes ) ) + self.assertEqual( sphere.capturedSampleTimes(), objectTimes if len( objectTimes ) > 1 else [] ) + for index, time in enumerate( objectTimes ) : + with Gaffer.Context() as context : + context.setFrame( time ) + self.assertEqual( sphere.capturedSamples()[index].radius, capsule.scene().object( "/group/sphere" ).radius ) + + for frame in ( 0, 1 ) : + for deformation in ( False, True ) : + for transform in ( False, True ) : + for shutter in ( imath.V2f( -0.25, 0.25 ), imath.V2f( 0, 0.5 ) ) : + for overrideShutter in ( False, True ) : + with self.subTest( frame = frame, deformation = deformation, transform = transform, shutter = shutter, overrideShutter = overrideShutter ) : + standardOptions["options"]["transformBlur"]["value"].setValue( transform ) + standardOptions["options"]["deformationBlur"]["value"].setValue( deformation ) + standardOptions["options"]["shutter"]["value"].setValue( shutter ) + camera["renderSettingOverrides"]["shutter"]["enabled"].setValue( overrideShutter ) + with Gaffer.Context() as context : + context.setFrame( frame ) + assertExpectedMotion( standardOptions["out"] ) + + def testCapsulePurposes( self ) : + + rootFilter = GafferScene.PathFilter() + rootFilter["paths"].setValue( IECore.StringVectorData( [ "*" ] ) ) + + cube = GafferScene.Cube() + + attributes = GafferScene.CustomAttributes() + attributes["in"].setInput( cube["out"] ) + attributes["filter"].setInput( rootFilter["out"] ) + attributes["attributes"].addChild( Gaffer.NameValuePlug( "usd:purpose", "${collect:rootName}" ) ) + + collect = GafferScene.CollectScenes() + collect["in"].setInput( attributes["out"] ) + collect["rootNames"].setValue( IECore.StringVectorData( [ "default", "render", "proxy", "guide" ] ) ) + + group = GafferScene.Group() + group["in"][0].setInput( collect["out"] ) + + encapsulate = GafferScene.Encapsulate() + encapsulate["in"].setInput( group["out"] ) + encapsulate["filter"].setInput( rootFilter["out"] ) + + standardOptions = GafferScene.StandardOptions() + standardOptions["in"].setInput( encapsulate["out"] ) + standardOptions["options"]["includedPurposes"]["enabled"].setValue( True ) + + for includedPurposes in [ + [ "default", "render" ], + [ "default", "proxy" ], + [ "default", "render", "proxy", "guide" ], + [ "default" ], + ] : + with self.subTest( includedPurposes = includedPurposes ) : + + standardOptions["options"]["includedPurposes"]["value"].setValue( IECore.StringVectorData( includedPurposes ) ) + + # Render to capture Capsule. + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + GafferScene.Private.RendererAlgo.outputObjects( + standardOptions["out"], GafferScene.Private.RendererAlgo.RenderOptions( standardOptions["out"] ), + GafferScene.Private.RendererAlgo.RenderSets( standardOptions["out"] ), GafferScene.Private.RendererAlgo.LightLinks(), + renderer + ) + + capsule = renderer.capturedObject( "/group" ) + capsule = capsule.capturedSamples()[0] + self.assertIsInstance( capsule, GafferScene.Capsule ) + + # Render again to expand contents of Capsule. + + capsuleRenderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + capsule.render( capsuleRenderer ) + + # Check that only objects with the right purpose have been included. + + for purpose in [ "default", "render", "proxy", "guide" ] : + if purpose in includedPurposes : + self.assertIsNotNone( capsuleRenderer.capturedObject( f"/{purpose}/cube" ) ) + else : + self.assertIsNone( capsuleRenderer.capturedObject( f"/{purpose}/cube" ) ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferSceneUI/EncapsulateUI.py b/python/GafferSceneUI/EncapsulateUI.py index a180c6eca86..17669037b12 100644 --- a/python/GafferSceneUI/EncapsulateUI.py +++ b/python/GafferSceneUI/EncapsulateUI.py @@ -61,11 +61,11 @@ > Note : Encapsulation currently has some limitations > - > - Motion blur options are taken from the globals at the - > point of Encapsulation, not the downstream globals - > at the point of rendering. > - Motion blur attributes are not inherited - only - > attributes within the encapsulate hierarchy are + > attributes within the encapsulated hierarchy are + > considered. + > - The `usd:purpose` attribute is not inherited - only + > attributes within the encapsulated hierarchy are > considered. """, diff --git a/src/GafferScene/Capsule.cpp b/src/GafferScene/Capsule.cpp index 9f1fa54b6dc..221dc405fe3 100644 --- a/src/GafferScene/Capsule.cpp +++ b/src/GafferScene/Capsule.cpp @@ -45,6 +45,8 @@ #include "boost/bind/bind.hpp" +#include + using namespace boost::placeholders; using namespace IECore; using namespace IECoreScene; @@ -53,6 +55,7 @@ using namespace GafferScene; namespace { + Gaffer::Context *capsuleContext( const Context &context ) { Gaffer::Context *result = new Gaffer::Context( context ); @@ -62,6 +65,10 @@ namespace result->remove( ScenePlug::scenePathContextName ); return result; } + +std::mutex g_renderOptionsMutex; +std::unordered_map g_renderOptions; + } IE_CORE_DEFINEOBJECTTYPEDESCRIPTION( Capsule ); @@ -84,6 +91,8 @@ Capsule::Capsule( Capsule::~Capsule() { + std::unique_lock renderOptionsLock( g_renderOptionsMutex ); + g_renderOptions.erase( this ); } bool Capsule::isEqualTo( const IECore::Object *other ) const @@ -103,6 +112,16 @@ void Capsule::hash( IECore::MurmurHash &h ) const Procedural::hash( h ); h.append( m_hash ); + + if( auto renderOptions = getRenderOptions() ) + { + // Hash only what affects our rendering, not everything in + // `RenderOptions::globals`. + h.append( renderOptions->transformBlur ); + h.append( renderOptions->deformationBlur ); + h.append( renderOptions->shutter ); + renderOptions->includedPurposes->hash( h ); + } } void Capsule::copyFrom( const IECore::Object *other, IECore::Object::CopyContext *context ) @@ -147,9 +166,13 @@ void Capsule::render( IECoreScenePreview::Renderer *renderer ) const { throwIfNoScene(); ScenePlug::GlobalScope scope( m_context.get() ); - IECore::ConstCompoundObjectPtr globals = m_scene->globalsPlug()->getValue(); + std::optional renderOptions = getRenderOptions(); + if( !renderOptions ) + { + renderOptions = GafferScene::Private::RendererAlgo::RenderOptions( m_scene ); + } GafferScene::Private::RendererAlgo::RenderSets renderSets( m_scene ); - GafferScene::Private::RendererAlgo::outputObjects( m_scene, globals.get(), renderSets, /* lightLinks = */ nullptr, renderer, m_root ); + GafferScene::Private::RendererAlgo::outputObjects( m_scene, *renderOptions, renderSets, /* lightLinks = */ nullptr, renderer, m_root ); } const ScenePlug *Capsule::scene() const @@ -170,6 +193,26 @@ const Gaffer::Context *Capsule::context() const return m_context.get(); } +void Capsule::setRenderOptions( const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions ) +{ + // This is not pretty, but it allows the capsule to render with the correct + // motion blur and `includedPurposes`, taken from the downstream node being + // rendered rather than from the capsule's own globals. + std::unique_lock renderOptionsLock( g_renderOptionsMutex ); + g_renderOptions[this] = renderOptions; +} + +std::optional Capsule::getRenderOptions() const +{ + std::unique_lock renderOptionsLock( g_renderOptionsMutex ); + auto it = g_renderOptions.find( this ); + if( it != g_renderOptions.end() ) + { + return it->second; + } + return std::nullopt; +} + void Capsule::throwIfNoScene() const { if( !m_scene ) diff --git a/src/GafferScene/Render.cpp b/src/GafferScene/Render.cpp index 4c2908f02af..1dd32b7cf46 100644 --- a/src/GafferScene/Render.cpp +++ b/src/GafferScene/Render.cpp @@ -297,14 +297,14 @@ void Render::executeInternal( bool flushCaches ) const return; } - ConstCompoundObjectPtr globals = adaptedInPlug()->globalsPlug()->getValue(); + GafferScene::Private::RendererAlgo::RenderOptions renderOptions( adaptedInPlug() ); if( !renderScope.sceneTranslationOnly() ) { - GafferScene::Private::RendererAlgo::createOutputDirectories( globals.get() ); + GafferScene::Private::RendererAlgo::createOutputDirectories( renderOptions.globals.get() ); } PerformanceMonitorPtr performanceMonitor; - if( const BoolData *d = globals->member( g_performanceMonitorOptionName ) ) + if( const BoolData *d = renderOptions.globals->member( g_performanceMonitorOptionName ) ) { if( d->readable() ) { @@ -313,8 +313,8 @@ void Render::executeInternal( bool flushCaches ) const } Monitor::Scope performanceMonitorScope( performanceMonitor ); - GafferScene::Private::RendererAlgo::outputOptions( globals.get(), renderer.get() ); - GafferScene::Private::RendererAlgo::outputOutputs( inPlug(), globals.get(), renderer.get() ); + GafferScene::Private::RendererAlgo::outputOptions( renderOptions.globals.get(), renderer.get() ); + GafferScene::Private::RendererAlgo::outputOutputs( inPlug(), renderOptions.globals.get(), renderer.get() ); { // Using nested scope so that we free the memory used by `renderSets` @@ -322,11 +322,11 @@ void Render::executeInternal( bool flushCaches ) const GafferScene::Private::RendererAlgo::RenderSets renderSets( adaptedInPlug() ); GafferScene::Private::RendererAlgo::LightLinks lightLinks; - GafferScene::Private::RendererAlgo::outputCameras( adaptedInPlug(), globals.get(), renderSets, renderer.get() ); - GafferScene::Private::RendererAlgo::outputLights( adaptedInPlug(), globals.get(), renderSets, &lightLinks, renderer.get() ); - GafferScene::Private::RendererAlgo::outputLightFilters( adaptedInPlug(), globals.get(), renderSets, &lightLinks, renderer.get() ); + GafferScene::Private::RendererAlgo::outputCameras( adaptedInPlug(), renderOptions, renderSets, renderer.get() ); + GafferScene::Private::RendererAlgo::outputLights( adaptedInPlug(), renderOptions, renderSets, &lightLinks, renderer.get() ); + GafferScene::Private::RendererAlgo::outputLightFilters( adaptedInPlug(), renderOptions, renderSets, &lightLinks, renderer.get() ); lightLinks.outputLightFilterLinks( adaptedInPlug() ); - GafferScene::Private::RendererAlgo::outputObjects( adaptedInPlug(), globals.get(), renderSets, &lightLinks, renderer.get() ); + GafferScene::Private::RendererAlgo::outputObjects( adaptedInPlug(), renderOptions, renderSets, &lightLinks, renderer.get() ); } if( renderScope.sceneTranslationOnly() ) diff --git a/src/GafferScene/RenderController.cpp b/src/GafferScene/RenderController.cpp index 9b2a89fd5d0..5150c2c7cbb 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" @@ -110,34 +111,6 @@ 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. @@ -201,6 +174,7 @@ struct ObjectInterfaceHandle : public boost::noncopyable using RemovalCallback = std::function; ObjectInterfaceHandle() + : m_isCapsule( false ) { } @@ -217,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 ) { @@ -225,6 +199,7 @@ struct ObjectInterfaceHandle : public boost::noncopyable } m_objectInterface = p; m_removalCallback = removalCallback; + m_isCapsule = isCapsule; } IECoreScenePreview::Renderer::ObjectInterface *operator->() const @@ -242,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; }; @@ -399,7 +380,7 @@ class RenderController::SceneGraph // Root - get attributes from globals. if( changedGlobals & GlobalsGlobalComponent ) { - if( updateAttributes( controller->m_globals.get() ) ) + if( updateAttributes( controller->m_renderOptions->globals.get() ) ) { m_changedComponents |= AttributesComponent; } @@ -420,7 +401,7 @@ class RenderController::SceneGraph // If attributes have changed, need to check if this has affected our motion sample times if( ( m_changedComponents & AttributesComponent ) || ( changedGlobals & TransformBlurGlobalComponent ) ) { - if( Private::RendererAlgo::transformMotionTimes( controller->m_motionBlurOptions.transformBlur, controller->m_motionBlurOptions.shutter, m_fullAttributes.get(), m_transformTimes ) ) + if( Private::RendererAlgo::transformMotionTimes( *controller->m_renderOptions, m_fullAttributes.get(), m_transformTimes ) ) { m_dirtyComponents |= TransformComponent; } @@ -428,7 +409,7 @@ class RenderController::SceneGraph if( ( m_changedComponents & AttributesComponent ) || ( changedGlobals & DeformationBlurGlobalComponent ) ) { - if( Private::RendererAlgo::deformationMotionTimes( controller->m_motionBlurOptions.deformationBlur, controller->m_motionBlurOptions.shutter, m_fullAttributes.get(), m_deformationTimes ) ) + if( Private::RendererAlgo::deformationMotionTimes( *controller->m_renderOptions, m_fullAttributes.get(), m_deformationTimes ) ) { m_dirtyComponents |= ObjectComponent; } @@ -461,7 +442,7 @@ class RenderController::SceneGraph if( ( m_changedComponents & AttributesComponent ) || ( changedGlobals & IncludedPurposesGlobalComponent ) ) { const bool purposeIncludedPreviously = m_purposeIncluded; - m_purposeIncluded = purposeIncluded( m_fullAttributes.get(), controller->m_globals.get() ); + m_purposeIncluded = controller->m_renderOptions->purposeIncluded( m_fullAttributes.get() ); if( m_purposeIncluded != purposeIncludedPreviously ) { // We'll need to hide or show the object by considering `m_purposeIncluded` in @@ -499,7 +480,14 @@ 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() ) ) + 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; } @@ -523,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_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++; @@ -815,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 ) @@ -882,7 +870,6 @@ class RenderController::SceneGraph } return true; - } vector samples; @@ -891,16 +878,12 @@ class RenderController::SceneGraph return false; } - bool isNull = true; - for( ConstObjectPtr &i : samples ) - { - if( !runTimeCast( i.get() ) ) - { - isNull = false; - } - } - - if( (type != LightType && type != LightFilterType) && isNull ) + if( + std::all_of( + samples.begin(), samples.end(), + [] ( const ConstObjectPtr &sample ) { return runTimeCast( sample.get() ); } + ) + ) { m_objectInterface = nullptr; return hadObjectInterface; @@ -938,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 ); } } @@ -991,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 { @@ -1354,7 +1348,7 @@ RenderController::RenderController( const ConstScenePlugPtr &scene, const Gaffer m_failedAttributeEdits( 0 ), m_dirtyGlobalComponents( NoGlobalComponent ), m_changedGlobalComponents( NoGlobalComponent ), - m_globals( new CompoundObject ) + m_renderOptions( make_unique() ) { for( int i = SceneGraph::FirstType; i <= SceneGraph::LastType; ++i ) { @@ -1564,10 +1558,9 @@ void RenderController::dirtySceneGraphs( unsigned components ) if( components & SceneGraph::ObjectComponent ) { - // We don't track dirtiness of different SceneGraphs separately anyway, - // so just recheck if a camera has changed a shutter override whenever - // any object is dirtied - m_changedGlobalComponents |= CameraShutterGlobalComponent; + // Changes to a camera object may include changing the + // shutter that overrides the global shutter. + m_dirtyGlobalComponents |= CameraShutterGlobalComponent; } } @@ -1634,50 +1627,46 @@ void RenderController::updateInternal( const ProgressCallback &callback, const I // Update globals if( m_dirtyGlobalComponents & GlobalsGlobalComponent ) { - ConstCompoundObjectPtr globals = m_scene->globalsPlug()->getValue(); - Private::RendererAlgo::outputOptions( globals.get(), m_globals.get(), m_renderer.get() ); - Private::RendererAlgo::outputOutputs( m_scene.get(), globals.get(), m_globals.get(), m_renderer.get() ); - if( !m_globals || *m_globals != *globals ) + RenderOptions renderOptions( m_scene.get() ); + Private::RendererAlgo::outputOptions( renderOptions.globals.get(), m_renderOptions->globals.get(), m_renderer.get() ); + Private::RendererAlgo::outputOutputs( m_scene.get(), renderOptions.globals.get(), m_renderOptions->globals.get(), m_renderer.get() ); + if( *renderOptions.globals != *m_renderOptions->globals ) { m_changedGlobalComponents |= GlobalsGlobalComponent; } - if( cameraGlobalsChanged( globals.get(), m_globals.get(), m_scene.get() ) ) + if( cameraGlobalsChanged( renderOptions.globals.get(), m_renderOptions->globals.get(), m_scene.get() ) ) { m_changedGlobalComponents |= CameraOptionsGlobalComponent; } - if( includedPurposesChanged( globals.get(), m_globals.get() ) ) + if( *renderOptions.includedPurposes != *m_renderOptions->includedPurposes ) { m_changedGlobalComponents |= IncludedPurposesGlobalComponent; } - m_globals = globals; - } - - // Update motion blur options - if( m_changedGlobalComponents & ( GlobalsGlobalComponent | CameraShutterGlobalComponent ) ) - { - const BoolData *transformBlurData = m_globals->member( g_transformBlurOptionName ); - bool transformBlur = transformBlurData ? transformBlurData->readable() : false; - - const BoolData *deformationBlurData = m_globals->member( g_deformationBlurOptionName ); - bool deformationBlur = deformationBlurData ? deformationBlurData->readable() : false; - - V2f shutter = SceneAlgo::shutter( m_globals.get(), m_scene.get() ); - - if( shutter != m_motionBlurOptions.shutter || transformBlur != m_motionBlurOptions.transformBlur ) + if( renderOptions.shutter != m_renderOptions->shutter || renderOptions.transformBlur != m_renderOptions->transformBlur ) { m_changedGlobalComponents |= TransformBlurGlobalComponent; } - if( shutter != m_motionBlurOptions.shutter || deformationBlur != m_motionBlurOptions.deformationBlur ) + if( renderOptions.shutter != m_renderOptions->shutter || renderOptions.deformationBlur != m_renderOptions->deformationBlur ) { m_changedGlobalComponents |= DeformationBlurGlobalComponent; } - if( shutter != m_motionBlurOptions.shutter ) + if( renderOptions.shutter != m_renderOptions->shutter ) { m_changedGlobalComponents |= CameraOptionsGlobalComponent; } - m_motionBlurOptions.transformBlur = transformBlur; - m_motionBlurOptions.deformationBlur = deformationBlur; - m_motionBlurOptions.shutter = shutter; + *m_renderOptions = renderOptions; + } + + if( ( m_dirtyGlobalComponents & CameraShutterGlobalComponent ) && !( m_dirtyGlobalComponents & GlobalsGlobalComponent ) ) + { + // Shutter override from a camera may have changed, and won't have been covered by + // the block above (because the globals weren't dirty). + const V2f shutter = SceneAlgo::shutter( m_renderOptions->globals.get(), m_scene.get() ); + if( shutter != m_renderOptions->shutter ) + { + m_renderOptions->shutter = shutter; + m_changedGlobalComponents |= ( DeformationBlurGlobalComponent | TransformBlurGlobalComponent ); + } } if( m_dirtyGlobalComponents & SetsGlobalComponent ) @@ -1786,7 +1775,7 @@ void RenderController::updateDefaultCamera() return; } - const StringData *cameraOption = m_globals->member( g_cameraGlobalName ); + const StringData *cameraOption = m_renderOptions->globals->member( g_cameraGlobalName ); m_defaultCamera = nullptr; if( cameraOption && !cameraOption->readable().empty() ) { @@ -1794,7 +1783,7 @@ void RenderController::updateDefaultCamera() } CameraPtr defaultCamera = new IECoreScene::Camera; - SceneAlgo::applyCameraGlobals( defaultCamera.get(), m_globals.get(), m_scene.get() ); + SceneAlgo::applyCameraGlobals( defaultCamera.get(), m_renderOptions->globals.get(), m_scene.get() ); IECoreScenePreview::Renderer::AttributesInterfacePtr defaultAttributes = m_renderer->attributes( m_scene->attributesPlug()->defaultValue() ); ConstStringDataPtr name = new StringData( "gaffer:defaultCamera" ); m_defaultCamera = m_renderer->camera( name->readable(), defaultCamera.get(), defaultAttributes.get() ); diff --git a/src/GafferScene/RendererAlgo.cpp b/src/GafferScene/RendererAlgo.cpp index 836614b6ca7..ade0b58ed06 100644 --- a/src/GafferScene/RendererAlgo.cpp +++ b/src/GafferScene/RendererAlgo.cpp @@ -36,6 +36,7 @@ #include "GafferScene/Private/RendererAlgo.h" +#include "GafferScene/Capsule.h" #include "GafferScene/Private/IECoreScenePreview/Renderer.h" #include "GafferScene/SceneAlgo.h" #include "GafferScene/SceneProcessor.h" @@ -77,6 +78,71 @@ using namespace IECoreScene; using namespace Gaffer; using namespace GafferScene; +////////////////////////////////////////////////////////////////////////// +// RenderOptions +////////////////////////////////////////////////////////////////////////// + +namespace +{ + +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" ); + +/// \todo We should really default to `{ "default", "render" }`, but can only +/// change that on a major version update. +const ConstStringVectorDataPtr g_defaultIncludedPurposes( new StringVectorData( { "default", "render", "proxy", "guide" } ) ); +const std::string g_defaultPurpose( "default" ); + +} // namespace + +namespace GafferScene::Private::RendererAlgo +{ + +RenderOptions::RenderOptions() + : globals( new CompoundObject ), + transformBlur( false ), + deformationBlur( false ), + shutter( V2f( -0.25, 0.25 ) ), + includedPurposes( g_defaultIncludedPurposes ) +{ +} + +RenderOptions::RenderOptions( const ScenePlug *scene ) +{ + globals = scene->globals(); + + const BoolData *transformBlurData = globals->member( g_transformBlurOptionName ); + transformBlur = transformBlurData ? transformBlurData->readable() : false; + + const BoolData *deformationBlurData = globals->member( g_deformationBlurOptionName ); + deformationBlur = deformationBlurData ? deformationBlurData->readable() : false; + + shutter = SceneAlgo::shutter( globals.get(), scene ); + + const StringVectorData *includedPurposesData = globals->member( g_includedPurposesOptionName ); + 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 ); + const std::string &purpose = purposeData ? purposeData->readable() : g_defaultPurpose; + const vector &purposes = includedPurposes->readable(); + return std::find( purposes.begin(), purposes.end(), purpose ) != purposes.end(); +} + +} // namespace GafferScene::Private::RendererAlgo + ////////////////////////////////////////////////////////////////////////// // RendererAlgo implementation ////////////////////////////////////////////////////////////////////////// @@ -157,14 +223,14 @@ bool motionTimes( bool motionBlur, const V2f &shutter, const CompoundObject *att return changed; } -bool transformMotionTimes( bool motionBlur, const V2f &shutter, const CompoundObject *attributes, std::vector × ) +bool transformMotionTimes( const RenderOptions &renderOptions, const CompoundObject *attributes, std::vector × ) { - return motionTimes( motionBlur, shutter, attributes, g_transformBlurAttributeName, g_transformBlurSegmentsAttributeName, times ); + return motionTimes( renderOptions.transformBlur, renderOptions.shutter, attributes, g_transformBlurAttributeName, g_transformBlurSegmentsAttributeName, times ); } -bool deformationMotionTimes( bool motionBlur, const V2f &shutter, const CompoundObject *attributes, std::vector × ) +bool deformationMotionTimes( const RenderOptions &renderOptions, const CompoundObject *attributes, std::vector × ) { - return motionTimes( motionBlur, shutter, attributes, g_deformationBlurAttributeName, g_deformationBlurSegmentsAttributeName, times ); + return motionTimes( renderOptions.deformationBlur, renderOptions.shutter, attributes, g_deformationBlurAttributeName, g_deformationBlurSegmentsAttributeName, times ); } bool transformSamples( const M44fPlug *transformPlug, const std::vector &sampleTimes, std::vector &samples, IECore::MurmurHash *hash ) @@ -1009,15 +1075,9 @@ 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" ); @@ -1036,19 +1096,9 @@ IECore::InternedString optionName( const IECore::InternedString &globalsName ) struct LocationOutput { - LocationOutput( IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, const ScenePlug::ScenePath &root, const ScenePlug *scene ) - : m_renderer( renderer ), m_attributes( SceneAlgo::globalAttributes( globals ) ), m_renderSets( renderSets ), m_root( root ) + LocationOutput( IECoreScenePreview::Renderer *renderer, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, const ScenePlug::ScenePath &root, const ScenePlug *scene ) + : m_renderer( renderer ), m_options( renderOptions ), m_attributes( root.empty() ? SceneAlgo::globalAttributes( renderOptions.globals.get() ) : new CompoundObject ), m_renderSets( renderSets ), m_root( root ) { - const BoolData *transformBlurData = globals->member( g_transformBlurOptionName ); - m_options.transformBlur = transformBlurData ? transformBlurData->readable() : false; - - const BoolData *deformationBlurData = globals->member( g_deformationBlurOptionName ); - m_options.deformationBlur = deformationBlurData ? deformationBlurData->readable() : false; - - m_options.shutter = SceneAlgo::shutter( globals, scene ); - - m_options.includedPurposes = globals->member( g_includedPurposesOptionName ); - m_transformSamples.push_back( M44f() ); } @@ -1077,17 +1127,14 @@ struct LocationOutput protected : - bool purposeIncluded() const + const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions() 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(); + return m_options; + } + bool purposeIncluded() const + { + return m_options.purposeIncluded( m_attributes.get() ); } std::string name( const ScenePlug::ScenePath &path ) const @@ -1114,7 +1161,7 @@ struct LocationOutput void deformationMotionTimes( std::vector × ) { - GafferScene::Private::RendererAlgo::deformationMotionTimes( m_options.deformationBlur, m_options.shutter, m_attributes.get(), times ); + GafferScene::Private::RendererAlgo::deformationMotionTimes( m_options, m_attributes.get(), times ); } const IECore::CompoundObject *attributes() const @@ -1179,7 +1226,7 @@ struct LocationOutput void updateTransform( const ScenePlug *scene ) { vector sampleTimes; - GafferScene::Private::RendererAlgo::transformMotionTimes( m_options.transformBlur, m_options.shutter, m_attributes.get(), sampleTimes ); + GafferScene::Private::RendererAlgo::transformMotionTimes( m_options, m_attributes.get(), sampleTimes ); vector samples; GafferScene::Private::RendererAlgo::transformSamples( scene->transformPlug(), sampleTimes, samples ); @@ -1240,15 +1287,7 @@ struct LocationOutput IECoreScenePreview::Renderer *m_renderer; - struct Options - { - bool transformBlur; - bool deformationBlur; - Imath::V2f shutter; - ConstStringVectorDataPtr includedPurposes; - }; - - Options m_options; + const GafferScene::Private::RendererAlgo::RenderOptions m_options; IECore::ConstCompoundObjectPtr m_attributes; const GafferScene::Private::RendererAlgo::RenderSets &m_renderSets; const ScenePlug::ScenePath &m_root; @@ -1261,8 +1300,8 @@ struct LocationOutput struct CameraOutput : public LocationOutput { - CameraOutput( IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, const ScenePlug::ScenePath &root, const ScenePlug *scene ) - : LocationOutput( renderer, globals, renderSets, root, scene ), m_globals( globals ), m_cameraSet( renderSets.camerasSet() ) + CameraOutput( IECoreScenePreview::Renderer *renderer, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, const ScenePlug::ScenePath &root, const ScenePlug *scene ) + : LocationOutput( renderer, renderOptions, renderSets, root, scene ), m_globals( renderOptions.globals.get() ), m_cameraSet( renderSets.camerasSet() ) { } @@ -1353,8 +1392,8 @@ struct CameraOutput : public LocationOutput struct LightOutput : public LocationOutput { - LightOutput( IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, GafferScene::Private::RendererAlgo::LightLinks *lightLinks, const ScenePlug::ScenePath &root, const ScenePlug *scene ) - : LocationOutput( renderer, globals, renderSets, root, scene ), m_lightSet( renderSets.lightsSet() ), m_lightLinks( lightLinks ) + LightOutput( IECoreScenePreview::Renderer *renderer, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, GafferScene::Private::RendererAlgo::LightLinks *lightLinks, const ScenePlug::ScenePath &root, const ScenePlug *scene ) + : LocationOutput( renderer, renderOptions, renderSets, root, scene ), m_lightSet( renderSets.lightsSet() ), m_lightLinks( lightLinks ) { } @@ -1399,8 +1438,8 @@ struct LightOutput : public LocationOutput struct LightFiltersOutput : public LocationOutput { - LightFiltersOutput( IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, GafferScene::Private::RendererAlgo::LightLinks *lightLinks, const ScenePlug::ScenePath &root, const ScenePlug *scene ) - : LocationOutput( renderer, globals, renderSets, root, scene ), m_lightFiltersSet( renderSets.lightFiltersSet() ), m_lightLinks( lightLinks ) + LightFiltersOutput( IECoreScenePreview::Renderer *renderer, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, GafferScene::Private::RendererAlgo::LightLinks *lightLinks, const ScenePlug::ScenePath &root, const ScenePlug *scene ) + : LocationOutput( renderer, renderOptions, renderSets, root, scene ), m_lightFiltersSet( renderSets.lightFiltersSet() ), m_lightLinks( lightLinks ) { } @@ -1445,8 +1484,8 @@ struct LightFiltersOutput : public LocationOutput struct ObjectOutput : public LocationOutput { - ObjectOutput( IECoreScenePreview::Renderer *renderer, const IECore::CompoundObject *globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, const GafferScene::Private::RendererAlgo::LightLinks *lightLinks, const ScenePlug::ScenePath &root, const ScenePlug *scene ) - : LocationOutput( renderer, globals, renderSets, root, scene ), m_cameraSet( renderSets.camerasSet() ), m_lightSet( renderSets.lightsSet() ), m_lightFiltersSet( renderSets.lightFiltersSet() ), m_lightLinks( lightLinks ) + ObjectOutput( IECoreScenePreview::Renderer *renderer, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, const GafferScene::Private::RendererAlgo::LightLinks *lightLinks, const ScenePlug::ScenePath &root, const ScenePlug *scene ) + : LocationOutput( renderer, renderOptions, renderSets, root, scene ), m_cameraSet( renderSets.camerasSet() ), m_lightSet( renderSets.lightsSet() ), m_lightFiltersSet( renderSets.lightFiltersSet() ), m_lightLinks( lightLinks ) { } @@ -1479,12 +1518,20 @@ struct ObjectOutput : public LocationOutput IECoreScenePreview::Renderer::ObjectInterfacePtr objectInterface; IECoreScenePreview::Renderer::AttributesInterfacePtr attributesInterface = this->attributesInterface(); - if( !sampleTimes.size() ) + if( samples.size() == 1 ) { - objectInterface = renderer()->object( name( path ), samples[0].get(), attributesInterface.get() ); + ConstObjectPtr sample = samples[0]; + if( auto capsule = runTimeCast( sample.get() ) ) + { + CapsulePtr capsuleCopy = capsule->copy(); + capsuleCopy->setRenderOptions( renderOptions() ); + sample = capsuleCopy; + } + objectInterface = renderer()->object( name( path ), sample.get(), attributesInterface.get() ); } else { + assert( sampleTimes.size() == samples.size() ); /// \todo Can we rejig things so this conversion isn't necessary? vector objectsVector; objectsVector.reserve( samples.size() ); for( const auto &sample : samples ) @@ -1708,9 +1755,9 @@ void outputOutputs( const ScenePlug *scene, const IECore::CompoundObject *global } } -void outputCameras( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, IECoreScenePreview::Renderer *renderer ) +void outputCameras( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, IECoreScenePreview::Renderer *renderer ) { - const StringData *cameraOption = globals->member( g_cameraOptionLegacyName ); + const StringData *cameraOption = renderOptions.globals->member( g_cameraOptionLegacyName ); if( cameraOption && !cameraOption->readable().empty() ) { ScenePlug::ScenePath cameraPath; ScenePlug::stringToPath( cameraOption->readable(), cameraPath ); @@ -1729,13 +1776,13 @@ void outputCameras( const ScenePlug *scene, const IECore::CompoundObject *global } const ScenePlug::ScenePath root; - CameraOutput output( renderer, globals, renderSets, root, scene ); + CameraOutput output( renderer, renderOptions, renderSets, root, scene ); SceneAlgo::parallelProcessLocations( scene, output ); if( !cameraOption || cameraOption->readable().empty() ) { CameraPtr defaultCamera = new IECoreScene::Camera; - SceneAlgo::applyCameraGlobals( defaultCamera.get(), globals, scene ); + SceneAlgo::applyCameraGlobals( defaultCamera.get(), renderOptions.globals.get(), scene ); IECoreScenePreview::Renderer::AttributesInterfacePtr defaultAttributes = renderer->attributes( scene->attributesPlug()->defaultValue() ); ConstStringDataPtr name = new StringData( "gaffer:defaultCamera" ); renderer->camera( name->readable(), defaultCamera.get(), defaultAttributes.get() ); @@ -1743,23 +1790,23 @@ void outputCameras( const ScenePlug *scene, const IECore::CompoundObject *global } } -void outputLightFilters( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ) +void outputLightFilters( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ) { const ScenePlug::ScenePath root; - LightFiltersOutput output( renderer, globals, renderSets, lightLinks, root, scene ); + LightFiltersOutput output( renderer, renderOptions, renderSets, lightLinks, root, scene ); SceneAlgo::parallelProcessLocations( scene, output ); } -void outputLights( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ) +void outputLights( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer ) { const ScenePlug::ScenePath root; - LightOutput output( renderer, globals, renderSets, lightLinks, root, scene ); + LightOutput output( renderer, renderOptions, renderSets, lightLinks, root, scene ); SceneAlgo::parallelProcessLocations( scene, output ); } -void outputObjects( const ScenePlug *scene, const IECore::CompoundObject *globals, const RenderSets &renderSets, const LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer, const ScenePlug::ScenePath &root ) +void outputObjects( const ScenePlug *scene, const RenderOptions &renderOptions, const RenderSets &renderSets, const LightLinks *lightLinks, IECoreScenePreview::Renderer *renderer, const ScenePlug::ScenePath &root ) { - ObjectOutput output( renderer, globals, renderSets, lightLinks, root, scene ); + ObjectOutput output( renderer, renderOptions, renderSets, lightLinks, root, scene ); SceneAlgo::parallelProcessLocations( scene, output, root ); } diff --git a/src/GafferSceneModule/HierarchyBinding.cpp b/src/GafferSceneModule/HierarchyBinding.cpp index f0c18077292..9b7a51284ff 100644 --- a/src/GafferSceneModule/HierarchyBinding.cpp +++ b/src/GafferSceneModule/HierarchyBinding.cpp @@ -67,6 +67,15 @@ using namespace GafferScene; namespace { +object getRenderOptionsWrapper( const Capsule &c ) +{ + if( auto o = c.getRenderOptions() ) + { + return object( *o ); + } + return object(); +} + ScenePlugPtr scene( const Capsule &c ) { return const_cast( c.scene() ); @@ -102,6 +111,8 @@ void GafferSceneModule::bindHierarchy() .def( "scene", &scene ) .def( "root", &root ) .def( "context", &context ) + .def( "setRenderOptions", &Capsule::setRenderOptions ) + .def( "getRenderOptions", &getRenderOptionsWrapper ) ; GafferBindings::DependencyNodeClass() diff --git a/src/GafferSceneModule/RenderBinding.cpp b/src/GafferSceneModule/RenderBinding.cpp index c74207961c3..c2286b12d4e 100644 --- a/src/GafferSceneModule/RenderBinding.cpp +++ b/src/GafferSceneModule/RenderBinding.cpp @@ -389,22 +389,22 @@ object transformSamplesWrapper( const Gaffer::M44fPlug &transformPlug, const std return pythonSamples; } -void outputCamerasWrapper( const ScenePlug &scene, const IECore::CompoundObject &globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, IECoreScenePreview::Renderer &renderer ) +void outputCamerasWrapper( const ScenePlug &scene, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, IECoreScenePreview::Renderer &renderer ) { IECorePython::ScopedGILRelease gilRelease; - GafferScene::Private::RendererAlgo::outputCameras( &scene, &globals, renderSets, &renderer ); + GafferScene::Private::RendererAlgo::outputCameras( &scene, renderOptions, renderSets, &renderer ); } -void outputLightsWrapper( const ScenePlug &scene, const IECore::CompoundObject &globals, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, GafferScene::Private::RendererAlgo::LightLinks &lightLinks, IECoreScenePreview::Renderer &renderer ) +void outputLightsWrapper( const ScenePlug &scene, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, const GafferScene::Private::RendererAlgo::RenderSets &renderSets, GafferScene::Private::RendererAlgo::LightLinks &lightLinks, IECoreScenePreview::Renderer &renderer ) { IECorePython::ScopedGILRelease gilRelease; - GafferScene::Private::RendererAlgo::outputLights( &scene, &globals, renderSets, &lightLinks, &renderer ); + GafferScene::Private::RendererAlgo::outputLights( &scene, renderOptions, 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 ) +void outputObjectsWrapper( const ScenePlug &scene, const GafferScene::Private::RendererAlgo::RenderOptions &renderOptions, 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 ); + GafferScene::Private::RendererAlgo::outputObjects( &scene, renderOptions, renderSets, &lightLinks, &renderer, root ); } } // namespace @@ -444,6 +444,16 @@ void GafferSceneModule::bindRender() scope rendererAlgomoduleScope( rendererAlgoModule ); + class_( "RenderOptions" ) + .def( init() ) + .def_readwrite( "globals", &GafferScene::Private::RendererAlgo::RenderOptions::globals ) + .def_readwrite( "transformBlur", &GafferScene::Private::RendererAlgo::RenderOptions::transformBlur ) + .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 ) ); def( "transformSamples", &transformSamplesWrapper, ( arg( "transformPlug" ), arg( "sampleTimes" ), arg( "hash" ) = object() ) );