diff --git a/Changes b/Changes index 14581c81ef..34f0a871f5 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,33 @@ 10.7.x.x (relative to 10.7.0.0a10) ======== +Features +-------- + +- PointInstancer : Added class to provide first-class encoding of point instancers. + +Improvements +------------ + +- PrimitiveVariable : Added Python bindings for `IndexedView` class. +- USDScene : Added support for writing `IECoreScene::PointInstancer` as `UsdGeomPointInstancer`. + +Fixes +----- + +- PrimitiveVariable : Made IndexedView `begin()` and `end()` const. +- PointsAlgo : + - Fixed loss of blind data in `deletePoints()`. + - Fixed loss of primitive type in `deletePoints()` (if processing a derived class of PointsPrimitive). + +Breaking Changes +---------------- + +- USD : + - Removed support for `IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES` environment variable. Prototype paths are now always specified as relative where possible. + - Removed `./` prefix from relative prototype paths. + - UsdGeomPointInstancer `inactiveIds` are now merged into the `invisibleIds` primitive variable on loading, instead of being loaded separately. + Build ----- diff --git a/contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.h b/contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.h index 1ef5ab0830..d2cf524557 100644 --- a/contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.h +++ b/contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.h @@ -62,6 +62,10 @@ IECOREUSD_API void writePrimitiveVariable( const std::string &name, const IECore IECOREUSD_API void writePrimitiveVariable( const std::string &name, const IECoreScene::PrimitiveVariable &primitiveVariable, const pxr::UsdGeomGprim &gprim, pxr::UsdTimeCode time ); /// As above, but redirects "P", "N" etc to the relevant attributes of `pointBased`. IECOREUSD_API void writePrimitiveVariable( const std::string &name, const IECoreScene::PrimitiveVariable &primitiveVariable, pxr::UsdGeomPointBased &pointBased, pxr::UsdTimeCode time ); + +/// Expands an IndexedView into a VtArray. +template +IECOREUSD_API pxr::VtValue toUSDExpanded( const IECoreScene::PrimitiveVariable::IndexedView &view ); /// Equivalent to `DataAlgo::toUSD( primitiveVariable.expandedData() )`, but avoiding /// the creation of the temporary expanded data. IECOREUSD_API pxr::VtValue toUSDExpanded( const IECoreScene::PrimitiveVariable &primitiveVariable, bool arrayRequired = false ); @@ -89,4 +93,6 @@ IECOREUSD_API IECoreScene::PrimitiveVariable::Interpolation fromUSD( pxr::TfToke } // namespace IECoreUSD +#include "IECoreUSD/PrimitiveAlgo.inl" + #endif // IECOREUSD_PRIMITIVEALGO_H diff --git a/contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.inl b/contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.inl new file mode 100644 index 0000000000..72b4e021b1 --- /dev/null +++ b/contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.inl @@ -0,0 +1,64 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of 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 IECOREUSD_PRIMITIVEALGO_INL +#define IECOREUSD_PRIMITIVEALGO_INL + +#include "IECoreUSD/DataAlgo.h" + +IECORE_PUSH_DEFAULT_VISIBILITY +#include "pxr/base/gf/quatf.h" +#include "pxr/base/gf/quatd.h" +IECORE_POP_DEFAULT_VISIBILITY + +namespace IECoreUSD::PrimitiveAlgo +{ + +template +pxr::VtValue toUSDExpanded( const IECoreScene::PrimitiveVariable::IndexedView &view ) +{ + using USDType = typename CortexTypeTraits::USDType; + pxr::VtArray array; + array.reserve( view.size() ); + for( const auto &e : view ) + { + array.push_back( DataAlgo::toUSD( static_cast( e ) ) ); + } + + return pxr::VtValue( array ); +} + +} // namespace IECoreUSD::PrimitiveAlgo + +#endif // IECOREUSD_PRIMITIVEALGO_INL diff --git a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp index de9f01c88b..ef51878307 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp @@ -36,12 +36,15 @@ #include "IECoreUSD/ObjectAlgo.h" #include "IECoreUSD/PrimitiveAlgo.h" -#include "IECoreScene/PointsPrimitive.h" +#include "IECoreScene/PointInstancer.h" IECORE_PUSH_DEFAULT_VISIBILITY #include "pxr/usd/usdGeom/pointInstancer.h" IECORE_POP_DEFAULT_VISIBILITY +#include "boost/algorithm/string/predicate.hpp" +#include "boost/container/flat_set.hpp" + using namespace IECore; using namespace IECoreScene; using namespace IECoreUSD; @@ -53,19 +56,6 @@ using namespace IECoreUSD; namespace { -bool checkEnvFlag( const char *envVar, bool def ) -{ - const char *value = getenv( envVar ); - if( value ) - { - return std::string( value ) != "0"; - } - else - { - return def; - } -} - IECore::ObjectPtr readPointInstancer( pxr::UsdGeomPointInstancer &pointInstancer, pxr::UsdTimeCode time, const Canceller *canceller ) { pxr::VtVec3fArray pointsData; @@ -73,8 +63,8 @@ IECore::ObjectPtr readPointInstancer( pxr::UsdGeomPointInstancer &pointInstancer Canceller::check( canceller ); IECore::V3fVectorDataPtr positionData = DataAlgo::fromUSD( pointsData ); - positionData->setInterpretation( GeometricData::Point ); - IECoreScene::PointsPrimitivePtr newPoints = new IECoreScene::PointsPrimitive( positionData ); + IECoreScene::PointInstancerPtr newPoints = new IECoreScene::PointInstancer( positionData->readable().size() ); + newPoints->setPosition( positionData ); // Per point attributes @@ -99,29 +89,49 @@ IECore::ObjectPtr readPointInstancer( pxr::UsdGeomPointInstancer &pointInstancer Canceller::check( canceller ); PrimitiveAlgo::readPrimitiveVariable( pointInstancer.GetAngularVelocitiesAttr(), time, newPoints.get(), "angularVelocity" ); + // Inactive and invisible IDs. These both do the same thing - prevent specific instances + // from rendering. Inactive IDs are metadata-based and therefore can't be animated. + // Invisible IDs are attribute-based and therefore can be animated. Since we're converting + // to Cortex PrimitiveVariables, the distinction is irrelevant - all PrimitiveVariables can + // be animated. Our PointInstancer therefore just has invisible IDs, which we merge both + // USD properties into. + + IECore::Int64VectorDataPtr invisibleIds; if( pointInstancer.GetInvisibleIdsAttr().HasAuthoredValue() ) { - DataPtr cortexInvisIds = DataAlgo::fromUSD( pointInstancer.GetInvisibleIdsAttr(), time, true ); - if( cortexInvisIds ) - { - newPoints->variables["invisibleIds"] = IECoreScene::PrimitiveVariable( - PrimitiveVariable::Constant, cortexInvisIds - ); - } + invisibleIds = IECore::runTimeCast( + DataAlgo::fromUSD( pointInstancer.GetInvisibleIdsAttr(), time, true ) + ); } pxr::SdfInt64ListOp inactiveIdsListOp; if( pointInstancer.GetPrim().GetMetadata( pxr::UsdGeomTokens->inactiveIds, &inactiveIdsListOp ) ) { - newPoints->variables["inactiveIds"] = IECoreScene::PrimitiveVariable( - PrimitiveVariable::Constant, - new IECore::Int64VectorData( inactiveIdsListOp.GetExplicitItems() ) - ); + const std::vector &inactiveIds = inactiveIdsListOp.GetExplicitItems(); + if( inactiveIds.size() ) + { + if( invisibleIds ) + { + invisibleIds->writable().insert( + invisibleIds->writable().end(), + inactiveIds.begin(), inactiveIds.end() + ); + std::sort( invisibleIds->writable().begin(), invisibleIds->writable().end() ); + invisibleIds->writable().erase( + std::unique( invisibleIds->writable().begin(), invisibleIds->writable().end() ), + invisibleIds->writable().end() + ); + } + else + { + invisibleIds = new Int64VectorData( inactiveIds ); + } + } } - // Prototype paths + newPoints->setInvisibleIDs( invisibleIds ); - const static bool g_relativePrototypes = checkEnvFlag( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", false ); + // Prototype paths pxr::SdfPathVector targets; Canceller::check( canceller ); @@ -134,17 +144,13 @@ IECore::ObjectPtr readPointInstancer( pxr::UsdGeomPointInstancer &pointInstancer prototypeRoots.reserve( targets.size() ); for( const auto &t : targets ) { - if( !g_relativePrototypes || !t.HasPrefix( primPath ) ) + if( !t.HasPrefix( primPath ) ) { prototypeRoots.push_back( t.GetString() ); } else { - // The ./ prefix shouldn't be necessary - we want to just use the absence of a leading - // slash to indicate relative paths. We can remove the prefix here once we deprecate the - // GAFFERSCENE_INSTANCER_EXPLICIT_ABSOLUTE_PATHS env var and have Gaffer always require a leading - // slash for absolute paths. - prototypeRoots.push_back( "./" + t.MakeRelativePath( primPath ).GetString() ); + prototypeRoots.push_back( t.MakeRelativePath( primPath ).GetString() ); } } @@ -178,3 +184,100 @@ bool pointInstancerMightBeTimeVarying( pxr::UsdGeomPointInstancer &instancer ) ObjectAlgo::ReaderDescription g_pointInstancerReaderDescription( pxr::TfToken( "PointInstancer" ), readPointInstancer, pointInstancerMightBeTimeVarying ); } // namespace + +////////////////////////////////////////////////////////////////////////// +// Writing +////////////////////////////////////////////////////////////////////////// + +namespace +{ + +const boost::container::flat_set g_exportedAsAttributes = { + "prototypeRoots", "prototypeIndex", "P", "scale", "orientation", + "id", "invisibleIds" +}; + +bool writePointInstancer( const IECoreScene::PointInstancer *instancer, const pxr::UsdStagePtr &stage, const pxr::SdfPath &path, pxr::UsdTimeCode time ) +{ + auto usdInstancer = pxr::UsdGeomPointInstancer::Define( stage, path ); + + // Export primitive variables with special meaning to attributes + // of the UsdGeomPointInstancer. + + if( auto prototypes = instancer->getPrototypes() ) + { + pxr::SdfPathVector targets; + targets.reserve( prototypes.size() ); + for( const auto &prototype : prototypes ) + { + pxr::SdfPath prototypePath; + if( boost::starts_with( prototype, "./" ) ) + { + prototypePath = pxr::SdfPath( prototype.substr( 2 ) ); + } + else + { + prototypePath = pxr::SdfPath( prototype ); + } + if( !prototypePath.IsAbsolutePath() ) + { + prototypePath = prototypePath.MakeAbsolutePath( path ); + } + targets.push_back( prototypePath ); + } + usdInstancer.CreatePrototypesRel().SetTargets( targets ); + } + + if( auto prototypeIndex = instancer->getPrototypeIndex() ) + { + usdInstancer.CreateProtoIndicesAttr().Set( PrimitiveAlgo::toUSDExpanded( prototypeIndex ), time ); + } + + if( auto position = instancer->getPosition() ) + { + usdInstancer.CreatePositionsAttr().Set( PrimitiveAlgo::toUSDExpanded( position ), time ); + } + + if( auto orientation = instancer->getOrientation() ) + { + // USD uses `half` for orientation, but Cortex only has a data type for + // `float` quaternions, so convert. + pxr::VtArray usdOrientation; + usdOrientation.reserve( orientation.size() ); + for( const auto &o : orientation ) + { + usdOrientation.push_back( pxr::GfQuath( DataAlgo::toUSD( o ) ) ); + } + usdInstancer.CreateOrientationsAttr().Set( usdOrientation, time ); + } + + if( auto scale = instancer->getScale() ) + { + usdInstancer.CreateScalesAttr().Set( PrimitiveAlgo::toUSDExpanded( scale ), time ); + } + + if( auto id = instancer->getID() ) + { + usdInstancer.CreateIdsAttr().Set( PrimitiveAlgo::toUSDExpanded( id ), time ); + } + + if( auto invisibleIds = instancer->getInvisibleIDs() ) + { + usdInstancer.CreateInvisibleIdsAttr().Set( PrimitiveAlgo::toUSDExpanded( invisibleIds ), time ); + } + + for( const auto &[name, primitiveVariable] : instancer->variables ) + { + if( g_exportedAsAttributes.count( name ) ) + { + continue; + } + PrimitiveAlgo::writePrimitiveVariable( name, primitiveVariable, pxr::UsdGeomPrimvarsAPI( usdInstancer ), time ); + } + + return true; +} + +ObjectAlgo::WriterDescription g_pointInstancerWriterDescription( writePointInstancer ); + +} // namespace diff --git a/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp index 95a6cd15c7..7cb620dac4 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp @@ -153,17 +153,8 @@ struct VtValueFromExpandedData template VtValue operator()( const IECore::TypedData> *data, const IECore::IntVectorData *indices, typename std::enable_if::USDType>::value>::type *enabler = nullptr ) const { - using USDType = typename CortexTypeTraits::USDType; - using ArrayType = VtArray; - ArrayType array; - array.reserve( indices->readable().size() ); - // Using universal reference (`&&`) for iteration for compatibility with the - // non-standard proxy returned by `vector`. - for( auto &&e : PrimitiveVariable::IndexedView( data->readable(), &indices->readable() ) ) - { - array.push_back( DataAlgo::toUSD( static_cast( e ) ) ); - } - return VtValue( array ); + PrimitiveVariable::IndexedView view( data->readable(), &indices->readable() ); + return IECoreUSD::PrimitiveAlgo::toUSDExpanded( view ); } VtValue operator()( const IECore::Data *data, const IECore::IntVectorData *indices ) const diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp index 24dbfcab1e..31c5791004 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp @@ -265,6 +265,10 @@ using PrimPredicate = bool (pxr::UsdPrim::*)() const; boost::container::flat_map g_schemaTypeSetPredicates = { { pxr::TfToken( "__cameras" ), &pxr::UsdPrim::IsA }, { pxr::TfToken( "__lights" ), &pxr::UsdPrim::HasAPI }, + /// \todo This was introduced before we had `IECoreScene::PointInstancer`, to allow + /// us to tell which `IECoreScene::PointsPrimitives` came from UsdGeomPointInstancer. + /// We'll need to keep it for backwards compatibility for a while, but it might make + /// sense to remove it at some point. { pxr::TfToken( "usd:pointInstancers" ), &pxr::UsdPrim::IsA } }; diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index 0a6c86995e..debde3c6e4 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -3924,7 +3924,7 @@ def testPointInstancerPrimvars( self ) : self.assertEqual( points.keys(), ['P', 'myColor', 'prototypeRoots'] ) - self.assertIsInstance( points, IECoreScene.PointsPrimitive ) + self.assertIsInstance( points, IECoreScene.PointInstancer ) self.assertIn( "myColor", points ) self.assertEqual( points["myColor"].data, @@ -3935,7 +3935,7 @@ def testPointInstancerPrimvars( self ) : # Now try deactivating some ids - pointInstancer.DeactivateIds( [ 0, 2 ] ) + pointInstancer.DeactivateIds( [ 0, 1, 2 ] ) pointInstancer.InvisIds( [ 1, 4 ], 0 ) stage.GetRootLayer().Save() @@ -3943,10 +3943,9 @@ def testPointInstancerPrimvars( self ) : root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) points = root.child( "points" ).readObject( 0 ) - self.assertEqual( points.keys(), ['P', 'inactiveIds', 'invisibleIds', 'myColor', 'prototypeRoots'] ) + self.assertEqual( points.keys(), ['P', 'invisibleIds', 'myColor', 'prototypeRoots'] ) - self.assertEqual( points["inactiveIds"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.Int64VectorData( [ 0, 2 ] ) ) ) - self.assertEqual( points["invisibleIds"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.Int64VectorData( [ 1, 4 ] ) ) ) + self.assertEqual( points["invisibleIds"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, IECore.Int64VectorData( [ 0, 1, 2, 4 ] ) ) ) def testArnoldArrayInputs( self ) : @@ -4614,7 +4613,7 @@ def testAssetPathSlashes ( self ) : self.assertNotIn( "\\", xform.readAttribute( "render:testAsset", 0 ).value ) self.assertTrue( pathlib.Path( xform.readAttribute( "render:testAsset", 0 ).value ).is_file() ) - def _testPointInstancerRelativePrototypes( self ) : + def testPointInstancerRelativePrototypes( self ) : root = IECoreScene.SceneInterface.create( os.path.join( os.path.dirname( __file__ ), "data", "pointInstancerWeirdPrototypes.usda" ), @@ -4623,30 +4622,39 @@ def _testPointInstancerRelativePrototypes( self ) : pointInstancer = root.child( "inst" ) obj = pointInstancer.readObject(0.0) - if os.environ.get( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", "0" ) != "0" : - self.assertEqual( obj["prototypeRoots"].data, IECore.StringVectorData( [ './Prototypes/sphere', '/cube' ] ) ) - else : - self.assertEqual( obj["prototypeRoots"].data, IECore.StringVectorData( [ '/inst/Prototypes/sphere', '/cube' ] ) ) + self.assertEqual( obj["prototypeRoots"].data, IECore.StringVectorData( [ 'Prototypes/sphere', '/cube' ] ) ) - def testPointInstancerRelativePrototypes( self ) : + def testPointInstancerRoundTrip( self ) : - for relative in [ "0", "1", None ] : + instancer = IECoreScene.PointInstancer( 4 ) + instancer.setPrototypes( IECore.StringVectorData( [ "prototypes/p1", "prototypes/p2" ] ) ) + instancer.setPrototypeIndex( IECore.IntVectorData( [ 0, 1, 0, 1 ] ) ) + instancer.setPosition( IECore.V3fVectorData( [ imath.V3f( x ) for x in range( 4 ) ] ) ) + instancer.setScale( IECore.V3fVectorData( [ imath.V3f( x + 1 ) for x in range( 4 ) ] ) ) + instancer.setOrientation( IECore.QuatfVectorData( [ imath.Quatf( x, x, x, x ) for x in range( 4 ) ] ) ) + instancer.setID( IECore.Int64VectorData( [ 10, 11, 12, 13 ] ) ) + instancer.setInvisibleIDs( IECore.Int64VectorData( [ 10 ] ) ) + instancer["test"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Constant, + IECore.StringData( "test" ) + ) - with self.subTest( relative = relative ) : + root = IECoreScene.SceneInterface.create( + os.path.join( self.temporaryDirectory(), "test.usda" ), + IECore.IndexedIO.OpenMode.Write + ) + root.createChild( "instancer" ).writeObject( instancer, 0 ) + del root - env = os.environ.copy() - if relative is not None : - env["IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES"] = relative - else : - env.pop( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", None ) + root = IECoreScene.SceneInterface.create( + os.path.join( self.temporaryDirectory(), "test.usda" ), + IECore.IndexedIO.OpenMode.Read + ) - try : - subprocess.check_output( - [ sys.executable, __file__, "USDSceneTest._testPointInstancerRelativePrototypes" ], - env = env, stderr = subprocess.STDOUT - ) - except subprocess.CalledProcessError as e : - self.fail( e.output ) + self.assertEqual( + root.child( "instancer").readObject( 0 ), + instancer + ) @unittest.skipIf( not haveVDB, "No IECoreVDB" ) def testUsdVolVolumeSlashes( self ) : diff --git a/include/IECoreScene/PointInstancer.h b/include/IECoreScene/PointInstancer.h new file mode 100644 index 0000000000..c12f0ada2c --- /dev/null +++ b/include/IECoreScene/PointInstancer.h @@ -0,0 +1,165 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the following +// disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the following +// disclaimer in the documentation and/or other materials provided with +// the distribution. +// +// * Neither the name of John Haddon nor the names of +// any other contributors to this software may be used to endorse or +// promote products derived from this software without specific prior +// written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +#include "IECoreScene/PointsPrimitive.h" + +#include + +namespace IECoreScene +{ + +class IECORESCENE_API PointInstancer : public IECoreScene::PointsPrimitive +{ + + public : + + PointInstancer( size_t numPoints = 0 ); + ~PointInstancer() override; + + IE_CORE_DECLAREEXTENSIONOBJECT( PointInstancer, IECoreScene::PointInstancerTypeId, IECoreScene::PointsPrimitive ); + + /// PrimitiveVariable accessors + /// =========================== + /// + /// Accessor methods are provided for all PrimitiveVariables which have + /// specific meaning to the PointInstancer. + /// + /// In USD, these are stored as bare attributes rather than primvars, + /// and as such they can't have indices. We prefer to use + /// PrimitiveVariables for uniformity of access in algorithms such as + /// `PointsAlgo::deletePoints()` and for nodes in Gaffer, meaning that + /// in Cortex they _can_ have indices. The accessors are designed to + /// discourage the use of indices, but remain robust should they be + /// encountered : + /// + /// - `set()` functions create a PrimitiveVariable with null `indices`. + /// Pass a `nullptr` value to remove the PrimitiveVariable. + /// - `get()` functions return an IndexedView. An invalid view is returned + /// if the PrimitiveVariable doesn't exist or has the wrong type or + /// interpolation. + /// + /// For full flexibility when needed, access `Primitive::variables` + /// directly instead. + /// + /// \todo We currently use our own names for each of these properties, for + /// backwards compatibility with Instancer workflows in Gaffer. But in future + /// we should align the names to USD. + + /// Sets the prototypes to be instanced. Interpretation is left to + /// the consuming rendering system, but in practice these are locations + /// in a SceneInterface or Gaffer scene. + void setPrototypes( IECore::StringVectorDataPtr &prototypes ); + /// Returns an invalid view if the variable doesn't exist, or if it + /// exists but has the wrong type or interpolation. + PrimitiveVariable::IndexedView getPrototypes() const; + + void setPrototypeIndex( const IECore::IntVectorDataPtr &prototypeIndex ); + PrimitiveVariable::IndexedView getPrototypeIndex() const; + + void setPosition( const IECore::V3fVectorDataPtr &position ); + PrimitiveVariable::IndexedView getPosition() const; + + void setScale( const IECore::V3fVectorDataPtr &scale ); + PrimitiveVariable::IndexedView getScale() const; + + void setOrientation( const IECore::QuatfVectorDataPtr &orientation ); + PrimitiveVariable::IndexedView getOrientation() const; + + void setID( const IECore::Int64VectorDataPtr &ids ); + PrimitiveVariable::IndexedView getID() const; + + /// Constant primitive variable named "invisibleIds". + /// + /// > Note : In addition to this, USD also has "inactiveIds" whose + /// > only distinguishing feature is not being animatable. Since we + /// > have no concept of a non-animatable PrimitiveVariable, we have + /// > only "invisibleIds". + void setInvisibleIDs( const IECore::Int64VectorDataPtr &invisibleIds ); + PrimitiveVariable::IndexedView getInvisibleIDs() const; + + /// Queries + /// ======= + /// + /// Higher-level functionality used to evaluate the instancing. + + struct IECORESCENE_API VisibilityQuery + { + + VisibilityQuery( const PointInstancer &pointInstancer ) + : m_id( pointInstancer.getID() ) + { + if( auto invisibleIds = pointInstancer.getInvisibleIDs() ) + { + m_invisibleIds.insert( invisibleIds.begin(), invisibleIds.end() ); + } + } + + bool visible( size_t pointIndex ) + { + const size_t id = m_id ? m_id[pointIndex] : pointIndex; + return m_invisibleIds.find( id ) == m_invisibleIds.end(); + } + + private : + + PrimitiveVariable::IndexedView m_id; + std::unordered_set m_invisibleIds; + + }; + + /// Utility class to query properties for individual instances. + struct IECORESCENE_API TransformQuery + { + + TransformQuery( const PointInstancer &pointInstancer ); + + Imath::M44f transform( size_t pointIndex ) const; + + private : + + PrimitiveVariable::IndexedView m_position; + PrimitiveVariable::IndexedView m_orientation; + PrimitiveVariable::IndexedView m_scale; + + }; + +}; + +IE_CORE_DECLAREPTR( PointInstancer ) + +} // namespace IECoreScene diff --git a/include/IECoreScene/Primitive.h b/include/IECoreScene/Primitive.h index 6ed1a3262c..efc94d085c 100644 --- a/include/IECoreScene/Primitive.h +++ b/include/IECoreScene/Primitive.h @@ -72,6 +72,7 @@ class IECORESCENE_API Primitive : public VisibleRenderable /// An IndexedView allows access to both indexed & non indexed primitive variables using a common API. /// If the required interpolation & datatype do not match then an empty optional is returned unless throwIfInvalid is true. /// The returned IndexedView lifetime must be bound by the primitive variable data it is viewing. + /// \todo Now that IndexedView has `operator bool()`, there is no need to wrap in `std::optional`. template std::optional> variableIndexedView( const std::string &name, diff --git a/include/IECoreScene/PrimitiveVariable.h b/include/IECoreScene/PrimitiveVariable.h index 50fef90747..a5a403c4c7 100644 --- a/include/IECoreScene/PrimitiveVariable.h +++ b/include/IECoreScene/PrimitiveVariable.h @@ -138,8 +138,8 @@ class PrimitiveVariable::IndexedView class Iterator; - Iterator begin(); - Iterator end(); + Iterator begin() const; + Iterator end() const; typename std::vector::const_reference operator[]( size_t i ) const { diff --git a/include/IECoreScene/PrimitiveVariable.inl b/include/IECoreScene/PrimitiveVariable.inl index 9f2b4e7d33..d407f9373b 100644 --- a/include/IECoreScene/PrimitiveVariable.inl +++ b/include/IECoreScene/PrimitiveVariable.inl @@ -161,7 +161,7 @@ class PrimitiveVariable::IndexedView::Iterator : public boost::iterator_facad }; template -typename PrimitiveVariable::IndexedView::Iterator PrimitiveVariable::IndexedView::begin() +typename PrimitiveVariable::IndexedView::Iterator PrimitiveVariable::IndexedView::begin() const { return Iterator( m_indices ? m_indices->data() : nullptr, @@ -170,7 +170,7 @@ typename PrimitiveVariable::IndexedView::Iterator PrimitiveVariable::IndexedV } template -typename PrimitiveVariable::IndexedView::Iterator PrimitiveVariable::IndexedView::end() +typename PrimitiveVariable::IndexedView::Iterator PrimitiveVariable::IndexedView::end() const { return Iterator( m_indices ? m_indices->data() + m_indices->size() : nullptr, diff --git a/include/IECoreScene/TypeIds.h b/include/IECoreScene/TypeIds.h index 41c2ed9d9b..6c65907a09 100644 --- a/include/IECoreScene/TypeIds.h +++ b/include/IECoreScene/TypeIds.h @@ -47,7 +47,7 @@ enum TypeId ShaderTypeId = 108004, PDCParticleReaderTypeId = 108005, PDCParticleWriterTypeId = 108006, - RendererTypeId = 108007, // Obsolete, available for reuse + PointInstancerTypeId = 108007, PrimitiveOpTypeId = 108008, ParticleReaderTypeId = 108009, ParticleWriterTypeId = 108010, diff --git a/src/IECoreScene/PointInstancer.cpp b/src/IECoreScene/PointInstancer.cpp new file mode 100644 index 0000000000..0e2be2ee37 --- /dev/null +++ b/src/IECoreScene/PointInstancer.cpp @@ -0,0 +1,245 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of 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 "IECoreScene/PointInstancer.h" + +#include +#include + +using namespace std; +using namespace Imath; +using namespace IECore; +using namespace IECoreScene; + +IE_CORE_DEFINEOBJECTTYPEDESCRIPTION( PointInstancer ); + +PointInstancer::PointInstancer( size_t numPoints ) + : PointsPrimitive( numPoints ) +{ +} + +PointInstancer::~PointInstancer() +{ +} + +void PointInstancer::copyFrom( const Object *other, IECore::Object::CopyContext *context ) +{ + PointsPrimitive::copyFrom( other, context ); +} + +void PointInstancer::save( IECore::Object::SaveContext *context ) const +{ + PointsPrimitive::save( context ); +} + +void PointInstancer::load( IECore::Object::LoadContextPtr context ) +{ + PointsPrimitive::load( context ); +} + +bool PointInstancer::isEqualTo( const Object *other ) const +{ + return PointsPrimitive::isEqualTo( other ); +} + +void PointInstancer::memoryUsage( Object::MemoryAccumulator &a ) const +{ + PointsPrimitive::memoryUsage( a ); +} + +void PointInstancer::hash( MurmurHash &h ) const +{ + PointsPrimitive::hash( h ); +} + +void PointInstancer::setPrototypes( IECore::StringVectorDataPtr &prototypes ) +{ + if( prototypes ) + { + variables["prototypeRoots"] = PrimitiveVariable( PrimitiveVariable::Constant, prototypes ); + } + else + { + variables.erase( "prototypeRoots" ); + } +} + +PrimitiveVariable::IndexedView PointInstancer::getPrototypes() const +{ + auto optionalView = variableIndexedView( "prototypeRoots", PrimitiveVariable::Constant ); + return optionalView ? *optionalView : PrimitiveVariable::IndexedView(); +} + +void PointInstancer::setPrototypeIndex( const IECore::IntVectorDataPtr &prototypeIndex ) +{ + if( prototypeIndex ) + { + variables["prototypeIndex"] = PrimitiveVariable( PrimitiveVariable::Vertex, prototypeIndex ); + } + else + { + variables.erase( "prototypeIndex" ); + } +} + +PrimitiveVariable::IndexedView PointInstancer::getPrototypeIndex() const +{ + auto optionalView = variableIndexedView( "prototypeIndex", PrimitiveVariable::Vertex ); + return optionalView ? *optionalView : PrimitiveVariable::IndexedView(); +} + +void PointInstancer::setPosition( const IECore::V3fVectorDataPtr &position ) +{ + if( position ) + { + position->setInterpretation( GeometricData::Interpretation::Point ); + variables["P"] = PrimitiveVariable( PrimitiveVariable::Vertex, position ); + } + else + { + variables.erase( "P" ); + } +} + +PrimitiveVariable::IndexedView PointInstancer::getPosition() const +{ + auto optionalView = variableIndexedView( "P", PrimitiveVariable::Vertex ); + return optionalView ? *optionalView : PrimitiveVariable::IndexedView(); +} + +void PointInstancer::setScale( const IECore::V3fVectorDataPtr &scale ) +{ + if( scale ) + { + variables["scale"] = PrimitiveVariable( PrimitiveVariable::Vertex, scale ); + } + else + { + variables.erase( "scale" ); + } +} + +PrimitiveVariable::IndexedView PointInstancer::getScale() const +{ + auto optionalView = variableIndexedView( "scale", PrimitiveVariable::Vertex ); + return optionalView ? *optionalView : PrimitiveVariable::IndexedView(); +} + +void PointInstancer::setOrientation( const IECore::QuatfVectorDataPtr &orientation ) +{ + if( orientation ) + { + variables["orientation"] = PrimitiveVariable( PrimitiveVariable::Vertex, orientation ); + } + else + { + variables.erase( "orientation" ); + } +} + +PrimitiveVariable::IndexedView PointInstancer::getOrientation() const +{ + auto optionalView = variableIndexedView( "orientation", PrimitiveVariable::Vertex ); + return optionalView ? *optionalView : PrimitiveVariable::IndexedView(); +} + +void PointInstancer::setID( const IECore::Int64VectorDataPtr &ids ) +{ + if( ids ) + { + variables["instanceId"] = PrimitiveVariable( PrimitiveVariable::Vertex, ids ); + } + else + { + variables.erase( "instanceId" ); + } +} + +PrimitiveVariable::IndexedView PointInstancer::getID() const +{ + auto optionalView = variableIndexedView( "instanceId", PrimitiveVariable::Vertex ); + return optionalView ? *optionalView : PrimitiveVariable::IndexedView(); +} + +void PointInstancer::setInvisibleIDs( const IECore::Int64VectorDataPtr &invisibleIds ) +{ + if( invisibleIds ) + { + variables["invisibleIds"] = PrimitiveVariable( PrimitiveVariable::Constant, invisibleIds ); + } + else + { + variables.erase( "invisibleIds" ); + } +} + +PrimitiveVariable::IndexedView PointInstancer::getInvisibleIDs() const +{ + auto optionalView = variableIndexedView( "invisibleIds", PrimitiveVariable::Constant ); + return optionalView ? *optionalView : PrimitiveVariable::IndexedView(); +} + +// Query +// ===== + +PointInstancer::TransformQuery::TransformQuery( const PointInstancer &pointInstancer ) + : m_position( pointInstancer.getPosition() ), + m_orientation( pointInstancer.getOrientation() ), + m_scale( pointInstancer.getScale() ) +{ +} + +Imath::M44f PointInstancer::TransformQuery::transform( size_t pointIndex ) const +{ + M44f result; + if( m_position ) + { + result.translate( m_position[pointIndex] ); + } + + if( m_orientation ) + { + /// \todo Gaffer's Instancer class uses a `normalizedIfNeeded()` function + /// that avoids normalizing quaternions that can't get any closer to normalized. + /// Is there any value in doing that here? + result = m_orientation[pointIndex].normalized().toMatrix44() * result; + } + + if( m_scale ) + { + result.scale( m_scale[pointIndex] ); + } + + return result; +} diff --git a/src/IECoreScene/PointsAlgo.cpp b/src/IECoreScene/PointsAlgo.cpp index ea2a7fc2f0..2e329870e1 100644 --- a/src/IECoreScene/PointsAlgo.cpp +++ b/src/IECoreScene/PointsAlgo.cpp @@ -108,7 +108,7 @@ struct PointsUniformToVertex template PointsPrimitivePtr deletePoints( const PointsPrimitive *pointsPrimitive, IECoreScene::PrimitiveVariable::IndexedView& deleteFlagView, bool invert, const Canceller *canceller ) { - PointsPrimitivePtr outPointsPrimitive = new PointsPrimitive( 0 ); + PointsPrimitivePtr outPointsPrimitive = pointsPrimitive->copy(); IECoreScene::PrimitiveVariableAlgos::DeleteFlaggedUniformFunctor vertexFunctor( deleteFlagView, invert ); @@ -133,13 +133,8 @@ PointsPrimitivePtr deletePoints( const PointsPrimitive *pointsPrimitive, IECoreS outPointsPrimitive->variables[it->first] = PrimitiveVariable( it->second.interpolation, indexedData.data, indexedData.indices ); break; } - case PrimitiveVariable::Uniform: - case PrimitiveVariable::Constant: - case PrimitiveVariable::Invalid: - { - outPointsPrimitive->variables[it->first] = it->second; + default : break; - } } } diff --git a/src/IECoreScene/bindings/IECoreScene.cpp b/src/IECoreScene/bindings/IECoreScene.cpp index 47d1a87626..05e36ba5d7 100644 --- a/src/IECoreScene/bindings/IECoreScene.cpp +++ b/src/IECoreScene/bindings/IECoreScene.cpp @@ -66,6 +66,7 @@ #include "ParticleWriterBinding.h" #include "PatchMeshPrimitiveBinding.h" #include "PointsAlgoBinding.h" +#include "PointInstancerBinding.h" #include "PointsPrimitiveBinding.h" #include "PointsPrimitiveEvaluatorBinding.h" #include "PrimitiveBinding.h" @@ -160,5 +161,5 @@ BOOST_PYTHON_MODULE(_IECoreScene) bindTypedObjectParameter(); bindTypeId(); bindSceneAlgo(); - + bindPointInstancer(); } diff --git a/src/IECoreScene/bindings/PointInstancerBinding.cpp b/src/IECoreScene/bindings/PointInstancerBinding.cpp new file mode 100644 index 0000000000..580b5bac35 --- /dev/null +++ b/src/IECoreScene/bindings/PointInstancerBinding.cpp @@ -0,0 +1,82 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of 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 "PointInstancerBinding.h" + +#include "IECoreScene/PointInstancer.h" + +#include "IECorePython/RunTimeTypedBinding.h" + +using namespace boost::python; +using namespace IECore; +using namespace IECorePython; +using namespace IECoreScene; + +namespace +{ + +} // namespace + +void IECoreSceneModule::bindPointInstancer() +{ + scope instancerScope = RunTimeTypedClass() + .def( init( ( arg_( "numPoints" ) = 0 ) ) ) + .def( "setPrototypes", &PointInstancer::setPrototypes ) + .def( "getPrototypes", &PointInstancer::getPrototypes ) + .def( "setPrototypeIndex", &PointInstancer::setPrototypeIndex ) + .def( "getPrototypeIndex", &PointInstancer::getPrototypeIndex ) + .def( "setPosition", &PointInstancer::setPosition ) + .def( "getPosition", &PointInstancer::getPosition ) + .def( "setScale", &PointInstancer::setScale ) + .def( "getScale", &PointInstancer::getScale ) + .def( "setOrientation", &PointInstancer::setOrientation ) + .def( "getOrientation", &PointInstancer::getOrientation ) + .def( "setID", &PointInstancer::setID ) + .def( "getID", &PointInstancer::getID ) + .def( "setInvisibleIDs", &PointInstancer::setInvisibleIDs ) + .def( "getInvisibleIDs", &PointInstancer::getInvisibleIDs ) + ; + + class_( "VisibilityQuery", no_init ) + .def( init() ) + .def( "visible", &PointInstancer::VisibilityQuery::visible ) + ; + + class_( "TransformQuery", no_init ) + .def( init() ) + .def( "transform", &PointInstancer::TransformQuery::transform ) + ; +} diff --git a/src/IECoreScene/bindings/PointInstancerBinding.h b/src/IECoreScene/bindings/PointInstancerBinding.h new file mode 100644 index 0000000000..b37728e88a --- /dev/null +++ b/src/IECoreScene/bindings/PointInstancerBinding.h @@ -0,0 +1,42 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are +// met: +// +// * Redistributions of source code must retain the above copyright +// notice, this list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright +// notice, this list of conditions and the following disclaimer in the +// documentation and/or other materials provided with the distribution. +// +// * Neither the name of 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. +// +////////////////////////////////////////////////////////////////////////// + +#pragma once + +namespace IECoreSceneModule +{ + +void bindPointInstancer(); + +} diff --git a/src/IECoreScene/bindings/PrimitiveVariableBinding.cpp b/src/IECoreScene/bindings/PrimitiveVariableBinding.cpp index 38e160660b..e6fb66e10e 100644 --- a/src/IECoreScene/bindings/PrimitiveVariableBinding.cpp +++ b/src/IECoreScene/bindings/PrimitiveVariableBinding.cpp @@ -215,6 +215,47 @@ string primitiveVariableRepr( const PrimitiveVariable &p ) return result; } +template +size_t indexedViewSizeWrapper( const PrimitiveVariable::IndexedView &view ) +{ + return view ? view.size() : 0; +} + +template +T indexedViewGetItemWrapper( const PrimitiveVariable::IndexedView &view, long index ) +{ + const long s = view ? view.size() : 0; + + if( index < 0 ) + { + index += s; + } + + if( index >= s || index < 0 ) + { + PyErr_SetString( PyExc_IndexError, "Index out of range" ); + throw_error_already_set(); + } + + return view[index]; +} + +template +void bindIndexedView( const char *name ) +{ + using ViewType = PrimitiveVariable::IndexedView; + + class_( name ) + .def( init() ) + .def( "size", &indexedViewSizeWrapper ) + .def( "index", &ViewType::index ) + .def( "__len__", &ViewType::size ) + .def( "__getitem__", &indexedViewGetItemWrapper ) + .def( "__bool__", &ViewType::isValid ) + .def( "isValid", &ViewType::isValid ) + ; +} + } // namespace namespace IECoreSceneModule @@ -239,6 +280,7 @@ void bindPrimitiveVariable() .def( self != self ) .def( "__repr__", &primitiveVariableRepr ) ; + enum_( "Interpolation" ) .value( "Invalid", PrimitiveVariable::Invalid ) .value( "Constant", PrimitiveVariable::Constant ) @@ -248,6 +290,18 @@ void bindPrimitiveVariable() .value( "FaceVarying", PrimitiveVariable::FaceVarying ) ; + bindIndexedView( "BoolIndexedView" ); + bindIndexedView( "IntIndexedView" ); + bindIndexedView( "Int64IndexedView" ); + bindIndexedView( "FloatIndexedView" ); + bindIndexedView( "StringIndexedView" ); + bindIndexedView( "V2fIndexedView" ); + bindIndexedView( "V3fIndexedView" ); + bindIndexedView( "Color3fIndexedView" ); + bindIndexedView( "Color4fIndexedView" ); + bindIndexedView( "M44fIndexedView" ); + bindIndexedView( "QuatfIndexedView" ); + } } diff --git a/src/IECoreScene/bindings/TypeIdBinding.cpp b/src/IECoreScene/bindings/TypeIdBinding.cpp index 19fc273cbc..b71ab4ed4e 100644 --- a/src/IECoreScene/bindings/TypeIdBinding.cpp +++ b/src/IECoreScene/bindings/TypeIdBinding.cpp @@ -80,7 +80,7 @@ void bindTypeId() .value( "Shader", ShaderTypeId ) .value( "PDCParticleReader", PDCParticleReaderTypeId ) .value( "PDCParticleWriter", PDCParticleWriterTypeId ) - .value( "Renderer", RendererTypeId ) + .value( "PointInstancer", PointInstancerTypeId ) .value( "PrimitiveOp", PrimitiveOpTypeId ) .value( "ParticleReader", ParticleReaderTypeId ) .value( "ParticleWriter", ParticleWriterTypeId ) diff --git a/test/IECoreScene/All.py b/test/IECoreScene/All.py index 8b8194178a..cb997c87f4 100644 --- a/test/IECoreScene/All.py +++ b/test/IECoreScene/All.py @@ -91,6 +91,7 @@ from SharedSceneInterfacesTest import SharedSceneInterfacesTest from SceneInterfaceTest import SceneInterfaceTest from TypedPrimitiveOp import TestTypedPrimitiveOp +from PointInstancerTest import PointInstancerTest if IECore.withFreeType() : from FontTest import * diff --git a/test/IECoreScene/PointInstancerTest.py b/test/IECoreScene/PointInstancerTest.py new file mode 100644 index 0000000000..7dfb1543bf --- /dev/null +++ b/test/IECoreScene/PointInstancerTest.py @@ -0,0 +1,102 @@ +########################################################################## +# +# Copyright (c) 2026, Cinesite VFX Ltd. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# * Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# * Neither the name of 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 math +import unittest +import imath + +import IECore +import IECoreScene + +class PointInstancerTest( unittest.TestCase ) : + + def testAccessors( self ) : + + instancer = IECoreScene.PointInstancer( 4 ) + + self.assertIsInstance( instancer.getPosition(), IECoreScene.PrimitiveVariable.V3fIndexedView ) + self.assertFalse( instancer.getPosition() ) + + instancer.setPosition( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 4 ) ] ) ) + self.assertIsInstance( instancer.getPosition(), IECoreScene.PrimitiveVariable.V3fIndexedView ) + self.assertEqual( list( instancer.getPosition() ), [ imath.V3f( x, 0, 0 ) for x in range( 4 ) ] ) + + instancer["P"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.V3fVectorData( [ imath.V3f( 1 ), imath.V3f( 2 ) ] ), + IECore.IntVectorData( [ 1, 0, 0, 1 ] ) + ) + self.assertEqual( list( instancer.getPosition() ), [ imath.V3f( 2 ), imath.V3f( 1 ), imath.V3f( 1 ), imath.V3f( 2 ) ] ) + + instancer["P"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Constant, + IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 4 ) ] ), + ) + self.assertFalse( instancer.getPosition() ) + + instancer["P"] = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.IntVectorData( range( 4 ) ), + ) + self.assertFalse( instancer.getPosition() ) + + def testVisibilityQuery( self ) : + + instancer = IECoreScene.PointInstancer( 4 ) + instancer.setInvisibleIDs( IECore.Int64VectorData( [ 1, 3 ] ) ) + + query = IECoreScene.PointInstancer.VisibilityQuery( instancer ) + self.assertEqual( + [ query.visible( i ) for i in range( 0, 4 ) ], + [ True, False, True, False ] + ) + + instancer.setID( IECore.Int64VectorData( [ 1, 3, 0, 4 ] ) ) + query = IECoreScene.PointInstancer.VisibilityQuery( instancer ) + self.assertEqual( + [ query.visible( i ) for i in range( 0, 4 ) ], + [ False, False, True, True ] + ) + + def testTransformQuery( self ) : + + instancer = IECoreScene.PointInstancer( 4 ) + instancer.setPosition( IECore.V3fVectorData( [ imath.V3f( x, 0, 0 ) for x in range( 4 ) ] ) ) + + query = IECoreScene.PointInstancer.TransformQuery( instancer ) + for i in range( 4 ) : + self.assertEqual( query.transform( i ), imath.M44f().translate( imath.V3f( i, 0, 0 ) ) ) + +if __name__ == "__main__": + unittest.main() diff --git a/test/IECoreScene/PointsAlgoTest.py b/test/IECoreScene/PointsAlgoTest.py index 90c3a6371e..63ae72b729 100644 --- a/test/IECoreScene/PointsAlgoTest.py +++ b/test/IECoreScene/PointsAlgoTest.py @@ -491,6 +491,29 @@ def testDeleteBadPrimVars( self ) : self.assertRaises( RuntimeError, IECoreScene.PointsAlgo.deletePoints, points, delete ) + def testPointInstancer( self ) : + + points = IECoreScene.PointInstancer( 2 ) + points["P"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.V3fVectorData( [ imath.V3f( 0 ), imath.V3f( 1 ) ] ) ) + points["a"] = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.IntVectorData( [ 1, 2 ] ) ) + + toDelete = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.IntVectorData( [ 1, 0 ] ) ) + points = IECoreScene.PointsAlgo.deletePoints( points, toDelete ) + + self.assertIsInstance( points, IECoreScene.PointInstancer ) + self.assertTrue( points.arePrimitiveVariablesValid() ) + self.assertEqual( points["P"].data, IECore.V3fVectorData( [ imath.V3f( 1 ) ] ) ) + self.assertEqual( points["a"].data, IECore.IntVectorData( [ 2 ] ) ) + + def testBlindData( self ) : + + points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( x ) for x in range( 4 ) ] ) ) + points.blindData()["test"] = IECore.IntData( 10 ) + + toDelete = IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Vertex, IECore.IntVectorData( [ 1, 0, 1, 0 ] ) ) + points = IECoreScene.PointsAlgo.deletePoints( points, toDelete ) + + self.assertEqual( points.blindData()["test"], IECore.IntData( 10 ) ) class MergePointsTest( unittest.TestCase ) : diff --git a/test/IECoreScene/PrimitiveVariableTest.py b/test/IECoreScene/PrimitiveVariableTest.py index c49315c8cb..369b8bf1f0 100644 --- a/test/IECoreScene/PrimitiveVariableTest.py +++ b/test/IECoreScene/PrimitiveVariableTest.py @@ -185,9 +185,34 @@ def testIndexedView( self ) : IECoreScene.testPrimitiveVariableIndexedView() + primitiveVariable = IECoreScene.PrimitiveVariable( + IECoreScene.PrimitiveVariable.Interpolation.Vertex, + IECore.StringVectorData( [ "a", "b", "c" ] ), + IECore.IntVectorData( [ 0, 2, 1, 0, 1 ] ) + ) + + indexedView = IECoreScene.PrimitiveVariable.StringIndexedView( primitiveVariable ) + self.assertEqual( len( indexedView ), 5 ) + self.assertTrue( indexedView ) + self.assertTrue( indexedView.isValid() ) + self.assertEqual( indexedView[1], "c" ) + self.assertEqual( indexedView[-1], "b" ) + self.assertEqual( list( indexedView ), [ "a", "c", "b", "a", "b" ] ) + with self.assertRaises( IndexError ) : + indexedView[5] + def testBoolIndexedView( self ) : IECoreScene.testPrimitiveVariableBoolIndexedView() + def testEmptyIndexedView( self ) : + + indexedView = IECoreScene.PrimitiveVariable.StringIndexedView() + self.assertFalse( indexedView ) + self.assertFalse( indexedView.isValid() ) + self.assertEqual( indexedView.size(), 0 ) + with self.assertRaises( IndexError ) : + indexedView[0] + if __name__ == "__main__": unittest.main()