diff --git a/Changes.md b/Changes.md index c057cc17a63..479c2af0996 100644 --- a/Changes.md +++ b/Changes.md @@ -11,6 +11,7 @@ Features - Inference : Loads ONNX models and performance inference using an array of input tensors. - ImageToTensor : Converts images to tensors for use with the Inference node. - TensorToImage : Converts tensors back to images following inference. +- PrimiitiveVariableTweaks : Added node for tweaking primitive variables. Can affect just part of a primitive based on ids or a mask. API --- diff --git a/include/GafferScene/PrimitiveVariableTweaks.h b/include/GafferScene/PrimitiveVariableTweaks.h new file mode 100644 index 00000000000..ad4613ca060 --- /dev/null +++ b/include/GafferScene/PrimitiveVariableTweaks.h @@ -0,0 +1,102 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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/ObjectProcessor.h" + +#include "Gaffer/TweakPlug.h" + +namespace GafferScene +{ + +class GAFFERSCENE_API PrimitiveVariableTweaks : public ObjectProcessor +{ + + public : + + enum class SelectionMode + { + All, + IdList, + IdListPrimitiveVariable, + MaskPrimitiveVariable + }; + + explicit PrimitiveVariableTweaks( const std::string &name=defaultName() ); + ~PrimitiveVariableTweaks() override; + + GAFFER_NODE_DECLARE_TYPE( GafferScene::PrimitiveVariableTweaks, PrimitiveVariableTweaksTypeId, ObjectProcessor ); + + Gaffer::IntPlug *interpolationPlug(); + const Gaffer::IntPlug *interpolationPlug() const; + + Gaffer::IntPlug *selectionModePlug(); + const Gaffer::IntPlug *selectionModePlug() const; + + Gaffer::Int64VectorDataPlug *idListPlug(); + const Gaffer::Int64VectorDataPlug *idListPlug() const; + + Gaffer::StringPlug *idListVariablePlug(); + const Gaffer::StringPlug *idListVariablePlug() const; + + Gaffer::StringPlug *idPlug(); + const Gaffer::StringPlug *idPlug() const; + + Gaffer::StringPlug *maskVariablePlug(); + const Gaffer::StringPlug *maskVariablePlug() const; + + Gaffer::BoolPlug *ignoreMissingPlug(); + const Gaffer::BoolPlug *ignoreMissingPlug() const; + + Gaffer::TweaksPlug *tweaksPlug(); + const Gaffer::TweaksPlug *tweaksPlug() const; + + protected : + + bool affectsProcessedObject( const Gaffer::Plug *input ) const override; + void hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + IECore::ConstObjectPtr computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const override; + + private : + + static size_t g_firstPlugIndex; + +}; + +IE_CORE_DECLAREPTR( PrimitiveVariableTweaks ) + +} // namespace GafferScene diff --git a/include/GafferScene/TypeIds.h b/include/GafferScene/TypeIds.h index 890bf4333e5..c8f4ec9a9e7 100644 --- a/include/GafferScene/TypeIds.h +++ b/include/GafferScene/TypeIds.h @@ -90,7 +90,7 @@ enum TypeId PruneTypeId = 110546, FreezeTransformTypeId = 110547, MeshDistortionTypeId = 110548, - OpenGLRenderTypeId = 110549, // Available for reuse + PrimitiveVariableTweaksTypeId = 110549, InteractiveRenderTypeId = 110550, CubeTypeId = 110551, SphereTypeId = 110552, diff --git a/python/GafferSceneTest/PrimitiveVariableTweaksTest.py b/python/GafferSceneTest/PrimitiveVariableTweaksTest.py new file mode 100644 index 00000000000..94b62531a59 --- /dev/null +++ b/python/GafferSceneTest/PrimitiveVariableTweaksTest.py @@ -0,0 +1,596 @@ +########################################################################## +# +# 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 imath + +import IECore +import IECoreScene + +import Gaffer +import GafferScene +import GafferSceneTest + +class PrimitiveVariableTweaksTest( GafferSceneTest.SceneTestCase ) : + + + def testTypes( self ) : + + s = GafferScene.Sphere() + create = GafferScene.PrimitiveVariableTweaks() + create["in"].setInput( s["out"] ) + + self.assertScenesEqual( s["out"], create["out"] ) + self.assertSceneHashesEqual( s["out"], create["out"] ) + + testData = { + "a" : IECore.IntData( 10 ), + "b" : IECore.V3fData( imath.V3f( 3 )), + "c" : IECore.V3fData( imath.V3f( 5 ), IECore.GeometricData.Interpretation.Point ), + "d" : IECore.V3fData( imath.V3f( 7 ), IECore.GeometricData.Interpretation.Normal ), + "e" : IECore.Color3fData( imath.Color3f( 0.7, 0.8, 0.6 ) ), + "f" : IECore.Color4fData( imath.Color4f( 0.1, 0.2, 0.3, 0.4 ) ), + "g" : IECore.StringData( "hello to a" ), + "h" : IECore.FloatVectorData( [ 3, 4, 5 ] ), + "i" : IECore.Color3fVectorData( [ imath.Color3f( 7 ) ] ), + } + + for ( name, val ) in testData.items(): + create["tweaks"].addChild( Gaffer.TweakPlug( name, val, Gaffer.TweakPlug.Mode.Create ) ) + + self.assertScenesEqual( s["out"], create["out"] ) + self.assertSceneHashesEqual( s["out"], create["out"] ) + + f = GafferScene.PathFilter() + f["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) ) + create["filter"].setInput( f["out"] ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot create primitive variable a when "interpolation" is set to "Any". Please select an interpolation.' ): + create["out"].object( "/sphere" ) + + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + o = create["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( testData.keys() ) + ['uv'] ) + for k in testData.keys(): + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, testData[k] ) ) + + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Invalid type "FloatVectorData" for non-constant primitive variable tweak "h".' ): + create["out"].object( "/sphere" ) + + create["tweaks"][-1]["enabled"].setValue( False ) + create["tweaks"][-2]["enabled"].setValue( False ) + + testDataNonConst = testData.copy() + del testDataNonConst["h"] + del testDataNonConst["i"] + + for interp in [ + IECoreScene.PrimitiveVariable.Interpolation.Uniform, + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECoreScene.PrimitiveVariable.Interpolation.Varying, + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying + ]: + create["interpolation"].setValue( interp ) + o = create["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( testDataNonConst.keys() ) + ['uv'] ) + for k in testDataNonConst.keys(): + dataType = getattr( IECore, testDataNonConst[k].typeName().replace( "Data", "VectorData" ) ) + compData = dataType( [ testDataNonConst[k].value ] * o.variableSize( interp ) ) + if hasattr( compData, "setInterpretation" ): + compData.setInterpretation( testDataNonConst[k].getInterpretation() ) + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( interp, compData ) ) + + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( create["out"] ) + tweak["filter"].setInput( f["out"] ) + + tweak["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.Color4fData(), Gaffer.TweakPlug.Mode.Replace ) ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "a" : Parameter should be of type "IntData" in order to apply to an element of "IntVectorData", but got "Color4fData" instead.' ): + tweak["out"].object( "/sphere" ) + + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + create["tweaks"][-1]["enabled"].setValue( True ) + create["tweaks"][-2]["enabled"].setValue( True ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "a" : Variable data of type "IntData" does not match parameter of type "Color4fData".' ): + tweak["out"].object( "/sphere" ) + + del tweak["tweaks"][0] + tweakData = { + "a" : IECore.IntData( 100 ), + "b" : IECore.V3fData( imath.V3f( 0.5 ) ), + "c" : IECore.V3fData( imath.V3f( 0.5 ) ), + "d" : IECore.V3fData( imath.V3f( 0.5 ) ), + "e" : IECore.Color3fData( imath.Color3f( 0.01, 0.02, 0.03 ) ), + "f" : IECore.Color4fData( imath.Color4f( 0.001, 0.002, 0.003, 0.004 ) ), + "g" : IECore.StringData( "to a world" ), + "h" : IECore.FloatVectorData( [ 13, 14, 15 ] ), + "i" : IECore.Color3fVectorData( [ imath.Color3f( 3 ) ] ), + } + + for ( name, val ) in tweakData.items(): + tweak["tweaks"].addChild( Gaffer.TweakPlug( name, val, Gaffer.TweakPlug.Mode.Replace ) ) + + o = tweak["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( testData.keys() ) + ['uv'] ) + for k in tweakData.keys(): + if not k in [ "c", "d" ]: + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, tweakData[k] ) ) + else: + # When replacing the value of a primvar, we keep the interpolation of the original + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.V3fData( imath.V3f( 0.5 ), testData[k].getInterpretation() ) ) ) + + for i in tweak["tweaks"]: + i["mode"].setValue( Gaffer.TweakPlug.Mode.Add ) + + expectedAdd = { + "a" : IECore.IntData( 110 ), + "b" : IECore.V3fData( imath.V3f( 3.5 )), + "c" : IECore.V3fData( imath.V3f( 5.5 ), IECore.GeometricData.Interpretation.Point ), + "d" : IECore.V3fData( imath.V3f( 7.5 ), IECore.GeometricData.Interpretation.Normal ), + "e" : IECore.Color3fData( imath.Color3f( 0.71, 0.82, 0.63 ) ), + "f" : IECore.Color4fData( imath.Color4f( 0.101, 0.202, 0.303, 0.404 ) ), + "g" : IECore.StringData( "hello to a world" ), + "h" : IECore.FloatVectorData( [ 3, 4, 5, 13, 14, 15 ] ), + "i" : IECore.Color3fVectorData( [ imath.Color3f( 7 ), imath.Color3f( 3 ) ] ), + } + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Add to "g" : Data type StringData not supported.' ): + tweak["out"].object( "/sphere" ) + + tweak["tweaks"][-3]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Add to "h" : Data type FloatVectorData not supported.' ): + tweak["out"].object( "/sphere" ) + + tweak["tweaks"][-2]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Add to "i" : Data type Color3fVectorData not supported.' ): + tweak["out"].object( "/sphere" ) + + tweak["tweaks"][-1]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + o = tweak["out"].object( "/sphere" ) + for k in tweakData.keys(): + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, expectedAdd[k] ) ) + + create["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + create["tweaks"][-1]["enabled"].setValue( False ) + create["tweaks"][-2]["enabled"].setValue( False ) + tweak["tweaks"][-1]["enabled"].setValue( False ) + tweak["tweaks"][-2]["enabled"].setValue( False ) + + for i in tweak["tweaks"]: + i["mode"].setValue( Gaffer.TweakPlug.Mode.Replace ) + + tweakDataNonConst = tweakData.copy() + del tweakDataNonConst["h"] + del tweakDataNonConst["i"] + + for interp in [ + IECoreScene.PrimitiveVariable.Interpolation.Uniform, + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECoreScene.PrimitiveVariable.Interpolation.Varying, + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying + ]: + create["interpolation"].setValue( interp ) + o = tweak["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( tweakDataNonConst.keys() ) + ['uv'] ) + for k in tweakDataNonConst.keys(): + dataType = getattr( IECore, tweakDataNonConst[k].typeName().replace( "Data", "VectorData" ) ) + compData = dataType( [ tweakDataNonConst[k].value ] * o.variableSize( interp ) ) + if hasattr( compData, "setInterpretation" ): + compData.setInterpretation( testDataNonConst[k].getInterpretation() ) + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( interp, compData ) ) + + del expectedAdd["h"] + del expectedAdd["i"] + + for i in tweak["tweaks"]: + i["mode"].setValue( Gaffer.TweakPlug.Mode.Add ) + + # listAppend mode works on string even when those strings are per-vertex + tweak["tweaks"][-3]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + for interp in [ + IECoreScene.PrimitiveVariable.Interpolation.Uniform, + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECoreScene.PrimitiveVariable.Interpolation.Varying, + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying + ]: + create["interpolation"].setValue( interp ) + o = tweak["out"].object( "/sphere" ) + self.assertEqual( o.keys(), ['N', 'P'] + list( expectedAdd.keys() ) + ['uv'] ) + for k in expectedAdd.keys(): + dataType = getattr( IECore, expectedAdd[k].typeName().replace( "Data", "VectorData" ) ) + compData = dataType( [ expectedAdd[k].value ] * o.variableSize( interp ) ) + if hasattr( compData, "setInterpretation" ): + compData.setInterpretation( expectedAdd[k].getInterpretation() ) + self.assertEqual( o[k], IECoreScene.PrimitiveVariable( interp, compData ) ) + + def testInterpolations( self ) : + + m = IECoreScene.MeshPrimitive.createPlane( imath.Box2f( imath.V2f( -1 ), imath.V2f( 1 ) ), imath.V2i( 2 ) ) + m["vertexIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 10, 11, 12, 20, 21, 21, 30, 31, 33 ] ) + ) + m["badIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1 ] ) + ) + + p = GafferScene.ObjectToScene() + p["name"].setValue( "plane" ) + p["object"].setValue( m ) + + f = GafferScene.PathFilter() + f["paths"].setValue( IECore.StringVectorData( [ "/plane" ] ) ) + + createConstant = GafferScene.PrimitiveVariableTweaks() + createConstant["in"].setInput( p["out"] ) + createConstant["filter"].setInput( f["out"] ) + createConstant["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + createConstant["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.FloatData( 7 ), Gaffer.TweakPlug.Mode.Create ) ) + createConstant["tweaks"].addChild( Gaffer.TweakPlug( "c", IECore.IntVectorData( [ 3, 4, 8 ] ), Gaffer.TweakPlug.Mode.Create ) ) + + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Primitive variable tweak failed - input primitive variables are not valid.' ): + createConstant["out"].object( "/plane" ) + + m["badIds"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( [ 0, 1 ] ), IECore.IntVectorData( [ 0, 0, 1, 1, 0, 0, 0, 1, 0 ] ) + ) + p["object"].setValue( m ) + + self.assertEqual( createConstant["out"].object( "/plane" )["a"].data, IECore.FloatData( 7 ) ) + + createUniform = GafferScene.PrimitiveVariableTweaks() + createUniform["in"].setInput( createConstant["out"] ) + createUniform["filter"].setInput( f["out"] ) + createUniform["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + createUniform["tweaks"].addChild( Gaffer.TweakPlug( "b", IECore.IntData( 42 ), Gaffer.TweakPlug.Mode.Create ) ) + + o = createUniform["out"].object( "/plane" ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 42, 42, 42, 42 ] ) ) ) + + createUniform["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + createUniform["idList"].setValue( IECore.Int64VectorData( [ 1, 2] ) ) + + o = createUniform["out"].object( "/plane" ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 0, 42, 42, 0 ] ) ) ) + + tweak = GafferScene.PrimitiveVariableTweaks() + tweak["in"].setInput( createUniform["out"] ) + tweak["filter"].setInput( f["out"] ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "x", IECore.IntData( 7 ), Gaffer.TweakPlug.Mode.Replace ) ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Replace to "x" : This parameter does not exist.' ): + tweak["out"].object( "/plane" ) + + del tweak["tweaks"][0] + tweak["tweaks"].addChild( Gaffer.TweakPlug( "x", IECore.FloatVectorData( [ 7 ] ), Gaffer.TweakPlug.Mode.ListAppend ) ) + + # Applying a ListAppend tweak when there is no source found will try to create the variable, which will + # fail if the interpolation isn't set + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot create primitive variable x when "interpolation" is set to "Any". Please select an interpolation.' ): + tweak["out"].object( "/plane" ) + + tweak["ignoreMissing"].setValue( True ) + + # This error is not affected by ignoreMissing + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot create primitive variable x when "interpolation" is set to "Any". Please select an interpolation.' ): + tweak["out"].object( "/plane" ) + tweak["ignoreMissing"].setValue( False ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Constant ) + self.assertEqual( tweak["out"].object( "/plane" )["x"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatVectorData( [ 7 ] ) ) ) + + del tweak["tweaks"][0] + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Invalid ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "c", IECore.IntVectorData( [ 7 ] ), Gaffer.TweakPlug.Mode.ListAppend ) ) + + # List append working as intended + self.assertEqual( tweak["out"].object( "/plane" )["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 3, 4, 8, 7 ] ) ) ) + + # List remove is considered successful if the target doesn't exist + tweak["tweaks"][0]["name"].setValue( "x" ) + tweak["tweaks"][0]["mode"].setValue( Gaffer.TweakPlug.Mode.ListRemove ) + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + tweak["tweaks"][0]["name"].setValue( "c" ) + tweak["tweaks"][0]["value"].setValue( IECore.IntVectorData( [ 4 ] ) ) + self.assertEqual( tweak["out"].object( "/plane" )["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 3, 8 ] ) ) ) + + + del tweak["tweaks"][0] + tweak["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.FloatData( 42 ), Gaffer.TweakPlug.Mode.Replace ) ) + self.assertEqual( tweak["out"].object( "/plane" )["a"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 42 ) ) ) + + tweak["tweaks"][0]["name"].setValue( "x" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode Replace to "x" : This parameter does not exist.' ): + tweak["out"].object( "/plane" ) + + tweak["ignoreMissing"].setValue( True ) + self.assertEqual( tweak["out"].object( "/plane" ), tweak["in"].object( "/plane" ) ) + + tweak["tweaks"][0]["name"].setValue( "a" ) + tweak["tweaks"][0]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak with mode ListAppend to "a" : Data type FloatData not supported.' ): + tweak["out"].object( "/plane" ) + + del tweak["tweaks"][0] + + tweak["tweaks"].addChild( Gaffer.TweakPlug( "P", IECore.V3fData( imath.V3f( 0.5 ) ), Gaffer.TweakPlug.Mode.Replace ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "N", IECore.V3fData( imath.V3f( 0.1 ) ), Gaffer.TweakPlug.Mode.Replace ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "uv", IECore.V2fData( imath.V2f( 10 ) ), Gaffer.TweakPlug.Mode.Replace ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "a", IECore.FloatData( 0.7 ), Gaffer.TweakPlug.Mode.Replace ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "b", IECore.IntData( 7 ), Gaffer.TweakPlug.Mode.Replace ) ) + tweak["tweaks"].addChild( Gaffer.TweakPlug( "c", IECore.IntVectorData( [7] ), Gaffer.TweakPlug.Mode.Replace ) ) + + uvIndices = tweak["in"].object( "/plane" )["uv"].indices + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "N", "P", "a", "b", "badIds", "c", "uv", "vertexIds" ] ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 0.1 ) ] * 9, IECore.GeometricData.Interpretation.Normal ) ) ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 0.5 ) ] * 9, IECore.GeometricData.Interpretation.Point ) ) ) + self.assertEqual( o["a"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 0.7 ) ) ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7 ] * 4 ) ) ) + self.assertEqual( o["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 7 ] ) ) ) + self.assertEqual( o["uv"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( [ imath.V2f( 10 ) ] * 9, IECore.GeometricData.Interpretation.UV ), uvIndices ) ) + + tweak["tweaks"][1]["mode"].setValue( Gaffer.TweakPlug.Mode.Remove ) + tweak["tweaks"][2]["mode"].setValue( Gaffer.TweakPlug.Mode.Remove ) + tweak["tweaks"][3]["mode"].setValue( Gaffer.TweakPlug.Mode.Remove ) + tweak["tweaks"][4]["mode"].setValue( Gaffer.TweakPlug.Mode.Remove ) + tweak["tweaks"][5]["mode"].setValue( Gaffer.TweakPlug.Mode.Remove ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "P", "badIds", "vertexIds" ] ) + + for i in tweak["tweaks"]: + i["mode"].setValue( Gaffer.TweakPlug.Mode.Add ) + tweak["tweaks"][-1]["mode"].setValue( Gaffer.TweakPlug.Mode.ListAppend ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "N", "P", "a", "b", "badIds", "c", "uv", "vertexIds" ] ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 0.1, 0.1, 1.1 ) ] * 9, IECore.GeometricData.Interpretation.Normal ) ) ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( + [ imath.V3f( x + 0.5, y + 0.5, 0.5 ) for x, y in + [ (-1, -1), (0, -1), (1, -1), ( -1, 0 ), ( 0, 0 ), (1, 0 ), ( -1, 1 ), ( 0, 1 ), ( 1, 1 ) ] ], + IECore.GeometricData.Interpretation.Point ) ) ) + self.assertEqual( o["a"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.FloatData( 7.7 ) ) ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7, 49, 49, 7 ] ) ) ) + self.assertEqual( o["c"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.IntVectorData( [ 3, 4, 8, 7 ] ) ) ) + self.assertEqual( o["uv"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( + [ imath.V2f( x + 10, y + 10 ) for x, y in + [ (0, 0), (0.5, 0), (1, 0), ( 0, 0.5 ), ( 0.5, 0.5 ), (1, 0.5 ), ( 0, 1 ), ( 0.5, 1 ), ( 1, 1 ) ] ], + IECore.GeometricData.Interpretation.UV ), uvIndices ) ) + + # Setting the selection mode does nothing while the interpolation is set to "Any" + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 0, 3, 4, 5 ] ) ) + + self.assertEqual( tweak["out"].object( "/plane" ), o ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "uv" : Interpolation "Vertex" doesn\'t match primitive variable interpolation "FaceVarying".' ): + tweak["out"].object( "/plane" ) + + tweak["tweaks"][2]["enabled"].setValue( False ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "a" : Interpolation "Vertex" doesn\'t match primitive variable interpolation "Constant".' ): + tweak["out"].object( "/plane" ) + + tweak["tweaks"][3]["enabled"].setValue( False ) + tweak["tweaks"][5]["enabled"].setValue( False ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Cannot apply tweak to "b" : Interpolation "Vertex" doesn\'t match primitive variable interpolation "Uniform".' ): + tweak["out"].object( "/plane" ) + + tweak["tweaks"][4]["enabled"].setValue( False ) + + inObj = tweak["in"].object( "/plane" ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o.keys(), [ "N", "P", "a", "b", "badIds", "c", "uv", "vertexIds" ] ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0.1, 0.1, 1.1), (0, 0, 1), (0, 0, 1), + (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), + (0, 0, 1), (0, 0, 1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( + [ imath.V3f( *i ) for i in + [ (-0.5, -0.5, 0.5), (0, -1, 0), (1, -1, 0), ( -0.5, 0.5, 0.5 ), ( 0.5, 0.5, 0.5 ), (1.5, 0.5, 0.5 ), ( -1, 1, 0 ), ( 0, 1, 0 ), ( 1, 1, 0 ) ] ], + IECore.GeometricData.Interpretation.Point ) ) ) + self.assertEqual( o["a"], inObj["a"] ) + self.assertEqual( o["b"], inObj["b"] ) + self.assertEqual( o["c"], inObj["c"] ) + self.assertEqual( o["uv"], inObj["uv"] ) + + # TODO - this is some pretty nasty behaviour that probably needs fixing. If an id occurs twice in + # the list, it gets tweaked twice. This is probably unexpected ( and it's definitely unexpected that + # it gets tweaked twice if there is no id primVar, but only once if there is an id primvar ). So we + # probably need to do something about this, though it's pretty unfortunate that this will mean + # unnecessarily sticking all the ids in an unordered_set in the common case where the id list doesn't + # contain duplicates. + tweak["idList"].setValue( IECore.Int64VectorData( [ 0, 3, 4, 5, 3 ] ) ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0.1, 0.1, 1.1), (0, 0, 1), (0, 0, 1), + (0.2, 0.2, 1.2), (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), + (0, 0, 1), (0, 0, 1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + + # Test IdListPrimVarMode + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable ) + tweak["idListVariable"].setValue( "bad" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Can\'t find id list primitive variable "bad".' ): + tweak["out"].object( "/plane" ) + tweak["idListVariable"].setValue( "a" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Invalid id list primitive variable "a". A constant IntVector or Int64Vector is required.' ): + tweak["out"].object( "/plane" ) + tweak["idListVariable"].setValue( "vertexIds" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Invalid id list primitive variable "vertexIds". A constant IntVector or Int64Vector is required.' ): + tweak["out"].object( "/plane" ) + + tweak["idListVariable"].setValue( "c" ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0, 0, 1), (0, 0, 1), (0, 0, 1), + (0.1, 0.1, 1.1), (0.1, 0.1, 1.1), (0, 0, 1), + (0, 0, 1), (0, 0, 1), (0.1, 0.1, 1.1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + + tweak["id"].setValue( "vertexIds" ) + + # The current id list doesn't match these new ids + self.assertEqual( tweak["out"].object( "/plane" ), inObj ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 11, 31 ] ) ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0, 0, 1), (0.1, 0.1, 1.1), (0, 0, 1), + (0, 0, 1), (0, 0, 1), (0, 0, 1), + (0, 0, 1), (0.1, 0.1, 1.1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + self.assertEqual( o["P"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( + [ imath.V3f( *i ) for i in + [ (-1, -1, 0), (0.5, -0.5, 0.5), (1, -1, 0), ( -1, 0, 0 ), ( 0, 0, 0 ), (1, 0, 0 ), ( -1, 1, 0 ), ( 0.5, 1.5, 0.5 ), ( 1, 1, 0 ) ] ], + IECore.GeometricData.Interpretation.Point ) ) ) + + tweak["id"].setValue( "badIds" ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Id variable "badIds" is not allowed to be indexed.' ): + tweak["out"].object( "/plane" ) + + + tweak["tweaks"][0]["enabled"].setValue( False ) + tweak["tweaks"][1]["enabled"].setValue( False ) + tweak["tweaks"][2]["enabled"].setValue( False ) + + tweak["tweaks"][4]["enabled"].setValue( True ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Uniform ) + + tweak["id"].setValue( "vertexIds" ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Id variable "vertexIds" : Interpolation "Vertex" doesn\'t match specified interpolation "Uniform".' ): + tweak["out"].object( "/plane" ) + + tweak["id"].setValue( "" ) + + # Check that only in-bound ids have any effect + self.assertEqual( tweak["out"].object( "/plane" ), inObj ) + + tweak["idList"].setValue( IECore.Int64VectorData( [ 2, 31 ] ) ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 0, 42, 49, 0 ] ) ) ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.All ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["b"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Uniform, IECore.IntVectorData( [ 7, 49, 49, 7 ] ) ) ) + + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.FaceVarying ) + + tweak["tweaks"][2]["enabled"].setValue( True ) + tweak["tweaks"][4]["enabled"].setValue( False ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["P"], inObj["P"] ) + self.assertEqual( o["N"], inObj["N"] ) + self.assertEqual( o["uv"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( + [ imath.V2f( x + 10, y + 10 ) for x, y in + [ (0, 0), (0.5, 0), (1, 0), ( 0, 0.5 ), ( 0.5, 0.5 ), (1, 0.5 ), ( 0, 1 ), ( 0.5, 1 ), ( 1, 1 ) ] ], + IECore.GeometricData.Interpretation.UV ), uvIndices ) ) + self.assertEqual( o["a"], inObj["a"] ) + self.assertEqual( o["b"], inObj["b"] ) + self.assertEqual( o["c"], inObj["c"] ) + + # When tweaking an indexed primvar, things are a bit more complex - any data that gets tweaked gets + # a new index, and any data that no longer has any indices referring to it is removed. + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList ) + tweak["idList"].setValue( IECore.Int64VectorData( [ 0, 1, 2, 3, 12, 13, 14, 15 ] ) ) + + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["uv"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, IECore.V2fVectorData( + [ imath.V2f( *i ) for i in + [ ( 0.5, 0 ), ( 1, 0 ), ( 0, 0.5 ), ( 0.5, 0.5 ), ( 1, 0.5 ), ( 0, 1 ), ( 0.5, 1 ), ( 10, 10 ), ( 10.5, 10 ), ( 10.5, 10.5 ), ( 10, 10.5 ), ( 11, 10.5 ), ( 11, 11 ), ( 10.5, 11 ) ] ], + IECore.GeometricData.Interpretation.UV ), + IECore.IntVectorData( [ 7, 8, 9, 10, 0, 1, 4, 3, 2, 3, 6, 5, 9, 11, 12, 13 ] ) + ) ) + + tweak["selectionMode"].setValue( GafferScene.PrimitiveVariableTweaks.SelectionMode.MaskPrimitiveVariable ) + tweak["interpolation"].setValue( IECoreScene.PrimitiveVariable.Interpolation.Vertex ) + + tweak["tweaks"][1]["enabled"].setValue( True ) + tweak["tweaks"][2]["enabled"].setValue( False ) + + with self.assertRaisesRegex( Gaffer.ProcessException, 'Can\'t find mask primitive variable "".' ): + tweak["out"].object( "/plane" ) + + tweak["idList"].setValue( IECore.Int64VectorData() ) + tweak["idListVariable"].setValue( "" ) + tweak["maskVariable"].setValue( "uv" ) + with self.assertRaisesRegex( Gaffer.ProcessException, 'Mask primitive variable "uv" has wrong interpolation "FaceVarying", expected "Vertex".' ): + tweak["out"].object( "/plane" ) + + tweak["maskVariable"].setValue( "badIds" ) + o = tweak["out"].object( "/plane" ) + self.assertEqual( o["N"], IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( *i ) for i in [ + (0, 0, 1), (0, 0, 1), (0.1, 0.1, 1.1), + (0.1, 0.1, 1.1), (0, 0, 1), (0, 0, 1), + (0, 0, 1), (0.1, 0.1, 1.1), (0, 0, 1) + ] ], IECore.GeometricData.Interpretation.Normal ) ) ) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferSceneUI/PrimitiveVariableTweaksUI.py b/python/GafferSceneUI/PrimitiveVariableTweaksUI.py new file mode 100644 index 00000000000..df6db89152d --- /dev/null +++ b/python/GafferSceneUI/PrimitiveVariableTweaksUI.py @@ -0,0 +1,383 @@ +########################################################################## +# +# 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 imath +import functools +import collections + +import IECore +import IECoreScene + +import Gaffer +import GafferUI +import GafferScene +import GafferSceneUI + +Gaffer.Metadata.registerNode( + + GafferScene.PrimitiveVariableTweaks, + + "description", + """ + Modify primitive variable values. Supports modifying values just for specific elements of the + primitive. + """, + + "layout:activator:selectionModeEnabled", lambda node : node["interpolation"].getValue() != IECoreScene.PrimitiveVariable.Interpolation.Invalid, + "layout:activator:idListExplicitVisible", lambda node : node["interpolation"].getValue() != IECoreScene.PrimitiveVariable.Interpolation.Invalid and node["selectionMode"].getValue() == GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList, + "layout:activator:idListVarVisible", lambda node : node["interpolation"].getValue() != IECoreScene.PrimitiveVariable.Interpolation.Invalid and node["selectionMode"].getValue() == GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable, + "layout:activator:idListVisible", lambda node : node["interpolation"].getValue() != IECoreScene.PrimitiveVariable.Interpolation.Invalid and node["selectionMode"].getValue() in [ GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList, GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable ], + "layout:activator:maskVarVisible", lambda node : node["interpolation"].getValue() != IECoreScene.PrimitiveVariable.Interpolation.Invalid and node["selectionMode"].getValue() == GafferScene.PrimitiveVariableTweaks.SelectionMode.MaskPrimitiveVariable, + plugs = { + + "interpolation" : [ + + "description", + """ + The interpolation of the target primitive variables. Using "Any" allows you to + operate on any primitive variable, but if you know your target, using a more + specific interpolation offers benefits: you can specify an idList to operate + on specific elements, and you can use "Create" mode to create new primitive + variables. + """, + + "preset:Any", IECoreScene.PrimitiveVariable.Interpolation.Invalid, + "preset:Constant", IECoreScene.PrimitiveVariable.Interpolation.Constant, + "preset:Uniform", IECoreScene.PrimitiveVariable.Interpolation.Uniform, + "preset:Vertex", IECoreScene.PrimitiveVariable.Interpolation.Vertex, + "preset:Varying", IECoreScene.PrimitiveVariable.Interpolation.Varying, + "preset:FaceVarying", IECoreScene.PrimitiveVariable.Interpolation.FaceVarying, + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + + ], + + "selectionMode" : [ + + "description", + """ + Choose how to select which elements are affected. Only takes effect if you + choose an interpolation other than "Any". + """, + + "preset:All", GafferScene.PrimitiveVariableTweaks.SelectionMode.All, + "preset:Id List", GafferScene.PrimitiveVariableTweaks.SelectionMode.IdList, + "preset:Id List Primitive Variable", GafferScene.PrimitiveVariableTweaks.SelectionMode.IdListPrimitiveVariable, + "preset:Mask Primitive Variable", GafferScene.PrimitiveVariableTweaks.SelectionMode.MaskPrimitiveVariable, + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + + "layout:activator", "selectionModeEnabled", + + ], + + "idList" : [ + + "description", + """ + A list of ids for the elements to affect, corresponding to the current interpolation. For + example, if you choose "Vertex" interpolation, these will be vertex ids. By default, ids + are based on the index, but if you specify an id primitive variable below, the ids in + this list will match the id primitive variable. + """, + + "layout:visibilityActivator", "idListExplicitVisible", + + ], + + "idListVariable" : [ + + "description", + """ + The name of a constant primitive variable containing a list of ids for the elements to affect, + corresponding to the current interpolation. For example, if you choose "Vertex" interpolation, + these will be vertex ids. By default, ids are based on the index, but if you specify an id + primitive variable below, the ids in this list will match the id primitive variable. + """, + + "layout:visibilityActivator", "idListVarVisible", + + ], + + "id" : [ + + "description", + """ + The name of the primitive variable to use as ids. Affects which elements are selected by the idList. + """, + + "layout:visibilityActivator", "idListVisible", + + ], + + "maskVariable" : [ + + "description", + """ + The name of a primitive variable containing a mask. The variable must match the specified interpolation. + Any elements where the mask variable is non-zero will be tweaked. + """, + + "layout:visibilityActivator", "maskVarVisible", + + ], + + "ignoreMissing" : [ + + "description", + """ + Ignores tweaks targeting missing primitive variables. When off, missing primitive variables + cause the node to error. + """ + + ], + + "tweaks" : [ + + "description", + """ + The tweaks to be made to the options. Arbitrary numbers of user defined + tweaks may be added as children of this plug via the user interface, or + using the OptionTweaks API via python. + """, + + "plugValueWidget:type", "GafferUI.LayoutPlugValueWidget", + "layout:customWidget:footer:widgetType", "GafferSceneUI.PrimitiveVariableTweaksUI._TweaksFooter", + "layout:customWidget:footer:index", -1, + + "nodule:type", "", + + ], + + "tweaks.*" : [ + + "tweakPlugValueWidget:propertyType", "primitive variable", + + ], + + "tweaks.*.value" : [ + "description", + """ + For a constant primitive variable, this is just the value of the primitive variable. For + non-constant primitive variables, this is the value for each element. + """, + ] + } +) + +########################################################################## +# _TweaksFooter +########################################################################## + +class _TweaksFooter( GafferUI.PlugValueWidget ) : + + def __init__( self, plug ) : + + row = GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal ) + + GafferUI.PlugValueWidget.__init__( self, row, plug ) + + with row : + + GafferUI.Spacer( imath.V2i( GafferUI.PlugWidget.labelWidth(), 1 ) ) + + self.__button = GafferUI.MenuButton( + image = "plus.png", + hasFrame = False, + menu = GafferUI.Menu( Gaffer.WeakMethod( self.__menuDefinition ) ) + ) + + GafferUI.Spacer( imath.V2i( 1 ), imath.V2i( 999999, 1 ), parenting = { "expand" : True } ) + + def _updateFromEditable( self ) : + + # Not using `_editable()` as it considers the whole plug to be non-editable if + # any child has an input connection, but that shouldn't prevent us adding a new + # tweak. + self.__button.setEnabled( self.getPlug().getInput() is None and not Gaffer.MetadataAlgo.readOnly( self.getPlug() ) ) + + def __menuDefinition( self ) : + + result = IECore.MenuDefinition() + + result.append( + "/From Affected", + { + "subMenu" : Gaffer.WeakMethod( self.__addFromAffectedMenuDefinition ) + } + ) + + result.append( + "/From Selection", + { + "subMenu" : Gaffer.WeakMethod( self.__addFromSelectedMenuDefinition ) + } + ) + + result.append( "/FromPathsDivider", { "divider" : True } ) + + for subMenu, items in [ + ( "", [ + Gaffer.BoolPlug, + Gaffer.FloatPlug, + Gaffer.IntPlug, + "NumericDivider", + Gaffer.StringPlug, + "StringDivider", + Gaffer.V2iPlug, + Gaffer.V3iPlug, + Gaffer.V2fPlug, + Gaffer.V3fPlug, + # \todo - specifying interpretation is only necessary for Create mode - is having these options + # in the menu worth it? + IECore.V3fData( imath.V3f( 0 ), IECore.GeometricData.Interpretation.Point ), + IECore.V3fData( imath.V3f( 0 ), IECore.GeometricData.Interpretation.Vector ), + IECore.V3fData( imath.V3f( 0 ), IECore.GeometricData.Interpretation.Normal ), + "VectorDivider", + Gaffer.Color3fPlug, + Gaffer.Color4fPlug, + "BoxDivider", + IECore.Box2iData( imath.Box2i( imath.V2i( 0 ), imath.V2i( 1 ) ) ), + IECore.Box2fData( imath.Box2f( imath.V2f( 0 ), imath.V2f( 1 ) ) ), + IECore.Box3iData( imath.Box3i( imath.V3i( 0 ), imath.V3i( 1 ) ) ), + IECore.Box3fData( imath.Box3f( imath.V3f( 0 ), imath.V3f( 1 ) ) ), + "ArrayDivider" + ] ), + ( "Array", [ + IECore.FloatVectorData(), + IECore.IntVectorData(), + IECore.Int64VectorData(), + "StringVectorDivider", + IECore.StringVectorData() + ] ) + ]: + for item in items: + prefix = "/" + subMenu if subMenu else "" + + if isinstance( item, str ) : + result.append( prefix + "/" + item, { "divider" : True } ) + else : + itemName = item.typeName() if isinstance( item, IECore.Data ) else item.__name__ + itemName = itemName.replace( "Plug", "" ).replace( "Data", "" ).replace( "Vector", "" ) + + if hasattr( item, "getInterpretation" ): + itemName += " (" + str( item.getInterpretation() ) + ")" + + result.append( + prefix + "/" + itemName, + { + "command" : functools.partial( Gaffer.WeakMethod( self.__addTweak ), "", item ), + } + ) + + return result + + def __addFromAffectedMenuDefinition( self ) : + + node = self.getPlug().node() + assert( isinstance( node, GafferScene.PrimitiveVariableTweaks ) ) + + pathMatcher = IECore.PathMatcher() + with self.context() : + GafferScene.SceneAlgo.matchingPaths( node["filter"], node["in"], pathMatcher ) + + return self.__addFromPathsMenuDefinition( pathMatcher.paths() ) + + def __addFromSelectedMenuDefinition( self ) : + + return self.__addFromPathsMenuDefinition( + GafferSceneUI.ScriptNodeAlgo.getSelectedPaths( self.scriptNode() ).paths() + ) + + def __addFromPathsMenuDefinition( self, paths ) : + + result = IECore.MenuDefinition() + + node = self.getPlug().node() + assert( isinstance( node, GafferScene.PrimitiveVariableTweaks ) ) + + possibilities = {} + with self.context() : + for path in paths : + obj = node["in"].object( path ) + for name in obj.keys(): + d = obj[name].data + typeName = d.typeName() + if obj[name].interpolation != IECoreScene.PrimitiveVariable.Interpolation.Constant: + # \todo - there must be a better way to get an element type from a vector type? + typeName = typeName.replace( "Vector", "" ) + newData = IECore.Object.create( IECore.Object.typeIdFromTypeName( typeName ) ) + if hasattr( d, "getInterpretation" ): + newData.setInterpretation( d.getInterpretation() ) + + possibilities[name] = newData + + existingTweaks = { tweak["name"].getValue() for tweak in node["tweaks"] } + + possibilities = collections.OrderedDict( sorted( possibilities.items() ) ) + + for key, value in possibilities.items() : + result.append( + "/" + key, + { + "command" : functools.partial( + Gaffer.WeakMethod( self.__addTweak ), + key, + value + ), + "active" : key not in existingTweaks + } + ) + + if not len( result.items() ) : + result.append( + "/No Primitive Variables Found", { "active" : False } + ) + return result + + return result + + def __addTweak( self, optionName, plugTypeOrValue ) : + + if isinstance( plugTypeOrValue, IECore.Data ) : + plug = Gaffer.TweakPlug( optionName, plugTypeOrValue ) + else : + plug = Gaffer.TweakPlug( optionName, plugTypeOrValue() ) + + plug.setName( "tweak0" ) + + with Gaffer.UndoScope( self.getPlug().ancestor( Gaffer.ScriptNode ) ) : + self.getPlug().addChild( plug ) diff --git a/python/GafferSceneUI/__init__.py b/python/GafferSceneUI/__init__.py index 40bbdf51ad5..589d721739c 100644 --- a/python/GafferSceneUI/__init__.py +++ b/python/GafferSceneUI/__init__.py @@ -203,6 +203,7 @@ from . import MergeMeshesUI from . import MergePointsUI from . import MergeCurvesUI +from . import PrimitiveVariableTweaksUI # then all the PathPreviewWidgets. note that the order # of import controls the order of display. diff --git a/src/GafferScene/PrimitiveVariableTweaks.cpp b/src/GafferScene/PrimitiveVariableTweaks.cpp new file mode 100644 index 00000000000..c1a9299661b --- /dev/null +++ b/src/GafferScene/PrimitiveVariableTweaks.cpp @@ -0,0 +1,769 @@ +////////////////////////////////////////////////////////////////////////// +// +// 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/PrimitiveVariableTweaks.h" + +#include "IECoreScene/Primitive.h" + +#include "IECore/DataAlgo.h" + +using namespace IECore; +using namespace IECoreScene; +using namespace Gaffer; +using namespace GafferScene; + +namespace { + +// Rather startling that this doesn't already exist, but it seems that there isn't anywhere else where we +// report exceptions with interpolations in C++. +std::string interpolationToString( PrimitiveVariable::Interpolation i ) +{ + switch( i ) + { + case PrimitiveVariable::Constant: + return "Constant"; + case PrimitiveVariable::Uniform: + return "Uniform"; + case PrimitiveVariable::Vertex: + return "Vertex"; + case PrimitiveVariable::Varying: + return "Varying"; + case PrimitiveVariable::FaceVarying: + return "FaceVarying"; + default: + return "Invalid"; + }; +} + +template +void removeUnusedElements( std::vector &indices, std::vector &data ) +{ + std::vector used( data.size(), -1 ); + + for( const int &i : indices ) + { + used[i] = 1; + } + + int accum = 0; + for( int &i : used ) + { + if( i != -1 ) + { + i = accum; + accum += 1; + } + } + + if( accum == (int)data.size() ) + { + // All elements were used + return; + } + + std::vector result; + result.reserve( accum ); + for( size_t j = 0; j < data.size(); j++ ) + { + if( used[j] != -1 ) + { + result.push_back( data[j] ); + } + } + + for( int &i : indices ) + { + i = used[i]; + } + + data.swap( result ); +} + +template< typename T> +bool constexpr hasZeroConstructor() +{ + // Some types, like V3f and Color3f, won't default initialize unless we explicitly + // pass 0 to the constructor. Other types don't have a constructor that accepts 0, + // so we need to distinguish the two somehow. Currently, I'm using a blacklist of + // types that don't need to be initialized to zero ... my rationale is that if a new + // type is added, I would rather get a compile error than get uninitialized memory. + return !( + TypeTraits::IsBox< T >::value || + TypeTraits::IsMatrix< T >::value || + TypeTraits::IsQuat< T >::value || + std::is_same_v< T, InternedString > || // I don't think InternedString primvars can exist, but the dispatch still covers this type + std::is_same_v< T, std::string > + ); +} + +void applyTweakToPrimVars( + Primitive *prim, PrimitiveVariable::Interpolation targetInterpolation, + const std::string &name, TweakPlug::Mode mode, IECore::DataPtr tweakData, bool ignoreMissing, + const Int64VectorData *idList +) +{ + if( name.empty() ) + { + return; + } + + if( mode == Gaffer::TweakPlug::Remove ) + { + prim->variables.erase( name ); + return; + } + + auto primVarIt = prim->variables.find( name ); + bool hasSource = primVarIt != prim->variables.end(); + + if( !prim->arePrimitiveVariablesValid() ) + { + throw IECore::Exception( "Primitive variable tweak failed - input primitive variables are not valid." ); + } + + if( !hasSource && mode == Gaffer::TweakPlug::ListRemove ) + { + // For consistency with the usual operation of TweakPlug, we consider this a success, whether + // or not ignoreMissing is set. + return; + } + + // There are several combinations of parameters that could result in "just create a fresh primvar" + if( + // Most obviously, the user could have selected Create + mode == Gaffer::TweakPlug::Create || mode == Gaffer::TweakPlug::CreateIfMissing || + + // Or if they are adding to a list that doesn't exist + ( !hasSource && ( mode == Gaffer::TweakPlug::ListAppend || mode == Gaffer::TweakPlug::ListPrepend ) ) + ) + { + if( mode == Gaffer::TweakPlug::CreateIfMissing && hasSource ) + { + // Don't need to create in this mode if there's already something there + return; + } + + if( targetInterpolation == PrimitiveVariable::Invalid ) + { + throw IECore::Exception( fmt::format( "Cannot create primitive variable {} when \"interpolation\" is set to \"Any\". Please select an interpolation.", name ) ); + } + else if( targetInterpolation == PrimitiveVariable::Constant ) + { + prim->variables[name] = PrimitiveVariable( PrimitiveVariable::Constant, tweakData ); + return; + } + + // Make a fresh primvar using the supplied value as every element of the vector + + const size_t variableSize = prim->variableSize( targetInterpolation ); + + prim->variables[name] = IECore::dispatch( tweakData.get(), + [&targetInterpolation, &variableSize, &name, &idList]( const auto *typedTweakData ) -> PrimitiveVariable + { + using DataType = typename std::remove_const_t >; + using ValueType = typename DataType::ValueType; + + if constexpr( + TypeTraits::IsTypedData< DataType >::value && + !TypeTraits::IsVectorTypedData< DataType >::value && + + // A bunch of things we're not allowed to make vectors of + !TypeTraits::IsTransformationMatrix< ValueType >::value && + !TypeTraits::IsSpline< ValueType >::value && + !std::is_same_v< ValueType, PathMatcher > && + !std::is_same_v< ValueType, boost::posix_time::ptime > + ) + { + constexpr bool isGeometric = TypeTraits::IsGeometricTypedData< DataType >::value; + using VectorDataType = std::conditional_t< + isGeometric, + IECore::GeometricTypedData< std::vector< ValueType > >, + IECore::TypedData< std::vector< ValueType > > + >; + + typename VectorDataType::Ptr vectorData = new VectorDataType(); + + if( idList ) + { + // If there is an idList, we will only give the specified value to the targeted ids. + // Everything else just gets default initialized. + + // Some types, like V3f and Color3f, won't default initialize unless we explicitly + // pass 0 to the constructor. Other types don't have a constructor that accepts 0, + // so we need to distinguish the two somehow. Currently, I'm using a blacklist of + // types that don't need to be initialized to zero ... my rationale is that if a new + // type is added, I would rather get a compile error than get uninitialized memory. + if constexpr( !hasZeroConstructor< ValueType >() ) + { + vectorData->writable().resize( variableSize, ValueType() ); + } + else + { + vectorData->writable().resize( variableSize, ValueType( 0 ) ); + } + } + else + { + // If there is no idList, we can immediately set everything to the correct value. + vectorData->writable().resize( variableSize, typedTweakData->readable() ); + } + + if constexpr( isGeometric ) + { + vectorData->setInterpretation( typedTweakData->getInterpretation() ); + } + + return PrimitiveVariable( targetInterpolation, std::move( vectorData ) ); + } + else + { + throw IECore::Exception( fmt::format( + "Invalid type \"{}\" for non-constant primitive variable tweak \"{}\".", + typedTweakData->typeName(), name + ) ); + } + } + ); + + if( idList ) + { + // Since we have an idList and we're only giving the specified value to part of the new primvar, + // we continue through the rest of this function as if we were in replace mode. + primVarIt = prim->variables.find( name ); + hasSource = true; + mode = Gaffer::TweakPlug::Replace; + } + else + { + return; + } + } + + + if( !hasSource ) + { + if( ignoreMissing ) + { + return; + } + else + { + throw IECore::Exception( fmt::format( "Cannot apply tweak with mode {} to \"{}\" : This parameter does not exist.", TweakPlug::modeToString( mode ), name ) ); + } + } + + PrimitiveVariable &targetVar = primVarIt->second; + + if( targetInterpolation != PrimitiveVariable::Invalid && targetVar.interpolation != targetInterpolation ) + { + // \todo - Throwing an exception here is probably not the most useful to users. More useful options might + // be "ignore primvars that don't match" or "resample primvars so they do match" ... but we're not sure + // which is right, and we don't want to add additional options to control this unless it's absolutely + // needed. For now, making it an exception makes it easier to modify this behaviour in the future. + // + // Note that one case where the correct behaviour is pretty easy to define is if we are in mode Uniform + // or Vertex, and we encounter a primvar with FaceVarying interpolation. The correct behaviour there is + // is pretty clearly to apply the tweak to all FaceVertices corresponding to the selected Faces or Vertices. + // We haven't implemented this yet, but it would be pretty straightforward to make things behave properly + // instead of throwing in that specific case at least. + throw IECore::Exception( fmt::format( + "Cannot apply tweak to \"{}\" : Interpolation \"{}\" doesn't match primitive variable interpolation \"{}\".", + name, interpolationToString( targetInterpolation ), interpolationToString( targetVar.interpolation ) + ) ); + } + + if( targetVar.interpolation == PrimitiveVariable::Constant ) + { + IECore::dispatch( targetVar.data.get(), + [&tweakData, &targetVar, &mode, &name]( auto *typedData ) + { + using SourceType = typename std::remove_pointer_t; + + if constexpr( TypeTraits::IsTypedData< SourceType >::value ) + { + auto &result = typedData->writable(); + + const SourceType* tweakDataTyped = IECore::runTimeCast< SourceType >( tweakData.get() ); + + if( !tweakDataTyped ) + { + throw IECore::Exception( fmt::format( + "Cannot apply tweak to \"{}\" : Variable data of type \"{}\" does not match " + "parameter of type \"{}\".", name, typedData->typeName(), tweakData->typeName() + ) ); + } + + result = TweakPlug::applyValueTweak( result, tweakDataTyped->readable(), mode, name ); + } + } + ); + return; + } + + IECore::dispatch( targetVar.data.get(), + [&tweakData, &targetVar, &mode, &name, &idList]( auto *typedData ) + { + using SourceType = typename std::remove_pointer_t; + if constexpr( TypeTraits::IsVectorTypedData< SourceType >::value ) + { + auto &result = typedData->writable(); + using ElementType = typename SourceType::ValueType::value_type; + using ElementDataType = IECore::TypedData< ElementType >; + + const ElementDataType* tweakDataTyped = IECore::runTimeCast< ElementDataType >( tweakData.get() ); + if( !tweakDataTyped ) + { + throw IECore::Exception( + fmt::format( + "Cannot apply tweak to \"{}\" : Parameter should be of type \"{}\" in order to apply " + "to an element of \"{}\", but got \"{}\" instead.", + name, ElementDataType::staticTypeName(), typedData->typeName(), tweakData->typeName() + ) + ); + } + + auto &tweak = tweakDataTyped->readable(); + + if( idList && targetVar.indices ) + { + // OK, this is a somewhat complex special case - we are only tweaking some data, based + // on indices, but some indices currently refer to the same data. If we end up tweaking + // only some of the indices that currently refer to the same data, then we're splitting + // it into two different values, and need to add a new piece of data to hold the new + // value. + + result.reserve( result.size() + idList->readable().size() ); + + std::vector &indices = targetVar.indices->writable(); + std::unordered_map< int, int > tweakedIndices; + + for( int64_t i : idList->readable() ) + { + if( i >= 0 && i <= (int64_t)indices.size() ) + { + auto[ it, inserted ] = tweakedIndices.try_emplace( indices[i], result.size() ); + if( inserted ) + { + result.push_back( TweakPlug::applyValueTweak( result[indices[i]], tweak, mode, name ) ); + } + indices[i] = it->second; + } + } + + // If we actually ended up tweaking all indices that used a piece of data, that data is now + // abandoned, so we should now do a scan to remove unused data. + removeUnusedElements( indices, result ); + } + else if( idList ) + { + // If there are no indices, then we just modify the data the ids point to + for( int64_t i : idList->readable() ) + { + if( i >= 0 && i <= (int64_t)result.size() ) + { + result[i] = TweakPlug::applyValueTweak( result[i], tweak, mode, name ); + } + } + } + else + { + // If there is no id list given, we're just modifying all the data, and it doesn't matter + // whether or not there are indices. + + // I probably should have paid more attention to what r-value references are in general, + // but in this case it seems like a pretty safe way to force this to work with the + // vector-of-bool weirdness + for( auto &&i : result ) + { + i = TweakPlug::applyValueTweak( i, tweak, mode, name ); + } + } + } + else + { + throw IECore::Exception( fmt::format( + "Found invalid primitive variable \"{}\" : Expected vector typed data, got \"{}\".", + name, typedData->typeName() + ) ); + } + } + ); +} + +} // namespace + +GAFFER_NODE_DEFINE_TYPE( PrimitiveVariableTweaks ); + +size_t PrimitiveVariableTweaks::g_firstPlugIndex = 0; + +PrimitiveVariableTweaks::PrimitiveVariableTweaks( const std::string &name ) + : ObjectProcessor( name ) +{ + storeIndexOfNextChild( g_firstPlugIndex ); + + addChild( new IntPlug( "interpolation", Plug::In, PrimitiveVariable::Invalid, PrimitiveVariable::Invalid, PrimitiveVariable::FaceVarying ) ); + addChild( new IntPlug( "selectionMode", Plug::In, (int)SelectionMode::All, (int)SelectionMode::All, (int)SelectionMode::MaskPrimitiveVariable ) ); + addChild( new Int64VectorDataPlug( "idList", Plug::In ) ); + addChild( new StringPlug( "idListVariable", Plug::In, "" ) ); + addChild( new StringPlug( "id", Plug::In, "" ) ); + addChild( new StringPlug( "maskVariable", Plug::In, "" ) ); + addChild( new BoolPlug( "ignoreMissing", Plug::In, false ) ); + addChild( new TweaksPlug( "tweaks" ) ); +} + +PrimitiveVariableTweaks::~PrimitiveVariableTweaks() +{ +} + +Gaffer::IntPlug *PrimitiveVariableTweaks::interpolationPlug() +{ + return getChild( g_firstPlugIndex + 0 ); +} + +const Gaffer::IntPlug *PrimitiveVariableTweaks::interpolationPlug() const +{ + return getChild( g_firstPlugIndex + 0 ); +} + +Gaffer::IntPlug *PrimitiveVariableTweaks::selectionModePlug() +{ + return getChild( g_firstPlugIndex + 1 ); +} + +const Gaffer::IntPlug *PrimitiveVariableTweaks::selectionModePlug() const +{ + return getChild( g_firstPlugIndex + 1 ); +} + +Gaffer::Int64VectorDataPlug *PrimitiveVariableTweaks::idListPlug() +{ + return getChild( g_firstPlugIndex + 2 ); +} + +const Gaffer::Int64VectorDataPlug *PrimitiveVariableTweaks::idListPlug() const +{ + return getChild( g_firstPlugIndex + 2 ); +} + +Gaffer::StringPlug *PrimitiveVariableTweaks::idListVariablePlug() +{ + return getChild( g_firstPlugIndex + 3 ); +} + +const Gaffer::StringPlug *PrimitiveVariableTweaks::idListVariablePlug() const +{ + return getChild( g_firstPlugIndex + 3 ); +} + +Gaffer::StringPlug *PrimitiveVariableTweaks::idPlug() +{ + return getChild( g_firstPlugIndex + 4 ); +} + +const Gaffer::StringPlug *PrimitiveVariableTweaks::idPlug() const +{ + return getChild( g_firstPlugIndex + 4 ); +} + +Gaffer::StringPlug *PrimitiveVariableTweaks::maskVariablePlug() +{ + return getChild( g_firstPlugIndex + 5 ); +} + +const Gaffer::StringPlug *PrimitiveVariableTweaks::maskVariablePlug() const +{ + return getChild( g_firstPlugIndex + 5 ); +} + +Gaffer::BoolPlug *PrimitiveVariableTweaks::ignoreMissingPlug() +{ + return getChild( g_firstPlugIndex + 6 ); +} + +const Gaffer::BoolPlug *PrimitiveVariableTweaks::ignoreMissingPlug() const +{ + return getChild( g_firstPlugIndex + 6 ); +} + +Gaffer::TweaksPlug *PrimitiveVariableTweaks::tweaksPlug() +{ + return getChild( g_firstPlugIndex + 7 ); +} + +const Gaffer::TweaksPlug *PrimitiveVariableTweaks::tweaksPlug() const +{ + return getChild( g_firstPlugIndex + 7 ); +} + +bool PrimitiveVariableTweaks::affectsProcessedObject( const Gaffer::Plug *input ) const +{ + return + ObjectProcessor::affectsProcessedObject( input ) || + input == interpolationPlug() || + input == selectionModePlug() || + input == idListPlug() || + input == idListVariablePlug() || + input == idPlug() || + input == maskVariablePlug() || + input == ignoreMissingPlug() || + tweaksPlug()->isAncestorOf( input ) + ; +} + +void PrimitiveVariableTweaks::hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + if( tweaksPlug()->children().empty() ) + { + h = inPlug()->objectPlug()->hash(); + } + else + { + ObjectProcessor::hashProcessedObject( path, context, h ); + interpolationPlug()->hash( h ); + selectionModePlug()->hash( h ); + idListPlug()->hash( h ); + idListVariablePlug()->hash( h ); + idPlug()->hash( h ); + maskVariablePlug()->hash( h ); + ignoreMissingPlug()->hash( h ); + tweaksPlug()->hash( h ); + } +} + +IECore::ConstObjectPtr PrimitiveVariableTweaks::computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const +{ + const Primitive *inputPrimitive = runTimeCast( inputObject ); + if( !inputPrimitive || tweaksPlug()->children().empty() ) + { + return inputObject; + } + + PrimitiveVariable::Interpolation targetInterpolation = (PrimitiveVariable::Interpolation)interpolationPlug()->getValue(); + + const bool ignoreMissing = ignoreMissingPlug()->getValue(); + + PrimitivePtr result = inputPrimitive->copy(); + + SelectionMode selectionMode = (SelectionMode)selectionModePlug()->getValue(); + ConstInt64VectorDataPtr idList; + if( ( selectionMode == SelectionMode::IdList || selectionMode == SelectionMode::IdListPrimitiveVariable ) && targetInterpolation != PrimitiveVariable::Invalid ) + { + if( selectionMode == SelectionMode::IdList ) + { + idList = idListPlug()->getValue(); + } + else + { + std::string idListVarName = idListVariablePlug()->getValue(); + auto idListVar = inputPrimitive->variables.find( idListVarName ); + if( idListVar == inputPrimitive->variables.end() ) + { + throw IECore::Exception( fmt::format( "Can't find id list primitive variable \"{}\".", idListVarName ) ); + + } + + if( idListVar->second.interpolation == PrimitiveVariable::Interpolation::Constant ) + { + if( const Int64VectorData *int64Data = IECore::runTimeCast( idListVar->second.data.get() ) ) + { + idList = int64Data; + } + else if( const IntVectorData *intData = IECore::runTimeCast( idListVar->second.data.get() ) ) + { + // For simplicity elsewhere, just convert to an Int64VectorData instead of supporting both types + Int64VectorDataPtr convertedData = new Int64VectorData(); + auto &converted = convertedData->writable(); + converted.reserve( intData->readable().size() ); + for( int i : intData->readable() ) + { + converted.push_back( i ); + } + + idList = convertedData; + } + } + + if( !idList ) + { + throw IECore::Exception( fmt::format( "Invalid id list primitive variable \"{}\". A constant IntVector or Int64Vector is required.", idListVarName ) ); + } + + } + + std::string idVarName = idPlug()->getValue(); + if( idVarName.size() ) + { + Int64VectorDataPtr mappedIdListData = new Int64VectorData(); + std::vector< int64_t > &mappedIdList = mappedIdListData->writable(); + mappedIdList.reserve( idList->readable().size() ); + + std::unordered_set< int64_t > idSet( idList->readable().begin(), idList->readable().end() ); + + auto idVar = inputPrimitive->variables.find( idVarName ); + if( idVar == inputPrimitive->variables.end() ) + { + throw IECore::Exception( fmt::format( "Id invalid, can't find primitive variable \"{}\".", idVarName ) ); + } + + if( idVar->second.interpolation != targetInterpolation ) + { + throw IECore::Exception( fmt::format( + "Id variable \"{}\" : Interpolation \"{}\" doesn't match specified interpolation \"{}\".", + idVarName, interpolationToString( idVar->second.interpolation ), interpolationToString( targetInterpolation ) + ) ); + } + + if( idVar->second.indices ) + { + throw IECore::Exception( fmt::format( "Id variable \"{}\" is not allowed to be indexed.", idVarName ) ); + } + + if( const IntVectorData *intIdsData = IECore::runTimeCast( idVar->second.data.get() ) ) + { + const std::vector &intIds = intIdsData->readable(); + for( size_t i = 0; i < intIds.size(); i++ ) + { + if( idSet.count( intIds[i] ) ) + { + mappedIdList.push_back( i ); + } + } + } + else if( const Int64VectorData *int64IdsData = IECore::runTimeCast( idVar->second.data.get() ) ) + { + const std::vector &intIds = int64IdsData->readable(); + for( size_t i = 0; i < intIds.size(); i++ ) + { + if( idSet.count( intIds[i] ) ) + { + mappedIdList.push_back( i ); + } + } + } + else + { + throw IECore::Exception( fmt::format( "Id invalid, can't find primitive variable \"{}\" of type IntVectorData or type Int64VectorData.", idVarName ) ); + } + + idList = mappedIdListData; + } + } + else if( selectionMode == SelectionMode::MaskPrimitiveVariable && targetInterpolation != PrimitiveVariable::Invalid ) + { + std::string maskVarName = maskVariablePlug()->getValue(); + auto maskVar = inputPrimitive->variables.find( maskVarName ); + + if( maskVar == inputPrimitive->variables.end() ) + { + throw IECore::Exception( fmt::format( "Can't find mask primitive variable \"{}\".", maskVarName ) ); + } + + if( maskVar->second.interpolation != targetInterpolation ) + { + throw IECore::Exception( fmt::format( + "Mask primitive variable \"{}\" has wrong interpolation \"{}\", expected \"{}\".", + maskVarName, interpolationToString( maskVar->second.interpolation ), interpolationToString( targetInterpolation ) + ) ); + } + + // It would be a bit more efficient to directly use the mask to set elements, but to avoid a combinatorial + // increase in the number of code paths, we just convert the mask into a list of ids to be tweaked. + Int64VectorDataPtr idListTranslatedData = new Int64VectorData(); + std::vector< int64_t > &idListTranslated = idListTranslatedData->writable(); + + IECore::dispatch( maskVar->second.data.get(), + [&idListTranslated, &maskVar, &maskVarName]( auto *typedData ) + { + using SourceType = typename std::remove_pointer_t; + + if constexpr( TypeTraits::IsVectorTypedData< SourceType >::value ) + { + using ValueType = typename SourceType::ValueType::value_type; + + if constexpr( hasZeroConstructor() ) + { + PrimitiveVariable::IndexedView indexedView( maskVar->second ); + ValueType defaultValue( 0 ); + + for( size_t i = 0; i < indexedView.size(); i++ ) + { + if( indexedView[i] != defaultValue ) + { + idListTranslated.push_back( i ); + } + } + return; + } + } + + throw IECore::Exception( fmt::format( + "Mask primitive variable \"{}\" has invalid type \"{}\".", + maskVarName, typedData->typeName() + ) ); + } + ); + + idList = idListTranslatedData; + + } + + for( const auto &tweak : TweakPlug::Range( *tweaksPlug() ) ) + { + // This reproduces most of the logic from TweakPlug::applyTweak, but for PrimVars instead of Data + if( !tweak->enabledPlug()->getValue() ) + { + continue; + } + + std::string name = tweak->namePlug()->getValue(); + + IECore::DataPtr tweakData = Gaffer::PlugAlgo::getValueAsData( tweak->valuePlug() ); + if( !tweakData ) + { + throw IECore::Exception( + fmt::format( "Cannot apply tweak to \"{}\" : Value plug has unsupported type \"{}\".", name, tweak->valuePlug()->typeName() ) + ); + } + + applyTweakToPrimVars( + result.get(), targetInterpolation, + name, static_cast( tweak->modePlug()->getValue() ), + std::move( tweakData ), ignoreMissing, idList.get() + ); + } + + return result; +} diff --git a/src/GafferSceneModule/PrimitiveVariablesBinding.cpp b/src/GafferSceneModule/PrimitiveVariablesBinding.cpp index 4fb8ba3fb87..6bbe63abb48 100644 --- a/src/GafferSceneModule/PrimitiveVariablesBinding.cpp +++ b/src/GafferSceneModule/PrimitiveVariablesBinding.cpp @@ -46,6 +46,7 @@ #include "GafferScene/CollectPrimitiveVariables.h" #include "GafferScene/PrimitiveVariableExists.h" #include "GafferScene/ShufflePrimitiveVariables.h" +#include "GafferScene/PrimitiveVariableTweaks.h" #include "GafferBindings/DependencyNodeBinding.h" @@ -64,4 +65,15 @@ void GafferSceneModule::bindPrimitiveVariables() GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); + { + boost::python::scope tweaksScope = GafferBindings::DependencyNodeClass(); + + boost::python::enum_( "SelectionMode" ) + .value( "All", PrimitiveVariableTweaks::SelectionMode::All ) + .value( "IdList", PrimitiveVariableTweaks::SelectionMode::IdList ) + .value( "IdListPrimitiveVariable", PrimitiveVariableTweaks::SelectionMode::IdListPrimitiveVariable ) + .value( "MaskPrimitiveVariable", PrimitiveVariableTweaks::SelectionMode::MaskPrimitiveVariable ) + ; + } + } diff --git a/startup/gui/menus.py b/startup/gui/menus.py index 260e976bb2c..6f72e491ac2 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -261,6 +261,7 @@ def __lightCreator( nodeName, shaderName, shape ) : nodeMenu.append( "/Scene/Object/Shuffle Primitive Variables", GafferScene.ShufflePrimitiveVariables, searchText = "ShufflePrimitiveVariables" ) nodeMenu.append( "/Scene/Object/Resample Primitive Variables", GafferScene.ResamplePrimitiveVariables, searchText = "ResamplePrimitiveVariables" ) nodeMenu.append( "/Scene/Object/Collect Primitive Variables", GafferScene.CollectPrimitiveVariables, searchText = "CollectPrimitiveVariables" ) +nodeMenu.append( "/Scene/Object/Primitive Variable Tweaks", GafferScene.PrimitiveVariableTweaks, searchText = "PrimitiveVariableTweaks" ) nodeMenu.append( "/Scene/Object/Orientation", GafferScene.Orientation ) nodeMenu.append( "/Scene/Object/Mesh Type", GafferScene.MeshType, searchText = "MeshType" ) nodeMenu.append( "/Scene/Object/Points Type", GafferScene.PointsType, searchText = "PointsType" )