From dadde484d28171f52a396eeaaa52f9f0c25afb2d Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Tue, 23 Apr 2024 09:46:28 -0700 Subject: [PATCH] ShaderTweakProxy : Allow using inputs from existing network in ShaderTweaks --- Changes.md | 5 + include/GafferScene/ShaderTweakProxy.h | 96 ++++++ include/GafferScene/TypeIds.h | 1 + .../GafferSceneTest/ShaderTweakProxyTest.py | 207 ++++++++++++ python/GafferSceneTest/ShaderTweaksTest.py | 2 +- python/GafferSceneTest/__init__.py | 1 + python/GafferSceneUI/ShaderTweakProxyUI.py | 305 ++++++++++++++++++ python/GafferSceneUI/ShaderTweaksUI.py | 21 ++ python/GafferSceneUI/__init__.py | 1 + src/GafferArnold/ArnoldShader.cpp | 5 + src/GafferCycles/CyclesShader.cpp | 5 + src/GafferOSL/OSLShader.cpp | 20 +- src/GafferScene/Shader.cpp | 32 +- src/GafferScene/ShaderPlug.cpp | 25 +- src/GafferScene/ShaderTweakProxy.cpp | 174 ++++++++++ src/GafferScene/ShaderTweaks.cpp | 184 ++++++++++- src/GafferSceneModule/TweaksBinding.cpp | 5 + src/GafferSceneTest/TestShader.cpp | 5 +- 18 files changed, 1080 insertions(+), 14 deletions(-) create mode 100644 include/GafferScene/ShaderTweakProxy.h create mode 100644 python/GafferSceneTest/ShaderTweakProxyTest.py create mode 100644 python/GafferSceneUI/ShaderTweakProxyUI.py create mode 100644 src/GafferScene/ShaderTweakProxy.cpp diff --git a/Changes.md b/Changes.md index e058c16cefe..7f07eb6cbb1 100644 --- a/Changes.md +++ b/Changes.md @@ -1,6 +1,11 @@ 1.4.x.x (relative to 1.4.4.0) ======= +Features +-------- + +- ShaderTweaks : Added support for creating ShaderTweakProxy nodes that allow making input connections to the original network. + Improvements ------------ diff --git a/include/GafferScene/ShaderTweakProxy.h b/include/GafferScene/ShaderTweakProxy.h new file mode 100644 index 00000000000..f465321ec88 --- /dev/null +++ b/include/GafferScene/ShaderTweakProxy.h @@ -0,0 +1,96 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Image Engine Design Inc. 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. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "GafferScene/Shader.h" + +namespace GafferScene +{ + +class GAFFERSCENE_API ShaderTweakProxy : public Shader +{ + + public : + + ShaderTweakProxy( const std::string &name = defaultName() ); + + ~ShaderTweakProxy() override; + + GAFFER_NODE_DECLARE_TYPE( GafferScene::ShaderTweakProxy, ShaderTweakProxyTypeId, Shader ); + + // Use this to set up a proxy for a specific type of shader - for auto proxies, call setupAutoProxy + // instead. The shader name passed in should start with a type prefix followed by a colon, to + // indicate how we need to load a shader in order to find its outputs to create a proxy. For example + // "osl:Conversion/ColorToFloat" means we will look for an OSL shader named "Conversion/ColorToFloat", + // and set up a proxy with matching output plugs. keepExistingValues is ignored, because proxies have + // only outputs. + void loadShader( const std::string &shaderName, bool keepExistingValues=false ) override; + + // Auto-proxies connect to the original input of whatever parameter you are tweaking on a ShaderTweaks. + // They use dynamic plugs to store the type of their output - the reference plug provides the type + // of plug to create. + void setupAutoProxy( const Gaffer::Plug* referencePlug ); + + // Parse the current shader name for the type prefix and source shader name + void typePrefixAndSourceShaderName( std::string &typePrefix, std::string &sourceShaderName ) const; + + // Identify if a shader is a proxy, created by ShaderTweakProxy + static bool isProxy( const IECoreScene::Shader *shader ); + + template + struct ShaderLoaderDescription + { + ShaderLoaderDescription( const std::string &typePrefix ) + { + registerShaderLoader( typePrefix, []() -> GafferScene::ShaderPtr{ return new T(); } ); + } + }; + + private : + + using ShaderLoaderCreator = std::function< ShaderPtr() >; + using ShaderLoaderCreatorMap = std::map< std::string, ShaderTweakProxy::ShaderLoaderCreator >; + static ShaderLoaderCreatorMap &shaderLoaderCreators(); + + static void registerShaderLoader( const std::string &typePrefix, ShaderLoaderCreator creator ); + + static size_t g_firstPlugIndex; +}; + +IE_CORE_DECLAREPTR( ShaderTweakProxy ) + +} // namespace GafferScene diff --git a/include/GafferScene/TypeIds.h b/include/GafferScene/TypeIds.h index 9e67835d112..734b592e57e 100644 --- a/include/GafferScene/TypeIds.h +++ b/include/GafferScene/TypeIds.h @@ -182,6 +182,7 @@ enum TypeId DeletePassesTypeId = 110638, MeshTessellateTypeId = 110639, RenderPassShaderTypeId = 110640, + ShaderTweakProxyTypeId = 110641, PreviewPlaceholderTypeId = 110647, PreviewGeometryTypeId = 110648, diff --git a/python/GafferSceneTest/ShaderTweakProxyTest.py b/python/GafferSceneTest/ShaderTweakProxyTest.py new file mode 100644 index 00000000000..ff5622c413f --- /dev/null +++ b/python/GafferSceneTest/ShaderTweakProxyTest.py @@ -0,0 +1,207 @@ +########################################################################## +# +# Copyright (c) 2024, Image Engine Design Inc. 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 unittest +import imath + +import IECore +import IECoreScene + +import Gaffer +import GafferScene +import GafferSceneTest + +class ShaderTweakProxyTest( GafferSceneTest.SceneTestCase ) : + + def test( self ) : + + plane = GafferScene.Plane() + shader = GafferSceneTest.TestShader( "surface" ) + shader["type"].setValue( "surface" ) + + textureShader1 = GafferSceneTest.TestShader( "texture1" ) + + textureShader2 = GafferSceneTest.TestShader( "texture2" ) + + shader["parameters"]["c"].setInput( textureShader1["out"] ) + textureShader1["parameters"]["c"].setInput( textureShader2["out"] ) + + planeFilter = GafferScene.PathFilter() + planeFilter["paths"].setValue( IECore.StringVectorData( [ "/plane" ] ) ) + + assignment = GafferScene.ShaderAssignment() + assignment["in"].setInput( plane["out"] ) + assignment["filter"].setInput( planeFilter["out"] ) + assignment["shader"].setInput( shader["out"] ) + + # Check the untweaked network + originalNetwork = assignment["out"].attributes( "/plane" )["surface"] + self.assertEqual( len( originalNetwork ), 3 ) + self.assertEqual( originalNetwork.input( ( "surface", "c" ) ), ( "texture1", "out" ) ) + + tweakShader = GafferSceneTest.TestShader( "tweakShader" ) + + tweaks = GafferScene.ShaderTweaks() + tweaks["in"].setInput( assignment["out"] ) + tweaks["filter"].setInput( planeFilter["out"] ) + tweaks["shader"].setValue( "surface" ) + + tweaks["tweaks"].addChild( Gaffer.TweakPlug( "c", Gaffer.Color3fPlug() ) ) + tweaks["tweaks"][0]["value"].setInput( tweakShader["out"] ) + + # If we replace the upstream network with a tweak, now we have just 2 nodes + tweakedNetwork = tweaks["out"].attributes( "/plane" )["surface"] + self.assertEqual( len( tweakedNetwork ), 2 ) + self.assertEqual( tweakedNetwork.input( ( "surface", "c" ) ), ( "tweakShader", "out" ) ) + + autoProxy = GafferScene.ShaderTweakProxy() + autoProxy.setupAutoProxy( Gaffer.Color3fPlug() ) + + # Using an auto proxy with no tweak shaders inserted recreates the original network + tweaks["tweaks"][0]["value"].setInput( autoProxy["out"]["auto"] ) + self.assertEqual( tweaks["out"].attributes( "/plane" )["surface"], originalNetwork ) + + # Test adding a tweak shader in the middle of the network using the proxy + tweakShader["parameters"]["c"].setInput( autoProxy["out"]["auto"] ) + tweaks["tweaks"][0]["value"].setInput( tweakShader["out"] ) + tweakedNetwork = tweaks["out"].attributes( "/plane" )["surface"] + self.assertEqual( len( tweakedNetwork ), 4 ) + self.assertEqual( tweakedNetwork.input( ( "surface", "c" ) ), ( "tweakShader", "out" ) ) + self.assertEqual( tweakedNetwork.input( ( "tweakShader", "c" ) ), ( "texture1", "out" ) ) + + # If we target the end of the network where there is no input, then the tweak gets inserted fine, + # and there is no input to the tweak, since there's nothing upstream + tweaks["tweaks"][0]["name"].setValue( "texture2.c" ) + tweakedNetwork = tweaks["out"].attributes( "/plane" )["surface"] + self.assertEqual( len( tweakedNetwork ), 4 ) + self.assertEqual( tweakedNetwork.input( ( "surface", "c" ) ), ( "texture1", "out" ) ) + self.assertEqual( tweakedNetwork.input( ( "texture1", "c" ) ), ( "texture2", "out" ) ) + self.assertEqual( tweakedNetwork.input( ( "texture2", "c" ) ), ( "tweakShader", "out" ) ) + self.assertEqual( tweakedNetwork.input( ( "tweakShader", "c" ) ), ( "", "" ) ) + + # Test using an auto-proxy on a parameter with no input ( it should apply the value to what the + # auto-proxy is connected to ) + textureShader2["parameters"]["c"].setValue( imath.Color3f( 5, 6, 7 ) ) + tweaks["tweaks"][0]["name"].setValue( "texture2.c" ) + tweakedNetwork = tweaks["out"].attributes( "/plane" )["surface"] + self.assertEqual( tweakedNetwork.getShader( "tweakShader" ).parameters["c"].value, imath.Color3f( 5, 6, 7 ) ) + + # Test proxying a specific node using a named handle + tweaks["tweaks"][0]["name"].setValue( "c" ) + + specificProxy = GafferScene.ShaderTweakProxy() + specificProxy.loadShader( "test:testShader" ) + + specificProxy["parameters"]["targetShader"].setValue( "texture2" ) + + tweakShader["parameters"]["c"].setInput( specificProxy["out"]["out"] ) + + tweakedNetwork = tweaks["out"].attributes( "/plane" )["surface"] + self.assertEqual( len( tweakedNetwork ), 3 ) + self.assertEqual( tweakedNetwork.input( ( "surface", "c" ) ), ( "tweakShader", "out" ) ) + self.assertEqual( tweakedNetwork.input( ( "tweakShader", "c" ) ), ( "texture2", "out" ) ) + + # Test error if we try to make a cycle + tweaks["tweaks"][0]["name"].setValue( "texture2.c" ) + specificProxy["parameters"]["targetShader"].setValue( "texture1" ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot use "texture1" in ShaderTweakProxy when tweaking "texture2", this would create cycle in shader network' ): + tweaks["out"].attributes( "/plane" ) + + def testAutoProxyValueTransferToComponent( self ) : + + plane = GafferScene.Plane() + shader = GafferSceneTest.TestShader( "surface" ) + shader["type"].setValue( "surface" ) + shader["parameters"]["i"].setValue( 42 ) + + planeFilter = GafferScene.PathFilter() + planeFilter["paths"].setValue( IECore.StringVectorData( [ "/plane" ] ) ) + + assignment = GafferScene.ShaderAssignment() + assignment["in"].setInput( plane["out"] ) + assignment["filter"].setInput( planeFilter["out"] ) + assignment["shader"].setInput( shader["out"] ) + + tweakShader = GafferSceneTest.TestShader( "tweakShader" ) + + tweaks = GafferScene.ShaderTweaks() + tweaks["in"].setInput( assignment["out"] ) + tweaks["filter"].setInput( planeFilter["out"] ) + tweaks["shader"].setValue( "surface" ) + + tweaks["tweaks"].addChild( Gaffer.TweakPlug( "i", Gaffer.IntPlug() ) ) + tweaks["tweaks"][0]["value"].setInput( tweakShader["out"]["r"] ) + + autoProxy = GafferScene.ShaderTweakProxy() + autoProxy.setupAutoProxy( Gaffer.IntPlug() ) + + tweakShader["parameters"]["c"]["g"].setInput( autoProxy["out"]["auto"] ) + + # This is quite a special case - there is no input to the parameter we are tweaking, so there is no + # connection to transfer, so we would expect the auto proxy to transfer the value - however the auto + # proxy output is connected to a subcomponent. + # + # The correct result is that the green component of tweakShader.c should be set to 42, transferring the value + # that was set. However, we have not yet added support for this fairly obscure case, so instead this test + # documents the current behaviour, which is to throw a semi-helpful exception. + + with self.assertRaisesRegex( Gaffer.ProcessException, 'CompoundData has no child named "c.g"' ): + tweaks["out"].attributes( "/plane" )["surface"] + + def testInvalidInShaderAssignment( self ) : + + plane = GafferScene.Plane() + + autoProxy = GafferScene.ShaderTweakProxy() + autoProxy.setupAutoProxy( Gaffer.Color3fPlug() ) + + planeFilter = GafferScene.PathFilter() + planeFilter["paths"].setValue( IECore.StringVectorData( [ "/plane" ] ) ) + + assignment = GafferScene.ShaderAssignment() + assignment["in"].setInput( plane["out"] ) + assignment["filter"].setInput( planeFilter["out"] ) + assignment["shader"].setInput( autoProxy["out"]["auto"] ) + + # Using a proxy in a shader assignment is invalid + with self.assertRaisesRegex( Gaffer.ProcessException, "ShaderTweakProxy only works with ShaderTweaks" ): + assignment["out"].attributes( "/plane" ) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferSceneTest/ShaderTweaksTest.py b/python/GafferSceneTest/ShaderTweaksTest.py index c369ab4da7a..3d519acaeb1 100644 --- a/python/GafferSceneTest/ShaderTweaksTest.py +++ b/python/GafferSceneTest/ShaderTweaksTest.py @@ -364,7 +364,7 @@ def testIgnoreMissing( self ) : inputShader = GafferSceneTest.TestShader() badTweak["value"].setInput( inputShader["out"]["r"] ) - with self.assertRaisesRegex( RuntimeError, "Cannot apply tweak \"badParameter\" because shader \"light\" does not have parameter \"badParameter\"" ) : + with self.assertRaisesRegex( RuntimeError, "Cannot apply tweak \"badParameter\" because shader \"__shader\" does not have parameter \"badParameter\"" ) : t["out"].attributes( "/light" ) badTweak["value"].setInput( None ) diff --git a/python/GafferSceneTest/__init__.py b/python/GafferSceneTest/__init__.py index 2f85bf71db3..da9ecb37aa6 100644 --- a/python/GafferSceneTest/__init__.py +++ b/python/GafferSceneTest/__init__.py @@ -112,6 +112,7 @@ from .FilteredSceneProcessorTest import FilteredSceneProcessorTest from .ShaderBallTest import ShaderBallTest from .ShaderTweaksTest import ShaderTweaksTest +from .ShaderTweakProxyTest import ShaderTweakProxyTest from .FilterResultsTest import FilterResultsTest from .RendererAlgoTest import RendererAlgoTest from .SetAlgoTest import SetAlgoTest diff --git a/python/GafferSceneUI/ShaderTweakProxyUI.py b/python/GafferSceneUI/ShaderTweakProxyUI.py new file mode 100644 index 00000000000..502d083d2a1 --- /dev/null +++ b/python/GafferSceneUI/ShaderTweakProxyUI.py @@ -0,0 +1,305 @@ +########################################################################## +# +# Copyright (c) 2024, Image Engine Design Inc. 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 Gaffer +import GafferUI +import GafferScene +import GafferSceneUI + +import IECore +import IECoreScene + +import functools +import imath + +Gaffer.Metadata.registerNode( + + GafferScene.ShaderTweakProxy, + + "description", + """ + Represents a shader in the shader network that a ShaderTweaks node is modifying. Allows forming + connections from existing shaders to shaders that are being inserted. + """, + + "icon", "shaderTweakProxy.png", + + plugs = { + + "name" : [ + + "description", "Hardcoded for ShaderTweakProxy nodes.", + "plugValueWidget:type", "", + + ], + + "type" : [ + + "description", "Hardcoded for ShaderTweakProxy nodes.", + "plugValueWidget:type", "", + + ], + + "parameters" : [ + + "plugValueWidget:type", "GafferUI.LayoutPlugValueWidget", + + ], + + "parameters.targetShader" : [ + + "description", + """ + The handle of the upstream shader being fetched by this proxy - or Auto, indicating that + the original input of the parameter being ShaderTweaked will be used. + """, + "readOnly", True, + "nodule:type", "", + "stringPlugValueWidget:placeholderText", "Auto", + + ], + + "out" : [ + + "plugValueWidget:type", "", + "nodule:type", "GafferUI::CompoundNodule" + + ], + + "out.*" : [ + + "description", + """ + The name of the output on the shader we are fetching, or "auto" for an auto proxy. + """, + + ], + + } + +) + +def __findConnectedShaderTweaks( startShader ): + shadersScanned = set() + shadersToScan = [ startShader ] + shaderTweaks = set() + + while len( shadersToScan ): + shader = shadersToScan.pop() + shadersScanned.add( shader ) + if isinstance( shader, GafferScene.ShaderTweaks ): + shaderTweaks.add( shader ) + continue + elif not isinstance( shader, GafferScene.Shader ): + continue + elif not "out" in shader: + continue + + possibleOutputs = [ shader["out"] ] + + outputs = [] + + while len( possibleOutputs ): + po = possibleOutputs.pop() + if po.outputs(): + outputs.append( po ) + else: + for c in po.children(): + possibleOutputs.append( c ) + + while len( outputs ): + o = outputs.pop() + if o.outputs(): + outputs += o.outputs() + else: + dest = Gaffer.PlugAlgo.findDestination( o, lambda plug : plug if not plug.outputs() else None ).node() + if not dest in shadersScanned: + shadersToScan.append( dest ) + + return shaderTweaks + +def __createShaderTweakProxy( plug, sourceHandle, sourceType, sourceName ): + + with Gaffer.UndoScope( plug.ancestor( Gaffer.ScriptNode ) ): + result = GafferScene.ShaderTweakProxy( sourceHandle or "Auto" ) + if sourceHandle: + sourceTypePrefix = sourceType.split( ":" )[0] + result.loadShader( sourceTypePrefix + ":" + sourceName ) + else: + result.setupAutoProxy( plug ) + result["parameters"]["targetShader"].setValue( sourceHandle ) + + plug.node().parent().addChild( result ) + plug.node().scriptNode().selection().clear() + plug.node().scriptNode().selection().add( result ) + + # See if there are any output plugs on the new proxy which can be connected to this plug + for p in result["out"].children(): + try: + plug.setInput( p ) + except: + continue + break + + # Make sure the target plug on the destination ShaderTweaks is visible if we're connecting to it + # ( might not be the case if we're doing this using the menu buttons on ShaderTweaks ) + if type( plug.node() ) == GafferScene.ShaderTweaks and plug.parent().parent() == plug.node()["tweaks"]: + Gaffer.Metadata.registerValue( plug.parent(), "noduleLayout:visible", True ) + + # \todo - It's probably bad that I'm doing this manually, instead of using GraphGadget.setNodePosition + # ... but it also feels wrong that that is a non-static member of GraphGadget ... it doesn't use + # any members of GraphGadget, and when creating a new ShaderTweakProxy from the Node Editor, this totally + # makes sense to do, even if there are no current GraphGadgets + if "__uiPosition" in plug.node(): + result.addChild( Gaffer.V2fPlug( "__uiPosition", Gaffer.Plug.Direction.In, flags = Gaffer.Plug.Flags.Default | Gaffer.Plug.Flags.Dynamic ) ) + result["__uiPosition"].setValue( plug.node()["__uiPosition"].getValue() + imath.V2f( -20, 0 ) ) + +def _shaderAttributes( context, nodes, paths, affectedOnly ) : + + result = {} + + with context : + for node in nodes: + useFullAttr = node["localise"].getValue() + attributeNamePatterns = node["shader"].getValue() if affectedOnly else "*" + for path in paths : + if not node["in"].exists( path ): + continue + + attributes = node["in"].fullAttributes( path ) if useFullAttr else node["in"].attributes( path ) + for name, attribute in attributes.items() : + if not IECore.StringAlgo.matchMultiple( name, attributeNamePatterns ) : + continue + if not isinstance( attribute, IECoreScene.ShaderNetwork ) or not len( attribute ) : + continue + result.setdefault( path, {} )[name] = attribute + + return result + +def __browseShaders( scriptWindow, plug, context, nodes, paths ) : + + shaderAttributes = _shaderAttributes( context, nodes, paths, affectedOnly = True ) + + uniqueNetworks = { n.hash(): n for a in shaderAttributes.values() for n in a.values() } + + browser = GafferSceneUI.ShaderUI._ShaderDialogue( uniqueNetworks.values(), "Select Source Shader" ) + + shaderHandle = browser.waitForShader( parentWindow = scriptWindow ) + + if shaderHandle is not None : + for n in uniqueNetworks.values(): + if shaderHandle in n.shaders().keys(): + shader = n.shaders()[shaderHandle] + __createShaderTweakProxy( plug, shaderHandle, shader.type, shader.name ) + break + +def _pathsFromAffected( context, nodes ) : + + pathMatcher = IECore.PathMatcher() + with context: + for node in nodes: + GafferScene.SceneAlgo.matchingPaths( node["filter"], node["in"], pathMatcher ) + + return pathMatcher.paths() + +def _pathsFromSelection( context ) : + + paths = GafferSceneUI.ContextAlgo.getSelectedPaths( context ) + paths = paths.paths() if paths else [] + + return paths + + +def __browseAffectedShaders( plug, shaderTweaksOverride, menu ) : + + context = plug.ancestor( Gaffer.ScriptNode ).context() + shaderTweaks = [ shaderTweaksOverride ] if shaderTweaksOverride else __findConnectedShaderTweaks( plug.node() ) + + __browseShaders( + menu.ancestor( GafferUI.Window ), plug, context, shaderTweaks, _pathsFromAffected( context, shaderTweaks ) + ) + +def __browseSelectedShaders( plug, shaderTweaksOverride, menu ) : + + context = plug.ancestor( Gaffer.ScriptNode ).context() + shaderTweaks = [ shaderTweaksOverride ] if shaderTweaksOverride else __findConnectedShaderTweaks( plug.node() ) + __browseShaders( + menu.ancestor( GafferUI.Window ), plug, context, shaderTweaks, _pathsFromSelection( context ) + ) + +def _plugContextMenu( plug, shaderTweaks ) : + + menuDefinition = IECore.MenuDefinition() + + # Find the actual node if we're looking at something like a box input + # NOTE : This could fail if a shader output is connected to 2 things, and the first thing is not a shader, + # but that seems like a pretty weird case, and we want to get to the early out without doing too much traversal + destPlug = Gaffer.PlugAlgo.findDestination( plug, lambda plug : plug if not plug.outputs() else None ) + + if not ( isinstance( destPlug.node(), GafferScene.Shader ) or isinstance( destPlug.node(), GafferScene.ShaderTweaks ) ): + return + + menuDefinition.append( + "Auto ( Original Input )", + { + "command" : functools.partial( __createShaderTweakProxy, plug, "", "", "" ), + "active" : not Gaffer.MetadataAlgo.readOnly( plug.node().parent() ), + } + ) + + menuDefinition.append( + "From Affected", + { + "command" : functools.partial( __browseAffectedShaders, plug, shaderTweaks ), + "active" : not Gaffer.MetadataAlgo.readOnly( plug.node().parent() ), + } + ) + menuDefinition.append( + "From Selected", + { + "command" : functools.partial( __browseSelectedShaders, plug, shaderTweaks ), + "active" : not Gaffer.MetadataAlgo.readOnly( plug.node().parent() ), + } + ) + + return menuDefinition + +def __plugContextMenuSignal( graphEditor, plug, menuDefinition ) : + menuDefinition.append( "/Create ShaderTweakProxy", + { "subMenu" : functools.partial( _plugContextMenu, plug, None ) } + ) + +GafferUI.GraphEditor.plugContextMenuSignal().connect( __plugContextMenuSignal, scoped = False ) diff --git a/python/GafferSceneUI/ShaderTweaksUI.py b/python/GafferSceneUI/ShaderTweaksUI.py index 95e7f850698..78e54f39781 100644 --- a/python/GafferSceneUI/ShaderTweaksUI.py +++ b/python/GafferSceneUI/ShaderTweaksUI.py @@ -129,6 +129,7 @@ "tweakPlugValueWidget:allowCreate", True, "tweakPlugValueWidget:allowRemove", True, "tweakPlugValueWidget:propertyType", "parameter", + "plugValueWidget:type", "GafferSceneUI.ShaderTweaksUI._ShaderTweakPlugValueWidget", ], @@ -327,6 +328,26 @@ def __addTweak( self, name, plugTypeOrValue ) : with Gaffer.UndoScope( self.getPlug().ancestor( Gaffer.ScriptNode ) ) : self.getPlug().addChild( plug ) +class _ShaderTweakPlugValueWidget( GafferUI.TweakPlugValueWidget ) : + + def __init__( self, plugs ): + GafferUI.TweakPlugValueWidget.__init__( self, plugs ) + self._TweakPlugValueWidget__row.append( + GafferUI.MenuButton( + image="shaderTweakProxyIcon.png", + hasFrame=False, + menu=GafferUI.Menu( Gaffer.WeakMethod( self.__createProxyMenuDefinition ), title = "Create Proxy" ), + toolTip = "Proxies allow making connections from the outputs of nodes in the input network." + ) + ) + + def __createProxyMenuDefinition( self ) : + return GafferSceneUI.ShaderTweakProxyUI._plugContextMenu( self.getPlug()["value"], self.getPlug().node() ) + + def __updateReadOnly( self ) : + + self.setEnabled( not Gaffer.MetadataAlgo.readOnly( self.__plugParent.node().parent() ) ) + ########################################################################## # PlugValueWidget context menu ########################################################################## diff --git a/python/GafferSceneUI/__init__.py b/python/GafferSceneUI/__init__.py index e7f6379ebb9..4491b37e1f4 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -129,6 +129,7 @@ from . import MeshToPointsUI from . import ShaderBallUI from . import ShaderTweaksUI +from . import ShaderTweakProxyUI from . import CameraTweaksUI from . import LightToCameraUI from . import FilterResultsUI diff --git a/src/GafferArnold/ArnoldShader.cpp b/src/GafferArnold/ArnoldShader.cpp index 4fd3d4ed973..0491da38be2 100644 --- a/src/GafferArnold/ArnoldShader.cpp +++ b/src/GafferArnold/ArnoldShader.cpp @@ -41,6 +41,8 @@ #include "GafferOSL/OSLShader.h" +#include "GafferScene/ShaderTweakProxy.h" + #include "Gaffer/CompoundNumericPlug.h" #include "Gaffer/NumericPlug.h" #include "Gaffer/StringPlug.h" @@ -75,6 +77,9 @@ namespace const bool g_oslRegistration = OSLShader::registerCompatibleShader( "ai:surface" ); const InternedString g_inputParameterName( "input" ); +ShaderTweakProxy::ShaderLoaderDescription g_arnoldShaderTweakProxyLoaderRegistration( "ai" ); + + } // namespace GAFFER_NODE_DEFINE_TYPE( ArnoldShader ); diff --git a/src/GafferCycles/CyclesShader.cpp b/src/GafferCycles/CyclesShader.cpp index f0f56607ee4..f6c640586cb 100644 --- a/src/GafferCycles/CyclesShader.cpp +++ b/src/GafferCycles/CyclesShader.cpp @@ -39,6 +39,8 @@ #include "GafferCycles/SocketHandler.h" +#include "GafferScene/ShaderTweakProxy.h" + #include "Gaffer/CompoundNumericPlug.h" #include "Gaffer/Metadata.h" #include "Gaffer/NumericPlug.h" @@ -73,6 +75,9 @@ bool g_oslRegistrationSurface = OSLShader::registerCompatibleShader( "cycles:sur bool g_oslRegistrationVolume = OSLShader::registerCompatibleShader( "cycles:volume" ); bool g_oslRegistrationDisplacement = OSLShader::registerCompatibleShader( "cycles:displacement" ); +ShaderTweakProxy::ShaderLoaderDescription g_cyclesShaderTweakProxyLoaderRegistration( "cycles" ); + + } // namespace IE_CORE_DEFINERUNTIMETYPED( CyclesShader ); diff --git a/src/GafferOSL/OSLShader.cpp b/src/GafferOSL/OSLShader.cpp index b6877c9193b..77c30607478 100644 --- a/src/GafferOSL/OSLShader.cpp +++ b/src/GafferOSL/OSLShader.cpp @@ -39,6 +39,8 @@ #include "GafferOSL/ClosurePlug.h" #include "GafferOSL/ShadingEngine.h" +#include "GafferScene/ShaderTweakProxy.h" + #include "Gaffer/CompoundNumericPlug.h" #include "Gaffer/Metadata.h" #include "Gaffer/NumericPlug.h" @@ -163,6 +165,13 @@ ShaderTypeSet &compatibleShaders() return g_compatibleShaders; } +// Auto-proxies are always allowed to connect, because we don't know what type they will be until we evaluate +// the graph. +const bool g_oslShaderTweakAutoProxyRegistration = OSLShader::registerCompatibleShader( "autoProxy" ); + +ShaderTweakProxy::ShaderLoaderDescription g_oslShaderTweakProxyLoaderRegistration( "osl" ); + + } // namespace ///////////////////////////////////////////////////////////////////////// @@ -266,7 +275,16 @@ bool OSLShader::acceptsInput( const Plug *plug, const Plug *inputPlug ) const return true; } - const IECore::InternedString sourceShaderType = sourceShader->typePlug()->getValue(); + std::string sourceShaderType; + if( const ShaderTweakProxy *shaderTweakProxy = IECore::runTimeCast< const ShaderTweakProxy >( sourceShader ) ) + { + std::string unusedSourceShaderName; + shaderTweakProxy->typePrefixAndSourceShaderName( sourceShaderType, unusedSourceShaderName ); + } + else + { + sourceShaderType = sourceShader->typePlug()->getValue(); + } const ShaderTypeSet &cs = compatibleShaders(); if( cs.find( sourceShaderType ) != cs.end() ) { diff --git a/src/GafferScene/Shader.cpp b/src/GafferScene/Shader.cpp index 93666fcd760..2bfba0f01a2 100644 --- a/src/GafferScene/Shader.cpp +++ b/src/GafferScene/Shader.cpp @@ -37,6 +37,8 @@ #include "GafferScene/Shader.h" +#include "GafferScene/ShaderTweakProxy.h" + #include "Gaffer/PlugAlgo.h" #include "Gaffer/Metadata.h" #include "Gaffer/NumericPlug.h" @@ -178,7 +180,7 @@ class Shader::NetworkBuilder public : NetworkBuilder( const Gaffer::Plug *output ) - : m_output( output ) + : m_output( output ), m_hasProxyNodes( false ) { } @@ -199,6 +201,8 @@ class Shader::NetworkBuilder IECoreScene::ConstShaderNetworkPtr network() { + static IECore::InternedString hasProxyNodesIdentifier( "__hasProxyNodes" ); + if( !m_network ) { m_network = new IECoreScene::ShaderNetwork; @@ -210,6 +214,12 @@ class Shader::NetworkBuilder } } } + + if( m_hasProxyNodes ) + { + m_network->blindData()->writable()[hasProxyNodesIdentifier] = new IECore::BoolData( true ); + } + return m_network; } @@ -365,25 +375,29 @@ class Shader::NetworkBuilder return handleAndHash.handle; } - std::string type = shaderNode->typePlug()->getValue(); - if( shaderNode != m_output->node() && !boost::ends_with( type, "shader" ) ) + IECoreScene::ShaderPtr shader = new IECoreScene::Shader( + shaderNode->namePlug()->getValue(), shaderNode->typePlug()->getValue() + ); + if( + !ShaderTweakProxy::isProxy( shader.get() ) && + shaderNode != m_output->node() && !boost::ends_with( shader->getType(), "shader" ) + ) { // Some renderers (Arnold for one) allow surface shaders to be connected // as inputs to other shaders, so we may need to change the shader type to // convert it into a standard shader. We must take care to preserve any // renderer specific prefix when doing this. - size_t i = type.find_first_of( ":" ); + size_t i = shader->getType().find_first_of( ":" ); if( i != std::string::npos ) { - type = type.substr( 0, i + 1 ) + "shader"; + shader->setType( shader->getType().substr( 0, i + 1 ) + "shader" ); } else { - type = "shader"; + shader->setType( "shader" ); } } - - IECoreScene::ShaderPtr shader = new IECoreScene::Shader( shaderNode->namePlug()->getValue(), type ); + m_hasProxyNodes |= ShaderTweakProxy::isProxy( shader.get() ); const std::string nodeName = shaderNode->nodeNamePlug()->getValue(); shader->blindData()->writable()["label"] = new IECore::StringData( nodeName ); @@ -759,6 +773,8 @@ class Shader::NetworkBuilder ShaderSet m_downstreamShaders; // Used for detecting cycles + bool m_hasProxyNodes; + }; ////////////////////////////////////////////////////////////////////////// diff --git a/src/GafferScene/ShaderPlug.cpp b/src/GafferScene/ShaderPlug.cpp index 09f4a22252b..3bd36f42649 100644 --- a/src/GafferScene/ShaderPlug.cpp +++ b/src/GafferScene/ShaderPlug.cpp @@ -45,6 +45,8 @@ #include "Gaffer/SubGraph.h" #include "Gaffer/Switch.h" +#include "IECoreScene/ShaderNetwork.h" + #include "IECore/MurmurHash.h" using namespace IECore; @@ -231,6 +233,7 @@ IECore::MurmurHash ShaderPlug::attributesHash() const IECore::ConstCompoundObjectPtr ShaderPlug::attributes() const { + static IECore::InternedString hasProxyNodesIdentifier( "__hasProxyNodes" ); if( const Gaffer::Plug *p = shaderOutPlug() ) { if( auto s = runTimeCast( p->node() ) ) @@ -242,7 +245,27 @@ IECore::ConstCompoundObjectPtr ShaderPlug::attributes() const outputParameter = p->relativeName( s->outPlug() ); scope.set( Shader::g_outputParameterContextName, &outputParameter ); } - return s->outAttributesPlug()->getValue(); + + IECore::ConstCompoundObjectPtr result = s->outAttributesPlug()->getValue(); + + // Check for outputs from ShaderTweakProxy, which should only be used with ShaderTweaks nodes + for( const auto &i : result->members() ) + { + if( const IECoreScene::ShaderNetwork *shaderNetwork = IECore::runTimeCast< const IECoreScene::ShaderNetwork >( i.second.get() ) ) + { + if( const BoolData *hasProxyNodes = shaderNetwork->blindData()->member( hasProxyNodesIdentifier ) ) + { + if( hasProxyNodes->readable() ) + { + throw IECore::Exception( + "ShaderTweakProxy only works with ShaderTweaks - it doesn't make sense to connect one here" + ); + + } + } + } + } + return result; } } return new CompoundObject; diff --git a/src/GafferScene/ShaderTweakProxy.cpp b/src/GafferScene/ShaderTweakProxy.cpp new file mode 100644 index 00000000000..96a8d8e82d9 --- /dev/null +++ b/src/GafferScene/ShaderTweakProxy.cpp @@ -0,0 +1,174 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2024, Image Engine Design Inc. 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. +// +////////////////////////////////////////////////////////////////////////// + +#include "GafferScene/ShaderTweakProxy.h" + +#include "Gaffer/StringPlug.h" +#include "Gaffer/PlugAlgo.h" + +#include + +using namespace Gaffer; +using namespace GafferScene; + +namespace +{ + +const std::string g_shaderTweakProxyIdentifier = "__SHADER_TWEAK_PROXY"; + +} // namespace + + +GAFFER_NODE_DEFINE_TYPE( ShaderTweakProxy ); + +size_t ShaderTweakProxy::g_firstPlugIndex; + +ShaderTweakProxy::ShaderTweakProxy( const std::string &name ) + : Shader( name ) +{ + storeIndexOfNextChild( g_firstPlugIndex ); + + addChild( new Plug( "out", Plug::Out ) ); + parametersPlug()->addChild( new StringPlug( "targetShader", Plug::Direction::In, "" ) ); +} + +ShaderTweakProxy::~ShaderTweakProxy() +{ +} + +ShaderTweakProxy::ShaderLoaderCreatorMap &ShaderTweakProxy::shaderLoaderCreators() +{ + // Deliberately "leaking" list, as it may contain Python functors which + // cannot be destroyed during program exit (because Python will have been + // shut down first). + static auto g_creators = new ShaderLoaderCreatorMap; + return *g_creators; +} + +void ShaderTweakProxy::typePrefixAndSourceShaderName( std::string &typePrefix, std::string &sourceShaderName ) const +{ + std::string shaderName = namePlug()->getValue(); + if( shaderName == "autoProxy" ) + { + typePrefix = "autoProxy"; + sourceShaderName = ""; + return; + } + + size_t sep = shaderName.find(":"); + if( sep == std::string::npos ) + { + throw IECore::Exception( fmt::format( + "Malformed ShaderTweakProxy shader name \"{}\". Must include type prefix.", shaderName + ) ); + } + + typePrefix = shaderName.substr(0, sep ); + sourceShaderName = shaderName.substr( sep + 1 ); +} + + +void ShaderTweakProxy::loadShader( const std::string &shaderName, bool keepExistingValues ) +{ + typePlug()->setValue( g_shaderTweakProxyIdentifier ); + namePlug()->setValue( shaderName ); + if( shaderName == "autoProxy" ) + { + // Auto-proxies use dynamic plugs to represent their outputs instead of serializing a specific + // shader type. + return; + } + + // If we're proxying a specific node type, we need to find out what the outputs of that node type are + outPlug()->clearChildren(); + + std::string shaderTypePrefix, sourceShaderName; + typePrefixAndSourceShaderName( shaderTypePrefix, sourceShaderName ); + + ShaderPtr loaderNode; + + // Find the correct node type to load this shader with, and create a temporary loader node + const ShaderLoaderCreatorMap& creatorMap = shaderLoaderCreators(); + auto match = creatorMap.find( shaderTypePrefix ); + if( match != creatorMap.end() ) + { + loaderNode = match->second(); + } + else + { + std::vector possibilityList; + for( const auto &i : creatorMap ) + { + possibilityList.push_back( "\"" + i.first + "\"" ); + } + std::string possibilities = boost::algorithm::join( possibilityList, ", " ); + throw IECore::Exception( fmt::format( "No ShaderTweakProxy shader loader registered for type prefix \"{}\", options are {}", shaderTypePrefix, possibilities ) ); + } + + loaderNode->loadShader( sourceShaderName ); + + if( loaderNode->outPlug()->isInstanceOf( Gaffer::ValuePlug::staticTypeId() ) ) + { + outPlug()->addChild( loaderNode->outPlug()->createCounterpart( "out", Plug::Direction::Out ) ); + } + else + { + // Not a value plug, which means it should have children + for( const auto &untypedChild : loaderNode->outPlug()->children() ) + { + Plug *child = IECore::runTimeCast< Plug >( untypedChild.get() ); + outPlug()->addChild( child->createCounterpart( child->getName(), Plug::Direction::Out ) ); + } + } +} + +void ShaderTweakProxy::setupAutoProxy( const Plug* referencePlug ) +{ + loadShader( "autoProxy" ); + PlugPtr newPlug = referencePlug->createCounterpart( "auto", Plug::Direction::Out ); + newPlug->setFlags( Plug::Flags::Dynamic, true ); + outPlug()->addChild( newPlug ); +} + +bool ShaderTweakProxy::isProxy( const IECoreScene::Shader *shader ) +{ + return shader->getType() == g_shaderTweakProxyIdentifier; +} + +void ShaderTweakProxy::registerShaderLoader( const std::string &typePrefix, ShaderLoaderCreator creator ) +{ + shaderLoaderCreators()[typePrefix] = creator; +} diff --git a/src/GafferScene/ShaderTweaks.cpp b/src/GafferScene/ShaderTweaks.cpp index 79bfcc98a1a..2865fc9ead3 100644 --- a/src/GafferScene/ShaderTweaks.cpp +++ b/src/GafferScene/ShaderTweaks.cpp @@ -37,6 +37,7 @@ #include "GafferScene/ShaderTweaks.h" #include "GafferScene/Shader.h" +#include "GafferScene/ShaderTweakProxy.h" #include "Gaffer/CompoundDataPlug.h" #include "Gaffer/TweakPlug.h" @@ -47,6 +48,8 @@ #include "IECore/SimpleTypedData.h" #include "IECore/StringAlgo.h" +#include "IECore/TypeTraits.h" +#include "IECore/DataAlgo.h" #include "fmt/format.h" @@ -77,6 +80,104 @@ std::pair shaderOutput( const return { nullptr, nullptr }; } +DataPtr castDataToType( const Data* source, const Data *target ) +{ + DataPtr result; + if( source->typeId() == target->typeId() ) + { + result = source->copy(); + } + + dispatch( target, + [source, &result]( const auto *targetTyped ) + { + using TargetType = typename std::remove_const_t >; + if constexpr( TypeTraits::IsSimpleTypedData::value ) + { + using TargetValueType = typename TargetType::ValueType; + if constexpr( std::is_arithmetic_v< TargetValueType > ) + { + dispatch( source, + [&result]( const auto *sourceTyped ) + { + using SourceType = typename std::remove_const_t >; + if constexpr( TypeTraits::IsNumericSimpleTypedData::value ) + { + result = new TargetType( sourceTyped->readable() ); + } + } + ); + return; + } + + if constexpr( TypeTraits::IsVec3::value || TypeTraits::IsColor::value ) + { + dispatch( source, + [&result]( const auto *sourceTyped ) + { + using SourceType = typename std::remove_const_t >; + if constexpr( TypeTraits::IsSimpleTypedData::value ) + { + using SourceValueType = typename SourceType::ValueType; + if constexpr( + TypeTraits::IsVec3TypedData::value || + TypeTraits::IsColor::value + ) + { + typename TargetType::ValueType r; + r[0] = sourceTyped->readable()[0]; + r[1] = sourceTyped->readable()[1]; + r[2] = sourceTyped->readable()[2]; + result = new TargetType( r ); + } + } + } + ); + return; + } + } + + } + ); + + if( !result ) + { + throw IECore::Exception( fmt::format( + "Cannot connect auto proxy from \"{}\" tweak to shader input of type \"{}\"", + source->typeName(), target->typeName() + ) ); + } + + return result; +} + +void checkForCycleWalkDownstream( const ShaderNetwork &network, const IECore::InternedString &shader, std::unordered_set &dependentShaders ) +{ + if( dependentShaders.insert( shader ).second ) + { + for( const auto &connection : network.outputConnections( shader ) ) + { + checkForCycleWalkDownstream( network, connection.destination.shader, dependentShaders ); + } + } +} + +void checkForCycle( const ShaderNetwork &network, const IECore::InternedString &destShader, std::unordered_set &dependentShadersCache, const IECore::InternedString &sourceShader ) +{ + if( !dependentShadersCache.size() ) + { + checkForCycleWalkDownstream( network, destShader, dependentShadersCache ); + } + + if( dependentShadersCache.find( sourceShader ) != dependentShadersCache.end() ) + { + throw IECore::Exception( fmt::format( + "Cannot use \"{}\" in ShaderTweakProxy when tweaking \"{}\", this would create cycle in shader network.", + sourceShader.string(), destShader.string() + ) ); + } +} + } // namespace GAFFER_NODE_DEFINE_TYPE( ShaderTweaks ); @@ -286,13 +387,14 @@ bool ShaderTweaks::applyTweaks( IECoreScene::ShaderNetwork *shaderNetwork, Tweak const TweakPlug::Mode mode = static_cast( tweakPlug->modePlug()->getValue() ); - if( auto input = shaderNetwork->input( parameter ) ) + ShaderNetwork::Parameter originalInput = shaderNetwork->input( parameter ); + if( originalInput ) { if( mode != TweakPlug::Mode::Replace ) { throw IECore::Exception( fmt::format( "Cannot apply tweak to \"{}\" : Mode must be \"Replace\" when a previous connection exists", name ) ); } - shaderNetwork->removeConnection( { input, parameter } ); + shaderNetwork->removeConnection( { originalInput, parameter } ); removedConnections = true; } @@ -331,8 +433,86 @@ bool ShaderTweaks::applyTweaks( IECoreScene::ShaderNetwork *shaderNetwork, Tweak { throw IECore::Exception( fmt::format( "Cannot apply tweak to \"{}\" : Mode must be \"Replace\" when inserting a connection", name ) ); } + const auto inputParameter = ShaderNetworkAlgo::addShaders( shaderNetwork, inputNetwork ); shaderNetwork->addConnection( { inputParameter, parameter } ); + + static IECore::InternedString hasProxyNodesIdentifier( "__hasProxyNodes" ); + + const BoolData* hasProxyNodes = inputNetwork->blindData()->member( hasProxyNodesIdentifier ); + if( hasProxyNodes && hasProxyNodes->readable() ) + { + // It would be more efficient to search for and process tweak sources just in + // `inputNetwork` before merging it to `shaderNetwork` ... but this would require + // dealing with weird connections where the input node handle is relative to `shaderNetwork`, + // but the output handle is relative to `inputNetwork`. This can't currenty be done if there + // are nodes in the two networks with the same name, which get uniquified during addShaders. + // This could be solved with an optional output unordered_map + // from addShaders(). For the moment, however, Doing this after merging simplifies all that. + + // If we need to check for cycles, we will need to populate a set of dependent shaders. + // We cache it in case there are multiple proxies connected to the same tweak. + std::unordered_set dependentShadersCache; + + for( const auto &i : shaderNetwork->shaders() ) + { + if( !ShaderTweakProxy::isProxy( i.second.get() ) ) + { + continue; + } + + const StringData* targetShaderData = + i.second->parametersData()->member( "targetShader" ); + if( !targetShaderData ) + { + throw IECore::Exception( "Cannot find target shader parameter on ShaderTweakProxy" ); + } + const std::string &sourceShader = targetShaderData->readable(); + + ShaderNetwork::ConnectionRange range = shaderNetwork->outputConnections( i.first ); + const std::vector outputConnections( range.begin(), range.end() ); + + + for( const auto &c : outputConnections ) + { + + shaderNetwork->removeConnection( c ); + removedConnections = true; + + if( sourceShader == "" ) + { + if( originalInput ) + { + shaderNetwork->addConnection( { originalInput, c.destination } ); + } + else + { + const IECoreScene::Shader *proxyConnectedShader = shaderNetwork->getShader( c.destination.shader ); + if( !proxyConnectedShader ) + { + throw IECore::Exception( fmt::format( "ShaderTweakProxy connected to non-existent shader \"{}\"", c.destination.shader.string() ) ); + } + + // Regular tweak + auto modifiedShader = modifiedShaders.insert( { c.destination.shader, nullptr } ); + if( modifiedShader.second ) + { + modifiedShader.first->second = proxyConnectedShader->copy(); + } + + const IECore::Data *origDestParameter = modifiedShader.first->second->parametersData()->member(c.destination.name, /* throwExceptions = */ true ); + modifiedShader.first->second->parameters()[c.destination.name] = castDataToType( shader->parametersData()->member( parameter.name, /* throwExceptions = */ true ), origDestParameter ); + } + } + else + { + checkForCycle( *shaderNetwork, parameter.shader, dependentShadersCache, sourceShader ); + shaderNetwork->addConnection( { { sourceShader, c.source.name }, c.destination } ); + } + } + } + } + appliedTweaks = true; } } diff --git a/src/GafferSceneModule/TweaksBinding.cpp b/src/GafferSceneModule/TweaksBinding.cpp index ba3c7233cbb..3ac1cf1871c 100644 --- a/src/GafferSceneModule/TweaksBinding.cpp +++ b/src/GafferSceneModule/TweaksBinding.cpp @@ -43,6 +43,7 @@ #include "GafferScene/CameraTweaks.h" #include "GafferScene/OptionTweaks.h" #include "GafferScene/ShaderTweaks.h" +#include "GafferScene/ShaderTweakProxy.h" #include "GafferBindings/DependencyNodeBinding.h" #include "GafferBindings/PlugBinding.h" @@ -60,4 +61,8 @@ void GafferSceneModule::bindTweaks() DependencyNodeClass(); DependencyNodeClass(); DependencyNodeClass(); + + DependencyNodeClass() + .def( "setupAutoProxy", &ShaderTweakProxy::setupAutoProxy ) + ; } diff --git a/src/GafferSceneTest/TestShader.cpp b/src/GafferSceneTest/TestShader.cpp index d34f5c58e5d..6298bad6c89 100644 --- a/src/GafferSceneTest/TestShader.cpp +++ b/src/GafferSceneTest/TestShader.cpp @@ -36,6 +36,8 @@ #include "GafferSceneTest/TestShader.h" +#include "GafferScene/ShaderTweakProxy.h" + #include "Gaffer/CompoundNumericPlug.h" #include "Gaffer/OptionalValuePlug.h" #include "Gaffer/PlugAlgo.h" @@ -95,7 +97,8 @@ Plug *setupOptionalValuePlug( return plug.get(); } -} // namespace +GafferScene::ShaderTweakProxy::ShaderLoaderDescription g_testShaderTweakProxyLoaderRegistration( "test" ); +} // namespace GAFFER_NODE_DEFINE_TYPE( TestShader )