From bbf93bb2922719d5c965cadec4e5d2f7e91f671f Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Mon, 16 May 2022 17:56:54 -0700 Subject: [PATCH 01/36] SceneShapeProxy: Add a proxy for `SceneShape` We noticed huge FPS drops in scenes with a huge amount of `SceneShapes`. Profiling showed that a lot of time is spend by `SubSceneOverride` requesting draw updates from the shapes in the scene. This also happens for shapes that are set as hidden, what we consider a bug/flaw in Maya's VP2 drawing API. To alleviate this issue, we derive the proxy from `SceneShape` and inherit all of it's behaviour with the exception, that we don't register it as drawable. This allows us to replace `SceneShape`s with its proxy implementation, where only the data reading capabilities are needed, e.g. layouts or rigs. --- include/IECoreMaya/MayaTypeIds.h | 1 + include/IECoreMaya/SceneShapeProxy.h | 67 +++++++++++++++++++ src/IECoreMaya/SceneShapeProxy.cpp | 55 +++++++++++++++ src/IECoreMaya/bindings/MayaTypeIdBinding.cpp | 1 + 4 files changed, 124 insertions(+) create mode 100644 include/IECoreMaya/SceneShapeProxy.h create mode 100644 src/IECoreMaya/SceneShapeProxy.cpp diff --git a/include/IECoreMaya/MayaTypeIds.h b/include/IECoreMaya/MayaTypeIds.h index 644b777ef6..82223ab78e 100644 --- a/include/IECoreMaya/MayaTypeIds.h +++ b/include/IECoreMaya/MayaTypeIds.h @@ -67,6 +67,7 @@ enum MayaTypeId GeometryCombinerId = 0x00110DD2, SceneShapeId = 0x00110DD3, SceneShapeInterfaceId = 0x00110DD4, + SceneShapeProxyId = 0x00110DD5, /// Don't forget to update MayaTypeIdsBinding.cpp LastId = 0x00110E3F, diff --git a/include/IECoreMaya/SceneShapeProxy.h b/include/IECoreMaya/SceneShapeProxy.h new file mode 100644 index 0000000000..e56ba90812 --- /dev/null +++ b/include/IECoreMaya/SceneShapeProxy.h @@ -0,0 +1,67 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IE_COREMAYA_SCENESHAPEPROXY_H +#define IE_COREMAYA_SCENESHAPEPROXY_H + +#include "IECoreMaya/SceneShape.h" + +namespace IECoreMaya +{ + +/// A proxy derived from the SceneShape which exposes the same functionality as the base clase +/// with the exception, that we never register it as a maya SubSceneOverride. The reasoning +/// behind this is that the SubSceneOverride does not take into account the visibility state of the shape. +/// During an update loop of the SubSceneOverride, all SceneShapes will be queried for their update state, +/// regardless their visibility in the scene. This query is slow and we get a huge drop in performance +/// when having a huge amount of SceneShapes in the scene. +/// This is considered to be a bug in the ViewPort 2 API. Our attempts to rewrite the code to use +/// "MPxGeometryOverride" or "MPxDrawOverride" prove themselves as unstable or not suitable for our +/// use case, why we decided to use this "hackery" and not register a proxy of the SceneShape for +/// drawing at all +class IECOREMAYA_API SceneShapeProxy : public SceneShape +{ + public : + + SceneShapeProxy(); + virtual ~SceneShapeProxy(); + + static void *creator(); + static MStatus initialize(); + static MTypeId id; +}; + +} + +#endif // IE_COREMAYA_SCENESHAPEPROXY_H diff --git a/src/IECoreMaya/SceneShapeProxy.cpp b/src/IECoreMaya/SceneShapeProxy.cpp new file mode 100644 index 0000000000..965d0f95ba --- /dev/null +++ b/src/IECoreMaya/SceneShapeProxy.cpp @@ -0,0 +1,55 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "IECoreMaya/SceneShapeProxy.h" +#include "IECoreMaya/MayaTypeIds.h" + +using namespace IECoreMaya; + +MTypeId SceneShapeProxy::id = SceneShapeProxyId; + +SceneShapeProxy::SceneShapeProxy() {} + +SceneShapeProxy::~SceneShapeProxy() {} + +void *SceneShapeProxy::creator() +{ + return new SceneShapeProxy; +} + +MStatus SceneShapeProxy::initialize() +{ + MStatus s = inheritAttributesFrom( "ieSceneShape" ); + return s; +} diff --git a/src/IECoreMaya/bindings/MayaTypeIdBinding.cpp b/src/IECoreMaya/bindings/MayaTypeIdBinding.cpp index 806a2082f8..cbe1e9f8c4 100644 --- a/src/IECoreMaya/bindings/MayaTypeIdBinding.cpp +++ b/src/IECoreMaya/bindings/MayaTypeIdBinding.cpp @@ -69,6 +69,7 @@ void bindMayaTypeId() .value( "GeometryCombiner", GeometryCombinerId ) .value( "SceneShapeInterface", SceneShapeInterfaceId ) .value( "SceneShape", SceneShapeId ) + .value( "SceneShapeProxy", SceneShapeProxyId ) ; } From b3e0082d5b5d2579cfcf0ffc0f68a56dfbf9d87c Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Tue, 17 May 2022 15:16:14 -0700 Subject: [PATCH 02/36] SceneShapeProxyUI: Add `SceneShapeProxyUI` --- include/IECoreMaya/SceneShapeProxyUI.h | 59 ++++++++++++++++++++++++++ src/IECoreMaya/SceneShapeProxyUI.cpp | 46 ++++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 include/IECoreMaya/SceneShapeProxyUI.h create mode 100644 src/IECoreMaya/SceneShapeProxyUI.cpp diff --git a/include/IECoreMaya/SceneShapeProxyUI.h b/include/IECoreMaya/SceneShapeProxyUI.h new file mode 100644 index 0000000000..8ed06afc05 --- /dev/null +++ b/include/IECoreMaya/SceneShapeProxyUI.h @@ -0,0 +1,59 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IECOREMAYA_SCENESHAPEPROXYUI_H +#define IECOREMAYA_SCENESHAPEPROXYUI_H + +#include "maya/MPxSurfaceShapeUI.h" +#include "maya/MTypes.h" +#include "IECoreMaya/Export.h" + +namespace IECoreMaya +{ + +/// The SceneShapeProxyUI is required for the registration of the SceneShapeProxy and we just make it a NoOp +/// TODO: It might be worth to see if the SceneShapeUI has any dependencies on the drawing capabilities of the +/// shape and if that's not the case, register SceneShapeProxy with the original implementation of SceneShapeUI +class IECOREMAYA_API SceneShapeProxyUI : public MPxSurfaceShapeUI +{ + + public : + + SceneShapeProxyUI(); + static void *creator(); +}; + +} // namespace IECoreMaya + +#endif // IECOREMAYA_SCENESHAPEPROXYUI_H diff --git a/src/IECoreMaya/SceneShapeProxyUI.cpp b/src/IECoreMaya/SceneShapeProxyUI.cpp new file mode 100644 index 0000000000..935348cf81 --- /dev/null +++ b/src/IECoreMaya/SceneShapeProxyUI.cpp @@ -0,0 +1,46 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "IECoreMaya/SceneShapeProxyUI.h" + +using namespace IECoreMaya; + +SceneShapeProxyUI::SceneShapeProxyUI() +{ +} + +void *SceneShapeProxyUI::creator() +{ + return new SceneShapeProxyUI; +} From b1960757cb8324725be2c25936da9bf0e95f5a4d Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Tue, 17 May 2022 15:29:55 -0700 Subject: [PATCH 03/36] IECoreMaya: Register `SceneShapeProxy` as `ieSceneShapeProxy` --- src/IECoreMaya/IECoreMaya.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/IECoreMaya/IECoreMaya.cpp b/src/IECoreMaya/IECoreMaya.cpp index 313d6be15c..2ba03f40ad 100644 --- a/src/IECoreMaya/IECoreMaya.cpp +++ b/src/IECoreMaya/IECoreMaya.cpp @@ -71,7 +71,9 @@ #include "IECoreMaya/DrawableHolder.h" #include "IECoreMaya/DrawableHolderUI.h" #include "IECoreMaya/SceneShape.h" +#include "IECoreMaya/SceneShapeProxy.h" #include "IECoreMaya/SceneShapeUI.h" +#include "IECoreMaya/SceneShapeProxyUI.h" #include "IECoreMaya/SceneShapeInterface.h" #include "IECoreMaya/SceneShapeSubSceneOverride.h" @@ -148,6 +150,12 @@ MStatus initialize(MFnPlugin &plugin) s = MHWRender::MDrawRegistry::registerSubSceneOverrideCreator( SceneShapeSubSceneOverride::drawDbClassification(), SceneShapeSubSceneOverride::drawDbId(), SceneShapeSubSceneOverride::Creator ); assert( s ); + /// Note the missing classification for the draw DB and the "missing" call to "MHWRender::MDrawRegistry::registerSubSceneOverrideCreator" + /// after registering the Shape itself. See the documentation of the SceneShapeProxy class in SceneShapeProxy.h for the reason behind this. + s = plugin.registerShape( "ieSceneShapeProxy", SceneShapeProxy::id, + SceneShapeProxy::creator, SceneShapeProxy::initialize, SceneShapeProxyUI::creator ); + assert( s ); + s = plugin.registerNode( "ieOpHolderNode", OpHolderNode::id, OpHolderNode::creator, OpHolderNode::initialize ); assert( s ); @@ -245,6 +253,7 @@ MStatus uninitialize(MFnPlugin &plugin) s = plugin.deregisterNode( ParameterisedHolderComponentShape::id ); s = plugin.deregisterNode( SceneShapeInterface::id ); s = plugin.deregisterNode( SceneShape::id ); + s = plugin.deregisterNode( SceneShapeProxy::id ); s = plugin.deregisterNode( OpHolderNode::id ); s = plugin.deregisterNode( ConverterHolder::id ); s = plugin.deregisterNode( TransientParameterisedHolderNode::id ); From 0075cec752654f208158a3fc408e41aafe76ad2d Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Wed, 24 Aug 2022 15:20:15 -0700 Subject: [PATCH 04/36] Add ieSceneShapeProxy AttributeEditorTemplate --- mel/IECoreMaya/IECoreMaya.mel | 1 + mel/IECoreMaya/ieSceneShapeProxyUI.mel | 63 ++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 mel/IECoreMaya/ieSceneShapeProxyUI.mel diff --git a/mel/IECoreMaya/IECoreMaya.mel b/mel/IECoreMaya/IECoreMaya.mel index 0f77a7c67b..87fcf1e320 100644 --- a/mel/IECoreMaya/IECoreMaya.mel +++ b/mel/IECoreMaya/IECoreMaya.mel @@ -53,3 +53,4 @@ source "IECoreMaya/ieDrawableHolderUI.mel"; source "IECoreMaya/ieGeometryCombinerUI.mel"; source "IECoreMaya/ieCurveCombinerUI.mel"; source "IECoreMaya/ieSceneShapeUI.mel"; +source "IECoreMaya/ieSceneShapeProxyUI.mel"; diff --git a/mel/IECoreMaya/ieSceneShapeProxyUI.mel b/mel/IECoreMaya/ieSceneShapeProxyUI.mel new file mode 100644 index 0000000000..4164df0eea --- /dev/null +++ b/mel/IECoreMaya/ieSceneShapeProxyUI.mel @@ -0,0 +1,63 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + + +global proc AEieSceneShapeProxyTemplate( string $nodeName ) +{ + editorTemplate -beginScrollLayout; + + editorTemplate -beginLayout "Inputs"; + editorTemplate -annotation "Path to the scene interface file." -addControl "file" ; + editorTemplate -annotation "Path in the scene interface where you start reading." -addControl "root"; + editorTemplate -annotation "If on, only read the object at the root path." -addControl "objectOnly"; + editorTemplate -addControl "time"; + + editorTemplate -endLayout; + + editorTemplate -beginLayout "Queries"; + editorTemplate -addControl "querySpace"; + editorTemplate -addControl "queryPaths"; + editorTemplate -addControl "queryAttributes"; + editorTemplate -addControl "queryConvertParameters"; + + editorTemplate -endLayout; + + editorTemplate -beginLayout "All Dynamic Attributes"; + editorTemplate -beginLayout "Open With Caution - Maya May Hang"; + editorTemplate -extraControlsLabel "Too Late Now!" -addExtraControls; + editorTemplate -endLayout; + editorTemplate -endLayout; + + editorTemplate -endScrollLayout; +} From 9a140a9c14bbd742805656c30325d5fcd4b2d300 Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Tue, 31 May 2022 16:20:18 -0700 Subject: [PATCH 05/36] SceneShape: Allow `findScene` to find `SceneShape` and `SceneShapeProxy` --- src/IECoreMaya/SceneShape.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IECoreMaya/SceneShape.cpp b/src/IECoreMaya/SceneShape.cpp index aa69e1d4fa..207552b6a7 100644 --- a/src/IECoreMaya/SceneShape.cpp +++ b/src/IECoreMaya/SceneShape.cpp @@ -190,7 +190,7 @@ SceneShape *SceneShape::findScene( const MDagPath &p, bool noIntermediate, MDagP MFnDagNode fnChildDag(childObject); MPxNode* userNode = fnChildDag.userNode(); - if( userNode && userNode->typeId() == SceneShapeId ) + if( userNode && ( userNode->typeId() == SceneShapeId || userNode->typeId() == SceneShapeProxyId ) ) { if ( noIntermediate && fnChildDag.isIntermediateObject() ) { From cd9686b4304132e38643e850de93545376cab9cd Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 13 Oct 2022 10:59:12 +0100 Subject: [PATCH 06/36] DataAlgo : Support PathMatcherData in `dispatch()` --- include/IECore/DataAlgo.inl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/include/IECore/DataAlgo.inl b/include/IECore/DataAlgo.inl index 71ea070209..ad64a51e2c 100644 --- a/include/IECore/DataAlgo.inl +++ b/include/IECore/DataAlgo.inl @@ -36,6 +36,7 @@ #define IECORE_DATAALGO_INL #include "IECore/DateTimeData.h" +#include "IECore/PathMatcherData.h" #include "IECore/SimpleTypedData.h" #include "IECore/SplineData.h" #include "IECore/TransformationMatrixData.h" @@ -193,6 +194,8 @@ typename std::invoke_result_t dispatch( Data *data, F &&fu return functor( static_cast( data ), std::forward( args )... ); case Color4fVectorDataTypeId : return functor( static_cast( data ), std::forward( args )... ); + case PathMatcherDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); default : throw InvalidArgumentException( boost::str ( boost::format( "Data has unknown type '%1%' / '%2%' " ) % typeId % data->typeName() ) ); } @@ -345,6 +348,8 @@ typename std::invoke_result_t dispatch( const Data * return functor( static_cast( data ), std::forward( args )... ); case Color4fVectorDataTypeId : return functor( static_cast( data ), std::forward( args )... ); + case PathMatcherDataTypeId : + return functor( static_cast( data ), std::forward( args )... ); default : throw InvalidArgumentException( boost::str ( boost::format( "Data has unknown type '%1%' / '%2%' " ) % typeId % data->typeName() ) ); } From 13ea9ca9cf4757ebe963cf0f1cbed2880513e32d Mon Sep 17 00:00:00 2001 From: Petru Ciobanu Date: Mon, 10 Oct 2022 22:11:47 -0700 Subject: [PATCH 07/36] allow building RV with older version of OIIO Newer version of RV (>=2022.3.0) is crashing randomly on startup. After few trial and error the combination that seems to work is to build cortex for RV by using OIIO libraries packaged with RV. That library is older enough so to not contain OpenImageIO_Util library which is assumed to exist in SConstruct configuration. These changes will allow to use that older version. There still occasional "double memory free" crushes on application close. But I could not yet figure out how to address that. Theoretically that crash is because of incompatible version of boost. However, using proper version of boost doesn't seem enough. --- SConstruct | 10 ++++++++-- config/ie/options | 4 ++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/SConstruct b/SConstruct index c61cb0c652..c920b4e8bc 100644 --- a/SConstruct +++ b/SConstruct @@ -260,6 +260,10 @@ o.Add( "", ) +o.Add( + BoolVariable( "WITH_OIIO_UTIL", "Build with OpenImageIO_Util", True ), +) + # Blosc options o.Add( @@ -1858,13 +1862,14 @@ imageEnvPrepends = { ], "LIBS" : [ "OpenImageIO$OIIO_LIB_SUFFIX", - "OpenImageIO_Util$OIIO_LIB_SUFFIX", ], "CXXFLAGS" : [ "-DIECoreImage_EXPORTS", systemIncludeArgument, "$OIIO_INCLUDE_PATH" ] } +if imageEnv.get( "WITH_OIIO_UTIL", True ): + imageEnvPrepends["LIBS"].append( "OpenImageIO_Util$OIIO_LIB_SUFFIX" ) imageEnv.Prepend( **imageEnvPrepends ) # Windows uses PATH for to find libraries, we must append to it to make sure we don't overwrite existing PATH entries. @@ -2216,11 +2221,12 @@ if env["WITH_GL"] and doConfigure : os.path.basename( imageEnv.subst( "$INSTALL_LIB_NAME" ) ), os.path.basename( sceneEnv.subst( "$INSTALL_LIB_NAME" ) ), "OpenImageIO$OIIO_LIB_SUFFIX", - "OpenImageIO_Util$OIIO_LIB_SUFFIX", "GLEW$GLEW_LIB_SUFFIX", "boost_wave$BOOST_LIB_SUFFIX", ] ) + if glEnv.get( "WITH_OIIO_UTIL", True ): + glEnv.Append( LIBS = [ "OpenImageIO_Util$OIIO_LIB_SUFFIX", ] ) if env["PLATFORM"]=="darwin" : glEnv.Append( diff --git a/config/ie/options b/config/ie/options index acd5cef34b..fc58f619c1 100644 --- a/config/ie/options +++ b/config/ie/options @@ -196,6 +196,7 @@ oiioRoot = os.path.join( "/software", "apps", "OpenImageIO", oiioVersion, platfo OIIO_INCLUDE_PATH = os.path.join( oiioRoot, "include" ) OIIO_LIB_PATH = os.path.join( oiioRoot, "lib64" ) OIIO_LIB_SUFFIX = IEEnv.BuildUtil.libSuffix( "OpenImageIO", oiioLibSuffix ) +WITH_OIIO_UTIL = "true" FREETYPE_LIB_PATH = os.path.join( "/software", "tools", "lib", platform, compiler, compilerVersion ) FREETYPE_INCLUDE_PATH = "/usr/include/freetype2" @@ -436,6 +437,9 @@ if targetApp=="rv" : # the default `OIIO_INCLUDE_PATH` value. OIIO_LIB_PATH = rvLibs OIIO_LIB_SUFFIX = rvReg.get( "OpenImageIOLibSuffix", OIIO_LIB_SUFFIX ) + # current version of OIIO used by RV doesn't include the Util library + # this variable will tell the build process to not require it + WITH_OIIO_UTIL = rvReg.get( "WithOpenImageIOUtil", WITH_OIIO_UTIL ) # find doxygen DOXYGEN = os.path.join( "/software/apps/doxygen", os.environ["DOXYGEN_VERSION"], platform, "bin", "doxygen" ) From 83fca5325f0770b2b5be6880e667702b1005ca06 Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 17 Oct 2022 18:52:34 -0400 Subject: [PATCH 08/36] Windows build : Update to dependencies `6.2.0` These dependencies include : - Binaries needed for building and running GafferCycles - Helpful graphical USD tools that have been asked for that were not included in past Windows releases. - `exrheader` which is needed for `GafferImageTest` --- .github/workflows/main.yml | 4 ++-- Changes | 8 ++++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4d4e5c40e4..ed4a9b5dc4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -70,7 +70,7 @@ jobs: os: windows-2019 buildType: RELEASE options: .github/workflows/main/options.windows - dependenciesURL: https://github.com/hypothetical-inc/gafferDependencies/releases/download/6.1.0/gafferDependencies-6.1.0-Python3-windows.zip + dependenciesURL: https://github.com/hypothetical-inc/gafferDependencies/releases/download/6.2.0/gafferDependencies-6.2.0-Python3-windows.zip tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB publish: true @@ -78,7 +78,7 @@ jobs: os: windows-2019 buildType: RELWITHDEBINFO options: .github/workflows/main/options.windows - dependenciesURL: https://github.com/hypothetical-inc/gafferDependencies/releases/download/6.1.0/gafferDependencies-6.1.0-Python3-windows.zip + dependenciesURL: https://github.com/hypothetical-inc/gafferDependencies/releases/download/6.2.0/gafferDependencies-6.2.0-Python3-windows.zip tests: testCore testCorePython testScene testImage testAlembic testUSD testVDB publish: false diff --git a/Changes b/Changes index c254e24455..4b4cb51c77 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,11 @@ +10.4.x.x (relative to 10.4.2.1) +======== + +Build +----- + +- Updated Windows dependencies to 6.2.0. + 10.4.2.1 (relative to 10.4.2.0) ======== From 2066419adddeca95b53a356fd3ec4453b6e041be Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 17 Oct 2022 18:48:34 -0400 Subject: [PATCH 09/36] SConstruct : Fix slash error on Windows --- Changes | 8 ++++++++ SConstruct | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Changes b/Changes index 9aac47ba6f..aa0c46fbe6 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,11 @@ +10.3.7.x (relative to 10.3.7.2) +======== + +Fixes +----- + +- IECoreUSD : Fixed error in `pluginfo.json` preventing USD on Windows from loading `IECoreUSD.dll`. + 10.3.7.2 (relative to 10.3.7.1) ======== diff --git a/SConstruct b/SConstruct index 0f1b73266b..03184790b1 100644 --- a/SConstruct +++ b/SConstruct @@ -3112,7 +3112,7 @@ if doConfigure : "!IECOREUSD_RELATIVE_LIB_FOLDER!" : os.path.relpath( usdLibraryInstall[0].get_path(), os.path.dirname( usdEnv.subst( "$INSTALL_USD_RESOURCE_DIR/IECoreUSD/plugInfo.json" ) ) - ).format( "\\", "\\\\" ), + ).replace( "\\", "\\\\" ), } ) usdEnv.AddPostAction( "$INSTALL_USD_RESOURCE_DIR/IECoreUSD", lambda target, source, env : makeSymLinks( usdEnv, usdEnv["INSTALL_USD_RESOURCE_DIR"] ) ) From 67807ee0f84a0b23cfa740c99b6717c996471f09 Mon Sep 17 00:00:00 2001 From: proberts Date: Fri, 23 Sep 2022 14:19:04 +0100 Subject: [PATCH 10/36] MurmurHash : Added string constructor and `fromString` function. --- Changes | 5 +++ include/IECore/MurmurHash.h | 4 +++ src/IECore/MurmurHash.cpp | 48 ++++++++++++++++++++++++-- src/IECorePython/MurmurHashBinding.cpp | 3 ++ test/IECore/MurmurHashTest.py | 33 ++++++++++++++++++ 5 files changed, 91 insertions(+), 2 deletions(-) diff --git a/Changes b/Changes index 4b4cb51c77..b2ce5a95b8 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,11 @@ 10.4.x.x (relative to 10.4.2.1) ======== +Improvements +------------ + +- Added string constructor and static `fromString` function to `IECore.MurmurHash`. + Build ----- diff --git a/include/IECore/MurmurHash.h b/include/IECore/MurmurHash.h index 55fdffff3d..91b20ea83a 100644 --- a/include/IECore/MurmurHash.h +++ b/include/IECore/MurmurHash.h @@ -61,6 +61,9 @@ class IECORE_API MurmurHash inline MurmurHash(); inline MurmurHash( const MurmurHash &other ); + // Construct directly from string representation + explicit MurmurHash( const std::string &repr ); + // Construct directly from known internal values inline MurmurHash( uint64_t h1, uint64_t h2 ); @@ -84,6 +87,7 @@ class IECORE_API MurmurHash inline bool operator < ( const MurmurHash &other ) const; std::string toString() const; + static MurmurHash fromString( const std::string &repr ); // Access internal storage for special cases inline uint64_t h1() const; diff --git a/src/IECore/MurmurHash.cpp b/src/IECore/MurmurHash.cpp index 21bda502cc..80dd523309 100644 --- a/src/IECore/MurmurHash.cpp +++ b/src/IECore/MurmurHash.cpp @@ -33,19 +33,63 @@ ////////////////////////////////////////////////////////////////////////// #include "IECore/MurmurHash.h" +#include "IECore/Exception.h" + +#include #include #include using namespace IECore; -std::string MurmurHash::toString() const +namespace +{ + +std::string internalToString( uint64_t const h1, uint64_t const h2 ) { std::stringstream s; - s << std::hex << std::setfill( '0' ) << std::setw( 16 ) << m_h1 << std::setw( 16 ) << m_h2; + s << std::hex << std::setfill( '0' ) << std::setw( 16 ) << h1 << std::setw( 16 ) << h2; return s.str(); } +void internalFromString( const std::string &repr, uint64_t &h1, uint64_t &h2 ) +{ + if( repr.length() != static_cast( 32 ) ) + { + throw Exception( + boost::str( + boost::format( + "Invalid IECore::MurmurHash string representation \"%s\", must have 32 characters" ) + % repr + ) ); + } + + std::stringstream s; + s.str( repr.substr( 0, 16 ) ); + s >> std::hex >> h1; + s.clear(); + s.str( repr.substr( 16, 16 ) ); + s >> std::hex >> h2; +} + +} // namespace + +MurmurHash::MurmurHash( const std::string &repr ) + : m_h1( 0 ), m_h2( 0 ) +{ + internalFromString( repr, m_h1, m_h2 ); +} + +std::string MurmurHash::toString() const +{ + return internalToString( m_h1, m_h2 ); +} + +MurmurHash MurmurHash::fromString( const std::string &repr ) +{ + return MurmurHash( repr ); +} + std::ostream &IECore::operator << ( std::ostream &o, const MurmurHash &hash ) { o << hash.toString(); diff --git a/src/IECorePython/MurmurHashBinding.cpp b/src/IECorePython/MurmurHashBinding.cpp index e12738ae39..649317be44 100644 --- a/src/IECorePython/MurmurHashBinding.cpp +++ b/src/IECorePython/MurmurHashBinding.cpp @@ -128,6 +128,7 @@ void IECorePython::bindMurmurHash() class_( "MurmurHash" ) .def( init<>() ) .def( init() ) + .def( init() ) .def( init() ) .def( "append", (MurmurHash &(MurmurHash::*)( const float & ))&MurmurHash::append, return_self<>() ) .def( "append", (MurmurHash &(MurmurHash::*)( const double & ))&MurmurHash::append, return_self<>() ) @@ -198,6 +199,8 @@ void IECorePython::bindMurmurHash() .def( "__str__", &MurmurHash::toString ) .def( "__hash__", &hash ) .def( "toString", &MurmurHash::toString ) + .def( "fromString", (MurmurHash (*)( const std::string & ))&MurmurHash::fromString ) + .staticmethod( "fromString" ) .def( "h1", &MurmurHash::h1 ) .def( "h2", &MurmurHash::h2 ) ; diff --git a/test/IECore/MurmurHashTest.py b/test/IECore/MurmurHashTest.py index f81c69da56..907a870885 100644 --- a/test/IECore/MurmurHashTest.py +++ b/test/IECore/MurmurHashTest.py @@ -34,6 +34,7 @@ import unittest import imath +import six import IECore @@ -45,6 +46,16 @@ def testConstructor( self ) : self.assertEqual( str( h ), "0" * 32 ) self.assertEqual( h, IECore.MurmurHash() ) + def testStringConstructor( self ) : + + h = IECore.MurmurHash() + h.append( 1 ) + h.append( "hello" ) + + s = h.toString() + hs = IECore.MurmurHash( s ) + self.assertEqual( h, hs ) + def testCopyConstructor( self ) : h = IECore.MurmurHash() @@ -54,6 +65,28 @@ def testCopyConstructor( self ) : self.assertEqual( h, IECore.MurmurHash( h ) ) self.assertNotEqual( h, IECore.MurmurHash() ) + def testRepr( self ) : + + h = IECore.MurmurHash() + h.append( 42 ) + h.append( "world" ) + h2 = eval( repr( h ) ) + + self.assertEqual( h, h2 ) + + def testFromString( self ) : + + h = IECore.MurmurHash() + h.append( 1 ) + h.append( "hello" ) + + s = h.toString() + hs = IECore.MurmurHash.fromString( s ) + self.assertEqual( h, hs ) + + with six.assertRaisesRegex( self, Exception, ".*must have 32 characters.*" ) : + IECore.MurmurHash.fromString( "InvalidStringRepresentation" ) + def testAppend( self ) : h = IECore.MurmurHash() From de2eccf432e8a256920a851cd1ff064942422bc5 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Thu, 27 Oct 2022 15:41:13 +0100 Subject: [PATCH 11/36] USDScene : Improve responsiveness of `readSet()` cancellation --- contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp index ce6139134f..f0c502e4d5 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp @@ -269,11 +269,15 @@ IECore::PathMatcher readSetInternal( const pxr::UsdPrim &prim, const pxr::TfToke const size_t prefixSize = prim.GetPath().GetPathElementCount(); if( auto collection = pxr::UsdCollectionAPI( prim, name ) ) { + Canceller::check( canceller ); pxr::UsdCollectionAPI::MembershipQuery membershipQuery = collection.ComputeMembershipQuery(); + + Canceller::check( canceller ); pxr::SdfPathSet includedPaths = collection.ComputeIncludedPaths( membershipQuery, prim.GetStage() ); for( const auto &path : includedPaths ) { + Canceller::check( canceller ); if( path.HasPrefix( prim.GetPath() ) ) { result.addPath( fromUSDWithoutPrefix( path, prefixSize ) ); From bd1be46e02d3c4d181a8c68e7b5fd2872eb1f21b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 31 Oct 2022 19:46:04 +1100 Subject: [PATCH 12/36] IECoreScene : MeshPrimitiveEvaluator assert typo fix --- src/IECoreScene/MeshPrimitiveEvaluator.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/IECoreScene/MeshPrimitiveEvaluator.cpp b/src/IECoreScene/MeshPrimitiveEvaluator.cpp index 7e90e45ab7..36a4a80df3 100644 --- a/src/IECoreScene/MeshPrimitiveEvaluator.cpp +++ b/src/IECoreScene/MeshPrimitiveEvaluator.cpp @@ -724,7 +724,7 @@ bool MeshPrimitiveEvaluator::signedDistance( const Imath::V3f &p, float &distanc else { assert( region == 6 ); - assert( closestVertex = 1 ); + assert( closestVertex == 1 ); } assert( triangleVertexIds[ closestVertex ] < (int)(m_vertexAngleWeightedNormals->readable().size()) ); From ab584f25df0cf572db22347112181236cce1c734 Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Tue, 1 Nov 2022 14:23:40 -0700 Subject: [PATCH 13/36] Changes : Updated to describe #1303 --- Changes | 1 + 1 file changed, 1 insertion(+) diff --git a/Changes b/Changes index b2ce5a95b8..2e4235a09c 100644 --- a/Changes +++ b/Changes @@ -10,6 +10,7 @@ Build ----- - Updated Windows dependencies to 6.2.0. +- Added WITH_OIIO_UTIL option. 10.4.2.1 (relative to 10.4.2.0) ======== From 2911ec633519da3c607480031527766288255458 Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Wed, 2 Nov 2022 14:18:59 -0700 Subject: [PATCH 14/36] Maya Python3 support: Backport changes from RB-10.4 We backport the Python3 compatibility changes for `FnSceneShape.py` and `__init__.py from the following commits: - 58d250fd58acdf6aac0942e5abf1ee8717bb673b - 8f7f614f5c4d65e2ce6349e3aa332363b29f983f This is to avoid merge conflicts later on --- python/IECoreMaya/FnSceneShape.py | 12 +-- python/IECoreMaya/__init__.py | 130 +++++++++++++++--------------- 2 files changed, 72 insertions(+), 70 deletions(-) diff --git a/python/IECoreMaya/FnSceneShape.py b/python/IECoreMaya/FnSceneShape.py index 9136e60b23..9fa84b6e5c 100644 --- a/python/IECoreMaya/FnSceneShape.py +++ b/python/IECoreMaya/FnSceneShape.py @@ -42,7 +42,9 @@ import IECore import IECoreScene import IECoreMaya -import StringUtil +from . import StringUtil +import six +from six.moves import range ## A function set for operating on the IECoreMaya::SceneShape type. @@ -73,7 +75,7 @@ class FnSceneShape( maya.OpenMaya.MFnDagNode ) : # either be an MObject or a node name in string or unicode form. # Note: Most of the member functions assume that this function set is initialized with the full dag path. def __init__( self, object ) : - if isinstance( object, basestring ) : + if isinstance( object, six.string_types ) : object = StringUtil.dagPathFromString( object ) maya.OpenMaya.MFnDagNode.__init__( self, object ) @@ -95,7 +97,7 @@ def create( parentName, transformParent = None, shadingEngine = None ) : @IECoreMaya.UndoFlush() def createShape( parentNode, shadingEngine = None ) : parentShort = parentNode.rpartition( "|" )[-1] - numbersMatch = re.search( "[0-9]+$", parentShort ) + numbersMatch = re.search( r"[0-9]+$", parentShort ) if numbersMatch is not None : numbers = numbersMatch.group() shapeName = parentShort[:-len(numbers)] + "SceneShape" + numbers @@ -163,7 +165,7 @@ def selectedComponentNames( self ) : ## Selects the components specified by the passed names. def selectComponentNames( self, componentNames ) : if not isinstance( componentNames, set ) : - if isinstance( componentNames, basestring ): + if isinstance( componentNames, six.string_types ): componentNames = set( (componentNames, ) ) else: componentNames = set( componentNames ) @@ -284,7 +286,7 @@ def __createChild( self, childName, sceneFile, sceneRoot, drawGeo = False, drawC # Set visible if I have any of the draw flags in my hierarchy, otherwise set hidden if drawTagsFilter: childTags = fnChild.sceneInterface().readTags( IECoreScene.SceneInterface.EveryTag ) - commonTags = filter( lambda x: str(x) in childTags, drawTagsFilter.split() ) + commonTags = [x for x in drawTagsFilter.split() if str(x) in childTags] if not commonTags: dgMod.newPlugValueBool( fnChildTransform.findPlug( "visibility" ), False ) else: diff --git a/python/IECoreMaya/__init__.py b/python/IECoreMaya/__init__.py index 7372b97d8a..a82438552d 100644 --- a/python/IECoreMaya/__init__.py +++ b/python/IECoreMaya/__init__.py @@ -34,72 +34,72 @@ __import__( "IECoreScene" ) -from _IECoreMaya import * +from ._IECoreMaya import * -from UIElement import UIElement -from ParameterUI import ParameterUI -from BoolParameterUI import BoolParameterUI -from StringParameterUI import StringParameterUI -from PathParameterUI import PathParameterUI -from FileNameParameterUI import FileNameParameterUI -from DirNameParameterUI import DirNameParameterUI -from FileSequenceParameterUI import FileSequenceParameterUI -from NumericParameterUI import NumericParameterUI -from VectorParameterUI import VectorParameterUI -from ColorParameterUI import ColorParameterUI -from BoxParameterUI import BoxParameterUI -from SplineParameterUI import SplineParameterUI -from NoteParameterUI import NoteParameterUI -from NodeParameter import NodeParameter -from DAGPathParameter import DAGPathParameter -from DAGPathVectorParameter import DAGPathVectorParameter -from mayaDo import mayaDo -from Menu import Menu -from BakeTransform import BakeTransform -from MeshOpHolderUtil import create -from MeshOpHolderUtil import createUI -from ScopedSelection import ScopedSelection -from FnParameterisedHolder import FnParameterisedHolder -from FnConverterHolder import FnConverterHolder -from StringUtil import * -from MayaTypeId import MayaTypeId -from ParameterPanel import ParameterPanel -from AttributeEditorControl import AttributeEditorControl -from OpWindow import OpWindow -from FnTransientParameterisedHolderNode import FnTransientParameterisedHolderNode -from UndoDisabled import UndoDisabled -from ModalDialogue import ModalDialogue -from Panel import Panel -from WaitCursor import WaitCursor -from FnOpHolder import FnOpHolder -from UITemplate import UITemplate -from FnParameterisedHolderSet import FnParameterisedHolderSet -from TemporaryAttributeValues import TemporaryAttributeValues -from GenericParameterUI import GenericParameterUI -from FnDagNode import FnDagNode -from CompoundParameterUI import CompoundParameterUI -from ClassParameterUI import ClassParameterUI -from ClassVectorParameterUI import ClassVectorParameterUI -from PresetsOnlyParameterUI import PresetsOnlyParameterUI -from TestCase import TestCase -from TestProgram import TestProgram -from FileBrowser import FileBrowser -from FileDialog import FileDialog -from GeometryCombinerUI import * -from PresetsUI import * -from ParameterClipboardUI import * -from NumericVectorParameterUI import NumericVectorParameterUI -from StringVectorParameterUI import StringVectorParameterUI -from ManipulatorUI import * -from TransformationMatrixParameterUI import TransformationMatrixParameterUI -from LineSegmentParameterUI import LineSegmentParameterUI -from Collapsible import Collapsible -from RefreshDisabled import RefreshDisabled -from UndoChunk import UndoChunk -from UndoFlush import UndoFlush +from .UIElement import UIElement +from .ParameterUI import ParameterUI +from .BoolParameterUI import BoolParameterUI +from .StringParameterUI import StringParameterUI +from .PathParameterUI import PathParameterUI +from .FileNameParameterUI import FileNameParameterUI +from .DirNameParameterUI import DirNameParameterUI +from .FileSequenceParameterUI import FileSequenceParameterUI +from .NumericParameterUI import NumericParameterUI +from .VectorParameterUI import VectorParameterUI +from .ColorParameterUI import ColorParameterUI +from .BoxParameterUI import BoxParameterUI +from .SplineParameterUI import SplineParameterUI +from .NoteParameterUI import NoteParameterUI +from .NodeParameter import NodeParameter +from .DAGPathParameter import DAGPathParameter +from .DAGPathVectorParameter import DAGPathVectorParameter +from .mayaDo import mayaDo +from .Menu import Menu +from .BakeTransform import BakeTransform +from .MeshOpHolderUtil import create +from .MeshOpHolderUtil import createUI +from .ScopedSelection import ScopedSelection +from .FnParameterisedHolder import FnParameterisedHolder +from .FnConverterHolder import FnConverterHolder +from .StringUtil import * +from .MayaTypeId import MayaTypeId +from .ParameterPanel import ParameterPanel +from .AttributeEditorControl import AttributeEditorControl +from .OpWindow import OpWindow +from .FnTransientParameterisedHolderNode import FnTransientParameterisedHolderNode +from .UndoDisabled import UndoDisabled +from .ModalDialogue import ModalDialogue +from .Panel import Panel +from .WaitCursor import WaitCursor +from .FnOpHolder import FnOpHolder +from .UITemplate import UITemplate +from .FnParameterisedHolderSet import FnParameterisedHolderSet +from .TemporaryAttributeValues import TemporaryAttributeValues +from .GenericParameterUI import GenericParameterUI +from .FnDagNode import FnDagNode +from .CompoundParameterUI import CompoundParameterUI +from .ClassParameterUI import ClassParameterUI +from .ClassVectorParameterUI import ClassVectorParameterUI +from .PresetsOnlyParameterUI import PresetsOnlyParameterUI +from .TestCase import TestCase +from .TestProgram import TestProgram +from .FileBrowser import FileBrowser +from .FileDialog import FileDialog +from .GeometryCombinerUI import * +from .PresetsUI import * +from .ParameterClipboardUI import * +from .NumericVectorParameterUI import NumericVectorParameterUI +from .StringVectorParameterUI import StringVectorParameterUI +from .ManipulatorUI import * +from .TransformationMatrixParameterUI import TransformationMatrixParameterUI +from .LineSegmentParameterUI import LineSegmentParameterUI +from .Collapsible import Collapsible +from .RefreshDisabled import RefreshDisabled +from .UndoChunk import UndoChunk +from .UndoFlush import UndoFlush -import Menus -import SceneShapeUI -from FnSceneShape import FnSceneShape +from . import Menus +from . import SceneShapeUI +from .FnSceneShape import FnSceneShape __import__( "IECore" ).loadConfig( "CORTEX_STARTUP_PATHS", subdirectory = "IECoreMaya" ) From 186a357d1c26f0ff401262a6b8f6b9f605fc353c Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Tue, 10 May 2022 10:38:53 -0700 Subject: [PATCH 15/36] FnSceneShape: Change `staticmethod`s to `classmethods`s Changing all `staticmethod`s to `classmethod`s and replacing all method calls from within the class to use either `self` or `cls` instead of the class name. This allows for easier inheritance and less code duplication in subclasses --- python/IECoreMaya/FnSceneShape.py | 44 +++++++++++++++---------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/python/IECoreMaya/FnSceneShape.py b/python/IECoreMaya/FnSceneShape.py index 9fa84b6e5c..3dea120694 100644 --- a/python/IECoreMaya/FnSceneShape.py +++ b/python/IECoreMaya/FnSceneShape.py @@ -81,21 +81,21 @@ def __init__( self, object ) : maya.OpenMaya.MFnDagNode.__init__( self, object ) ## Creates a new node under a transform of the specified name. Returns a function set instance operating on this new node. - @staticmethod + @classmethod @IECoreMaya.UndoFlush() - def create( parentName, transformParent = None, shadingEngine = None ) : + def create( cls, parentName, transformParent = None, shadingEngine = None ) : try: parentNode = maya.cmds.createNode( "transform", name=parentName, skipSelect=True, parent = transformParent ) except: # The parent name is supposed to be the children names in a sceneInterface, they could be numbers, maya doesn't like that. Use a prefix. parentNode = maya.cmds.createNode( "transform", name="sceneShape_"+parentName, skipSelect=True, parent = transformParent ) - return FnSceneShape.createShape( parentNode, shadingEngine=shadingEngine ) + return cls.createShape( parentNode, shadingEngine=shadingEngine ) ## Create a scene shape under the given node. Returns a function set instance operating on this shape. - @staticmethod + @classmethod @IECoreMaya.UndoFlush() - def createShape( parentNode, shadingEngine = None ) : + def createShape( cls, parentNode, shadingEngine = None ) : parentShort = parentNode.rpartition( "|" )[-1] numbersMatch = re.search( r"[0-9]+$", parentShort ) if numbersMatch is not None : @@ -105,11 +105,11 @@ def createShape( parentNode, shadingEngine = None ) : shapeName = parentShort + "SceneShape" dagMod = maya.OpenMaya.MDagModifier() - shapeNode = dagMod.createNode( FnSceneShape._mayaNodeType(), IECoreMaya.StringUtil.dependencyNodeFromString( parentNode ) ) + shapeNode = dagMod.createNode( cls._mayaNodeType(), IECoreMaya.StringUtil.dependencyNodeFromString( parentNode ) ) dagMod.renameNode( shapeNode, shapeName ) dagMod.doIt() - fnScS = FnSceneShape( shapeNode ) + fnScS = cls( shapeNode ) maya.cmds.sets( fnScS.fullPathName(), edit=True, forceElement=shadingEngine or "initialShadingGroup" ) fnScS.findPlug( "objectOnly" ).setLocked( True ) @@ -123,13 +123,13 @@ def createShape( parentNode, shadingEngine = None ) : ## Registers a new callback triggered when a new child shape is created during geometry expansion. # Signature: `callback( dagPath )` - @staticmethod - def addChildCreatedCallback( func ): - FnSceneShape.__childCreatedCallbacks.add( func ) + @classmethod + def addChildCreatedCallback( cls, func ): + cls.__childCreatedCallbacks.add( func ) - @staticmethod - def __executeChildCreatedCallbacks( dagPath ): - for callback in FnSceneShape.__childCreatedCallbacks: + @classmethod + def __executeChildCreatedCallbacks( cls, dagPath ): + for callback in cls.__childCreatedCallbacks: try: callback( dagPath ) except Exception as exc: @@ -266,11 +266,11 @@ def __createChild( self, childName, sceneFile, sceneRoot, drawGeo = False, drawC if maya.cmds.objExists(childPath): shape = maya.cmds.listRelatives( childPath, fullPath=True, type="ieSceneShape" ) if shape: - fnChild = IECoreMaya.FnSceneShape( shape[0] ) + fnChild = self.__class__( shape[0] ) else: - fnChild = IECoreMaya.FnSceneShape.createShape( childPath, shadingEngine=shadingGroup ) + fnChild = self.createShape( childPath, shadingEngine=shadingGroup ) else: - fnChild = IECoreMaya.FnSceneShape.create( childName, transformParent=parentPath, shadingEngine=shadingGroup ) + fnChild = self.create( childName, transformParent=parentPath, shadingEngine=shadingGroup ) fnChildTransform = maya.OpenMaya.MFnDagNode( fnChild.parent( 0 ) ) @@ -298,24 +298,24 @@ def __createChild( self, childName, sceneFile, sceneRoot, drawGeo = False, drawC outTransform = self.findPlug( "outTransform" ).elementByLogicalIndex( index ) childTranslate = fnChildTransform.findPlug( "translate" ) - FnSceneShape.__disconnectPlug( dgMod, childTranslate ) + self.__disconnectPlug( dgMod, childTranslate ) dgMod.connect( outTransform.child( self.attribute( "outTranslate" ) ), childTranslate ) childRotate = fnChildTransform.findPlug( "rotate" ) - FnSceneShape.__disconnectPlug( dgMod, childRotate) + self.__disconnectPlug( dgMod, childRotate) dgMod.connect( outTransform.child( self.attribute( "outRotate" ) ), childRotate ) childScale = fnChildTransform.findPlug( "scale" ) - FnSceneShape.__disconnectPlug( dgMod, childScale ) + self.__disconnectPlug( dgMod, childScale ) dgMod.connect( outTransform.child( self.attribute( "outScale" ) ), childScale ) childTime = fnChild.findPlug( "time" ) - FnSceneShape.__disconnectPlug( dgMod, childTime ) + self.__disconnectPlug( dgMod, childTime ) dgMod.connect( self.findPlug( "outTime" ), childTime ) dgMod.doIt() - FnSceneShape.__executeChildCreatedCallbacks( fnChild.fullPathName() ) + self.__executeChildCreatedCallbacks( fnChild.fullPathName() ) return fnChild @@ -691,7 +691,7 @@ def __cortexTypeToMayaType( self, querySceneInterface, attributeName ): timePlug = self.findPlug( 'time', False ) time = timePlug.asMTime().asUnits( maya.OpenMaya.MTime.kSeconds ) cortexData = querySceneInterface.readAttribute( attributeName, time ) - return FnSceneShape.__cortexToMayaDataTypeMap.get( cortexData.typeId() ) + return self.__cortexToMayaDataTypeMap.get( cortexData.typeId() ) ## Returns a list of attribute names which can be promoted to maya plugs. # \param queryPath Path to the scene from which we want return attribute names. Defaults to root '/' From 25f0c6346c7f5475e59b6e7468667cee05ba3a7d Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Mon, 30 May 2022 14:22:50 -0700 Subject: [PATCH 16/36] FnSceneShape: Add `_FnSceneShapeProxy` class --- python/IECoreMaya/FnSceneShape.py | 12 ++++++++++++ python/IECoreMaya/__init__.py | 1 + 2 files changed, 13 insertions(+) diff --git a/python/IECoreMaya/FnSceneShape.py b/python/IECoreMaya/FnSceneShape.py index 3dea120694..3580d2e635 100644 --- a/python/IECoreMaya/FnSceneShape.py +++ b/python/IECoreMaya/FnSceneShape.py @@ -793,3 +793,15 @@ def promoteAttribute( self, attributeName, queryPath='/', nodePath='', mayaAttri @classmethod def _mayaNodeType( cls ): return "ieSceneShape" + + +# A derived function set for operating on the IECoreMaya::SceneShapeProxy type. +# It inherits all functionality from the base clase and we only override the `_mayaNodeType` method. +# This object is used in the __new__ method of FnSceneShape to create the correct object depending +# on the passed in node type +class _FnSceneShapeProxy( FnSceneShape ) : + + # Returns the maya node type that this function set operates on + @classmethod + def _mayaNodeType( cls ): + return "ieSceneShapeProxy" diff --git a/python/IECoreMaya/__init__.py b/python/IECoreMaya/__init__.py index a82438552d..c5f39ab6fc 100644 --- a/python/IECoreMaya/__init__.py +++ b/python/IECoreMaya/__init__.py @@ -101,5 +101,6 @@ from . import Menus from . import SceneShapeUI from .FnSceneShape import FnSceneShape +from .FnSceneShape import _FnSceneShapeProxy __import__( "IECore" ).loadConfig( "CORTEX_STARTUP_PATHS", subdirectory = "IECoreMaya" ) From 08f4d30b7cbad2096e532791d3348a7590aab8d6 Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Mon, 30 May 2022 14:20:24 -0700 Subject: [PATCH 17/36] FnSceneShape: Instantiate object based on passed node type With overriding the `__new__` method of the `FnSceneShape` class we change the object type during construction and before initialization. This gives us the ability to use `FnSceneShape` regardless if it's dealing with a `SceneShape` or it's proxy version. --- python/IECoreMaya/FnSceneShape.py | 32 +++++++++++++++++++++++------ test/IECoreMaya/FnSceneShapeTest.py | 10 +++++++++ 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/python/IECoreMaya/FnSceneShape.py b/python/IECoreMaya/FnSceneShape.py index 3580d2e635..ccc785ff9c 100644 --- a/python/IECoreMaya/FnSceneShape.py +++ b/python/IECoreMaya/FnSceneShape.py @@ -47,7 +47,7 @@ from six.moves import range -## A function set for operating on the IECoreMaya::SceneShape type. +## A function set for operating on the IECoreMaya::SceneShape and IECoreMaya::SceneShapeProxy types. class FnSceneShape( maya.OpenMaya.MFnDagNode ) : __MayaAttributeDataType = namedtuple('__MayaAttributeDataType', 'namespace type') @@ -74,11 +74,31 @@ class FnSceneShape( maya.OpenMaya.MFnDagNode ) : ## Initialise the function set for the given procedural object, which may # either be an MObject or a node name in string or unicode form. # Note: Most of the member functions assume that this function set is initialized with the full dag path. - def __init__( self, object ) : - if isinstance( object, six.string_types ) : - object = StringUtil.dagPathFromString( object ) - - maya.OpenMaya.MFnDagNode.__init__( self, object ) + def __init__( self, mayaObject ) : + + if isinstance( mayaObject, six.string_types ) : + mayaObject = StringUtil.dagPathFromString( mayaObject ) + maya.OpenMaya.MFnDagNode.__init__( self, mayaObject ) + + # We use pythons metaprogramming capabilities and dynamically change the object that is created based + # on the node type type that is passed in. We do this so that we can use FnSceneShape for two different + # node types, i.e. SceneShape and its derived proxy class SceneShapeProxy, without having to change huge + # parts of the codebase. SceneShape and SceneShapeProxy both provide the same core functionality of reading + # a SceneInterface, with the difference that SceneShapeProxy can't be drawn in the ViewPort and therefore + # drawing/selecting related methods in those class have no effect. See SceneShapeProxy.h for more information. + def __new__( cls, mayaObject ): + + if isinstance( mayaObject, six.string_types ) : + mayaObject = StringUtil.dagPathFromString( mayaObject ) + else: + dagPath = maya.OpenMaya.MDagPath() + maya.OpenMaya.MDagPath.getAPathTo(mayaObject, dagPath) + mayaObject = dagPath + + if maya.cmds.nodeType(mayaObject.fullPathName()) == "ieSceneShape": + return object.__new__(FnSceneShape) + if maya.cmds.nodeType(mayaObject.fullPathName()) == "ieSceneShapeProxy": + return object.__new__(_FnSceneShapeProxy) ## Creates a new node under a transform of the specified name. Returns a function set instance operating on this new node. @classmethod diff --git a/test/IECoreMaya/FnSceneShapeTest.py b/test/IECoreMaya/FnSceneShapeTest.py index bad6e48725..8f78474864 100644 --- a/test/IECoreMaya/FnSceneShapeTest.py +++ b/test/IECoreMaya/FnSceneShapeTest.py @@ -113,6 +113,16 @@ def __setupTableProp( self ): mat = IECore.TransformationMatrixd( s, r, t ) tableTop_GEO.writeTransform( IECore.TransformationMatrixdData(mat), 0 ) + def testClassTypeInstantiation( self ): + + sceneShapeNode = maya.cmds.createNode( "ieSceneShape" ) + sceneShapeFn = IECoreMaya.FnSceneShape( sceneShapeNode ) + self.assertEqual( sceneShapeFn.__class__, IECoreMaya.FnSceneShape ) + + sceneShapeProxyNode = maya.cmds.createNode( "ieSceneShapeProxy" ) + sceneShapeProxyFn = IECoreMaya.FnSceneShape( sceneShapeProxyNode ) + self.assertEqual( sceneShapeProxyFn.__class__, IECoreMaya._FnSceneShapeProxy ) + def testSceneInterface( self ) : maya.cmds.file( new=True, f=True ) From 03afa0fe1c896ffcffe2fe7e83e132b33d8ca933 Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Wed, 1 Jun 2022 17:03:14 -0700 Subject: [PATCH 18/36] FnSceneShape: Add `shapeType` argument to create methods We add the `shapeType` argument to the methods that can create a `SceneShape` node to specify if we want to create an `ieSceneShape` or its proxy version. If we don't pass in the argument `FnSceneShape` will create shapes of the type it was initially created with. --- python/IECoreMaya/FnSceneShape.py | 8 ++++---- test/IECoreMaya/FnSceneShapeTest.py | 16 ++++++++++++++++ 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/python/IECoreMaya/FnSceneShape.py b/python/IECoreMaya/FnSceneShape.py index ccc785ff9c..6057f4922f 100644 --- a/python/IECoreMaya/FnSceneShape.py +++ b/python/IECoreMaya/FnSceneShape.py @@ -103,19 +103,19 @@ def __new__( cls, mayaObject ): ## Creates a new node under a transform of the specified name. Returns a function set instance operating on this new node. @classmethod @IECoreMaya.UndoFlush() - def create( cls, parentName, transformParent = None, shadingEngine = None ) : + def create( cls, parentName, transformParent = None, shadingEngine = None, shapeType=None ) : try: parentNode = maya.cmds.createNode( "transform", name=parentName, skipSelect=True, parent = transformParent ) except: # The parent name is supposed to be the children names in a sceneInterface, they could be numbers, maya doesn't like that. Use a prefix. parentNode = maya.cmds.createNode( "transform", name="sceneShape_"+parentName, skipSelect=True, parent = transformParent ) - return cls.createShape( parentNode, shadingEngine=shadingEngine ) + return cls.createShape( parentNode, shadingEngine=shadingEngine, shapeType=shapeType ) ## Create a scene shape under the given node. Returns a function set instance operating on this shape. @classmethod @IECoreMaya.UndoFlush() - def createShape( cls, parentNode, shadingEngine = None ) : + def createShape( cls, parentNode, shadingEngine = None, shapeType=None ) : parentShort = parentNode.rpartition( "|" )[-1] numbersMatch = re.search( r"[0-9]+$", parentShort ) if numbersMatch is not None : @@ -125,7 +125,7 @@ def createShape( cls, parentNode, shadingEngine = None ) : shapeName = parentShort + "SceneShape" dagMod = maya.OpenMaya.MDagModifier() - shapeNode = dagMod.createNode( cls._mayaNodeType(), IECoreMaya.StringUtil.dependencyNodeFromString( parentNode ) ) + shapeNode = dagMod.createNode( shapeType if shapeType else cls._mayaNodeType(), IECoreMaya.StringUtil.dependencyNodeFromString( parentNode ) ) dagMod.renameNode( shapeNode, shapeName ) dagMod.doIt() diff --git a/test/IECoreMaya/FnSceneShapeTest.py b/test/IECoreMaya/FnSceneShapeTest.py index 8f78474864..a91332a73b 100644 --- a/test/IECoreMaya/FnSceneShapeTest.py +++ b/test/IECoreMaya/FnSceneShapeTest.py @@ -123,6 +123,22 @@ def testClassTypeInstantiation( self ): sceneShapeProxyFn = IECoreMaya.FnSceneShape( sceneShapeProxyNode ) self.assertEqual( sceneShapeProxyFn.__class__, IECoreMaya._FnSceneShapeProxy ) + def testCreateShapeType( self ): + + sceneShapeTransform = maya.cmds.createNode( "transform" ) + sceneShapeFn = IECoreMaya.FnSceneShape.createShape( sceneShapeTransform, shapeType="ieSceneShape") + self.assertEqual( sceneShapeFn.__class__, IECoreMaya.FnSceneShape ) + + sceneShapeNode = maya.cmds.listRelatives( sceneShapeTransform, shapes=True) + self.assertTrue( maya.cmds.objectType( sceneShapeNode, isType="ieSceneShape" )) + + sceneShapeProxyTransform = maya.cmds.createNode( "transform" ) + sceneShapeProxyFn = IECoreMaya.FnSceneShape.createShape( sceneShapeProxyTransform, shapeType="ieSceneShapeProxy" ) + self.assertEqual( sceneShapeProxyFn.__class__, IECoreMaya._FnSceneShapeProxy ) + + sceneShapeProxyNode = maya.cmds.listRelatives( sceneShapeProxyTransform, shapes=True) + self.assertTrue( maya.cmds.objectType( sceneShapeProxyNode, isType="ieSceneShapeProxy" )) + def testSceneInterface( self ) : maya.cmds.file( new=True, f=True ) From 248464a5880b31dde2a09099745d6fdcd035642f Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Tue, 27 Sep 2022 14:45:16 -0700 Subject: [PATCH 19/36] SceneShapeTest: Create maya node in `setUp` method Moving the node creation in the `setUp` method, which is called before every test method, gives subclasses the ability to run the same test cases with a different node. This is e.g. useful for `ieSceneShapeProxy` as we need to make sure that it behaves the same way as it's non proxy version --- test/IECoreMaya/SceneShapeTest.py | 32 +++++++++++-------------------- 1 file changed, 11 insertions(+), 21 deletions(-) diff --git a/test/IECoreMaya/SceneShapeTest.py b/test/IECoreMaya/SceneShapeTest.py index aa66d6841e..79864ef68a 100644 --- a/test/IECoreMaya/SceneShapeTest.py +++ b/test/IECoreMaya/SceneShapeTest.py @@ -48,9 +48,9 @@ class SceneShapeTest( IECoreMaya.TestCase ) : __testPlugAnimFile = "test/testPlugAnim.scc" __testPlugAttrFile = "test/testPlugAttr.scc" - def setUp( self ) : - - maya.cmds.file( new=True, f=True ) + def setUp( self ): + super( SceneShapeTest, self ).setUp() + self._node = maya.cmds.createNode( "ieSceneShape" ) def writeSCC( self, file, rotation=imath.V3d( 0, 0, 0 ), time=0 ) : @@ -136,8 +136,7 @@ def testComputePlugs( self ) : self.writeSCC( file = SceneShapeTest.__testFile ) - maya.cmds.file( new=True, f=True ) - node = maya.cmds.createNode( 'ieSceneShape' ) + node = self._node maya.cmds.setAttr( node+'.file', SceneShapeTest.__testFile,type='string' ) maya.cmds.setAttr( node+'.root',"/",type='string' ) @@ -179,13 +178,11 @@ def testComputePlugs( self ) : self.assertEqual( maya.cmds.getAttr( node+".outBound", mi=True ), [1]) self.assertEqual( maya.cmds.getAttr( node+".outObjects", mi=True ), [2]) - def testPlugValues( self ) : self.writeSCC( file=SceneShapeTest.__testPlugFile, rotation = imath.V3d( 0, 0, IECore.degreesToRadians( -30 ) ) ) - maya.cmds.file( new=True, f=True ) - node = maya.cmds.createNode( 'ieSceneShape' ) + node = self._node maya.cmds.setAttr( node+'.file', SceneShapeTest.__testPlugFile,type='string' ) maya.cmds.setAttr( node+'.root',"/",type='string' ) @@ -291,13 +288,11 @@ def testPlugValues( self ) : maya.cmds.setAttr( node+'.time', 5 ) self.assertEqual( maya.cmds.getAttr( node+".outTime" ), 5 ) - def testAnimPlugValues( self ) : self.writeAnimSCC( file=SceneShapeTest.__testPlugAnimFile ) - maya.cmds.file( new=True, f=True ) - node = maya.cmds.createNode( 'ieSceneShape' ) + node = self._node maya.cmds.connectAttr( "time1.outTime", node+".time" ) maya.cmds.setAttr( node+'.file', SceneShapeTest.__testPlugAnimFile,type='string' ) @@ -413,13 +408,11 @@ def testAnimPlugValues( self ) : self.assertAlmostEqual( maya.cmds.getAttr( node+".outTransform[2].outRotateZ"), 0.0 ) self.assertEqual( maya.cmds.getAttr( node+".outTransform[2].outScale"), [(1.0, 1.0, 1.0)] ) - - def testqueryAttributes( self ) : + def testQueryAttributes( self ) : self.writeAttributeSCC( file=SceneShapeTest.__testPlugAttrFile ) - maya.cmds.file( new=True, f=True ) - node = maya.cmds.createNode( 'ieSceneShape' ) + node = self._node maya.cmds.setAttr( node+'.file', SceneShapeTest.__testPlugAttrFile,type='string' ) maya.cmds.setAttr( node+".queryPaths[0]", "/1", type="string") @@ -458,8 +451,7 @@ def testTags( self ) : self.writeTagSCC( file=SceneShapeTest.__testFile ) - maya.cmds.file( new=True, f=True ) - node = maya.cmds.createNode( 'ieSceneShape' ) + node = self._node fn = IECoreMaya.FnSceneShape( node ) transform = str(maya.cmds.listRelatives( node, parent=True )[0]) maya.cmds.setAttr( node+'.file', SceneShapeTest.__testFile, type='string' ) @@ -492,8 +484,7 @@ def testLiveSceneTags( self ) : self.writeTagSCC( file=SceneShapeTest.__testFile ) - maya.cmds.file( new=True, f=True ) - node = maya.cmds.createNode( 'ieSceneShape' ) + node = self._node fn = IECoreMaya.FnSceneShape( node ) transform = str(maya.cmds.listRelatives( node, parent=True )[0]) maya.cmds.setAttr( node+'.file', SceneShapeTest.__testFile, type='string' ) @@ -528,8 +519,7 @@ def testLinkedLiveSceneTags( self ) : self.writeTagSCC( file=SceneShapeTest.__testFile ) - maya.cmds.file( new=True, f=True ) - node = maya.cmds.createNode( 'ieSceneShape' ) + node = self._node fn = IECoreMaya.FnSceneShape( node ) transform = str(maya.cmds.listRelatives( node, parent=True )[0]) maya.cmds.setAttr( node+'.file', SceneShapeTest.__testFile, type='string' ) From d0cd1f4ce2bac5affdbc2ea199b9f4e8fe0e5d0a Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Tue, 17 May 2022 15:07:42 -0700 Subject: [PATCH 20/36] tests: Add SceneShapeProxyTest --- test/IECoreMaya/All.py | 1 + test/IECoreMaya/SceneShapeProxyTest.py | 48 ++++++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 test/IECoreMaya/SceneShapeProxyTest.py diff --git a/test/IECoreMaya/All.py b/test/IECoreMaya/All.py index 071b9e2df3..4303730cc7 100644 --- a/test/IECoreMaya/All.py +++ b/test/IECoreMaya/All.py @@ -81,6 +81,7 @@ from ToMayaCameraConverterTest import ToMayaCameraConverterTest from LiveSceneTest import * from SceneShapeTest import SceneShapeTest +from SceneShapeProxyTest import SceneShapeProxyTest from FnSceneShapeTest import FnSceneShapeTest from FromMayaLocatorConverterTest import FromMayaLocatorConverterTest from ToMayaLocatorConverterTest import ToMayaLocatorConverterTest diff --git a/test/IECoreMaya/SceneShapeProxyTest.py b/test/IECoreMaya/SceneShapeProxyTest.py new file mode 100644 index 0000000000..67f90a1b9c --- /dev/null +++ b/test/IECoreMaya/SceneShapeProxyTest.py @@ -0,0 +1,48 @@ +########################################################################## +# +# Copyright (c) 2022, 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 Image Engine Design 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 IECoreMaya +import maya.cmds + +import SceneShapeTest + +class SceneShapeProxyTest( SceneShapeTest.SceneShapeTest ): + + def setUp( self ): + IECoreMaya.TestCase.setUp(self) + self._node = maya.cmds.createNode( "ieSceneShapeProxy" ) + +if __name__ == "__main__": + IECoreMaya.TestProgram( plugins = [ "ieCore" ] ) From 49720a50cc045b3b5899f4030ce894e596edb496 Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Thu, 15 Feb 2018 17:17:17 -0800 Subject: [PATCH 21/36] Convert: Added conversion from DD::Image::Box3 to Imath::Box3f and Imath::Box3d and Imath::M44d to DD::Image::Matrix4. --- include/IECoreNuke/Convert.h | 9 +++++++++ src/IECoreNuke/Convert.cpp | 26 ++++++++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/include/IECoreNuke/Convert.h b/include/IECoreNuke/Convert.h index 93ffb3b8ce..e1ce198c33 100644 --- a/include/IECoreNuke/Convert.h +++ b/include/IECoreNuke/Convert.h @@ -114,12 +114,21 @@ IECORENUKE_API Imath::M44f convert( const DD::Image::Matrix4 &from ); template<> IECORENUKE_API Imath::M44d convert( const DD::Image::Matrix4 &from ); +template<> +IECORENUKE_API DD::Image::Matrix4 convert( const Imath::M44d &from ); + template<> IECORENUKE_API Imath::Box2i convert( const DD::Image::Box &from ); template<> IECORENUKE_API DD::Image::Box3 convert( const Imath::Box3f &from ); +template<> +IECORENUKE_API Imath::Box3f convert( const DD::Image::Box3 &from ); + +template<> +IECORENUKE_API Imath::Box3d convert( const DD::Image::Box3 &from ); + } // namespace IECore #endif // IECORENUKE_CONVERT_H diff --git a/src/IECoreNuke/Convert.cpp b/src/IECoreNuke/Convert.cpp index e3c1076a52..e9e8baf8f3 100644 --- a/src/IECoreNuke/Convert.cpp +++ b/src/IECoreNuke/Convert.cpp @@ -149,6 +149,20 @@ Imath::M44d convert( const DD::Image::Matrix4 &from ) return result; } +template<> +DD::Image::Matrix4 convert( const Imath::M44d& transform ) +{ + DD::Image::Matrix4 nukeMatrix; + for ( unsigned x=0; x < 4; x++ ) + { + for ( unsigned y=0; y < 4; y++ ) + { + nukeMatrix[x][y] = transform[x][y]; + } + } + return nukeMatrix; +} + template<> Imath::Box2i convert( const DD::Image::Box &from ) { @@ -161,4 +175,16 @@ DD::Image::Box3 convert( const Imath::Box3f &from ) return DD::Image::Box3( convert( from.min ), convert( from.max ) ); } +template<> +Imath::Box3f convert( const DD::Image::Box3 &from ) +{ + return Imath::Box3f( convert( from.min() ), convert( from.max() ) ); +} + +template<> +Imath::Box3d convert( const DD::Image::Box3 &from ) +{ + return Imath::Box3d( convert( from.min() ), convert( from.max() ) ); +} + } // namespace IECore From b05cd3b4249798a74b0915651d0f05f95234df20 Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Wed, 19 Oct 2022 17:25:53 -0700 Subject: [PATCH 22/36] SceneCacheReader: Stop transforming the mesh into world space. We want to maintain the world space matrix instead of transforming the points so we can more easily round trip scene cache in/out of nuke. This allows to write the bounds as the SceneInterface expects ( local space ). Due to some kind of reset happening in the SourceGeo source code, we need to store the matrix in create_geometry and apply in the geometry_engine. I think this should work fine considering that Op are instantiate per output context ( frame ) so we shouldn't have a clash in the map data structure. --- include/IECoreNuke/SceneCacheReader.h | 2 ++ src/IECoreNuke/SceneCacheReader.cpp | 16 +++++++--------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/include/IECoreNuke/SceneCacheReader.h b/include/IECoreNuke/SceneCacheReader.h index d609549c5b..e04c1a9cd8 100644 --- a/include/IECoreNuke/SceneCacheReader.h +++ b/include/IECoreNuke/SceneCacheReader.h @@ -172,6 +172,8 @@ class IECORENUKE_API SceneCacheReader : public DD::Image::SourceGeo // only the first reader allocates the shared data SharedData *m_data; + + std::map m_indexToWorldTransform; }; } // namespace IECoreNuke diff --git a/src/IECoreNuke/SceneCacheReader.cpp b/src/IECoreNuke/SceneCacheReader.cpp index e647810a97..b740fc79a2 100644 --- a/src/IECoreNuke/SceneCacheReader.cpp +++ b/src/IECoreNuke/SceneCacheReader.cpp @@ -982,7 +982,7 @@ void SceneCacheReader::geometry_engine( DD::Image::Scene& scene, GeometryList& o for( unsigned i = 0; i < out.size(); i++ ) { - out[i].matrix = m_baseParentMatrix; + out[i].matrix = m_baseParentMatrix * m_indexToWorldTransform[i]; } } @@ -995,7 +995,7 @@ void SceneCacheReader::create_geometry( DD::Image::Scene &scene, DD::Image::Geom // something in nuke assumes we won't change anything if // rebuilding isn't needed - we get crashes if we rebuild // when not necessary. - if( !rebuild( Mask_Attributes ) && !rebuild( Mask_Matrix ) ) + if( !rebuild( Mask_Attributes )) { return; } @@ -1009,7 +1009,7 @@ void SceneCacheReader::create_geometry( DD::Image::Scene &scene, DD::Image::Geom try { - if( rebuild( Mask_Primitives ) ) + if( rebuild( Mask_Primitives ) || rebuild( Mask_Matrix ) ) { out.delete_objects(); @@ -1079,16 +1079,14 @@ void SceneCacheReader::loadPrimitive( DD::Image::GeometryList &out, const std::s Imath::M44d transformd; transformd = worldTransform( sceneInterface, rootPath, time ); - IECoreScene::TransformOpPtr transformer = new IECoreScene::TransformOp(); - transformer->inputParameter()->setValue( const_cast< IECore::Object * >(object.get()) ); // safe const_cast because the Op will copy the input object. - transformer->copyParameter()->setTypedValue( true ); - transformer->matrixParameter()->setValue( new IECore::M44dData( transformd ) ); - object = transformer->operate(); - + IECoreNuke::ToNukeGeometryConverterPtr converter = IECoreNuke::ToNukeGeometryConverter::create( object ); if (converter) { converter->convert( out ); + // store the world matrix to apply in geometry_engine because + // somewhere after the create_geometry nuke reset the matrix in the SourceGeo base class. + m_indexToWorldTransform[out.objects()-1] = IECore::convert( transformd ); } } } From 6369d9b9f32e8b0be05fc1a65da8cfdc823fd516 Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Thu, 15 Feb 2018 17:17:59 -0800 Subject: [PATCH 23/36] LiveScene: Implementation of LiveScene for Nuke. Allow to query the Nuke's scene representation through the SceneInterface API. --- include/IECoreNuke/LiveScene.h | 132 +++++ include/IECoreNuke/TypeIds.h | 1 + .../IECoreNuke/bindings/LiveSceneBinding.h | 45 ++ python/IECoreNuke/__init__.py | 21 +- src/IECoreNuke/LiveScene.cpp | 486 ++++++++++++++++++ src/IECoreNuke/bindings/IECoreNuke.cpp | 2 + src/IECoreNuke/bindings/LiveSceneBinding.cpp | 51 ++ 7 files changed, 729 insertions(+), 9 deletions(-) create mode 100644 include/IECoreNuke/LiveScene.h create mode 100644 include/IECoreNuke/bindings/LiveSceneBinding.h create mode 100644 src/IECoreNuke/LiveScene.cpp create mode 100644 src/IECoreNuke/bindings/LiveSceneBinding.cpp diff --git a/include/IECoreNuke/LiveScene.h b/include/IECoreNuke/LiveScene.h new file mode 100644 index 0000000000..1c2edab373 --- /dev/null +++ b/include/IECoreNuke/LiveScene.h @@ -0,0 +1,132 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IECORENUKE_LIVESCENE_H +#define IECORENUKE_LIVESCENE_H + +#include "DDImage/GeoOp.h" +#include "DDImage/GeometryList.h" + +#include "IECore/PathMatcher.h" + +#include "IECoreScene/SceneInterface.h" + +#include "IECoreNuke/Export.h" +#include "IECoreNuke/TypeIds.h" + +namespace IECoreNuke +{ + +IE_CORE_FORWARDDECLARE( LiveScene ); + +/// A read-only class for representing a live Nuke scene as an IECore::SceneInterface +class IECORENUKE_API LiveScene : public IECoreScene::SceneInterface +{ + public : + + static const std::string& nameAttribute; + + IE_CORE_DECLARERUNTIMETYPEDEXTENSION( LiveScene, LiveSceneTypeId, IECoreScene::SceneInterface ); + + LiveScene(); + LiveScene( DD::Image::GeoOp *op, const std::string rootPath="/" ); + + ~LiveScene() override; + + std::string fileName() const override; + + Name name() const override; + void path( Path &p ) const override; + + Imath::Box3d readBound( double time ) const override; + void writeBound( const Imath::Box3d &bound, double time ); + + IECore::ConstDataPtr readTransform( double time ) const override; + Imath::M44d readTransformAsMatrix( double time ) const override; + IECore::ConstDataPtr readWorldTransform( double time ) const; + Imath::M44d readWorldTransformAsMatrix( double time ) const; + void writeTransform( const IECore::Data *transform, double time ) override; + + bool hasAttribute( const Name &name ) const override; + void attributeNames( NameList &attrs ) const override; + IECore::ConstObjectPtr readAttribute( const Name &name, double time ) const override; + void writeAttribute( const Name &name, const IECore::Object *attribute, double time ) override; + + bool hasTag( const Name &name, int filter = SceneInterface::LocalTag ) const override; + void readTags( NameList &tags, int filter = SceneInterface::LocalTag ) const override; + void writeTags( const NameList &tags ) override; + + NameList setNames( bool includeDescendantSets = true ) const override; + IECore::PathMatcher readSet( const Name &name, bool includeDescendantSets = true, const IECore::Canceller *canceller = nullptr ) const override; + void writeSet( const Name &name, const IECore::PathMatcher &set ) override; + void hashSet( const Name& setName, IECore::MurmurHash &h ) const override; + + bool hasObject() const override; + IECore::ConstObjectPtr readObject( double time, const IECore::Canceller *canceller = nullptr ) const override; + IECoreScene::PrimitiveVariableMap readObjectPrimitiveVariables( const std::vector &primVarNames, double time ) const override; + void writeObject( const IECore::Object *object, double time ) override; + + void childNames( NameList &childNames ) const override; + bool hasChild( const Name &name ) const override; + IECoreScene::SceneInterfacePtr child( const Name &name, MissingBehaviour missingBehaviour = SceneInterface::ThrowIfMissing ) override; + IECoreScene::ConstSceneInterfacePtr child( const Name &name, MissingBehaviour missingBehaviour = SceneInterface::ThrowIfMissing ) const override; + IECoreScene::SceneInterfacePtr createChild( const Name &name ) override; + + IECoreScene::SceneInterfacePtr scene( const Path &path, MissingBehaviour missingBehaviour = SceneInterface::ThrowIfMissing ) override; + IECoreScene::ConstSceneInterfacePtr scene( const Path &path, MissingBehaviour missingBehaviour = SceneInterface::ThrowIfMissing ) const override; + + void hash( HashType hashType, double time, IECore::MurmurHash &h ) const override; + + static double timeToFrame( const double& time ); + static double frameToTime( const int& frame ); + + private: + + DD::Image::GeoOp *op() const; + DD::Image::GeometryList geometryList( const double* time=nullptr ) const; + + std::string geoInfoPath( const int& index ) const; + + DD::Image::GeoOp *m_op; + std::string m_rootPath; + IECore::PathMatcher m_pathMatcher; + typedef std::map objectPathMap; + mutable objectPathMap m_objectPathMap; +}; + +IE_CORE_DECLAREPTR( LiveScene ); + +} // namespace IECoreNuke + +#endif // IECORENUKE_LIVESCENE_H diff --git a/include/IECoreNuke/TypeIds.h b/include/IECoreNuke/TypeIds.h index 273957acf7..84ecedb260 100644 --- a/include/IECoreNuke/TypeIds.h +++ b/include/IECoreNuke/TypeIds.h @@ -48,6 +48,7 @@ enum TypeId FromNukeCameraConverterTypeId = 107005, FromNukeTileConverterTypeId = 107006, NukeDisplayDriverTypeId = 107007, + LiveSceneTypeId = 107008, LastCoreNukeTypeId = 107999 }; diff --git a/include/IECoreNuke/bindings/LiveSceneBinding.h b/include/IECoreNuke/bindings/LiveSceneBinding.h new file mode 100644 index 0000000000..0df2ed96a9 --- /dev/null +++ b/include/IECoreNuke/bindings/LiveSceneBinding.h @@ -0,0 +1,45 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IECORENUKE_LIVESCENEBINDING_H +#define IECORENUKE_LIVESCENEBINDING_H + +namespace IECoreNuke +{ + +void bindLiveScene(); + +} + +#endif // IECORENUKE_LIVESCENEBINDING_H diff --git a/python/IECoreNuke/__init__.py b/python/IECoreNuke/__init__.py index 18fe02d499..7fd11c4e81 100644 --- a/python/IECoreNuke/__init__.py +++ b/python/IECoreNuke/__init__.py @@ -34,15 +34,18 @@ __import__( "IECore" ) -from _IECoreNuke import * +# importing IECoreScene before _IECoreNuke required for LiveScene binding to work as we need the SceneInterface binding loading first. +import IECoreScene -from KnobAccessors import setKnobValue, getKnobValue -from FnAxis import FnAxis -from StringUtil import nukeFileSequence, ieCoreFileSequence -from KnobConverters import registerParameterKnobConverters, createKnobsFromParameter, setKnobsFromParameter, setParameterFromKnobs -from FnParameterisedHolder import FnParameterisedHolder -from UndoManagers import UndoState, UndoDisabled, UndoEnabled, UndoBlock -from TestCase import TestCase -from FnOpHolder import FnOpHolder +from ._IECoreNuke import * + +from .KnobAccessors import setKnobValue, getKnobValue +from .FnAxis import FnAxis +from .StringUtil import nukeFileSequence, ieCoreFileSequence +from .KnobConverters import registerParameterKnobConverters, createKnobsFromParameter, setKnobsFromParameter, setParameterFromKnobs +from .FnParameterisedHolder import FnParameterisedHolder +from .UndoManagers import UndoState, UndoDisabled, UndoEnabled, UndoBlock +from .TestCase import TestCase +from .FnOpHolder import FnOpHolder __import__( "IECore" ).loadConfig( "CORTEX_STARTUP_PATHS", subdirectory = "IECoreNuke" ) diff --git a/src/IECoreNuke/LiveScene.cpp b/src/IECoreNuke/LiveScene.cpp new file mode 100644 index 0000000000..30bd508a1a --- /dev/null +++ b/src/IECoreNuke/LiveScene.cpp @@ -0,0 +1,486 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "IECoreNuke/LiveScene.h" + +#include "DDImage/Scene.h" + +#include "IECoreNuke/Convert.h" +#include "IECoreNuke/MeshFromNuke.h" + +#include "IECore/Exception.h" +#include "IECore/NullObject.h" +#include "IECore/TransformationMatrixData.h" + +#include "OpenEXR/ImathBoxAlgo.h" + +#include "boost/algorithm/string.hpp" +#include "boost/format.hpp" +#include "boost/tokenizer.hpp" + +using namespace IECore; +using namespace IECoreScene; +using namespace IECoreNuke; +using namespace DD::Image; + +namespace +{ + +IECore::TransformationMatrixd convertTransformMatrix( DD::Image::Matrix4& from ) +{ + auto to = IECore::TransformationMatrixd(); + DD::Image::Vector3 rotation, translation, scale, shear; + from.decompose( rotation, translation, scale, shear, DD::Image::Matrix4::RotationOrder::eXYZ ); + to.scale = IECore::convert( scale ); + to.shear = IECore::convert( shear ); + to.rotate = IECore::convert( rotation ); + to.translate = IECore::convert( translation ); + return to; +} + +} + +const std::string& LiveScene::nameAttribute( "ieName" ); + +IE_CORE_DEFINERUNTIMETYPED( LiveScene ); + +LiveScene::LiveScene() : m_op( nullptr ) +{ +} + +LiveScene::LiveScene( GeoOp *op, const std::string rootPath ) : m_op( op ), m_rootPath( rootPath ) +{ + m_pathMatcher = IECore::PathMatcher(); + m_pathMatcher.addPath( m_rootPath ); +} + +LiveScene::~LiveScene() +{ +} + +GeoOp *LiveScene::op() const +{ + return m_op; +} + +double LiveScene::timeToFrame( const double& time ) +{ + return time * DD::Image::root_real_fps(); +} + +double LiveScene::frameToTime( const int& frame ) +{ + return frame / double( DD::Image::root_real_fps() ); +} + +std::string LiveScene::geoInfoPath( const int& index ) const +{ + auto it = m_objectPathMap.find( index ); + if ( it != m_objectPathMap.end() ) + { + return it->second; + } + else + { + auto info = geometryList().object( index ); + std::string nameValue; + if( auto nameAttrib = info.get_group_attribute( GroupType::Group_Object, nameAttribute.data() ) ) + { + nameValue = nameAttrib->stdstring(); + } + else + { + nameValue = "/object" + std::to_string( index ); + } + + m_objectPathMap[index] = nameValue; + + return nameValue; + } +} + +GeometryList LiveScene::geometryList( const double* time ) const +{ + auto oc = OutputContext(); + if ( time ) + { + oc.setFrame( timeToFrame( *time ) ); + } + else + { + oc.setFrame( m_op->outputContext().frame() ); + } + + auto nodeInputOp = m_op->node_input( 0, Op::EXECUTABLE_INPUT, &oc ); + auto geoOp = dynamic_cast( nodeInputOp ); + + geoOp->validate(true); + boost::shared_ptr scene( new DD::Image::Scene() ); + geoOp->build_scene( *scene ); + + return *scene->object_list(); +} + +std::string LiveScene::fileName() const +{ + throw Exception( "IECoreNuke::LiveScene does not support fileName()." ); +} + +SceneInterface::Name LiveScene::name() const +{ + IECoreScene::SceneInterface::Path path; + IECoreScene::SceneInterface::stringToPath( m_rootPath, path ); + if ( path.empty() ) + { + return IECoreScene::SceneInterface::rootName; + } + + return *path.rbegin(); +} + +void LiveScene::path( Path &p ) const +{ + p.clear(); + IECoreScene::SceneInterface::stringToPath( m_rootPath, p ); +} + +Imath::Box3d LiveScene::readBound( double time ) const +{ + Imath::Box3d bound; + IECoreScene::SceneInterface::Path rootPath, currentPath; + for( unsigned i=0; i < geometryList( &time ).objects(); ++i ) + { + auto nameValue = geoInfoPath( i ); + auto result = m_pathMatcher.match( nameValue ); + if ( ( result != IECore::PathMatcher::AncestorMatch ) && ( result != IECore::PathMatcher::ExactMatch ) ) + { + continue; + } + IECoreScene::SceneInterface::stringToPath( m_rootPath, rootPath ); + IECoreScene::SceneInterface::stringToPath( nameValue, currentPath ); + + GeoInfo info = geometryList( &time ).object( i ); + Box3 objectBound; + if ( ( currentPath.size() > 1 ) && ( ( currentPath.size() == rootPath.size() + 1 ) || ( nameValue == m_rootPath ) ) ) + { + // object space bound + objectBound = info.bbox(); + } + else + { + objectBound = info.getTransformedBBox(); + } + Imath::Box3d b = IECore::convert( objectBound ); + + if( b.hasVolume() ) + { + bound.extendBy( b ); + } + } + + return bound; +} + +void LiveScene::writeBound( const Imath::Box3d &bound, double time ) +{ + throw Exception( "IECoreNuke::LiveScene::writeBound: write operations not supported!" ); +} + +ConstDataPtr LiveScene::readTransform( double time ) const +{ + + for( unsigned i=0; i < geometryList().objects(); ++i ) + { + auto nameValue = geoInfoPath( i ); + auto result = m_pathMatcher.match( nameValue ); + if ( result == IECore::PathMatcher::ExactMatch ) + { + auto geoInfo = geometryList( &time ).object( i ); + auto from = geoInfo.matrix; + return new TransformationMatrixdData( convertTransformMatrix( from ) ); + } + } + + return new TransformationMatrixdData( IECore::TransformationMatrixd() ); +} + +Imath::M44d LiveScene::readTransformAsMatrix( double time ) const +{ + return runTimeCast< const TransformationMatrixdData >( readTransform( time ) )->readable().transform(); +} + +void LiveScene::writeTransform( const Data *transform, double time ) +{ + throw Exception( "IECoreNuke::LiveScene::writeTransform: write operations not supported!" ); +} + +bool LiveScene::hasAttribute( const Name &name ) const +{ + throw Exception( "IECoreNuke::LiveScene does not support hasAttribute()." ); +} + +void LiveScene::attributeNames( NameList &attrs ) const +{ + throw Exception( "IECoreNuke::LiveScene does not support attributeNames()." ); +} + +ConstObjectPtr LiveScene::readAttribute( const Name &name, double time ) const +{ + throw Exception( "IECoreNuke::LiveScene does not support readAttribute()." ); +} + +void LiveScene::writeAttribute( const Name &name, const Object *attribute, double time ) +{ + throw Exception( "IECoreNuke::LiveScene::writeAttribute: write operations not supported!" ); +} + +bool LiveScene::hasTag( const Name &name, int filter ) const +{ + throw Exception( "IECoreNuke::LiveScene does not support hasTag()." ); +} + +void LiveScene::readTags( NameList &tags, int filter ) const +{ + throw Exception( "IECoreNuke::LiveScene does not support readTags()." ); +} + +void LiveScene::writeTags( const NameList &tags ) +{ + throw Exception( "IECoreNuke::LiveScene::writeTags not supported" ); +} + +SceneInterface::NameList LiveScene::setNames( bool includeDescendantSets ) const +{ + throw Exception( "IECoreNuke::LiveScene::setNames not supported" ); +} + +IECore::PathMatcher LiveScene::readSet( const Name &name, bool includeDescendantSets, const IECore::Canceller *canceller ) const +{ + throw Exception( "IECoreNuke::LiveScene::readSet not supported" ); +} + +void LiveScene::writeSet( const Name &name, const IECore::PathMatcher &set ) +{ + throw Exception( "IECoreNuke::LiveScene::writeSet not supported" ); +} + +void LiveScene::hashSet( const Name& setName, IECore::MurmurHash &h ) const +{ + throw Exception( "IECoreNuke::LiveScene::hashSet not supported" ); +} + +bool LiveScene::hasObject() const +{ + for( unsigned i=0; i < geometryList().objects(); ++i ) + { + auto nameValue = geoInfoPath( i ); + auto result = m_pathMatcher.match( nameValue ); + if ( result == IECore::PathMatcher::ExactMatch ) + { + return true; + } + } + + return false; +} + +ConstObjectPtr LiveScene::readObject( double time, const IECore::Canceller *canceller) const +{ + for( unsigned i=0; i < geometryList().objects(); ++i ) + { + auto nameValue = geoInfoPath( i ); + auto result = m_pathMatcher.match( nameValue ); + if ( result == IECore::PathMatcher::ExactMatch ) + { + auto geoInfo = geometryList( &time ).object( i ); + MeshFromNukePtr converter = new IECoreNuke::MeshFromNuke( &geoInfo ); + return converter->convert(); + } + } + + return IECore::NullObject::defaultNullObject(); +} + +PrimitiveVariableMap LiveScene::readObjectPrimitiveVariables( const std::vector &primVarNames, double time ) const +{ + throw Exception( "IECoreNuke::readObjectPrimitiveVariables() not implemented!" ); +} + +void LiveScene::writeObject( const Object *object, double time ) +{ + throw Exception( "IECoreNuke::LiveScene::writeObject: write operations not supported!" ); +} + +void LiveScene::childNames( NameList &childNames ) const +{ + childNames.clear(); + std::vector allPaths; + + for( unsigned i=0; i < geometryList().objects(); ++i ) + { + auto nameValue = geoInfoPath( i ); + auto result = m_pathMatcher.match( nameValue ); + if ( ( result == IECore::PathMatcher::AncestorMatch ) || ( result == IECore::PathMatcher::ExactMatch ) ) + { + allPaths.push_back( nameValue ); + } + } + + // filter only children + IECoreScene::SceneInterface::Path allPath, rootPath; + IECoreScene::SceneInterface::stringToPath( m_rootPath, rootPath ); + for ( auto& path : allPaths ) + { + allPath.clear(); + IECoreScene::SceneInterface::stringToPath( path, allPath ); + if ( rootPath.size() < allPath.size() ) + { + childNames.push_back( allPath[rootPath.size()] ); + } + } +} + +bool LiveScene::hasChild( const Name &name ) const +{ + IECoreScene::SceneInterface::NameList names; + childNames( names ); + + return find( names.cbegin(), names.cend(), name ) != names.cend(); +} + +SceneInterfacePtr LiveScene::child( const Name &name, MissingBehaviour missingBehaviour ) +{ + IECoreScene::SceneInterface::NameList names; + childNames( names ); + + if( find( names.cbegin(), names.cend(), name ) == names.cend() ) + { + switch ( missingBehaviour ) + { + case MissingBehaviour::ThrowIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name \"" + name.string() + "\" is not a valid childName." ); + case MissingBehaviour::NullIfMissing: + return nullptr; + case MissingBehaviour::CreateIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name\"" + name.string() + "\" is missing and LiveScene is read-only" ); + } + } + return new LiveScene( m_op, m_rootPath + "/" + name.string() ); +} + +ConstSceneInterfacePtr LiveScene::child( const Name &name, MissingBehaviour missingBehaviour ) const +{ + IECoreScene::SceneInterface::NameList names; + childNames( names ); + + if( find( names.cbegin(), names.cend(), name ) == names.cend() ) + { + switch ( missingBehaviour ) + { + case MissingBehaviour::ThrowIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name \"" + name.string() + "\" is not a valid childName." ); + case MissingBehaviour::NullIfMissing: + return nullptr; + case MissingBehaviour::CreateIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name\"" + name.string() + "\" is missing and LiveScene is read-only" ); + } + } + return new LiveScene( m_op, m_rootPath + "/" + name.string() ); +} + +SceneInterfacePtr LiveScene::createChild( const Name &name ) +{ + throw Exception( "IECoreNuke::LiveScene is read-only" ); +} + +ConstSceneInterfacePtr LiveScene::scene( const Path &path, MissingBehaviour missingBehaviour ) const +{ + IECoreNuke::ConstLiveScenePtr currentScene( this ); + for ( const auto& child : path ) + { + if ( auto childScene = currentScene->child( child, missingBehaviour ) ) + { + currentScene = dynamic_cast( childScene.get() ); + } + else + { + switch ( missingBehaviour ) + { + case MissingBehaviour::ThrowIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name \"" + child.string() + "\" is not a valid childName." ); + case MissingBehaviour::NullIfMissing: + return nullptr; + case MissingBehaviour::CreateIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name\"" + child.string() + "\" is missing and LiveScene is read-only" ); + } + } + } + + std::string pathStr; + IECoreScene::SceneInterface::pathToString( path, pathStr ); + return new LiveScene( m_op, pathStr ); +} + +SceneInterfacePtr LiveScene::scene( const Path &path, MissingBehaviour missingBehaviour ) +{ + IECoreNuke::LiveScenePtr currentScene( this ); + for ( const auto& child : path ) + { + if ( auto childScene = currentScene->child( child, missingBehaviour ) ) + { + currentScene = dynamic_cast( childScene.get() ); + } + else + { + switch ( missingBehaviour ) + { + case MissingBehaviour::ThrowIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name \"" + child.string() + "\" is not a valid childName." ); + case MissingBehaviour::NullIfMissing: + return nullptr; + case MissingBehaviour::CreateIfMissing: + throw Exception( "IECoreNuke::LiveScene: Name\"" + child.string() + "\" is missing and LiveScene is read-only" ); + } + } + } + + std::string pathStr; + IECoreScene::SceneInterface::pathToString( path, pathStr ); + return new LiveScene( m_op, pathStr ); +} + +void LiveScene::hash( HashType hashType, double time, MurmurHash &h ) const +{ + throw Exception( "Hashes currently not supported in IECoreNuke::LiveScene objects." ); +} diff --git a/src/IECoreNuke/bindings/IECoreNuke.cpp b/src/IECoreNuke/bindings/IECoreNuke.cpp index 6f5f9ab29c..40b61f67d9 100644 --- a/src/IECoreNuke/bindings/IECoreNuke.cpp +++ b/src/IECoreNuke/bindings/IECoreNuke.cpp @@ -37,6 +37,7 @@ #include "IECoreNuke/bindings/FnOpHolderBinding.h" #include "IECoreNuke/bindings/FnParameterisedHolderBinding.h" #include "IECoreNuke/bindings/ObjectKnobBinding.h" +#include "IECoreNuke/bindings/LiveSceneBinding.h" using namespace boost::python; using namespace IECoreNuke; @@ -44,6 +45,7 @@ using namespace IECoreNuke; BOOST_PYTHON_MODULE( _IECoreNuke ) { bindObjectKnob(); + bindLiveScene(); bindFnParameterisedHolder(); bindFnOpHolder(); } diff --git a/src/IECoreNuke/bindings/LiveSceneBinding.cpp b/src/IECoreNuke/bindings/LiveSceneBinding.cpp new file mode 100644 index 0000000000..8376013cb4 --- /dev/null +++ b/src/IECoreNuke/bindings/LiveSceneBinding.cpp @@ -0,0 +1,51 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "boost/python.hpp" + +#include "IECoreNuke/LiveScene.h" +#include "IECoreNuke/bindings/LiveSceneBinding.h" + +#include "IECorePython/IECoreBinding.h" +#include "IECorePython/RunTimeTypedBinding.h" + +using namespace IECoreNuke; +using namespace boost::python; + +void IECoreNuke::bindLiveScene() +{ + IECorePython::RunTimeTypedClass() + .def( init<>() ) + ; +} From 26d6000e8be970e88222849f2b20e938624911f8 Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Thu, 13 Oct 2022 11:52:02 -0700 Subject: [PATCH 24/36] SceneCacheReader: Add path attribute in geometry converter so we can round trip hierarchy. --- include/IECoreNuke/ToNukeGeometryConverter.h | 3 +++ src/IECoreNuke/SceneCacheReader.cpp | 1 + src/IECoreNuke/ToNukeGeometryConverter.cpp | 8 ++++++++ 3 files changed, 12 insertions(+) diff --git a/include/IECoreNuke/ToNukeGeometryConverter.h b/include/IECoreNuke/ToNukeGeometryConverter.h index 8bde60eb44..576b825e54 100644 --- a/include/IECoreNuke/ToNukeGeometryConverter.h +++ b/include/IECoreNuke/ToNukeGeometryConverter.h @@ -39,6 +39,7 @@ #include "IECore/NumericParameter.h" #include "IECore/Object.h" +#include "IECore/SimpleTypedParameter.h" #include "DDImage/GeometryList.h" @@ -97,6 +98,8 @@ class IECORENUKE_API ToNukeGeometryConverter : public ToNukeConverter static TypesToFnsMap *typesToFns(); IECore::IntParameterPtr m_objIndexParameter; + IECore::StringParameterPtr m_pathParameter; + }; } // namespace IECoreNuke diff --git a/src/IECoreNuke/SceneCacheReader.cpp b/src/IECoreNuke/SceneCacheReader.cpp index b740fc79a2..ee52da86f6 100644 --- a/src/IECoreNuke/SceneCacheReader.cpp +++ b/src/IECoreNuke/SceneCacheReader.cpp @@ -1083,6 +1083,7 @@ void SceneCacheReader::loadPrimitive( DD::Image::GeometryList &out, const std::s IECoreNuke::ToNukeGeometryConverterPtr converter = IECoreNuke::ToNukeGeometryConverter::create( object ); if (converter) { + converter->parameters()->parameter( "path" )->setValue( new IECore::StringData( itemPath ) ); converter->convert( out ); // store the world matrix to apply in geometry_engine because // somewhere after the create_geometry nuke reset the matrix in the SourceGeo base class. diff --git a/src/IECoreNuke/ToNukeGeometryConverter.cpp b/src/IECoreNuke/ToNukeGeometryConverter.cpp index 704e13605b..fc5d3ac415 100644 --- a/src/IECoreNuke/ToNukeGeometryConverter.cpp +++ b/src/IECoreNuke/ToNukeGeometryConverter.cpp @@ -33,6 +33,7 @@ ////////////////////////////////////////////////////////////////////////// #include "IECoreNuke/ToNukeGeometryConverter.h" +#include "IECoreNuke/LiveScene.h" #include "IECore/CompoundData.h" #include "IECore/CompoundParameter.h" @@ -53,6 +54,9 @@ ToNukeGeometryConverter::ToNukeGeometryConverter( const std::string &description m_objIndexParameter = new IntParameter( "objIndex", "Index for the first object inserted on the GeometryList. Use -1 to simply add on the next index available", -1 ); parameters()->addParameter( m_objIndexParameter ); + m_pathParameter = new StringParameter( "path", "The object path in the hierarchy.", new StringData() ); + parameters()->addParameter( m_pathParameter ); + } void ToNukeGeometryConverter::convert( GeometryList &geoList ) const @@ -63,6 +67,10 @@ void ToNukeGeometryConverter::convert( GeometryList &geoList ) const objIndex = (int)geoList.objects(); } geoList.add_object(objIndex); + + // add path attribute + auto nameAttribute = geoList.writable_attribute( objIndex, GroupType::Group_Object, IECoreNuke::LiveScene::nameAttribute.data(), AttribType::STD_STRING_ATTRIB); + nameAttribute->stdstring() = m_pathParameter->getTypedValue(); ConstCompoundObjectPtr operands = parameters()->getTypedValidatedValue(); doConversion( srcParameter()->getValidatedValue(), geoList, objIndex, operands.get() ); From 06dc99c2c6767887a31491f119f63e7ce6c7fdbd Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Wed, 12 Oct 2022 15:45:13 -0700 Subject: [PATCH 25/36] LiveSceneKnob: A knob to query the incoming 3d scene using the SceneInterface --- include/IECoreNuke/LiveSceneKnob.h | 94 +++++++++++++++++++ .../bindings/LiveSceneKnobBinding.h | 45 +++++++++ src/IECoreNuke/LiveSceneKnob.cpp | 89 ++++++++++++++++++ src/IECoreNuke/bindings/IECoreNuke.cpp | 4 +- .../bindings/LiveSceneKnobBinding.cpp | 87 +++++++++++++++++ 5 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 include/IECoreNuke/LiveSceneKnob.h create mode 100644 include/IECoreNuke/bindings/LiveSceneKnobBinding.h create mode 100644 src/IECoreNuke/LiveSceneKnob.cpp create mode 100644 src/IECoreNuke/bindings/LiveSceneKnobBinding.cpp diff --git a/include/IECoreNuke/LiveSceneKnob.h b/include/IECoreNuke/LiveSceneKnob.h new file mode 100644 index 0000000000..e5ed2716d8 --- /dev/null +++ b/include/IECoreNuke/LiveSceneKnob.h @@ -0,0 +1,94 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IECORENUKE_LIVESCENEKNOB_H +#define IECORENUKE_LIVESCENEKNOB_H + +#include "IECoreNuke/Export.h" + +#include "IECoreNuke/LiveSceneHolder.h" +#include "IECoreNuke/LiveScene.h" + +IECORE_PUSH_DEFAULT_VISIBILITY +#include "DDImage/Knobs.h" +IECORE_POP_DEFAULT_VISIBILITY + +namespace IECoreNuke +{ + +/// A nuke knob capable of holding arbitrary IECore::LiveScenes. +class IECORENUKE_API LiveSceneKnob : public DD::Image::Knob +{ + + public : + + IECoreNuke::LiveScenePtr getValue(); + + /// Call this from an Op::knobs() implementation to create an LiveSceneKnob. + static LiveSceneKnob *sceneKnob( DD::Image::Knob_Callback f, IECoreNuke::LiveSceneHolder* op, const char *name, const char *label ); + + protected : + + LiveSceneKnob( DD::Image::Knob_Closure *f, IECoreNuke::LiveSceneHolder* op, const char *name, const char *label = 0 ); + virtual ~LiveSceneKnob(); + + virtual const char *Class() const; + + private : + + IECoreNuke::LiveScenePtr m_value; + IECoreNuke::LiveSceneHolder* m_op; + +}; + +namespace Detail +{ + +// Used to implement the python binding +struct PythonLiveSceneKnob : public IECore::RefCounted +{ + + IE_CORE_DECLAREMEMBERPTR( PythonLiveSceneKnob ); + + LiveSceneKnob *sceneKnob; + +}; + +IE_CORE_DECLAREPTR( PythonLiveSceneKnob ); + +} // namespace Detail + +} // namespace IECoreNuke + +#endif // IECORENUKE_LIVESCENEKNOB_H diff --git a/include/IECoreNuke/bindings/LiveSceneKnobBinding.h b/include/IECoreNuke/bindings/LiveSceneKnobBinding.h new file mode 100644 index 0000000000..245b132689 --- /dev/null +++ b/include/IECoreNuke/bindings/LiveSceneKnobBinding.h @@ -0,0 +1,45 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IECORENUKE_LIVESCENEKNOBBINDING_H +#define IECORENUKE_LIVESCENEKNOBBINDING_H + +namespace IECoreNuke +{ + +void bindLiveSceneKnob(); + +} + +#endif // IECORENUKE_LIVESCENEKNOBBINDING_H diff --git a/src/IECoreNuke/LiveSceneKnob.cpp b/src/IECoreNuke/LiveSceneKnob.cpp new file mode 100644 index 0000000000..69c207431a --- /dev/null +++ b/src/IECoreNuke/LiveSceneKnob.cpp @@ -0,0 +1,89 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "boost/python.hpp" + +#include "IECoreNuke/LiveSceneKnob.h" + +#include "IECorePython/ScopedGILLock.h" + +using namespace IECoreNuke; +using namespace DD::Image; +using namespace boost::python; + +LiveSceneKnob::LiveSceneKnob( DD::Image::Knob_Closure* f, IECoreNuke::LiveSceneHolder* op, const char *name, const char *label ) + : DD::Image::Knob( f, name, label ), m_value( nullptr ), m_op(op) +{ + + set_flag( NO_ANIMATION ); + + // set up the object that will provide the python binding + IECorePython::ScopedGILLock gilLock; + Detail::PythonLiveSceneKnobPtr pythonKnob = new Detail::PythonLiveSceneKnob; + pythonKnob->sceneKnob = this; + object pythonKnobLiveScene( pythonKnob ); + Py_INCREF( pythonKnobLiveScene.ptr() ); + setPyObject( pythonKnobLiveScene.ptr() ); +} + +LiveSceneKnob::~LiveSceneKnob() +{ + // tidy up the object for the python binding + IECorePython::ScopedGILLock gilLock; + object pythonKnobLiveScene( handle<>( borrowed( (PyObject *)pyObject() ) ) ); + Detail::PythonLiveSceneKnobPtr pythonKnob = extract( pythonKnobLiveScene ); + pythonKnob->sceneKnob = nullptr; + Py_DECREF( pythonKnobLiveScene.ptr() ); +} + +IECoreNuke::LiveScenePtr LiveSceneKnob::getValue() +{ + if( auto geoOp = dynamic_cast( m_op ) ) + { + geoOp->validate(true); + m_value.reset(); + m_value = new IECoreNuke::LiveScene( m_op ); + } + return m_value; +} + +LiveSceneKnob *LiveSceneKnob::sceneKnob( DD::Image::Knob_Callback f, IECoreNuke::LiveSceneHolder* op, const char *name, const char *label ) +{ + return CustomKnob2( LiveSceneKnob, f, op, name, label ); +} + +const char *LiveSceneKnob::Class() const +{ + return "LiveSceneKnob"; +} diff --git a/src/IECoreNuke/bindings/IECoreNuke.cpp b/src/IECoreNuke/bindings/IECoreNuke.cpp index 40b61f67d9..cd5ffeabcc 100644 --- a/src/IECoreNuke/bindings/IECoreNuke.cpp +++ b/src/IECoreNuke/bindings/IECoreNuke.cpp @@ -38,14 +38,16 @@ #include "IECoreNuke/bindings/FnParameterisedHolderBinding.h" #include "IECoreNuke/bindings/ObjectKnobBinding.h" #include "IECoreNuke/bindings/LiveSceneBinding.h" +#include "IECoreNuke/bindings/LiveSceneKnobBinding.h" using namespace boost::python; using namespace IECoreNuke; BOOST_PYTHON_MODULE( _IECoreNuke ) { - bindObjectKnob(); bindLiveScene(); + bindLiveSceneKnob(); + bindObjectKnob(); bindFnParameterisedHolder(); bindFnOpHolder(); } diff --git a/src/IECoreNuke/bindings/LiveSceneKnobBinding.cpp b/src/IECoreNuke/bindings/LiveSceneKnobBinding.cpp new file mode 100644 index 0000000000..80ed648a65 --- /dev/null +++ b/src/IECoreNuke/bindings/LiveSceneKnobBinding.cpp @@ -0,0 +1,87 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "boost/python.hpp" + +#include "IECoreNuke/LiveSceneKnob.h" + +#include "IECorePython/RefCountedBinding.h" + +#include "IECore/Exception.h" + +using namespace boost::python; + +namespace IECoreNuke +{ + +// always check your knob before using it +static void check( Detail::PythonLiveSceneKnob &knob ) +{ + if( !knob.sceneKnob ) + { + throw( IECore::InvalidArgumentException( "Knob not alive." ) ); + } +} + +static const char *name( Detail::PythonLiveSceneKnob &knob ) +{ + check( knob ); + return knob.sceneKnob->name().c_str(); +} + +static const char *label( Detail::PythonLiveSceneKnob &knob ) +{ + check( knob ); + return knob.sceneKnob->label().c_str(); +} + +static IECoreNuke::LiveScenePtr getValue( Detail::PythonLiveSceneKnob &knob ) +{ + check( knob ); + IECoreNuke::LiveScenePtr v = knob.sceneKnob->getValue(); + return v; +} + +void bindLiveSceneKnob() +{ + + IECorePython::RefCountedClass( "LiveSceneKnob" ) + .def( "name", &name ) + .def( "label", &label ) + .def( "getValue", &getValue ) + ; + +} + +} // namespace IECoreNuke From 49eca0f91c6592b333b95ae1e2f0805fe45877a3 Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Tue, 4 Oct 2022 17:20:59 -0700 Subject: [PATCH 26/36] LiveSceneHolder: Node to hold a LiveSceneKnob to query the input scene as a cortex SceneInterface --- SConstruct | 2 +- include/IECoreNuke/LiveSceneHolder.h | 70 +++++++++++++++++++++++++++ src/IECoreNuke/LiveSceneHolder.cpp | 72 ++++++++++++++++++++++++++++ src/IECoreNuke/plugin/menu.py | 1 + 4 files changed, 144 insertions(+), 1 deletion(-) create mode 100644 include/IECoreNuke/LiveSceneHolder.h create mode 100644 src/IECoreNuke/LiveSceneHolder.cpp diff --git a/SConstruct b/SConstruct index 0f1b73266b..b687e2d21f 100644 --- a/SConstruct +++ b/SConstruct @@ -2656,7 +2656,7 @@ if doConfigure : nukePythonSources = sorted( glob.glob( "src/IECoreNuke/bindings/*.cpp" ) ) nukePythonScripts = glob.glob( "python/IECoreNuke/*.py" ) nukePluginSources = sorted( glob.glob( "src/IECoreNuke/plugin/*.cpp" ) ) - nukeNodeNames = [ "ieObject", "ieOp", "ieDrawable", "ieDisplay" ] + nukeNodeNames = [ "ieObject", "ieOp", "ieDrawable", "ieDisplay", "ieLiveScene" ] # nuke library nukeEnv.Append( LIBS = [ "boost_signals" + env["BOOST_LIB_SUFFIX"] ] ) diff --git a/include/IECoreNuke/LiveSceneHolder.h b/include/IECoreNuke/LiveSceneHolder.h new file mode 100644 index 0000000000..c998fa9e00 --- /dev/null +++ b/include/IECoreNuke/LiveSceneHolder.h @@ -0,0 +1,70 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IECORENUKE_LIVESCENEHOLDER_H +#define IECORENUKE_LIVESCENEHOLDER_H + +#include "DDImage/GeoOp.h" + +#include "IECoreNuke/Export.h" +#include "IECoreNuke/LiveScene.h" + + +namespace IECoreNuke +{ + +/// This Op does no processing, but simply provides a single LiveSceneKnob. +/// This is mainly used for the LiveSceneKnob test cases. +class IECORENUKE_API LiveSceneHolder : public DD::Image::GeoOp +{ + + public : + + LiveSceneHolder( Node *node ); + virtual ~LiveSceneHolder(); + + virtual void knobs( DD::Image::Knob_Callback f ); + virtual const char *Class() const; + virtual const char *node_help() const; + + private : + + static const Description g_description; + static DD::Image::Op *build( Node *node ); + +}; + +} // namespace IECoreNuke + +#endif // IECORENUKE_LIVESCENEHOLDER_H diff --git a/src/IECoreNuke/LiveSceneHolder.cpp b/src/IECoreNuke/LiveSceneHolder.cpp new file mode 100644 index 0000000000..f5630d614f --- /dev/null +++ b/src/IECoreNuke/LiveSceneHolder.cpp @@ -0,0 +1,72 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "IECoreNuke/LiveSceneHolder.h" + +#include "IECoreNuke/LiveSceneKnob.h" + +using namespace IECoreNuke; + +const DD::Image::Op::Description LiveSceneHolder::g_description( "ieLiveScene", build ); + +LiveSceneHolder::LiveSceneHolder( Node *node ) + : DD::Image::GeoOp( node ) +{ +} + +LiveSceneHolder::~LiveSceneHolder() +{ +} + +void LiveSceneHolder::knobs( DD::Image::Knob_Callback f ) +{ + Op::knobs( f ); + + LiveSceneKnob::sceneKnob( f, this, "scene", "Scene" ); +} + +DD::Image::Op *LiveSceneHolder::build( Node *node ) +{ + return new LiveSceneHolder( node ); +} + +const char *LiveSceneHolder::Class() const +{ + return g_description.name; +} + +const char *LiveSceneHolder::node_help() const +{ + return "Holds cortex live scene on the \"scene\" knob."; +} diff --git a/src/IECoreNuke/plugin/menu.py b/src/IECoreNuke/plugin/menu.py index 9ab7609e0a..2233b8cbb3 100644 --- a/src/IECoreNuke/plugin/menu.py +++ b/src/IECoreNuke/plugin/menu.py @@ -52,5 +52,6 @@ cortexMenu = nodesMenu.addMenu( "Cortex", icon="CortexMenu.png" ) cortexMenu.addCommand( "Display", "nuke.createNode( 'ieDisplay' )" ) + cortexMenu.addCommand( "LiveScene", "nuke.createNode( 'ieLiveScene' )" ) cortexMenu.addCommand( "LensDistort", "nuke.createNode( 'ieLensDistort' )" ) cortexMenu.addCommand( "SceneCacheReader", "nuke.createNode( 'ieSceneCacheReader' )" ) From ef7a09a5a6cfdd408b37db0464ac1108fae069c6 Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Wed, 12 Oct 2022 15:46:17 -0700 Subject: [PATCH 27/36] LiveSceneKnobTest: Added test for LiveSceneKnob and LiveSceneHolder node --- test/IECoreNuke/All.py | 1 + test/IECoreNuke/LiveSceneKnobTest.py | 331 ++++++++++++++++++ .../scripts/data/animatedTransform.scc | Bin 0 -> 19033 bytes .../IECoreNuke/scripts/data/liveSceneData.scc | Bin 0 -> 189650 bytes 4 files changed, 332 insertions(+) create mode 100644 test/IECoreNuke/LiveSceneKnobTest.py create mode 100644 test/IECoreNuke/scripts/data/animatedTransform.scc create mode 100644 test/IECoreNuke/scripts/data/liveSceneData.scc diff --git a/test/IECoreNuke/All.py b/test/IECoreNuke/All.py index a1ed4da447..1f46312623 100644 --- a/test/IECoreNuke/All.py +++ b/test/IECoreNuke/All.py @@ -48,6 +48,7 @@ from OpHolderTest import OpHolderTest from SceneCacheReaderTest import SceneCacheReaderTest from PNGReaderTest import PNGReaderTest +from LiveSceneKnobTest import LiveSceneKnobTest unittest.TestProgram( testRunner = unittest.TextTestRunner( diff --git a/test/IECoreNuke/LiveSceneKnobTest.py b/test/IECoreNuke/LiveSceneKnobTest.py new file mode 100644 index 0000000000..65c0bd3e3c --- /dev/null +++ b/test/IECoreNuke/LiveSceneKnobTest.py @@ -0,0 +1,331 @@ +########################################################################## +# +# Copyright (c) 2022, 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 Image Engine Design nor the names of any +# other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import unittest + +import nuke + +import IECore +import IECoreNuke + +class LiveSceneKnobTest( IECoreNuke.TestCase ) : + + def testNameAndLabel( self ) : + + n = nuke.createNode( "ieLiveScene" ) + + k = n.knob( "scene" ) + + self.assertEqual( k.name(), "scene" ) + self.assertEqual( k.label(), "Scene" ) + + def testAccessors( self ) : + + n = nuke.createNode( "ieLiveScene" ) + + k = n.knob( "scene" ) + + self.assertTrue( isinstance( k, IECoreNuke.LiveSceneKnob ) ) + + self.assertTrue( isinstance( k.getValue(), IECoreNuke.LiveScene ) ) + + def testQueryScene( self ) : + + sph = nuke.createNode( "Sphere" ) + cub = nuke.createNode( "Cube" ) + sn = nuke.createNode( "Scene") + sn.setInput( 0, sph ) + sn.setInput( 1, cub ) + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sn ) + + k = n.knob( "scene" ) + + self.assertTrue( isinstance( k, IECoreNuke.LiveSceneKnob ) ) + + self.assertTrue( isinstance( k.getValue(), IECoreNuke.LiveScene ) ) + + def testNameAndPath( self ): + import IECoreScene + sph = nuke.createNode( "Sphere" ) + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sph ) + + liveScene = n.knob( "scene" ).getValue() + + self.assertEqual( liveScene.name(), "/" ) + self.assertEqual( liveScene.path(), [] ) + + sceneFile = "test/IECoreNuke/scripts/animatedSpheres.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + n.setInput( 0, sceneReader ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/A/a', '/root/B/b'] ) + + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + self.assertEqual( liveScene.name(), expectedScene.name() ) + self.assertEqual( liveScene.path(), expectedScene.path() ) + + sceneA = liveScene.scene( ["A"] ) + expectedSceneA = expectedScene.scene( ["A"] ) + + self.assertEqual( sceneA.name(), expectedSceneA.name() ) + self.assertEqual( sceneA.path(), expectedSceneA.path() ) + + sceneBb = liveScene.scene( ["B", "b"] ) + expectedSceneBb = expectedScene.scene( ["B", "b"] ) + + self.assertEqual( sceneBb.name(), expectedSceneBb.name() ) + self.assertEqual( sceneBb.path(), expectedSceneBb.path() ) + + def testChildNames( self ): + import IECoreScene + + sceneFile = "test/IECoreNuke/scripts/data/liveSceneData.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/A/a', '/root/B/b'] ) + + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sceneReader ) + + liveScene = n.knob( "scene" ).getValue() + + self.assertEqual( sorted( liveScene.childNames() ) , sorted( expectedScene.childNames() ) ) + + self.assertTrue( liveScene.hasChild( "B" ) ) + self.assertFalse( liveScene.hasChild( "wrongChild" ) ) + + self.assertRaises( RuntimeError, liveScene.child, "wrongChild" ) + self.assertFalse( liveScene.child( "wrongChild", IECoreScene.SceneInterface.MissingBehaviour.NullIfMissing ) ) + self.assertRaises( RuntimeError, liveScene.child, "wrongChild", IECoreScene.SceneInterface.MissingBehaviour.CreateIfMissing ) + + for subPath in ( ["B"], ["A"] ): + subScene = liveScene.scene( subPath ) + subExpectedScene = expectedScene.scene( subPath ) + + self.assertEqual( subScene.childNames(), subExpectedScene.childNames() ) + + self.assertRaises( RuntimeError, liveScene.scene, ["B", "wrongChild"] ) + self.assertFalse( liveScene.scene( ["B", "wrongChild"], IECoreScene.SceneInterface.MissingBehaviour.NullIfMissing ) ) + self.assertRaises( RuntimeError, liveScene.scene, ["B", "wrongChild"], IECoreScene.SceneInterface.MissingBehaviour.CreateIfMissing ) + + def assertAlmostEqualBound( self, box1, box2 ): + for dim in range( 3 ): + self.assertAlmostEqual( box1.size()[dim], box2.size()[dim], 4 ) + + def testBounds( self ): + import IECoreScene + + sceneFile = "test/IECoreNuke/scripts/data/liveSceneData.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/A/a', '/root/B/b'] ) + + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sceneReader ) + + liveScene = n.knob( "scene" ).getValue() + for time in (0, 1, 2, 3): + liveSceneBound = liveScene.readBound( time ) + expectedSceneBound = expectedScene.readBound( time ) + self.assertAlmostEqualBound( liveSceneBound, expectedSceneBound ) + + for subPath in ( ["B"], ["A"] ): + subScene = liveScene.scene( subPath ) + subExpectedScene = expectedScene.scene( subPath ) + + self.assertEqual( subScene.readBound(0), subExpectedScene.readBound(0) ) + + def testAnimatedBounds( self ): + import IECoreScene + + sceneFile = "test/IECoreNuke/scripts/data/animatedTransform.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/group/cube'] ) + + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sceneReader ) + + liveScene = n.knob( "scene" ).getValue() + for time in (0, 1, 2): + liveSceneBound = liveScene.readBound( time ) + expectedSceneBound = expectedScene.readBound( time ) + self.assertAlmostEqualBound( liveSceneBound, expectedSceneBound ) + + for subPath in ( ["group"], ["group", "cube"] ): + subScene = liveScene.scene( subPath ) + subExpectedScene = expectedScene.scene( subPath ) + + self.assertEqual( subScene.readBound(0), subExpectedScene.readBound(0) ) + + def assertAlmostEqualTransform( self, tran1, tran2 ): + for x in range( 4 ): + for y in range( 4 ): + self.assertAlmostEqual( tran1[x][y], tran2[x][y], 4 ) + + def _checkNukeSceneTransform( self, liveScene, expectedScene, matrix, time ): + import imath + # combine the matrix of each parent as the SceneCacheReader does + matrix = expectedScene.readTransform( time ).value * matrix + if liveScene.childNames(): + # each parent location have an identity matrix + self.assertEqual( liveScene.readTransform( time ).value.transform, imath.M44d() ) + for childName in liveScene.childNames(): + self._checkNukeSceneTransform( liveScene.child( childName ), expectedScene.child( childName ), matrix, time ) + else: + # check the leaf matrix is the same as the combined parents' matrix + self.assertAlmostEqualTransform( liveScene.readTransform( time ).value.transform, matrix ) + + def testTransform( self ): + import IECoreScene + + sceneFile = "test/IECoreNuke/scripts/data/liveSceneData.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/A/a', '/root/B/b'] ) + + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sceneReader ) + + liveScene = n.knob( "scene" ).getValue() + for time in (0, 1, 2, 3): + liveSceneTransform = liveScene.readTransform( time ) + expectedSceneTransform = expectedScene.readTransform( time ) + self.assertEqual( liveSceneTransform.value.transform, expectedSceneTransform.value ) + + # check the leaf has the world matrix + for childName in liveScene.childNames(): + # root transform + matrix = expectedScene.readTransform( 0 ).value + self._checkNukeSceneTransform( liveScene.child( childName ), expectedScene.child( childName ), matrix, 0 ) + + def testAnimatedTransform( self ): + import IECoreScene + + sceneFile = "test/IECoreNuke/scripts/data/animatedTransform.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/group/cube'] ) + + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sceneReader ) + + liveScene = n.knob( "scene" ).getValue() + for time in (0, 1, 2): + liveSceneTransform = liveScene.readTransform( time ) + expectedSceneTransform = expectedScene.readTransform( time ) + self.assertEqual( liveSceneTransform.value.transform, expectedSceneTransform.value ) + + # check the leaf has the world matrix + for childName in liveScene.childNames(): + # root transform + matrix = expectedScene.readTransform( 0 ).value + self._checkNukeSceneTransform( liveScene.child( childName ), expectedScene.child( childName ), matrix, 0 ) + + def testHasObject( self ): + import IECoreScene + + sceneFile = "test/IECoreNuke/scripts/data/liveSceneData.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/A/a', '/root/B/b'] ) + + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sceneReader ) + + liveScene = n.knob( "scene" ).getValue() + self.assertFalse( liveScene.hasObject() ) + + for subPath in ( ["A"], ["B"] ): + self.assertFalse( liveScene.scene( subPath ).hasObject() ) + + for subPath in ( ["A", "a"], ["B", "b"] ): + self.assertTrue( liveScene.scene( subPath ).hasObject() ) + + def testReadObjet( self ): + import IECoreScene + + sceneFile = "test/IECoreNuke/scripts/data/liveSceneData.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/A/a', '/root/B/b'] ) + + n = nuke.createNode( "ieLiveScene" ) + n.setInput( 0, sceneReader ) + + liveScene = n.knob( "scene" ).getValue() + objectA = liveScene.scene( [ "A", "a" ] ).readObject( 0 ) + expectedObjectA = expectedScene.scene( [ "A", "a" ] ).readObject( 0 ) + + self.assertEqual( objectA.topologyHash(), expectedObjectA.topologyHash() ) + self.assertEqual( objectA.keys(), [ "P", "uv" ] ) + + +if __name__ == "__main__": + unittest.main() + diff --git a/test/IECoreNuke/scripts/data/animatedTransform.scc b/test/IECoreNuke/scripts/data/animatedTransform.scc new file mode 100644 index 0000000000000000000000000000000000000000..5012579de1ebdc6e9dacf6aa8b5d057a03ccbc36 GIT binary patch literal 19033 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

Pbkk%3FSlc#D3=Wjye{i6VVas$Lm+s^AE@oim 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 Date: Fri, 21 Oct 2022 21:24:54 -0700 Subject: [PATCH 28/36] SceneCacheWriter: Plugin for nuke WriteGeo to write SceneCache file --- SConstruct | 9 +- include/IECoreNuke/SceneCacheWriter.h | 73 +++++++++++++++++ src/IECoreNuke/SceneCacheWriter.cpp | 114 ++++++++++++++++++++++++++ 3 files changed, 194 insertions(+), 2 deletions(-) create mode 100644 include/IECoreNuke/SceneCacheWriter.h create mode 100644 src/IECoreNuke/SceneCacheWriter.cpp diff --git a/SConstruct b/SConstruct index b687e2d21f..b009dbfba3 100644 --- a/SConstruct +++ b/SConstruct @@ -2656,7 +2656,7 @@ if doConfigure : nukePythonSources = sorted( glob.glob( "src/IECoreNuke/bindings/*.cpp" ) ) nukePythonScripts = glob.glob( "python/IECoreNuke/*.py" ) nukePluginSources = sorted( glob.glob( "src/IECoreNuke/plugin/*.cpp" ) ) - nukeNodeNames = [ "ieObject", "ieOp", "ieDrawable", "ieDisplay", "ieLiveScene" ] + nukeNodeNames = [ "ieObject", "ieOp", "ieDrawable", "ieDisplay", "ieLiveScene", "sccWriter" ] # nuke library nukeEnv.Append( LIBS = [ "boost_signals" + env["BOOST_LIB_SUFFIX"] ] ) @@ -2717,7 +2717,12 @@ if doConfigure : for nodeName in nukeNodeNames : nukeStubEnv = nukePluginEnv.Clone( IECORE_NAME=nodeName ) - nukeStubName = "plugins/nuke/" + os.path.basename( nukeStubEnv.subst( "$INSTALL_NUKEPLUGIN_NAME" ) ) + ".tcl" + # In order to have our custom file format (scc) displayed in the file_type knob of the WriteGeo node, we need to install + # a dummy library with "[fileExtension]Writer" + if nodeName == "sccWriter": + nukeStubName = "plugins/nuke/" + os.path.basename( nukeStubEnv.subst( "$INSTALL_NUKEPLUGIN_NAME$SHLIBSUFFIX" ) ) + else: + nukeStubName = "plugins/nuke/" + os.path.basename( nukeStubEnv.subst( "$INSTALL_NUKEPLUGIN_NAME" ) ) + ".tcl" nukeStub = nukePluginEnv.Command( nukeStubName, nukePlugin, "echo 'load ieCore' > $TARGET" ) nukeStubInstall = nukeStubEnv.Install( os.path.dirname( nukeStubEnv.subst( "$INSTALL_NUKEPLUGIN_NAME" ) ), nukeStub ) nukeStubEnv.Alias( "install", nukeStubInstall ) diff --git a/include/IECoreNuke/SceneCacheWriter.h b/include/IECoreNuke/SceneCacheWriter.h new file mode 100644 index 0000000000..ebc7aaa550 --- /dev/null +++ b/include/IECoreNuke/SceneCacheWriter.h @@ -0,0 +1,73 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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. +// +////////////////////////////////////////////////////////////////////////// + +#ifndef IECORENUKE_SCENECACHE_WRITER_H +#define IECORENUKE_SCENECACHE_WRITER_H + +#include "IECoreNuke/Export.h" +#include "IECoreNuke/LiveScene.h" + +#include "DDImage/GeoWriter.h" +#include "DDImage/Scene.h" + +namespace IECoreNuke +{ + +/// A class to support writing SceneCache supported files out of Nuke using the WriteGeo node. +class IECORENUKE_API SceneCacheWriter : public DD::Image::GeoWriter +{ + public : + + SceneCacheWriter( DD::Image::WriteGeo* writeNode ); + + void execute( DD::Image::Scene& scene ) override; + bool open(); + + static DD::Image::GeoWriter* Build( DD::Image::WriteGeo* readNode ); + static DD::Image::GeoWriter::Description description; + + bool animation() const override; + + private : + + void writeLocation( IECoreScene::ConstSceneInterfacePtr inScene, IECoreScene::SceneInterfacePtr outScene, const IECore::InternedString& childName ); + + IECoreNuke::LiveScenePtr m_liveScene; + IECoreScene::SceneInterfacePtr m_writer; + +}; + +} // namespace IECoreNuke + +#endif // IECORENUKE_SCENECACHE_WRITER_H diff --git a/src/IECoreNuke/SceneCacheWriter.cpp b/src/IECoreNuke/SceneCacheWriter.cpp new file mode 100644 index 0000000000..c78616cc1f --- /dev/null +++ b/src/IECoreNuke/SceneCacheWriter.cpp @@ -0,0 +1,114 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2022, 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 Image Engine Design 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 "IECoreNuke/SceneCacheWriter.h" + +using namespace IECore; +using namespace IECoreNuke; +using namespace IECoreScene; + +DD::Image::GeoWriter::Description SceneCacheWriter::description( "scc\0", SceneCacheWriter::Build ); + +DD::Image::GeoWriter* SceneCacheWriter::Build( DD::Image::WriteGeo* readNode ) +{ + return new SceneCacheWriter( readNode ); +} + +SceneCacheWriter::SceneCacheWriter( DD::Image::WriteGeo* writeNode ) : + DD::Image::GeoWriter( writeNode ) +{ +} + +void SceneCacheWriter::execute( DD::Image::Scene& scene ) +{ + open(); + if ( auto geoOp = dynamic_cast( geo ) ) + { + m_liveScene = new IECoreNuke::LiveScene( geoOp ); + } + + IECoreScene::SceneInterface::NameList names; + m_liveScene->childNames( names ); + for ( const auto& name : names ) + { + writeLocation( m_liveScene, m_writer, name ); + } +} + +bool SceneCacheWriter::animation() const +{ + return true; +} + +bool SceneCacheWriter::open() +{ + if ( !m_writer ) + { + m_writer = IECoreScene::SceneInterface::create( std::string( filename() ), IECore::IndexedIO::Write ); + } + + return true; +} + +void SceneCacheWriter::writeLocation( ConstSceneInterfacePtr inScene, SceneInterfacePtr outScene, const IECore::InternedString& childName ) +{ + auto time = LiveScene::frameToTime( frame() ); + ConstSceneInterfacePtr inChild = inScene->child( childName, SceneInterface::MissingBehaviour::ThrowIfMissing ); + auto outChild = outScene->child( childName, IECoreScene::SceneInterface::MissingBehaviour::CreateIfMissing ); + try + { + outChild->writeTransform( inChild->readTransform( time ).get(), time ); + } + catch ( ... ) + { + } + if ( inChild->hasObject() ) + { + try + { + outChild->writeObject( inChild->readObject( time ).get(), time ); + } + catch ( ... ) + { + } + } + + // recursion + SceneInterface::NameList grandChildNames; + inChild->childNames( grandChildNames ); + for ( auto& grandChildName : grandChildNames ) + { + writeLocation( inChild, outChild, grandChildName ); + } +} From b56b9b88db32a5a5c54d58c601c6248d2691c3bb Mon Sep 17 00:00:00 2001 From: Lucien Fostier Date: Mon, 24 Oct 2022 17:49:45 -0700 Subject: [PATCH 29/36] SceneCacheWriterTest: Test for SceneCacheWriter plugin. --- test/IECoreNuke/All.py | 1 + test/IECoreNuke/SceneCacheWriterTest.py | 128 ++++++++++++++++++++++++ 2 files changed, 129 insertions(+) create mode 100644 test/IECoreNuke/SceneCacheWriterTest.py diff --git a/test/IECoreNuke/All.py b/test/IECoreNuke/All.py index 1f46312623..e5a564c734 100644 --- a/test/IECoreNuke/All.py +++ b/test/IECoreNuke/All.py @@ -49,6 +49,7 @@ from SceneCacheReaderTest import SceneCacheReaderTest from PNGReaderTest import PNGReaderTest from LiveSceneKnobTest import LiveSceneKnobTest +from SceneCacheWriterTest import SceneCacheWriterTest unittest.TestProgram( testRunner = unittest.TextTestRunner( diff --git a/test/IECoreNuke/SceneCacheWriterTest.py b/test/IECoreNuke/SceneCacheWriterTest.py new file mode 100644 index 0000000000..95dc4af641 --- /dev/null +++ b/test/IECoreNuke/SceneCacheWriterTest.py @@ -0,0 +1,128 @@ +########################################################################## +# +# Copyright (c) 2022, 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 Image Engine Design nor the names of any +# other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import unittest +import os +import shutil +import tempfile + +import nuke + +import imath + +import IECore +import IECoreScene +import IECoreNuke + +class SceneCacheWriterTest( IECoreNuke.TestCase ) : + + def setUp( self ) : + self.__temporaryDirectory = None + + def tearDown( self ) : + if self.__temporaryDirectory is not None : + shutil.rmtree( self.__temporaryDirectory ) + + def temporaryDirectory( self ) : + + if self.__temporaryDirectory is None : + self.__temporaryDirectory = tempfile.mkdtemp( prefix = "ieCoreNukeTest" ) + + return self.__temporaryDirectory + + def testWriteSimpleSphere( self ): + + outputFile = os.path.join( self.temporaryDirectory(), "sphere.scc" ) + + sphere = nuke.createNode( "Sphere" ) + writer = nuke.createNode( "WriteGeo" ) + writer["file"].fromScript( outputFile ) + + nuke.execute( writer, 1001, 1001 ) + + self.assertTrue( os.path.exists( outputFile ) ) + + scene = IECoreScene.SharedSceneInterfaces.get( outputFile ) + + self.assertEqual( scene.childNames(), ["object0"] ) + self.assertEqual( scene.readTransform( 0 ).value, imath.M44d() ) + + liveSceneHolder = nuke.createNode( "ieLiveScene" ) + liveSceneHolder.setInput( 0, sphere ) + + liveScene = liveSceneHolder["scene"].getValue() + + liveSceneMesh = liveScene.scene( ["object0"] ).readObject( 0 ) + mesh = scene.scene( ["object0"] ).readObject( 0 ) + + self.assertEqual( mesh.topologyHash(), liveSceneMesh.topologyHash() ) + + + def testWriteSceneCacheReader( self ): + import random + import IECoreScene + + outputFile = os.path.join( self.temporaryDirectory(), "scene.scc" ) + + sceneFile = "test/IECoreNuke/scripts/data/liveSceneData.scc" + sceneReader = nuke.createNode( "ieSceneCacheReader" ) + sceneReader.knob( "file" ).setValue( sceneFile ) + expectedScene = IECoreScene.SharedSceneInterfaces.get( sceneFile ) + + sceneReader.forceValidate() + widget = sceneReader.knob( "sceneView" ) + widget.setSelectedItems( ['/root/A/a', '/root/B/b'] ) + + writer = nuke.createNode( "WriteGeo" ) + writer["file"].fromScript( outputFile ) + + nuke.execute( writer, 1, 48 ) + + scene = IECoreScene.SharedSceneInterfaces.get( outputFile ) + + self.assertEqual( scene.childNames(), expectedScene.childNames() ) + for time in range( 0, 3 ): + self.assertAlmostEqual( scene.readBound( time ).min(), expectedScene.readBound( time ).min() ) + mesh = scene.scene( ["B", "b"] ).readObject( time ) + expectedMesh = expectedScene.scene( ["B", "b"] ).readObject( time ) + random.seed( 12 ) + for i in range( 12 ): + pointIndex = random.choice( range( len( mesh["P"].data ) ) ) + self.assertAlmostEqual( mesh["P"].data[pointIndex], expectedMesh["P"].data[pointIndex], 4 ) + + +if __name__ == "__main__": + unittest.main() + + From b6182a1f64ffafb013b144147b3a74c3371f2636 Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Thu, 3 Nov 2022 16:28:30 -0700 Subject: [PATCH 30/36] Changes : Updated for PR #1310 --- Changes | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Changes b/Changes index aa0c46fbe6..168690865d 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,13 @@ -10.3.7.x (relative to 10.3.7.2) +10.3.8.x (relative to 10.3.7.2) ======== +Features +-------- + +- IECoreNuke : Add LiveScene support for Nuke (#1310). + - LiveSceneKnob : Knob to interface with LiveScene from Python. + - LiveSceneHolder : Node to hold LiveSceneKnob to provide a Python interface with LiveScene. + Fixes ----- From a4af7e62b955c9d87c3498fa50ad34dae7d7c09a Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Thu, 3 Nov 2022 16:33:18 -0700 Subject: [PATCH 31/36] Changes : Updated for PR #1311 --- Changes | 1 + 1 file changed, 1 insertion(+) diff --git a/Changes b/Changes index 168690865d..daef579f18 100644 --- a/Changes +++ b/Changes @@ -7,6 +7,7 @@ Features - IECoreNuke : Add LiveScene support for Nuke (#1310). - LiveSceneKnob : Knob to interface with LiveScene from Python. - LiveSceneHolder : Node to hold LiveSceneKnob to provide a Python interface with LiveScene. +- IECoreMaya : Add non-drawable `ieSceneShapeProxy` subclassed from `ieSceneShape` (#1311). Fixes ----- From a03c5184cfef162da2ac75a71002c383a0b254be Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Thu, 3 Nov 2022 16:34:20 -0700 Subject: [PATCH 32/36] Changes : v10.3.8.0 --- Changes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changes b/Changes index daef579f18..d7ba0d94ca 100644 --- a/Changes +++ b/Changes @@ -1,4 +1,4 @@ -10.3.8.x (relative to 10.3.7.2) +10.3.8.0 (relative to 10.3.7.2) ======== Features From 13082dc0eb6448237aec0e8fdc1ca63225a596a0 Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Thu, 3 Nov 2022 16:35:27 -0700 Subject: [PATCH 33/36] SConstruct : Bumped to 10.3.8.0 --- SConstruct | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SConstruct b/SConstruct index 44a7dfd1f1..69aeda27b0 100644 --- a/SConstruct +++ b/SConstruct @@ -56,8 +56,8 @@ SConsignFile() ieCoreMilestoneVersion = 10 # for announcing major milestones - may contain all of the below ieCoreMajorVersion = 3 # backwards-incompatible changes -ieCoreMinorVersion = 7 # new backwards-compatible features -ieCorePatchVersion = 2 # bug fixes +ieCoreMinorVersion = 8 # new backwards-compatible features +ieCorePatchVersion = 0 # bug fixes ieCoreVersionSuffix = "" # used for alpha/beta releases. Example: "a1", "b2", etc. ########################################################################################### From 1d48e178a77ca0340b8ea5b5a2c217e1ed850c4c Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Fri, 4 Nov 2022 14:07:17 -0700 Subject: [PATCH 34/36] Changes : Updated for #1307 --- Changes | 1 + 1 file changed, 1 insertion(+) diff --git a/Changes b/Changes index 8fa71330be..d71d6a0ba7 100644 --- a/Changes +++ b/Changes @@ -18,6 +18,7 @@ Fixes ----- - IECoreUSD : Fixed error in `pluginfo.json` preventing USD on Windows from loading `IECoreUSD.dll`. +- IECoreScene : Fixed MeshPrimitiveEvaluator assert typo. Build ----- From 62fbbee8a1d06b6f2a01e74c748c23ec3ff99fa2 Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Fri, 4 Nov 2022 14:56:36 -0700 Subject: [PATCH 35/36] Changes : Updated to 10.4.3.0 --- Changes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Changes b/Changes index d71d6a0ba7..dde0822df9 100644 --- a/Changes +++ b/Changes @@ -1,4 +1,4 @@ -10.4.x.x (relative to 10.4.2.1) +10.4.3.0 (relative to 10.4.2.1) ======== Features From c6c8336af2a3230aa12751c820b1b94eeb9e0fd9 Mon Sep 17 00:00:00 2001 From: Ivan Imanishi Date: Fri, 4 Nov 2022 14:57:09 -0700 Subject: [PATCH 36/36] SConstruct : Bumped to 10.4.3.0 --- SConstruct | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SConstruct b/SConstruct index 4c163acd26..0b0a517c43 100644 --- a/SConstruct +++ b/SConstruct @@ -56,8 +56,8 @@ SConsignFile() ieCoreMilestoneVersion = 10 # for announcing major milestones - may contain all of the below ieCoreMajorVersion = 4 # backwards-incompatible changes -ieCoreMinorVersion = 2 # new backwards-compatible features -ieCorePatchVersion = 1 # bug fixes +ieCoreMinorVersion = 3 # new backwards-compatible features +ieCorePatchVersion = 0 # bug fixes ieCoreVersionSuffix = "" # used for alpha/beta releases. Example: "a1", "b2", etc. ###########################################################################################