diff --git a/include/GafferScene/ShaderTweakProxy.h b/include/GafferScene/ShaderTweakProxy.h new file mode 100644 index 00000000000..033190a736d --- /dev/null +++ b/include/GafferScene/ShaderTweakProxy.h @@ -0,0 +1,80 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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" + +#include "IECoreScene/ShaderNetwork.h" +#include "IECore/ObjectVector.h" +#include "IECore/VectorTypedData.h" + +#include + +namespace GafferScene +{ + +class GAFFERSCENE_API ShaderTweakProxy : public Shader +{ + + public : + + // Create a ShaderTweakProxy, set up to proxy a particular node in the network being tweaked. + ShaderTweakProxy( const std::string &sourceNode, const IECore::StringVectorData *outputNames, const IECore::ObjectVector *outputTypes, const std::string &name=defaultName()); + + // Should only be called by serializer, to construct ShaderTweakProxies that already have their plugs + // set up ( It might be slightly cleaner to make the plugs not dynamic, and instead use a custom + // serializer that calls the constructor above, but that seems like more work ). + ShaderTweakProxy( const std::string &name ); + + ~ShaderTweakProxy() override; + + GAFFER_NODE_DECLARE_TYPE( GafferScene::ShaderTweakProxy, ShaderTweakProxyTypeId, Shader ); + + // This is implemented to do nothing, because ShaderTweakProxy isn't really a shader, it just + // acts like one to store connections before they get replumbed to their actual targets. + void loadShader( const std::string &shaderName, bool keepExistingValues=false ) override; + + static const std::string &shaderTweakProxyIdentifier(); + + private : + + static size_t g_firstPlugIndex; +}; + +IE_CORE_DECLAREPTR( ShaderTweakProxy ) + +} // namespace GafferScene diff --git a/include/GafferScene/TypeIds.h b/include/GafferScene/TypeIds.h index 8d197a7bb43..c2171db7736 100644 --- a/include/GafferScene/TypeIds.h +++ b/include/GafferScene/TypeIds.h @@ -181,6 +181,7 @@ enum TypeId PassesTypeId = 110637, DeletePassesTypeId = 110638, MeshTessellateTypeId = 110639, + ShaderTweakProxyTypeId = 110640, PreviewPlaceholderTypeId = 110647, PreviewGeometryTypeId = 110648, diff --git a/python/GafferSceneTest/ShaderTweakProxyTest.py b/python/GafferSceneTest/ShaderTweakProxyTest.py new file mode 100644 index 00000000000..55276b0814c --- /dev/null +++ b/python/GafferSceneTest/ShaderTweakProxyTest.py @@ -0,0 +1,129 @@ +########################################################################## +# +# 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( "", IECore.StringVectorData( [ "auto" ] ), IECore.ObjectVector( [ imath.Color3f() ] ), name = "auto" ) + + # 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 proxying a specific node using a named handle + tweaks["tweaks"][0]["name"].setValue( "c" ) + + specificProxy = GafferScene.ShaderTweakProxy( "texture2", IECore.StringVectorData( [ "out" ] ), IECore.ObjectVector( [ imath.Color3f() ] ), name = "specificProxy" ) + + 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" ) ) + + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferSceneTest/__init__.py b/python/GafferSceneTest/__init__.py index 4aa6d29b5cb..0b0d059cb67 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..892508bb0d0 --- /dev/null +++ b/python/GafferSceneUI/ShaderTweakProxyUI.py @@ -0,0 +1,310 @@ +########################################################################## +# +# 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.shaderTweakProxySourceNode" : [ + + "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. + """, + "label", "Source Node", + "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, sourceNode, outputNames, outputTypes ): + + with Gaffer.UndoScope( plug.ancestor( Gaffer.ScriptNode ) ): + result = GafferScene.ShaderTweakProxy( sourceNode, outputNames, outputTypes, name = sourceNode or "Auto" ) + plug.node().parent().addChild( 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 + + # \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 __getOutputs( shaderType, shaderName ) : + dummyLoader = GafferSceneUI.ShaderTweaksUI.getShaderLoader( shaderType )() + dummyLoader.loadShader( shaderName ) + + # If "out" is a ValuePlug, that means we have a single output + if isinstance( dummyLoader["out"], Gaffer.ValuePlug ): + return IECore.CompoundData( + { "out" : Gaffer.PlugAlgo.extractDataFromPlug( dummyLoader["out"] ) } + ) + + return ( IECore.StringVectorData( dummyLoader["out"].keys() ), IECore.ObjectVector( + [ Gaffer.PlugAlgo.extractDataFromPlug( dummyLoader["out"][k] ) for k in dummyLoader["out"].keys() ] + ) ) + +def __browseShaders( scriptWindow, plug, context, nodes, paths, title ) : + + 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(), title ) + + 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, *__getOutputs( 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( graphEditor, plug, shaderTweaksOverride ) : + + scriptWindow = graphEditor.ancestor( GafferUI.ScriptWindow ) + context = plug.ancestor( Gaffer.ScriptNode ).context() + shaderTweaks = [ shaderTweaksOverride ] if shaderTweaksOverride else __findConnectedShaderTweaks( plug.node() ) + + __browseShaders( scriptWindow, plug, context, shaderTweaks, _pathsFromAffected( context, shaderTweaks ), "Affected Shaders" ) + +def __browseSelectedShaders( graphEditor, plug, shaderTweaksOverride ) : + + scriptWindow = graphEditor.ancestor( GafferUI.ScriptWindow ) + context = plug.ancestor( Gaffer.ScriptNode ).context() + shaderTweaks = [ shaderTweaksOverride ] if shaderTweaksOverride else __findConnectedShaderTweaks( plug.node() ) + __browseShaders( scriptWindow, plug, context, shaderTweaks, _pathsFromSelection( context ), "Selected Shaders" ) + +def _plugContextMenu( graphEditor, 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 + + # Note the "createCounterpart" here, which is just to work around there not being a more direct way to + # get the default value as data. + valueData = Gaffer.PlugAlgo.getValueAsData( plug.createCounterpart( "unused", Gaffer.Plug.Direction.Out ) ) + + menuDefinition.append( + "Auto ( Original Input )", + { + "command" : functools.partial( __createShaderTweakProxy, + plug, "", IECore.StringVectorData( [ "auto" ] ), IECore.ObjectVector( [ valueData ] ) + ), + "active" : not Gaffer.MetadataAlgo.readOnly( plug.node().parent() ), + } + ) + + menuDefinition.append( + "From Affected", + { + "command" : functools.partial( __browseAffectedShaders, graphEditor, plug, shaderTweaks ), + "active" : not Gaffer.MetadataAlgo.readOnly( plug.node().parent() ), + } + ) + menuDefinition.append( + "From Selected", + { + "command" : functools.partial( __browseSelectedShaders, graphEditor, 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, graphEditor, plug, None ) } + ) + +GafferUI.GraphEditor.plugContextMenuSignal().connect( __plugContextMenuSignal, scoped = False ) diff --git a/python/GafferSceneUI/ShaderTweaksUI.py b/python/GafferSceneUI/ShaderTweaksUI.py index e916c8baf8f..de87049a79f 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, 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 a686a3c2e4e..f25132b9184 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -128,6 +128,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/GafferOSL/OSLShader.cpp b/src/GafferOSL/OSLShader.cpp index b6877c9193b..d225428b862 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,10 @@ ShaderTypeSet &compatibleShaders() return g_compatibleShaders; } +// Allow shader tweak proxy nodes to be connected to OSL shaders +const bool g_oslShaderTweakProxyRegistration = OSLShader::registerCompatibleShader( ShaderTweakProxy::shaderTweakProxyIdentifier() ); + + } // namespace ///////////////////////////////////////////////////////////////////////// diff --git a/src/GafferScene/ShaderAssignment.cpp b/src/GafferScene/ShaderAssignment.cpp index 7074518d111..66f367042c5 100644 --- a/src/GafferScene/ShaderAssignment.cpp +++ b/src/GafferScene/ShaderAssignment.cpp @@ -37,6 +37,8 @@ #include "GafferScene/ShaderAssignment.h" +#include "GafferScene/ShaderTweakProxy.h" + #include "Gaffer/Metadata.h" #include "IECoreScene/ShaderNetwork.h" @@ -166,6 +168,16 @@ IECore::ConstCompoundObjectPtr ShaderAssignment::computeProcessedAttributes( con if( const auto *network = runTimeCast( attribute.second.get() ) ) { + for( const auto &i : network->shaders() ) + { + if( i.second->getName() == ShaderTweakProxy::shaderTweakProxyIdentifier() ) + { + throw IECore::Exception( + "ShaderTweakProxy only works with ShaderTweaks - it doesn't make sense to connect one to a ShaderAssignement" + ); + } + } + if( !labelOverride.empty() ) { IECoreScene::ShaderNetworkPtr renamedNetwork = network->copy(); diff --git a/src/GafferScene/ShaderTweakProxy.cpp b/src/GafferScene/ShaderTweakProxy.cpp new file mode 100644 index 00000000000..164d6f400c2 --- /dev/null +++ b/src/GafferScene/ShaderTweakProxy.cpp @@ -0,0 +1,95 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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" + +using namespace Gaffer; +using namespace GafferScene; + +GAFFER_NODE_DEFINE_TYPE( ShaderTweakProxy ); + +size_t ShaderTweakProxy::g_firstPlugIndex; + +ShaderTweakProxy::ShaderTweakProxy( const std::string &sourceNode, const IECore::StringVectorData *outputNames, const IECore::ObjectVector *outputTypes, const std::string &name ) + : Shader( name ) +{ + storeIndexOfNextChild( g_firstPlugIndex ); + + addChild( new Plug( "out", Plug::Out ) ); + namePlug()->setValue( shaderTweakProxyIdentifier() ); + typePlug()->setValue( shaderTweakProxyIdentifier() ); + + parametersPlug()->addChild( new StringPlug( "shaderTweakProxySourceNode", Plug::Direction::In, sourceNode, Plug::Flags::Default | Plug::Flags::Dynamic ) ); + + if( !outputNames || !outputTypes || outputNames->readable().size() != outputTypes->members().size() ) + { + throw IECore::Exception( "ShaderTweakProxy must be constructed with matching outputNames and outputTypes" ); + } + + for( unsigned int i = 0; i < outputNames->readable().size(); i++ ) + { + outPlug()->addChild( + PlugAlgo::createPlugFromData( outputNames->readable()[i], Plug::Direction::Out, Plug::Flags::Default | Plug::Flags::Dynamic, IECore::runTimeCast( outputTypes->members()[i].get() ) ) + ); + } +} + +ShaderTweakProxy::ShaderTweakProxy( const std::string &name ) + : Shader( name ) +{ + storeIndexOfNextChild( g_firstPlugIndex ); + + addChild( new Plug( "out", Plug::Out ) ); + namePlug()->setValue( shaderTweakProxyIdentifier() ); + typePlug()->setValue( shaderTweakProxyIdentifier() ); +} + +ShaderTweakProxy::~ShaderTweakProxy() +{ +} + +void ShaderTweakProxy::loadShader( const std::string &shaderName, bool keepExistingValues ) +{ +} + +const std::string& ShaderTweakProxy::shaderTweakProxyIdentifier() +{ + static const std::string id = "__SHADER_TWEAK_PROXY"; + return id; +} diff --git a/src/GafferScene/ShaderTweaks.cpp b/src/GafferScene/ShaderTweaks.cpp index 75b097ba02c..07cfaa3a13b 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" @@ -271,6 +272,11 @@ bool ShaderTweaks::applyTweaks( IECoreScene::ShaderNetwork *shaderNetwork, Tweak const IECoreScene::Shader *shader = shaderNetwork->getShader( parameter.shader ); if( !shader ) { + // TODO - It feels inconsistent that we don't test if parameters exist here? + // Setting a value of a parameter that doesn't exist with a constant using applyTweak is an + // error unless ignoreMissing is set, and silent if it is set. + // But if you do the same thing by connecting a shader, you get an OSL warning printout + // whether or not ignoreMissing is set if( missingMode != TweakPlug::MissingMode::Ignore ) { throw IECore::Exception( fmt::format( @@ -286,13 +292,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; } @@ -316,8 +323,66 @@ 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 } ); + + // TODO - it would be substantially 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 gets very confusing if there are + // nodes in the two networks with the same name, which get uniquified during addShaders. + // Doing this after merging simplifies all that. + std::vector shadersToDelete; + for( const auto &i : shaderNetwork->shaders() ) + { + if( i.second->getName() == ShaderTweakProxy::shaderTweakProxyIdentifier() ) + { + shadersToDelete.push_back( i.first ); + + ShaderNetwork::ConnectionRange range = shaderNetwork->outputConnections( i.first ); + const std::vector outputConnections( range.begin(), range.end() ); + + for( const auto &c : outputConnections ) + { + ShaderNetwork::Parameter dest = c.destination; + + StringData *sourceNodeData = nullptr; + try + { + sourceNodeData = IECore::runTimeCast< StringData >( + i.second->parameters().at( "shaderTweakProxySourceNode" ).get() + ); + } + catch( ... ) + { + } + + if( !sourceNodeData ) + { + throw IECore::Exception( "Cannot find source node parameter on ShaderTweakProxy" ); + } + const std::string sourceNode = sourceNodeData->readable(); + + shaderNetwork->removeConnection( c ); + removedConnections = true; + + if( sourceNode == "" ) + { + if( originalInput ) + { + shaderNetwork->addConnection( { originalInput, dest } ); + } + } + else + { + shaderNetwork->addConnection( { { sourceNode, c.source.name }, dest } ); + } + + } + } + } + appliedTweaks = true; } } diff --git a/src/GafferSceneModule/TweaksBinding.cpp b/src/GafferSceneModule/TweaksBinding.cpp index ba3c7233cbb..35069b56be0 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,16 @@ void GafferSceneModule::bindTweaks() DependencyNodeClass(); DependencyNodeClass(); DependencyNodeClass(); + + DependencyNodeClass() + .def( boost::python::init( + ( + boost::python::arg_( "sourceNode" ), + boost::python::arg_( "outputNames" ), + boost::python::arg_( "outputTypes" ), + boost::python::arg_( "name" ) = Gaffer::GraphComponent::defaultName() + ) + ) + ) + ; } diff --git a/startup/gui/menus.py b/startup/gui/menus.py index fec7f327605..0a09c3b52d7 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -287,6 +287,7 @@ def __lightCreator( nodeName, shaderName, shape ) : nodeMenu.append( "/Scene/Object/UV Sampler", GafferScene.UVSampler, searchText = "UVSampler" ) nodeMenu.append( "/Scene/Attributes/Shader Assignment", GafferScene.ShaderAssignment, searchText = "ShaderAssignment" ) nodeMenu.append( "/Scene/Attributes/Shader Tweaks", GafferScene.ShaderTweaks, searchText = "ShaderTweaks" ) +nodeMenu.append( "/Scene/Attributes/Shader Tweak Sources", GafferScene.ShaderTweakProxy, searchText = "ShaderTweakProxy" ) nodeMenu.append( "/Scene/Attributes/Standard Attributes", GafferScene.StandardAttributes, searchText = "StandardAttributes" ) nodeMenu.append( "/Scene/Attributes/Custom Attributes", GafferScene.CustomAttributes, searchText = "CustomAttributes" ) nodeMenu.append( "/Scene/Attributes/Delete Attributes", GafferScene.DeleteAttributes, searchText = "DeleteAttributes" )