diff --git a/python/GafferSceneTest/RenderAdaptorTest.py b/python/GafferSceneTest/RenderAdaptorTest.py index c9a32f786bc..290625265b9 100644 --- a/python/GafferSceneTest/RenderAdaptorTest.py +++ b/python/GafferSceneTest/RenderAdaptorTest.py @@ -549,3 +549,127 @@ def assertCameraVisibleObjects( paths, cameraInclusions = None, cameraExclusions assertCameraVisibleObjects( { "/groupA/cube", "/groupA/sphere" }, cameraInclusions = "CUBE", inclusionOverrides = "/groupA/sphere" ) assertCameraVisibleObjects( { "/groupA/cube", "/groupA/sphere" }, cameraInclusions = "", inclusionOverrides = "A" ) assertCameraVisibleObjects( {}, cameraInclusions = "/", exclusionOverrides = "A" ) + + def testMatteAdaptor( self ) : + + # /groupA + # /cube (A, CUBE) + # /sphere (A, SPHERE) + + cubeA = GafferScene.Cube() + cubeA["sets"].setValue( "A CUBE" ) + + sphereA = GafferScene.Sphere() + sphereA["sets"].setValue( "A SPHERE" ) + + groupA = GafferScene.Group() + groupA["name"].setValue( "groupA" ) + groupA["in"][0].setInput( cubeA["out"] ) + groupA["in"][1].setInput( sphereA["out"] ) + + customOptions = GafferScene.CustomOptions() + customOptions["in"].setInput( groupA["out"] ) + customOptions["options"].addChild( Gaffer.NameValuePlug( "render:matteInclusions", "", True, "matteInclusions" ) ) + customOptions["options"].addChild( Gaffer.NameValuePlug( "render:matteExclusions", "", True, "matteExclusions" ) ) + + inclusionAttributesFilter = GafferScene.SetFilter() + inclusionAttributes = GafferScene.CustomAttributes() + inclusionAttributes["in"].setInput( customOptions["out"] ) + inclusionAttributes["filter"].setInput( inclusionAttributesFilter["out"] ) + inclusionAttributes["attributes"].addChild( Gaffer.NameValuePlug( "ai:matte", True, True, "aiMatte" ) ) + inclusionAttributes["attributes"].addChild( Gaffer.NameValuePlug( "cycles:use_holdout", True, True, "cyclesUseHoldout" ) ) + inclusionAttributes["attributes"].addChild( Gaffer.NameValuePlug( "dl:matte", True, True, "dlMatte" ) ) + + exclusionAttributesFilter = GafferScene.SetFilter() + exclusionAttributes = GafferScene.CustomAttributes() + exclusionAttributes["in"].setInput( inclusionAttributes["out"] ) + exclusionAttributes["filter"].setInput( exclusionAttributesFilter["out"] ) + exclusionAttributes["attributes"].addChild( Gaffer.NameValuePlug( "ai:matte", False, True, "aiMatte" ) ) + exclusionAttributes["attributes"].addChild( Gaffer.NameValuePlug( "cycles:use_holdout", False, True, "cyclesUseHoldout" ) ) + exclusionAttributes["attributes"].addChild( Gaffer.NameValuePlug( "dl:matte", False, True, "dlMatte" ) ) + + # Create adaptors for the CapturingRenderer + testAdaptors = GafferScene.SceneAlgo.createRenderAdaptors() + testAdaptors["in"].setInput( exclusionAttributes["out"] ) + + def assertMatte( paths, matteInclusions = None, matteExclusions = None, inclusionOverrides = "", exclusionOverrides = "" ) : + + if matteInclusions is not None : + customOptions["options"]["matteInclusions"]["value"].setValue( matteInclusions ) + customOptions["options"]["matteInclusions"]["enabled"].setValue( matteInclusions is not None ) + + if matteExclusions is not None : + customOptions["options"]["matteExclusions"]["value"].setValue( matteExclusions ) + customOptions["options"]["matteExclusions"]["enabled"].setValue( matteExclusions is not None ) + + inclusionAttributesFilter["setExpression"].setValue( inclusionOverrides ) + exclusionAttributesFilter["setExpression"].setValue( exclusionOverrides ) + + allPaths = { + "/groupA/cube", + "/groupA/sphere", + } + + renderer = GafferScene.Private.IECoreScenePreview.CapturingRenderer( + GafferScene.Private.IECoreScenePreview.Renderer.RenderType.Batch + ) + GafferScene.Private.RendererAlgo.outputObjects( + testAdaptors["out"], GafferScene.Private.RendererAlgo.RenderOptions( testAdaptors["out"] ), GafferScene.Private.RendererAlgo.RenderSets( testAdaptors["out"] ), GafferScene.Private.RendererAlgo.LightLinks(), + renderer + ) + + for path in allPaths : + capturedObject = renderer.capturedObject( path ) + for attribute in [ "ai:matte", "cycles:use_holdout", "dl:matte" ] : + if path in paths : + # path is matte only by the presence of the attribute with a value of True + self.assertTrue( attribute in capturedObject.capturedAttributes().attributes() ) + self.assertTrue( capturedObject.capturedAttributes().attributes()[attribute].value ) + else : + # path isn't matte by the absence of the attribute, or its presence with a value of False + if attribute in capturedObject.capturedAttributes().attributes() : + self.assertFalse( capturedObject.capturedAttributes().attributes()[attribute].value ) + + # Nothing should be matte when matte inclusions and exclusions are empty or undefined + assertMatte( {} ) + assertMatte( {}, matteInclusions = "" ) + assertMatte( {}, matteExclusions = "" ) + assertMatte( {}, matteInclusions = "", matteExclusions = "" ) + + # Including the root of the scene should make everything matte + assertMatte( { "/groupA/cube", "/groupA/sphere" }, matteInclusions = "/" ) + + # Including the group should make its descendants matte + assertMatte( { "/groupA/cube", "/groupA/sphere" }, matteInclusions = "/groupA" ) + # Unless a descendant has been excluded + assertMatte( { "/groupA/cube" }, matteInclusions = "/groupA", matteExclusions = "/groupA/sphere" ) + + # Including a specific location should not affect its siblings + assertMatte( { "/groupA/cube" }, matteInclusions = "/groupA/cube" ) + + # Test a variety of set expressions + assertMatte( { "/groupA/cube", "/groupA/sphere" }, matteInclusions = "A" ) + assertMatte( { "/groupA/sphere" }, matteInclusions = "SPHERE" ) + assertMatte( { "/groupA/cube" }, matteInclusions = "CUBE" ) + assertMatte( { "/groupA/sphere" }, matteInclusions = "A - CUBE" ) + + # Exclusions should override inclusions + assertMatte( {}, matteInclusions = "A", matteExclusions = "A" ) + assertMatte( {}, matteInclusions = "/groupA/sphere", matteExclusions = "/groupA/sphere" ) + assertMatte( { "/groupA/sphere" }, matteInclusions = "A", matteExclusions = "CUBE" ) + assertMatte( { "/groupA/cube" }, matteInclusions = "/groupA", matteExclusions = "SPHERE" ) + assertMatte( { "/groupA/sphere" }, matteInclusions = "A", matteExclusions = "/groupA/cube" ) + + # Matte inclusions override matte exclusions at lower locations + assertMatte( { "/groupA/sphere" }, matteInclusions = "/groupA/sphere", matteExclusions = "/groupA" ) + + # Test interaction with scene attributes + assertMatte( { "/groupA/sphere" }, inclusionOverrides = "/groupA/sphere" ) + assertMatte( { "/groupA/cube", "/groupA/sphere" }, matteInclusions = "/groupA/cube", inclusionOverrides = "/groupA/sphere" ) + assertMatte( { "/groupA/sphere" }, matteExclusions = "/groupA/sphere", inclusionOverrides = "/groupA/sphere" ) + assertMatte( { "/groupA/sphere" }, matteExclusions = "/groupA", inclusionOverrides = "/groupA/sphere" ) + assertMatte( { "/groupA/cube", "/groupA/sphere" }, matteInclusions = "/groupA", matteExclusions = "/groupA/sphere", inclusionOverrides = "/groupA/sphere" ) + + assertMatte( {}, matteInclusions = "/groupA/sphere", exclusionOverrides = "/groupA/sphere" ) + assertMatte( { "/groupA/sphere" }, matteInclusions = "/groupA/sphere", exclusionOverrides = "/groupA" ) + assertMatte( { "/groupA/sphere" }, matteInclusions = "/groupA", exclusionOverrides = "/groupA/cube" ) diff --git a/startup/GafferScene/matteAdaptor.py b/startup/GafferScene/matteAdaptor.py new file mode 100644 index 00000000000..2f36c0a5b4f --- /dev/null +++ b/startup/GafferScene/matteAdaptor.py @@ -0,0 +1,111 @@ +########################################################################## +# +# 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 inspect + +import Gaffer +import GafferScene + +def __matteAdaptor() : + + processor = GafferScene.SceneProcessor() + + processor["__optionQuery"] = GafferScene.OptionQuery() + processor["__optionQuery"]["scene"].setInput( processor["in"] ) + + processor["__optionQuery"].addQuery( Gaffer.StringPlug() ) + processor["__optionQuery"].addQuery( Gaffer.StringPlug() ) + + processor["__optionQuery"]["queries"][0]["name"].setValue( "render:matteInclusions" ) + processor["__optionQuery"]["queries"][1]["name"].setValue( "render:matteExclusions" ) + + processor["__matteInclusionsFilter"] = GafferScene.SetFilter() + processor["__matteInclusionsExpression"] = Gaffer.Expression() + processor["__matteInclusionsExpression"].setExpression( + inspect.cleandoc( + """ + matteInclusions = parent["__optionQuery"]["out"]["out0"]["value"] + matteExclusions = parent["__optionQuery"]["out"]["out1"]["value"] + parent["__matteInclusionsFilter"]["setExpression"] = "({}) - ({})".format( matteInclusions, matteExclusions ) if ( matteInclusions and matteExclusions ) else matteInclusions + """ + ) + ) + + processor["__filterQuery"] = GafferScene.FilterQuery() + processor["__filterQuery"]["scene"].setInput( processor["in"] ) + processor["__filterQuery"]["filter"].setInput( processor["__matteInclusionsFilter"]["out"] ) + processor["__filterQuery"]["location"].setValue( "/" ) + + processor["__allMatte"] = GafferScene.CustomAttributes() + processor["__allMatte"]["in"].setInput( processor["in"] ) + processor["__allMatte"]["attributes"].addChild( Gaffer.NameValuePlug( "ai:matte", Gaffer.BoolPlug( defaultValue = True ) ) ) + processor["__allMatte"]["attributes"].addChild( Gaffer.NameValuePlug( "cycles:use_holdout", Gaffer.BoolPlug( defaultValue = True ) ) ) + processor["__allMatte"]["attributes"].addChild( Gaffer.NameValuePlug( "dl:matte", Gaffer.BoolPlug( defaultValue = True ) ) ) + processor["__allMatte"]["global"].setValue( True ) + # all locations are matte if `render:matteInclusions` matches the root of the scene + processor["__allMatte"]["enabled"].setInput( processor["__filterQuery"]["exactMatch"] ) + + processor["__matteInclusions"] = GafferScene.AttributeTweaks() + processor["__matteInclusions"]["in"].setInput( processor["__allMatte"]["out"] ) + processor["__matteInclusions"]["tweaks"].addChild( Gaffer.TweakPlug( "ai:matte", Gaffer.BoolPlug( defaultValue = True ), mode = Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + processor["__matteInclusions"]["tweaks"].addChild( Gaffer.TweakPlug( "cycles:use_holdout", Gaffer.BoolPlug( defaultValue = True ), mode = Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + processor["__matteInclusions"]["tweaks"].addChild( Gaffer.TweakPlug( "dl:matte", Gaffer.BoolPlug( defaultValue = True ), mode = Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + + processor["__matteInclusions"]["filter"].setInput( processor["__matteInclusionsFilter"]["out"] ) + # __matteInclusions is only required when `render:matteInclusions` exists and the root of the scene hasn't been set as matte + processor["__matteInclusionsEnabledExpression"] = Gaffer.Expression() + processor["__matteInclusionsEnabledExpression"].setExpression( + """parent["__matteInclusions"]["enabled"] = parent["__optionQuery"]["out"]["out0"]["exists"] and not parent["__filterQuery"]["exactMatch"]""" + ) + + processor["__matteExclusionsFilter"] = GafferScene.SetFilter() + processor["__matteExclusionsFilter"]["setExpression"].setInput( processor["__optionQuery"]["out"][1]["value"] ) + + processor["__matteExclusions"] = GafferScene.AttributeTweaks() + processor["__matteExclusions"]["in"].setInput( processor["__matteInclusions"]["out"] ) + processor["__matteExclusions"]["tweaks"].addChild( Gaffer.TweakPlug( "ai:matte", Gaffer.BoolPlug(), mode = Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + processor["__matteExclusions"]["tweaks"].addChild( Gaffer.TweakPlug( "cycles:use_holdout", Gaffer.BoolPlug(), mode = Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + processor["__matteExclusions"]["tweaks"].addChild( Gaffer.TweakPlug( "dl:matte", Gaffer.BoolPlug(), mode = Gaffer.TweakPlug.Mode.CreateIfMissing ) ) + + processor["__matteExclusions"]["filter"].setInput( processor["__matteExclusionsFilter"]["out"] ) + # __matteExclusions is only required when `render:matteExclusions` exists + processor["__matteExclusions"]["enabled"].setInput( processor["__optionQuery"]["out"][1]["exists"] ) + + processor["out"].setInput( processor["__matteExclusions"]["out"] ) + + return processor + +GafferScene.SceneAlgo.registerRenderAdaptor( "MatteAdaptor", __matteAdaptor )