From a9771667bf11c74a9a7fe50e0e9f6df3d519a47a Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Mar 2024 09:54:35 +0000 Subject: [PATCH 1/7] Renderer : Improve type registration API - Make `registerType()` public, and add Python bindings. - Add `deregisterType()` method. - Allow a Creator to replace itself dynamically, which will allow us to delay loading of the libraries that supply Renderers. - Simplify map management in `registerType()` - we don't need two lookups. --- .../Private/IECoreScenePreview/Renderer.h | 9 +-- .../IECoreScenePreviewTest/RendererTest.py | 81 +++++++++++++++++++ .../IECoreScenePreviewTest/__init__.py | 1 + .../IECoreScenePreview/Renderer.cpp | 35 ++++---- src/GafferSceneModule/RenderBinding.cpp | 25 ++++++ 5 files changed, 130 insertions(+), 21 deletions(-) create mode 100644 python/GafferSceneTest/IECoreScenePreviewTest/RendererTest.py diff --git a/include/GafferScene/Private/IECoreScenePreview/Renderer.h b/include/GafferScene/Private/IECoreScenePreview/Renderer.h index f299824a5c7..ef36ab5e72c 100644 --- a/include/GafferScene/Private/IECoreScenePreview/Renderer.h +++ b/include/GafferScene/Private/IECoreScenePreview/Renderer.h @@ -305,6 +305,10 @@ class GAFFERSCENE_API Renderer : public IECore::RefCounted /// Performs an arbitrary renderer-specific action. virtual IECore::DataPtr command( const IECore::InternedString name, const IECore::CompoundDataMap ¶meters = IECore::CompoundDataMap() ); + using Creator = std::function; + static void registerType( const IECore::InternedString &typeName, Creator creator ); + static void deregisterType( const IECore::InternedString &typeName ); + protected : Renderer(); @@ -331,11 +335,6 @@ class GAFFERSCENE_API Renderer : public IECore::RefCounted }; - private : - - static void registerType( const IECore::InternedString &typeName, Ptr (*creator)( RenderType, const std::string &, const IECore::MessageHandlerPtr & ) ); - - }; IE_CORE_DECLAREPTR( Renderer ) diff --git a/python/GafferSceneTest/IECoreScenePreviewTest/RendererTest.py b/python/GafferSceneTest/IECoreScenePreviewTest/RendererTest.py new file mode 100644 index 00000000000..ed45cdf1694 --- /dev/null +++ b/python/GafferSceneTest/IECoreScenePreviewTest/RendererTest.py @@ -0,0 +1,81 @@ +########################################################################## +# +# Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of John Haddon nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import unittest + +import GafferTest +import GafferScene + +class RendererTest( GafferTest.TestCase ) : + + def testReentrantPythonFactory( self ) : + + # Check that we can register a renderer alias from Python, by forwarding to some + # other creator that is registered dynamically by the first one. We can use this + # to implement delayed loading of the true renderer modules. + + self.assertNotIn( "Test", GafferScene.Private.IECoreScenePreview.Renderer.types() ) + + self.addCleanup( + GafferScene.Private.IECoreScenePreview.Renderer.deregisterType, "Test" + ) + + def creator1( renderType, fileName, messageHandler ) : + + GafferScene.Private.IECoreScenePreview.Renderer.registerType( "Test", creator2 ) + return GafferScene.Private.IECoreScenePreview.Renderer.create( "Test", renderType, fileName, messageHandler ) + + def creator2( renderType, fileName, messageHandler ) : + + return GafferScene.Private.IECoreScenePreview.Renderer.create( "Capturing", renderType, fileName, messageHandler ) + + GafferScene.Private.IECoreScenePreview.Renderer.registerType( "Test", creator1 ) + self.assertIn( "Test", GafferScene.Private.IECoreScenePreview.Renderer.types() ) + + self.assertIsInstance( + GafferScene.Private.IECoreScenePreview.Renderer.create( "Test", GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Interactive ), + GafferScene.Private.IECoreScenePreview.CapturingRenderer + ) + + self.assertIn( "Test", GafferScene.Private.IECoreScenePreview.Renderer.types() ) + + self.assertIsInstance( + GafferScene.Private.IECoreScenePreview.Renderer.create( "Test", GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Interactive ), + GafferScene.Private.IECoreScenePreview.CapturingRenderer + ) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferSceneTest/IECoreScenePreviewTest/__init__.py b/python/GafferSceneTest/IECoreScenePreviewTest/__init__.py index 9849102ea6a..16d8ef228c7 100644 --- a/python/GafferSceneTest/IECoreScenePreviewTest/__init__.py +++ b/python/GafferSceneTest/IECoreScenePreviewTest/__init__.py @@ -37,6 +37,7 @@ from .CapturingRendererTest import CapturingRendererTest from .CompoundRendererTest import CompoundRendererTest from .PlaceholderTest import PlaceholderTest +from .RendererTest import RendererTest if __name__ == "__main__": import unittest diff --git a/src/GafferScene/IECoreScenePreview/Renderer.cpp b/src/GafferScene/IECoreScenePreview/Renderer.cpp index 509aa107c52..a72ee03890b 100644 --- a/src/GafferScene/IECoreScenePreview/Renderer.cpp +++ b/src/GafferScene/IECoreScenePreview/Renderer.cpp @@ -48,19 +48,17 @@ using namespace IECoreScenePreview; namespace { -using Creator = Renderer::Ptr (*)( Renderer::RenderType, const std::string &, const IECore::MessageHandlerPtr & ); - vector &types() { static vector g_types; return g_types; } -using CreatorMap = map; +using CreatorMap = map; CreatorMap &creators() { - static CreatorMap g_creators; - return g_creators; + static CreatorMap *g_creators = new CreatorMap; + return *g_creators; } } // namespace @@ -112,19 +110,24 @@ Renderer::Ptr Renderer::create( const IECore::InternedString &type, RenderType r { return nullptr; } - return it->second( renderType, fileName, messageHandler ); + // Take copy of creator, since it is allowed to do a switcheroo + // by calling `registerType( name, theRealCreator )`. + Creator creator = it->second; + return creator( renderType, fileName, messageHandler ); } - -void Renderer::registerType( const IECore::InternedString &typeName, Ptr (*creator)( RenderType, const std::string &, const IECore::MessageHandlerPtr & ) ) +void Renderer::registerType( const IECore::InternedString &typeName, Creator creator ) { - CreatorMap &c = creators(); - CreatorMap::iterator it = c.find( typeName ); - if( it != c.end() ) - { - it->second = creator; - return; - } - c[typeName] = creator; + creators()[typeName] = creator; ::types().push_back( typeName ); } + +void Renderer::deregisterType( const IECore::InternedString &typeName ) +{ + creators().erase( typeName ); + auto &t = ::types(); + t.erase( + std::remove( t.begin(), t.end(), typeName ), + t.end() + ); +} diff --git a/src/GafferSceneModule/RenderBinding.cpp b/src/GafferSceneModule/RenderBinding.cpp index 2977311c460..e75e94b102c 100644 --- a/src/GafferSceneModule/RenderBinding.cpp +++ b/src/GafferSceneModule/RenderBinding.cpp @@ -118,6 +118,29 @@ void interactiveRenderSetContext( InteractiveRender &r, Context &context ) r.setContext( &context ); } +void registerTypeWrapper( const std::string &name, object creator ) +{ + // The function we register will be held and destroyed from C++. + // Wrap it so that we correctly acquire the GIL before the captured + // Python object is destroyed. + auto creatorPtr = std::shared_ptr( + new boost::python::object( creator ), + []( boost::python::object *o ) { + IECorePython::ScopedGILLock gilLock; + delete o; + } + ); + + Renderer::registerType( + name, + [creatorPtr] ( Renderer::RenderType renderType, const std::string &fileName, const IECore::MessageHandlerPtr &messageHandler ) -> Renderer::Ptr { + IECorePython::ScopedGILLock gilLock; + object o = (*creatorPtr)( renderType, fileName, messageHandler ); + return extract( o ); + } + ); +} + list rendererTypes() { std::vector t = Renderer::types(); @@ -524,6 +547,8 @@ void GafferSceneModule::bindRender() renderer + .def( "registerType", ®isterTypeWrapper ) + .def( "deregisterType", &Renderer::deregisterType ) .def( "types", &rendererTypes ) .staticmethod( "types" ) .def( "create", &Renderer::create, ( arg( "type" ), arg( "renderType" ) = Renderer::Batch, arg( "fileName" ) = "", arg( "messageHandler" ) = IECore::MessageHandlerPtr() ) ) From a36525eb3d784649843313f8d4ec579c42af7900 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Mar 2024 10:16:25 +0000 Subject: [PATCH 2/7] GafferScene startup : Register on-demand loaders for renderers --- startup/GafferScene/renderers.py | 68 ++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 startup/GafferScene/renderers.py diff --git a/startup/GafferScene/renderers.py b/startup/GafferScene/renderers.py new file mode 100644 index 00000000000..fbe5f47f733 --- /dev/null +++ b/startup/GafferScene/renderers.py @@ -0,0 +1,68 @@ +########################################################################## +# +# Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of Cinesite VFX Ltd. nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import sys +import functools + +import IECore +import GafferScene + +# Register functions to load renderers on demand. This allows us to find a renderer +# even if the relevant Python module hasn't been imported (in `gaffer execute` for instance). + +def __creator( renderType, fileName, messageHandler, renderer, module ) : + + # Import module to replace ourselves with the true renderer creation function. + __import__( module ) + # And then call `create()` again to use it. + return GafferScene.Private.IECoreScenePreview.Renderer.create( renderer, renderType, fileName, messageHandler ) + +for renderer, module in [ + ( "Cycles", "GafferCycles" ), + ( "Arnold", "IECoreArnold" ), + ( "3Delight", "IECoreDelight" ), +] : + if renderer in GafferScene.Private.IECoreScenePreview.Renderer.types() : + # Already registered + continue + if not IECore.SearchPath( sys.path ).find( module ) : + # Renderer not available + continue + + # Register creator that will load module to provide renderer on demand. + GafferScene.Private.IECoreScenePreview.Renderer.registerType( + renderer, functools.partial( __creator, renderer = renderer, module = module ) + ) From 5568c173f360e3ea72edfea140ee05d1354b423a Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Mar 2024 14:20:25 +0000 Subject: [PATCH 3/7] RenderTest : Add new base class to consolidate testing of Render nodes This contains some initial tests scraped together from the renderer-specific tests that existed already. Those tests will now run for all renderers. --- python/GafferArnoldTest/ArnoldRenderTest.py | 156 +----------- python/GafferCyclesTest/CyclesRenderTest.py | 46 ++++ python/GafferCyclesTest/__init__.py | 1 + python/GafferDelightTest/DelightRenderTest.py | 68 +---- python/GafferSceneTest/OpenGLRenderTest.py | 81 +----- python/GafferSceneTest/RenderTest.py | 238 ++++++++++++++++++ python/GafferSceneTest/__init__.py | 1 + 7 files changed, 297 insertions(+), 294 deletions(-) create mode 100644 python/GafferCyclesTest/CyclesRenderTest.py create mode 100644 python/GafferSceneTest/RenderTest.py diff --git a/python/GafferArnoldTest/ArnoldRenderTest.py b/python/GafferArnoldTest/ArnoldRenderTest.py index d4b05aceaf5..403fb901be3 100644 --- a/python/GafferArnoldTest/ArnoldRenderTest.py +++ b/python/GafferArnoldTest/ArnoldRenderTest.py @@ -59,7 +59,10 @@ import GafferArnold import GafferArnoldTest -class ArnoldRenderTest( GafferSceneTest.SceneTestCase ) : +class ArnoldRenderTest( GafferSceneTest.RenderTest ) : + + renderer = "Arnold" + sceneDescriptionSuffix = ".ass" def setUp( self ) : @@ -73,110 +76,6 @@ def tearDown( self ) : GafferScene.SceneAlgo.deregisterRenderAdaptor( "Test" ) - def testExecute( self ) : - - s = Gaffer.ScriptNode() - - s["plane"] = GafferScene.Plane() - s["render"] = GafferArnold.ArnoldRender() - s["render"]["mode"].setValue( s["render"].Mode.SceneDescriptionMode ) - s["render"]["in"].setInput( s["plane"]["out"] ) - - s["expression"] = Gaffer.Expression() - s["expression"].setExpression( f"""parent['render']['fileName'] = '{( self.temporaryDirectory() / "test.%d.ass" ).as_posix()}' % int( context['frame'] )""" ) - - s["fileName"].setValue( self.__scriptFileName ) - s.save() - - p = subprocess.Popen( - f"gaffer execute {self.__scriptFileName} -frames 1-3", - shell=True, - stderr = subprocess.PIPE, - ) - p.wait() - self.assertFalse( p.returncode ) - - for i in range( 1, 4 ) : - self.assertTrue( ( self.temporaryDirectory() / f"test.{i}.ass" ).exists() ) - - def testWaitForImage( self ) : - - s = Gaffer.ScriptNode() - - s["plane"] = GafferScene.Plane() - - s["outputs"] = GafferScene.Outputs() - s["outputs"].addOutput( - "beauty", - IECoreScene.Output( - str( self.temporaryDirectory() / "test.tif" ), - "tiff", - "rgba", - {} - ) - ) - s["outputs"]["in"].setInput( s["plane"]["out"] ) - - s["render"] = GafferArnold.ArnoldRender() - s["render"]["in"].setInput( s["outputs"]["out"] ) - s["render"]["task"].execute() - - self.assertTrue( ( self.temporaryDirectory() / "test.tif" ).exists() ) - - def testExecuteWithStringSubstitutions( self ) : - - s = Gaffer.ScriptNode() - - s["plane"] = GafferScene.Plane() - s["render"] = GafferArnold.ArnoldRender() - s["render"]["mode"].setValue( s["render"].Mode.SceneDescriptionMode ) - s["render"]["in"].setInput( s["plane"]["out"] ) - s["render"]["fileName"].setValue( self.temporaryDirectory() / "test.####.ass" ) - - s["fileName"].setValue( self.__scriptFileName ) - s.save() - - p = subprocess.Popen( - f"gaffer execute {self.__scriptFileName} -frames 1-3", - shell=True, - stderr = subprocess.PIPE, - ) - p.wait() - self.assertFalse( p.returncode ) - - for i in range( 1, 4 ) : - self.assertTrue( ( self.temporaryDirectory() / f"test.{i:04d}.ass" ).exists() ) - - def testImageOutput( self ) : - - s = Gaffer.ScriptNode() - - s["plane"] = GafferScene.Plane() - - s["outputs"] = GafferScene.Outputs() - s["outputs"].addOutput( - "beauty", - IECoreScene.Output( - str( self.temporaryDirectory() / "test.####.tif" ), - "tiff", - "rgba", - {} - ) - ) - s["outputs"]["in"].setInput( s["plane"]["out"] ) - - s["render"] = GafferArnold.ArnoldRender() - s["render"]["in"].setInput( s["outputs"]["out"] ) - - c = Gaffer.Context() - for i in range( 1, 4 ) : - c.setFrame( i ) - with c : - s["render"]["task"].execute() - - for i in range( 1, 4 ) : - self.assertTrue( ( self.temporaryDirectory() / f"test.{i:04d}.tif" ).exists() ) - def testTypeNamePrefixes( self ) : self.assertTypeNamesArePrefixed( GafferArnold ) @@ -192,53 +91,6 @@ def testNodesConstructWithDefaultValues( self ) : self.assertNodesConstructWithDefaultValues( GafferArnold ) self.assertNodesConstructWithDefaultValues( GafferArnoldTest ) - def testDirectoryCreation( self ) : - - s = Gaffer.ScriptNode() - s["variables"].addChild( Gaffer.NameValuePlug( "renderDirectory", ( self.temporaryDirectory() / "renderTests" ).as_posix() ) ) - s["variables"].addChild( Gaffer.NameValuePlug( "assDirectory", ( self.temporaryDirectory() / "assTests" ).as_posix() ) ) - - s["plane"] = GafferScene.Plane() - - s["outputs"] = GafferScene.Outputs() - s["outputs"]["in"].setInput( s["plane"]["out"] ) - s["outputs"].addOutput( - "beauty", - IECoreScene.Output( - "$renderDirectory/test.####.exr", - "exr", - "rgba", - {} - ) - ) - - s["render"] = GafferArnold.ArnoldRender() - s["render"]["in"].setInput( s["outputs"]["out"] ) - s["render"]["fileName"].setValue( "$assDirectory/test.####.ass" ) - s["render"]["mode"].setValue( s["render"].Mode.SceneDescriptionMode ) - - self.assertFalse( ( self.temporaryDirectory() / "renderTests" ).exists() ) - self.assertFalse( ( self.temporaryDirectory() / "assTests" ).exists() ) - self.assertFalse( ( self.temporaryDirectory() / "assTests" / "test.0001.ass" ).exists() ) - - s["fileName"].setValue( self.temporaryDirectory() / "test.gfr" ) - - with s.context() : - s["render"]["task"].execute() - - self.assertTrue( ( self.temporaryDirectory() / "renderTests" ).exists() ) - self.assertTrue( ( self.temporaryDirectory() / "assTests" ).exists()) - self.assertTrue( ( self.temporaryDirectory() / "assTests"/ "test.0001.ass" ).exists() ) - - # check it can cope with everything already existing - - with s.context() : - s["render"]["task"].execute() - - self.assertTrue( ( self.temporaryDirectory() / "renderTests" ).exists() ) - self.assertTrue( ( self.temporaryDirectory() / "assTests" ).exists() ) - self.assertTrue( ( self.temporaryDirectory() / "assTests" / "test.0001.ass" ).exists() ) - def testWedge( self ) : s = Gaffer.ScriptNode() diff --git a/python/GafferCyclesTest/CyclesRenderTest.py b/python/GafferCyclesTest/CyclesRenderTest.py new file mode 100644 index 00000000000..d0f73c1b141 --- /dev/null +++ b/python/GafferCyclesTest/CyclesRenderTest.py @@ -0,0 +1,46 @@ +########################################################################## +# +# Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of John Haddon nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import unittest + +import GafferSceneTest + +class CyclesRenderTest( GafferSceneTest.RenderTest ) : + + renderer = "Cycles" + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferCyclesTest/__init__.py b/python/GafferCyclesTest/__init__.py index 3adb106dbec..45062d50a20 100644 --- a/python/GafferCyclesTest/__init__.py +++ b/python/GafferCyclesTest/__init__.py @@ -39,6 +39,7 @@ from .ModuleTest import ModuleTest from .CyclesLightTest import CyclesLightTest from .CyclesShaderTest import CyclesShaderTest +from .CyclesRenderTest import CyclesRenderTest from .IECoreCyclesPreviewTest import * diff --git a/python/GafferDelightTest/DelightRenderTest.py b/python/GafferDelightTest/DelightRenderTest.py index 6072c29be69..d77162b75e9 100644 --- a/python/GafferDelightTest/DelightRenderTest.py +++ b/python/GafferDelightTest/DelightRenderTest.py @@ -34,76 +34,14 @@ # ########################################################################## -import pathlib import unittest -import IECore -import IECoreScene - -import Gaffer -import GafferScene import GafferSceneTest -import GafferDelight - -class DelightRenderTest( GafferSceneTest.SceneTestCase ) : - - def testSceneDescriptionMode( self ) : - - plane = GafferScene.Plane() - render = GafferDelight.DelightRender() - render["in"].setInput( plane["out"] ) - render["mode"].setValue( render.Mode.SceneDescriptionMode ) - render["fileName"].setValue( self.temporaryDirectory() / "test.nsi" ) - - render["task"].execute() - self.assertTrue( pathlib.Path( render["fileName"].getValue() ).exists() ) - - def testRenderMode( self ) : - - plane = GafferScene.Plane() - - outputs = GafferScene.Outputs() - outputs.addOutput( - "beauty", - IECoreScene.Output( - str( self.temporaryDirectory() / "test.exr" ), - "exr", - "rgba", - {} - ) - ) - - render = GafferDelight.DelightRender() - render["in"].setInput( outputs["out"] ) - render["mode"].setValue( render.Mode.RenderMode ) - - render["task"].execute() - self.assertTrue( ( self.temporaryDirectory() / "test.exr" ).exists() ) - - def testSceneTranslationOnly( self ) : - - plane = GafferScene.Plane() - - outputs = GafferScene.Outputs() - outputs.addOutput( - "beauty", - IECoreScene.Output( - str( self.temporaryDirectory() / "test.exr" ), - "exr", - "rgba", - {} - ) - ) - - render = GafferDelight.DelightRender() - render["in"].setInput( outputs["out"] ) - render["mode"].setValue( render.Mode.RenderMode ) - with Gaffer.Context() as context : - context["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) - render["task"].execute() +class DelightRenderTest( GafferSceneTest.RenderTest ) : - self.assertFalse( ( self.temporaryDirectory() / "test.exr" ).exists() ) + renderer = "3Delight" + sceneDescriptionSuffix = ".nsi" if __name__ == "__main__": unittest.main() diff --git a/python/GafferSceneTest/OpenGLRenderTest.py b/python/GafferSceneTest/OpenGLRenderTest.py index cab30b1361f..f3bd7441d4e 100644 --- a/python/GafferSceneTest/OpenGLRenderTest.py +++ b/python/GafferSceneTest/OpenGLRenderTest.py @@ -49,9 +49,11 @@ import GafferSceneTest @unittest.skipIf( GafferTest.inCI(), "OpenGL not set up" ) -class OpenGLRenderTest( GafferSceneTest.SceneTestCase ) : +class OpenGLRenderTest( GafferSceneTest.RenderTest ) : - def test( self ) : + renderer = "OpenGL" + + def testTextureFromImagePlug( self ) : self.assertFalse( ( self.temporaryDirectory() / "test.exr" ).exists() ) @@ -106,80 +108,5 @@ def test( self ) : self.assertAlmostEqual( imageSampler["color"]["g"].getValue(), 0.666666, delta = 0.001 ) self.assertEqual( imageSampler["color"]["b"].getValue(), 0 ) - def testOutputDirectoryCreation( self ) : - - s = Gaffer.ScriptNode() - s["variables"].addChild( Gaffer.NameValuePlug( "renderDirectory", ( self.temporaryDirectory() / "openGLRenderTest" ).as_posix() ) ) - - s["plane"] = GafferScene.Plane() - - s["outputs"] = GafferScene.Outputs() - s["outputs"]["in"].setInput( s["plane"]["out"] ) - s["outputs"].addOutput( - "beauty", - IECoreScene.Output( - "$renderDirectory/test.####.exr", - "exr", - "rgba", - {} - ) - ) - - s["render"] = GafferScene.OpenGLRender() - s["render"]["in"].setInput( s["outputs"]["out"] ) - - self.assertFalse( ( self.temporaryDirectory() / "openGLRenderTest" ).exists() ) - self.assertFalse( ( self.temporaryDirectory() / "openGLRenderTest" / "test.0001.exr" ).exists() ) - - with s.context() : - s["render"]["task"].execute() - - self.assertTrue( ( self.temporaryDirectory() / "openGLRenderTest" ).exists() ) - self.assertTrue( ( self.temporaryDirectory() / "openGLRenderTest" / "test.0001.exr" ).exists() ) - - def testHash( self ) : - - c = Gaffer.Context() - c.setFrame( 1 ) - c2 = Gaffer.Context() - c2.setFrame( 2 ) - - s = Gaffer.ScriptNode() - s["plane"] = GafferScene.Plane() - s["outputs"] = GafferScene.Outputs() - s["outputs"]["in"].setInput( s["plane"]["out"] ) - s["outputs"].addOutput( "beauty", IECoreScene.Output( "$renderDirectory/test.####.exr", "exr", "rgba", {} ) ) - s["render"] = GafferScene.OpenGLRender() - - # no input scene produces no effect - self.assertEqual( s["render"].hash( c ), IECore.MurmurHash() ) - - # now theres an scene to render, we get some output - s["render"]["in"].setInput( s["outputs"]["out"] ) - self.assertNotEqual( s["render"].hash( c ), IECore.MurmurHash() ) - - # output varies by time - self.assertNotEqual( s["render"].hash( c ), s["render"].hash( c2 ) ) - - # output varies by new Context entries - current = s["render"].hash( c ) - c["renderDirectory"] = ( self.temporaryDirectory() / "openGLRenderTest" ).as_posix() - self.assertNotEqual( s["render"].hash( c ), current ) - - # output varies by changed Context entries - current = s["render"].hash( c ) - c["renderDirectory"] = ( self.temporaryDirectory() / "openGLRenderTest2" ).as_posix() - self.assertNotEqual( s["render"].hash( c ), current ) - - # output doesn't vary by ui Context entries - current = s["render"].hash( c ) - c["ui:something"] = "alterTheUI" - self.assertEqual( s["render"].hash( c ), current ) - - # also varies by input node - current = s["render"].hash( c ) - s["render"]["in"].setInput( s["plane"]["out"] ) - self.assertNotEqual( s["render"].hash( c ), current ) - if __name__ == "__main__": unittest.main() diff --git a/python/GafferSceneTest/RenderTest.py b/python/GafferSceneTest/RenderTest.py new file mode 100644 index 00000000000..f05c6b7f444 --- /dev/null +++ b/python/GafferSceneTest/RenderTest.py @@ -0,0 +1,238 @@ +########################################################################## +# +# Copyright (c) 2024, Cinesite VFX Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of John Haddon nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import pathlib +import subprocess +import unittest + +import IECore +import IECoreScene + +import Gaffer +import GafferScene +import GafferSceneTest + +## \todo Transfer more tests from subclasses to this base class, so that we +# run them for all renderers. +class RenderTest( GafferSceneTest.SceneTestCase ) : + + # Derived classes should set `renderer` to the name of the renderer + # to be tested. + renderer = None + # And set this to the file extension used for scene description, if + # scene description is supported. + sceneDescriptionSuffix = None + + @classmethod + def setUpClass( cls ) : + + GafferSceneTest.SceneTestCase.setUpClass() + + if cls.renderer is None : + # We expect derived classes to set the renderer, and will + # run the tests there. + raise unittest.SkipTest( "No renderer available" ) + + def testOutputDirectoryCreation( self ) : + + s = Gaffer.ScriptNode() + s["variables"].addChild( Gaffer.NameValuePlug( "renderDirectory", ( self.temporaryDirectory() / "renderTest" ).as_posix() ) ) + + s["plane"] = GafferScene.Plane() + + s["outputs"] = GafferScene.Outputs() + s["outputs"]["in"].setInput( s["plane"]["out"] ) + s["outputs"].addOutput( + "beauty", + IECoreScene.Output( + "$renderDirectory/test.####.exr", + "exr", + "rgba", + {} + ) + ) + + s["render"] = GafferScene.Render() + s["render"]["renderer"].setValue( self.renderer ) + s["render"]["in"].setInput( s["outputs"]["out"] ) + + self.assertFalse( ( self.temporaryDirectory() / "renderTest" ).exists() ) + self.assertFalse( ( self.temporaryDirectory() / "renderTest" / "test.0001.exr" ).exists() ) + + for i in range( 0, 2 ) : # Test twice, to check we can cope with the directory already existing. + + with s.context() : + s["render"]["task"].execute() + + self.assertTrue( ( self.temporaryDirectory() / "renderTest" ).exists() ) + self.assertTrue( ( self.temporaryDirectory() / "renderTest" / "test.0001.exr" ).exists() ) + + def testHash( self ) : + + c = Gaffer.Context() + c.setFrame( 1 ) + c2 = Gaffer.Context() + c2.setFrame( 2 ) + + s = Gaffer.ScriptNode() + s["plane"] = GafferScene.Plane() + s["outputs"] = GafferScene.Outputs() + s["outputs"]["in"].setInput( s["plane"]["out"] ) + s["outputs"].addOutput( "beauty", IECoreScene.Output( "$renderDirectory/test.####.exr", "exr", "rgba", {} ) ) + s["render"] = GafferScene.Render() + s["render"]["renderer"].setValue( self.renderer ) + + # no input scene produces no effect + self.assertEqual( s["render"].hash( c ), IECore.MurmurHash() ) + + # now theres an scene to render, we get some output + s["render"]["in"].setInput( s["outputs"]["out"] ) + self.assertNotEqual( s["render"].hash( c ), IECore.MurmurHash() ) + + # output varies by time + self.assertNotEqual( s["render"].hash( c ), s["render"].hash( c2 ) ) + + # output varies by new Context entries + current = s["render"].hash( c ) + c["renderDirectory"] = ( self.temporaryDirectory() / "renderTest" ).as_posix() + self.assertNotEqual( s["render"].hash( c ), current ) + + # output varies by changed Context entries + current = s["render"].hash( c ) + c["renderDirectory"] = ( self.temporaryDirectory() / "renderTest2" ).as_posix() + self.assertNotEqual( s["render"].hash( c ), current ) + + # output doesn't vary by ui Context entries + current = s["render"].hash( c ) + c["ui:something"] = "alterTheUI" + self.assertEqual( s["render"].hash( c ), current ) + + # also varies by input node + current = s["render"].hash( c ) + s["render"]["in"].setInput( s["plane"]["out"] ) + self.assertNotEqual( s["render"].hash( c ), current ) + + def testSceneTranslationOnly( self ) : + + outputs = GafferScene.Outputs() + outputs.addOutput( + "beauty", + IECoreScene.Output( + str( self.temporaryDirectory() / "test.exr" ), + "exr", + "rgba", + {} + ) + ) + + render = GafferScene.Render() + render["in"].setInput( outputs["out"] ) + render["mode"].setValue( render.Mode.RenderMode ) + render["renderer"].setValue( self.renderer ) + + with Gaffer.Context() as context : + context["scene:render:sceneTranslationOnly"] = IECore.BoolData( True ) + render["task"].execute() + + self.assertFalse( ( self.temporaryDirectory() / "test.exr" ).exists() ) + + def testRenderMode( self ) : + + outputs = GafferScene.Outputs() + outputs.addOutput( + "beauty", + IECoreScene.Output( + str( self.temporaryDirectory() / "test.exr" ), + "exr", + "rgba", + {} + ) + ) + + render = GafferScene.Render() + render["in"].setInput( outputs["out"] ) + render["mode"].setValue( render.Mode.RenderMode ) + render["renderer"].setValue( self.renderer ) + + render["task"].execute() + self.assertTrue( ( self.temporaryDirectory() / "test.exr" ).exists() ) + + def testSceneDescriptionMode( self ) : + + if self.sceneDescriptionSuffix is None : + raise unittest.SkipTest( "Scene description not supported" ) + + plane = GafferScene.Plane() + + render = GafferScene.Render() + render["in"].setInput( plane["out"] ) + render["mode"].setValue( render.Mode.SceneDescriptionMode ) + render["fileName"].setValue( ( self.temporaryDirectory() / "subdirectory" / f"test{self.sceneDescriptionSuffix}" ) ) + render["renderer"].setValue( self.renderer ) + + for i in range( 0, 2 ) : # Test twice, to check we can cope with the directory already existing. + + render["task"].execute() + self.assertTrue( pathlib.Path( render["fileName"].getValue() ).exists() ) + + def testExecute( self ) : + + if self.sceneDescriptionSuffix is None : + raise unittest.SkipTest( "Scene description not supported" ) + + s = Gaffer.ScriptNode() + + s["plane"] = GafferScene.Plane() + s["render"] = GafferScene.Render() + s["render"]["mode"].setValue( s["render"].Mode.SceneDescriptionMode ) + s["render"]["renderer"].setValue( self.renderer ) + s["render"]["fileName"].setValue( ( self.temporaryDirectory() / f"test.#{self.sceneDescriptionSuffix}" ) ) + s["render"]["in"].setInput( s["plane"]["out"] ) + + scriptFileName = self.temporaryDirectory() / "test.gfr" + s["fileName"].setValue( scriptFileName ) + s.save() + + subprocess.check_call( + f"gaffer execute {scriptFileName} -frames 1-3", + shell=True, + ) + + for i in range( 1, 4 ) : + self.assertTrue( ( self.temporaryDirectory() / f"test.{i}{self.sceneDescriptionSuffix}" ).exists() ) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferSceneTest/__init__.py b/python/GafferSceneTest/__init__.py index 8b56ed9c103..5aa14a8e898 100644 --- a/python/GafferSceneTest/__init__.py +++ b/python/GafferSceneTest/__init__.py @@ -67,6 +67,7 @@ from .ScenePathTest import ScenePathTest from .LightTest import LightTest from .OpenGLShaderTest import OpenGLShaderTest +from .RenderTest import RenderTest from .OpenGLRenderTest import OpenGLRenderTest from .TransformTest import TransformTest from .AimConstraintTest import AimConstraintTest From a0750ded49323110d3394f6663f424537ec96196 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Mar 2024 12:14:28 +0000 Subject: [PATCH 4/7] Render : Support `render:defaultRenderer` option The fact that we now compute the globals in `hash()` might be a bit of a concern for folks who have made their scene globals slow to compute for some reason. But it's the logical thing to do, and it also has the potential to improve the hash result in future (see todo). --- Changes.md | 8 +++ python/GafferSceneTest/RenderTest.py | 48 ++++++++++++++++++ python/GafferSceneUI/RenderUI.py | 75 +++++++++++++++++++++++++++- src/GafferScene/Render.cpp | 35 +++++++++++-- startup/gui/menus.py | 1 + 5 files changed, 162 insertions(+), 5 deletions(-) diff --git a/Changes.md b/Changes.md index 305f763c01b..c988269769b 100644 --- a/Changes.md +++ b/Changes.md @@ -1,3 +1,11 @@ +1.3.x.x (relative to 1.3.13.1) +======= + +Features +-------- + +- Render : Added a new node capable of rendering to any supported renderer, and using the`render:defaultRenderer` option to determine which to use by default. + 1.3.13.1 (relative to 1.3.13.0) ======== diff --git a/python/GafferSceneTest/RenderTest.py b/python/GafferSceneTest/RenderTest.py index f05c6b7f444..e7f4f5626c5 100644 --- a/python/GafferSceneTest/RenderTest.py +++ b/python/GafferSceneTest/RenderTest.py @@ -234,5 +234,53 @@ def testExecute( self ) : for i in range( 1, 4 ) : self.assertTrue( ( self.temporaryDirectory() / f"test.{i}{self.sceneDescriptionSuffix}" ).exists() ) + def testRendererOption( self ) : + + outputs = GafferScene.Outputs() + outputs.addOutput( + "beauty", + IECoreScene.Output( + str( self.temporaryDirectory() / "test.exr" ), + "exr", + "rgba", + {} + ) + ) + + customOptions = GafferScene.CustomOptions() + customOptions["in"].setInput( outputs["out"] ) + customOptions["options"].addChild( Gaffer.NameValuePlug( "render:defaultRenderer", self.renderer ) ) + + render = GafferScene.Render() + render["in"].setInput( customOptions["out"] ) + render["mode"].setValue( render.Mode.RenderMode ) + self.assertEqual( render["renderer"].getValue(), "" ) + + render["task"].execute() + self.assertTrue( ( self.temporaryDirectory() / "test.exr" ).exists() ) + + def testNoRenderer( self ) : + + outputs = GafferScene.Outputs() + outputs.addOutput( + "beauty", + IECoreScene.Output( + str( self.temporaryDirectory() / "test.exr" ), + "exr", + "rgba", + {} + ) + ) + + render = GafferScene.Render() + render["in"].setInput( outputs["out"] ) + render["mode"].setValue( render.Mode.RenderMode ) + self.assertEqual( render["renderer"].getValue(), "" ) + + self.assertEqual( render["task"].hash(), IECore.MurmurHash() ) + + render["task"].execute() + self.assertFalse( ( self.temporaryDirectory() / "test.exr" ).exists() ) + if __name__ == "__main__": unittest.main() diff --git a/python/GafferSceneUI/RenderUI.py b/python/GafferSceneUI/RenderUI.py index 4abd5089acb..57c523692b0 100644 --- a/python/GafferSceneUI/RenderUI.py +++ b/python/GafferSceneUI/RenderUI.py @@ -34,9 +34,24 @@ # ########################################################################## +import IECore + import Gaffer +import GafferUI import GafferScene +from GafferUI.PlugValueWidget import sole + +def rendererPresetNames( plug ) : + + blacklist = { "Capturing" } + return IECore.StringVectorData( + sorted( + t for t in GafferScene.Private.IECoreScenePreview.Renderer.types() + if t not in blacklist + ) + ) + Gaffer.Metadata.registerNode( GafferScene.Render, @@ -68,9 +83,17 @@ "description", """ - The renderer to use. + The renderer to use. Default mode uses the `render:defaultRenderer` option from + the input scene globals to choose the renderer. This can be authored using + the StandardOptions node. """, + "plugValueWidget:type", "GafferSceneUI.RenderUI.RendererPlugValueWidget", + + "preset:Default", "", + "presetNames", rendererPresetNames, + "presetValues", rendererPresetNames, + ], "mode" : [ @@ -112,3 +135,53 @@ } ) + +# Augments PresetsPlugValueWidget label with the renderer name +# when preset is "Default". Since this involves computing the +# scene globals, we do the work in the background via an auxiliary +# plug passed to `_valuesForUpdate()`. +class RendererPlugValueWidget( GafferUI.PresetsPlugValueWidget ) : + + def __init__( self, plugs, **kw ) : + + GafferUI.PresetsPlugValueWidget.__init__( self, plugs, **kw ) + + @staticmethod + def _valuesForUpdate( plugs, auxiliaryPlugs ) : + + presets = GafferUI.PresetsPlugValueWidget._valuesForUpdate( plugs, [ [] for p in plugs ] ) + + result = [] + for preset, globalsPlugs in zip( presets, auxiliaryPlugs ) : + + defaultRenderer = "" + if len( globalsPlugs ) and preset == "Default" : + with IECore.IgnoredExceptions( Gaffer.ProcessException ) : + defaultRenderer = globalsPlugs[0].getValue().get( "option:render:defaultRenderer" ) + defaultRenderer = defaultRenderer.value if defaultRenderer is not None else "" + + result.append( { + "preset" : preset, + "defaultRenderer" : defaultRenderer + } ) + + return result + + def _updateFromValues( self, values, exception ) : + + GafferUI.PresetsPlugValueWidget._updateFromValues( self, [ v["preset"] for v in values ], exception ) + + if self.menuButton().getText() == "Default" : + defaultRenderer = sole( v["defaultRenderer"] for v in values ) + self.menuButton().setText( + "Default ({})".format( + defaultRenderer if defaultRenderer else + ( "None" if defaultRenderer == "" else "---" ) + ) + ) + + def _auxiliaryPlugs( self, plug ) : + + node = plug.node() + if isinstance( node, ( GafferScene.Render, GafferScene.InteractiveRender ) ) : + return [ node["in"]["globals"] ] diff --git a/src/GafferScene/Render.cpp b/src/GafferScene/Render.cpp index 1dd32b7cf46..43d8d572d91 100644 --- a/src/GafferScene/Render.cpp +++ b/src/GafferScene/Render.cpp @@ -62,6 +62,7 @@ namespace { const InternedString g_performanceMonitorOptionName( "option:render:performanceMonitor" ); +const InternedString g_rendererOptionName( "option:render:defaultRenderer" ); const InternedString g_sceneTranslationOnlyContextName( "scene:render:sceneTranslationOnly" ); struct RenderScope : public Context::EditableScope @@ -206,10 +207,18 @@ IECore::MurmurHash Render::hash( const Gaffer::Context *context ) const RenderScope renderScope( context ); - const std::string rendererType = rendererPlug()->getValue(); + std::string rendererType = rendererPlug()->getValue(); if( rendererType.empty() ) { - return IECore::MurmurHash(); + ConstCompoundObjectPtr globals = adaptedInPlug()->globals(); + if( auto rendererData = globals->member( g_rendererOptionName ) ) + { + rendererType = rendererData->readable(); + } + if( rendererType.empty() ) + { + return IECore::MurmurHash(); + } } const Mode mode = static_cast( modePlug()->getValue() ); @@ -219,6 +228,12 @@ IECore::MurmurHash Render::hash( const Gaffer::Context *context ) const return IECore::MurmurHash(); } + /// \todo Since we're computing the globals now (see above), + /// maybe our hash should be the hash of the output definitions? + /// Then we'd know which parts of the context we were sensitive to + /// and wouldn't have such a pessimistic hash that includes all + /// context variables. + IECore::MurmurHash h = TaskNode::hash( context ); h.append( (uint64_t)inPlug()->source() ); h.append( context->hash() ); @@ -260,10 +275,22 @@ void Render::executeInternal( bool flushCaches ) const RenderScope renderScope( Context::current() ); - const std::string rendererType = rendererPlug()->getValue(); + std::string rendererType = rendererPlug()->getValue(); if( rendererType.empty() ) { - return; + /// \todo We're evaluating the globals twice, once here and once in + /// `RenderOptions` below. When we remove the `scene:renderer` context + /// variable, we'll be able to move the RenderOptions here and only do + /// one evaluation. + ConstCompoundObjectPtr globals = adaptedInPlug()->globals(); + if( auto rendererData = globals->member( g_rendererOptionName ) ) + { + rendererType = rendererData->readable(); + } + if( rendererType.empty() ) + { + return; + } } renderScope.set( g_rendererContextName, &rendererType ); diff --git a/startup/gui/menus.py b/startup/gui/menus.py index 76d3ed15e97..068b53b09ff 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -345,6 +345,7 @@ def __lightCreator( nodeName, shaderName, shape ) : nodeMenu.append( "/Scene/Passes/Render Passes", GafferScene.RenderPasses, searchText = "RenderPasses" ) nodeMenu.append( "/Scene/Passes/Delete Render Passes", GafferScene.DeleteRenderPasses, searchText = "DeleteRenderPasses" ) nodeMenu.append( "/Scene/Passes/Render Pass Wedge", GafferScene.RenderPassWedge, searchText = "RenderPassWedge" ) +nodeMenu.append( "/Scene/Render/Render", GafferScene.Render ) # Image nodes From 47c09eb62eae33230d1a19cb6790ab671f113b6e Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 14 Mar 2024 10:02:02 +0000 Subject: [PATCH 5/7] InteractiveRender : Support `render:defaultRenderer` option It would be nice to be able to support live edits to this, switching the currently running renderer on the fly. Technically I think this could be done relatively simply by storing the current renderer type when making a renderer, then checking to see if we should recreate when the globals are dirtied. We'd need an ABI break to store that though. And I think there is a better approach that we should take in the longer term : let the RenderController create and recreate the renderers itself. That would allow us to keep the globals evaluations in a background thread for the viewer, and would also let renderers reject certain option edits (e.g. `cycles:device`), forcing the controller to create a new renderer. That would be a bigger API/ABI break, so I'm not tackling it now, because we're targeting `1.3_maintenance` with this work. --- Changes.md | 2 +- .../InteractiveArnoldRenderTest.py | 1 + .../InteractiveCyclesRenderTest.py | 1 + .../InteractiveDelightRenderTest.py | 1 + .../GafferSceneTest/InteractiveRenderTest.py | 64 +++++++++++++++++-- python/GafferSceneUI/InteractiveRenderUI.py | 21 +++++- src/GafferScene/InteractiveRender.cpp | 26 +++++++- startup/gui/menus.py | 1 + 8 files changed, 108 insertions(+), 9 deletions(-) diff --git a/Changes.md b/Changes.md index c988269769b..3c8933a3255 100644 --- a/Changes.md +++ b/Changes.md @@ -4,7 +4,7 @@ Features -------- -- Render : Added a new node capable of rendering to any supported renderer, and using the`render:defaultRenderer` option to determine which to use by default. +- Render, InteractiveRender : Added new nodes capable of rendering to any supported renderer, and using the`render:defaultRenderer` option to determine which to use by default. 1.3.13.1 (relative to 1.3.13.0) ======== diff --git a/python/GafferArnoldTest/InteractiveArnoldRenderTest.py b/python/GafferArnoldTest/InteractiveArnoldRenderTest.py index e8ac5a97fac..66b977edcd8 100644 --- a/python/GafferArnoldTest/InteractiveArnoldRenderTest.py +++ b/python/GafferArnoldTest/InteractiveArnoldRenderTest.py @@ -56,6 +56,7 @@ class InteractiveArnoldRenderTest( GafferSceneTest.InteractiveRenderTest ) : interactiveRenderNodeClass = GafferArnold.InteractiveArnoldRender + renderer = "Arnold" # Arnold outputs licensing warnings that would cause failures failureMessageLevel = IECore.MessageHandler.Level.Error diff --git a/python/GafferCyclesTest/InteractiveCyclesRenderTest.py b/python/GafferCyclesTest/InteractiveCyclesRenderTest.py index 0295dada824..7fb83a1171a 100644 --- a/python/GafferCyclesTest/InteractiveCyclesRenderTest.py +++ b/python/GafferCyclesTest/InteractiveCyclesRenderTest.py @@ -45,6 +45,7 @@ class InteractiveCyclesRenderTest( GafferSceneTest.InteractiveRenderTest ) : interactiveRenderNodeClass = GafferCycles.InteractiveCyclesRender + renderer = "Cycles" @unittest.skip( "Resolution edits not supported yet" ) def testEditResolution( self ) : diff --git a/python/GafferDelightTest/InteractiveDelightRenderTest.py b/python/GafferDelightTest/InteractiveDelightRenderTest.py index 6644a90082d..457243e2214 100644 --- a/python/GafferDelightTest/InteractiveDelightRenderTest.py +++ b/python/GafferDelightTest/InteractiveDelightRenderTest.py @@ -48,6 +48,7 @@ class InteractiveDelightRenderTest( GafferSceneTest.InteractiveRenderTest ) : interactiveRenderNodeClass = GafferDelight.InteractiveDelightRender + renderer = "3Delight" # Temporarily disable this test (which is implemented in the # base class) because it fails. The issue is that we're automatically diff --git a/python/GafferSceneTest/InteractiveRenderTest.py b/python/GafferSceneTest/InteractiveRenderTest.py index 29943726666..6b9d82ea7d3 100644 --- a/python/GafferSceneTest/InteractiveRenderTest.py +++ b/python/GafferSceneTest/InteractiveRenderTest.py @@ -53,9 +53,12 @@ # rather than GafferScene.InteractiveRender, which we hope to phase out. class InteractiveRenderTest( GafferSceneTest.SceneTestCase ) : - # Derived classes should set cls.interactiveRenderNodeClass to - # the class of their interactive render node + ## \todo Phase out InteractiveRender subclasses and just use the `renderer` + # field below. interactiveRenderNodeClass = None + # Derived classes should set `cls.renderer` to the type of + # renderer to be tested. + renderer = None @classmethod def setUpClass( cls ) : @@ -2151,6 +2154,51 @@ def testEditCropWindow( self ) : self.assertNotIn( "gaffer:isRendering", script["catalogue"]["out"].metadata() ) + def testRendererOption( self ): + + script = Gaffer.ScriptNode() + + script["outputs"] = GafferScene.Outputs() + script["outputs"].addOutput( + "beauty", + IECoreScene.Output( + "test", + "ieDisplay", + "rgba", + { + "driverType" : "ImageDisplayDriver", + "handle" : "testRendererOption", + } + ) + ) + + script["customOptions"] = GafferScene.CustomOptions() + script["customOptions"]["in"].setInput( script["outputs"]["out"] ) + + script["renderer"] = self._createInteractiveRender( useNodeClass = False ) + script["renderer"]["renderer"].setValue( "" ) + script["renderer"]["in"].setInput( script["customOptions"]["out"] ) + + # No renderer specified yet, so if we start the render we don't + # get an image. + + script["renderer"]["state"].setValue( script["renderer"].State.Running ) + time.sleep( 1.0 ) + self.assertIsNone( IECoreImage.ImageDisplayDriver.storedImage( "testRendererOption" ) ) + self.ignoreMessage( IECore.Msg.Level.Error, "InteractiveRender", "`render:defaultRenderer` option not set" ) + + # Set renderer option and start again. We should now get an image. + + script["renderer"]["state"].setValue( script["renderer"].State.Stopped ) + script["customOptions"]["options"].addChild( + Gaffer.NameValuePlug( "render:defaultRenderer", self.renderer ) + ) + script["renderer"]["state"].setValue( script["renderer"].State.Running ) + time.sleep( 1.0 ) + + image = IECoreImage.ImageDisplayDriver.storedImage( "testRendererOption" ) + self.assertIsInstance( image, IECoreImage.ImagePrimitive ) + def tearDown( self ) : GafferSceneTest.SceneTestCase.tearDown( self ) @@ -2160,10 +2208,14 @@ def tearDown( self ) : ## Should be used in test cases to create an InteractiveRender node # suitably configured for error reporting. If failOnError is # True, then the node's error signal will cause the test to fail. - def _createInteractiveRender( self, failOnError = True ) : - - assert( issubclass( self.interactiveRenderNodeClass, GafferScene.InteractiveRender ) ) - node = self.interactiveRenderNodeClass() + def _createInteractiveRender( self, failOnError = True, useNodeClass = True ) : + + if useNodeClass : + assert( issubclass( self.interactiveRenderNodeClass, GafferScene.InteractiveRender ) ) + node = self.interactiveRenderNodeClass() + else : + node = GafferScene.InteractiveRender() + node["renderer"].setValue( self.renderer ) if failOnError : diff --git a/python/GafferSceneUI/InteractiveRenderUI.py b/python/GafferSceneUI/InteractiveRenderUI.py index 9210a73c613..dd61081092f 100644 --- a/python/GafferSceneUI/InteractiveRenderUI.py +++ b/python/GafferSceneUI/InteractiveRenderUI.py @@ -41,6 +41,7 @@ import Gaffer import GafferImage import GafferScene +import GafferSceneUI import GafferUI import GafferImageUI @@ -374,6 +375,13 @@ def _updateFromValues( self, values, exception ) : # Metadata for InteractiveRender node. ########################################################################## +def __rendererPresetNames( plug ) : + + return IECore.StringVectorData( [ + x for x in GafferSceneUI.RenderUI.rendererPresetNames( plug ) + if x != "OpenGL" + ] ) + Gaffer.Metadata.registerNode( GafferScene.InteractiveRender, @@ -409,9 +417,20 @@ def _updateFromValues( self, values, exception ) : "description", """ - The renderer to use. + The renderer to use. Default mode uses the `render:defaultRenderer` option from + the input scene globals to choose the renderer. This can be authored using + the StandardOptions node. + + > Note : Changing renderer currently requires that the current render is + > manually stopped and restarted. """, + "plugValueWidget:type", "GafferSceneUI.RenderUI.RendererPlugValueWidget", + + "preset:Default", "", + "presetNames", __rendererPresetNames, + "presetValues", __rendererPresetNames, + ], "state" : [ diff --git a/src/GafferScene/InteractiveRender.cpp b/src/GafferScene/InteractiveRender.cpp index c0f891e1759..57bb9d5d278 100644 --- a/src/GafferScene/InteractiveRender.cpp +++ b/src/GafferScene/InteractiveRender.cpp @@ -83,6 +83,8 @@ PendingUpdates &pendingUpdates() return *p; } +const InternedString g_rendererOptionName( "option:render:defaultRenderer" ); + } // anon namespace // A thread-safe message handler for render messaging @@ -314,8 +316,30 @@ void InteractiveRender::update() { m_messageHandler->clear(); + std::string rendererType = rendererPlug()->getValue(); + if( rendererType.empty() ) + { + /// \todo It'd be great if we could deal with live edits to the `render:defaultRenderer` + /// option, to switch renderer on the fly. The best way of doing this is probably + /// to move the renderer creation to the RenderController. That approach would also + /// allow the RenderController to recreate the renderer when unsupported option edits + /// are made - for instance, changing `cycles:device`. + ConstCompoundObjectPtr globals = adaptedInPlug()->globals(); + if( auto rendererData = globals->member( g_rendererOptionName ) ) + { + rendererType = rendererData->readable(); + } + if( rendererType.empty() ) + { + m_messageHandler->handle( + IECore::Msg::Error, "InteractiveRender", "`render:defaultRenderer` option not set" + ); + return; + } + } + m_renderer = IECoreScenePreview::Renderer::create( - rendererPlug()->getValue(), + rendererType, IECoreScenePreview::Renderer::Interactive, "", m_messageHandler.get() diff --git a/startup/gui/menus.py b/startup/gui/menus.py index 068b53b09ff..4f8156a208a 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -346,6 +346,7 @@ def __lightCreator( nodeName, shaderName, shape ) : nodeMenu.append( "/Scene/Passes/Delete Render Passes", GafferScene.DeleteRenderPasses, searchText = "DeleteRenderPasses" ) nodeMenu.append( "/Scene/Passes/Render Pass Wedge", GafferScene.RenderPassWedge, searchText = "RenderPassWedge" ) nodeMenu.append( "/Scene/Render/Render", GafferScene.Render ) +nodeMenu.append( "/Scene/Render/Interactive Render", GafferScene.InteractiveRender, searchText = "InteractiveRender" ) # Image nodes From 992d11e80d51df899f639fa0d3746de10ae8a153 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 13 Mar 2024 16:55:38 +0000 Subject: [PATCH 6/7] StandardOptions : Add `render:defaultRenderer` option --- Changes.md | 1 + python/GafferSceneUI/StandardOptionsUI.py | 35 ++++++++++++++++++++++- python/GafferSceneUI/__init__.py | 2 +- src/GafferScene/StandardOptions.cpp | 4 +++ 4 files changed, 40 insertions(+), 2 deletions(-) diff --git a/Changes.md b/Changes.md index 3c8933a3255..1795a6b7483 100644 --- a/Changes.md +++ b/Changes.md @@ -5,6 +5,7 @@ Features -------- - Render, InteractiveRender : Added new nodes capable of rendering to any supported renderer, and using the`render:defaultRenderer` option to determine which to use by default. +- StandardOptions : Added `render:defaultRenderer` option, allowing the scene globals to specify which renderer is used by the Render and InteractiveRender nodes. 1.3.13.1 (relative to 1.3.13.0) ======== diff --git a/python/GafferSceneUI/StandardOptionsUI.py b/python/GafferSceneUI/StandardOptionsUI.py index c40259d4445..ee2c5f80df0 100644 --- a/python/GafferSceneUI/StandardOptionsUI.py +++ b/python/GafferSceneUI/StandardOptionsUI.py @@ -42,6 +42,7 @@ import Gaffer import GafferUI import GafferScene +import GafferSceneUI from GafferUI.PlugValueWidget import sole @@ -77,6 +78,13 @@ def __cameraSummary( plug ) : return ", ".join( info ) +def __rendererSummary( plug ) : + + if plug["defaultRenderer"]["enabled"].getValue() : + return plug["defaultRenderer"]["value"].getValue() + + return "" + def __renderSetSummary( plug ) : info = [] @@ -123,6 +131,7 @@ def __statisticsSummary( plug ) : "options" : [ "layout:section:Camera:summary", __cameraSummary, + "layout:section:Renderer:summary", __rendererSummary, "layout:section:Render Set:summary", __renderSetSummary, "layout:section:Motion Blur:summary", __motionBlurSummary, "layout:section:Statistics:summary", __statisticsSummary, @@ -340,7 +349,31 @@ def __statisticsSummary( plug ) : "layout:section", "Camera", ], - # Purpose + # Renderer + + "options.defaultRenderer" : [ + + "description", + """ + Specifies the default renderer to be used by the Render and + InteractiveRender nodes. + """, + + "label", "Default Renderer", + "layout:section", "Renderer", + + ], + + "options.defaultRenderer.value" : [ + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + "preset:None", "", + "presetNames", GafferSceneUI.RenderUI.rendererPresetNames, + "presetValues", GafferSceneUI.RenderUI.rendererPresetNames, + + ], + + # Render Set "options.includedPurposes" : [ diff --git a/python/GafferSceneUI/__init__.py b/python/GafferSceneUI/__init__.py index 078fe7c6043..a00e317b185 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -63,6 +63,7 @@ from . import OptionsUI from . import OpenGLAttributesUI from . import SceneWriterUI +from . import RenderUI from . import StandardOptionsUI from . import StandardAttributesUI from . import ShaderUI @@ -125,7 +126,6 @@ from . import AttributeVisualiserUI from . import FilterProcessorUI from . import MeshToPointsUI -from . import RenderUI from . import ShaderBallUI from . import ShaderTweaksUI from . import CameraTweaksUI diff --git a/src/GafferScene/StandardOptions.cpp b/src/GafferScene/StandardOptions.cpp index 9d3a29732b8..a56edaea43d 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" ) ); + // Renderer + + options->addChild( new Gaffer::NameValuePlug( "render:defaultRenderer", new IECore::StringData(), false, "defaultRenderer" ) ); + // Render set options->addChild( new Gaffer::NameValuePlug( "render:includedPurposes", new IECore::StringVectorData( { "default", "render" } ), false, "includedPurposes" ) ); From 8f140c08c0ce0715baaa5a8795a37f002a9fa7fa Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 14 Mar 2024 11:06:14 +0000 Subject: [PATCH 7/7] RenderPassEditor : Add `render:defaultRenderer` column This is in desperate need of custom widgets for editing options, so we can provide a presets menu. But I think that will be better tackled more holistically, considering the requirements of other columns too. The current goal is to get the basic functionality in quickly to avoid Cinesite having to roll their own. --- Changes.md | 1 + startup/GafferScene/standardOptions.py | 11 +++++++++++ startup/gui/renderPassEditor.py | 1 + 3 files changed, 13 insertions(+) diff --git a/Changes.md b/Changes.md index 1795a6b7483..97476aaeba5 100644 --- a/Changes.md +++ b/Changes.md @@ -6,6 +6,7 @@ Features - Render, InteractiveRender : Added new nodes capable of rendering to any supported renderer, and using the`render:defaultRenderer` option to determine which to use by default. - StandardOptions : Added `render:defaultRenderer` option, allowing the scene globals to specify which renderer is used by the Render and InteractiveRender nodes. +- RenderPassEditor : Added a column for the `render:defaultRenderer` option, allowing each pass to be rendered in a different renderer. 1.3.13.1 (relative to 1.3.13.0) ======== diff --git a/startup/GafferScene/standardOptions.py b/startup/GafferScene/standardOptions.py index 8665a18c416..b9acb555fdc 100644 --- a/startup/GafferScene/standardOptions.py +++ b/startup/GafferScene/standardOptions.py @@ -110,6 +110,17 @@ """ ) +Gaffer.Metadata.registerValue( "option:render:defaultRenderer", "label", "Renderer" ) +Gaffer.Metadata.registerValue( "option:render:defaultRenderer", "defaultValue", "" ) +Gaffer.Metadata.registerValue( + "option:render:defaultRenderer", + "description", + """ + Specifies the default renderer to be used by the Render and + InteractiveRender nodes. + """ +) + Gaffer.Metadata.registerValue( "option:render:inclusions", "label", "Inclusions" ) Gaffer.Metadata.registerValue( "option:render:inclusions", "defaultValue", IECore.StringData( "/" ) ) Gaffer.Metadata.registerValue( diff --git a/startup/gui/renderPassEditor.py b/startup/gui/renderPassEditor.py index 3fc28293af4..ef91f11fbff 100644 --- a/startup/gui/renderPassEditor.py +++ b/startup/gui/renderPassEditor.py @@ -45,6 +45,7 @@ GafferSceneUI.RenderPassEditor.registerOption( "*", "render:exclusions" ) GafferSceneUI.RenderPassEditor.registerOption( "*", "render:additionalLights" ) +GafferSceneUI.RenderPassEditor.registerOption( "*", "render:defaultRenderer", "Render" ) GafferSceneUI.RenderPassEditor.registerOption( "*", "render:camera", "Render" ) GafferSceneUI.RenderPassEditor.registerOption( "*", "render:resolution", "Render" ) GafferSceneUI.RenderPassEditor.registerOption( "*", "render:resolutionMultiplier", "Render" )