From bbf93bb2922719d5c965cadec4e5d2f7e91f671f Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Mon, 16 May 2022 17:56:54 -0700 Subject: [PATCH 01/26] 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/26] 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/26] 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/26] 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/26] 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 2066419adddeca95b53a356fd3ec4453b6e041be Mon Sep 17 00:00:00 2001 From: Eric Mehl Date: Mon, 17 Oct 2022 18:48:34 -0400 Subject: [PATCH 06/26] 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 2911ec633519da3c607480031527766288255458 Mon Sep 17 00:00:00 2001 From: Yannic Schoof Date: Wed, 2 Nov 2022 14:18:59 -0700 Subject: [PATCH 07/26] 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 08/26] 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 09/26] 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 10/26] 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 11/26] 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 12/26] 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 13/26] 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 14/26] 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 15/26] 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 16/26] 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 17/26] 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 18/26] 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 19/26] 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 20/26] 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 21/26] 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 22/26] 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 23/26] 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 24/26] 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 25/26] 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 26/26] 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. ###########################################################################################