From 40a0c24aa9d090d4b64de45a9422de759a5af470 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 9 Jun 2026 11:16:33 +0100 Subject: [PATCH 1/9] PrimitiveVariableBinding : Bind IndexedView --- Changes | 5 ++ .../bindings/PrimitiveVariableBinding.cpp | 54 +++++++++++++++++++ test/IECoreScene/PrimitiveVariableTest.py | 25 +++++++++ 3 files changed, 84 insertions(+) diff --git a/Changes b/Changes index 14581c81ef..423f3dcd2e 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,11 @@ 10.7.x.x (relative to 10.7.0.0a10) ======== +Improvements +------------ + +- PrimitiveVariable : Added Python bindings for `IndexedView` class. + Build ----- 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/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() From 852a5d94ca2e7b43feb0f1fc4d7da2c34c64bb9c Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 9 Jun 2026 17:46:12 +0100 Subject: [PATCH 2/9] PrimitiveVariable : Make IndexedView `begin()` and `end()` const They don't mutate the view in any way. --- Changes | 5 +++++ include/IECoreScene/PrimitiveVariable.h | 4 ++-- include/IECoreScene/PrimitiveVariable.inl | 4 ++-- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/Changes b/Changes index 423f3dcd2e..ed9e16ad9f 100644 --- a/Changes +++ b/Changes @@ -6,6 +6,11 @@ Improvements - PrimitiveVariable : Added Python bindings for `IndexedView` class. +Fixes +----- + +- PrimitiveVariable : Made IndexedView `begin()` and `end()` const. + Build ----- 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, From 1226dd486b4af7f38e7ecdd5e7f48269ad884991 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 9 Jun 2026 11:20:29 +0100 Subject: [PATCH 3/9] Primitive : Add todo --- include/IECoreScene/Primitive.h | 1 + 1 file changed, 1 insertion(+) 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, From 14770f3966f8f8e67aeb852db7a79fd5e8200748 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 5 May 2026 16:37:13 +0100 Subject: [PATCH 4/9] PointInstancer : Add first-class PointInstancer type It's worth calling out a few design decisions here... Inheritance from PointsPrimitive -------------------------------- This isn't analogous to UsdGeomPointInstancer, which isn't even a UsdGeomGprim. But it gives us strong backwards compatibility when loading from USD, and lets us use all our existing PrimitiveVariable-based algo and nodes. Query classes ------------- Most queries need to do a name-based lookup to get PrimitiveVariables. The query classes allow us to do the lookup once in the constructor, so that it isn't repeated for every point. Providing a point-based API allows client code to do threading however it wants, and to copy values directly into renderer buffers without generating intermediate arrays. The VisibilityQuery class is separate from TransformQuery because it has greater construction costs, that you might not want to pay if you don't need visibility queries. This also makes it logical for us to have additional query classes in future, which allows us to extend queries without ABI issues. No Inactive IDs, only Invisible IDs ----------------------------------- USD has both, but this choice is due to USD constraints (the former isn't animateable, the latter is). Since we have no way of preventing animation in Cortex or Gaffer, it doesn't make sense to have both in Cortex. We'll merge the two on loading from USD, and then we can write back to `invisibleIds` successfully no matter whether or not it is animated in Cortex/Gaffer. This does sacrifice round-tripping, but makes life simpler in Cortex/Gaffer. The counter-argument here might be that the distinction might be useful in a PromotePointInstances node in Gaffer. Inactive IDs wouldn't be output at all, but invisible ones could be output as scene locations with `scene:visible = false`. I don't know how useful that would be though. --- Changes | 5 + include/IECoreScene/PointInstancer.h | 165 ++++++++++++ include/IECoreScene/TypeIds.h | 2 +- src/IECoreScene/PointInstancer.cpp | 245 ++++++++++++++++++ src/IECoreScene/bindings/IECoreScene.cpp | 3 +- .../bindings/PointInstancerBinding.cpp | 82 ++++++ .../bindings/PointInstancerBinding.h | 42 +++ src/IECoreScene/bindings/TypeIdBinding.cpp | 2 +- test/IECoreScene/All.py | 1 + test/IECoreScene/PointInstancerTest.py | 102 ++++++++ 10 files changed, 646 insertions(+), 3 deletions(-) create mode 100644 include/IECoreScene/PointInstancer.h create mode 100644 src/IECoreScene/PointInstancer.cpp create mode 100644 src/IECoreScene/bindings/PointInstancerBinding.cpp create mode 100644 src/IECoreScene/bindings/PointInstancerBinding.h create mode 100644 test/IECoreScene/PointInstancerTest.py diff --git a/Changes b/Changes index ed9e16ad9f..d4a5636c7f 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,11 @@ 10.7.x.x (relative to 10.7.0.0a10) ======== +Features +-------- + +- PointInstancer : Added class to provide first-class encoding of point instancers. + Improvements ------------ diff --git a/include/IECoreScene/PointInstancer.h b/include/IECoreScene/PointInstancer.h new file mode 100644 index 0000000000..f0c7ed514f --- /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 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 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/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/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/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() From b869b9fb09ed329806a9f800fd45e6f6bb3b9c86 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Mon, 8 Jun 2026 21:19:42 +0100 Subject: [PATCH 5/9] PointsAlgo : Preserve input properties in `deletePoints()` Start with a copy of the input rather than an empty PointsPrimitive. --- Changes | 3 +++ src/IECoreScene/PointsAlgo.cpp | 9 ++------- test/IECoreScene/PointsAlgoTest.py | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/Changes b/Changes index d4a5636c7f..d57da141d2 100644 --- a/Changes +++ b/Changes @@ -15,6 +15,9 @@ 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). Build ----- 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/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 ) : From cde1cd13cb72aad1b2848f47bf7b37cba0246ecd Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 10 Jun 2026 10:14:18 +0100 Subject: [PATCH 6/9] USD PrimitiveAlgo : Add conversion from `PrimitiveVariable::IndexedView` --- .../include/IECoreUSD/PrimitiveAlgo.h | 6 ++ .../include/IECoreUSD/PrimitiveAlgo.inl | 64 +++++++++++++++++++ .../IECoreUSD/src/IECoreUSD/PrimitiveAlgo.cpp | 13 +--- 3 files changed, 72 insertions(+), 11 deletions(-) create mode 100644 contrib/IECoreUSD/include/IECoreUSD/PrimitiveAlgo.inl 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/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 From 433232b2ee7e4bc784a61c8013dc3a08d1fdbbe8 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Wed, 10 Jun 2026 12:37:35 +0100 Subject: [PATCH 7/9] USD PointInstancerAlgo : Drop IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES Always load prototype paths as we want them - relative, and without a `./` prefix. --- Changes | 7 +++++ .../src/IECoreUSD/PointInstancerAlgo.cpp | 23 ++-------------- .../IECoreUSD/test/IECoreUSD/USDSceneTest.py | 27 ++----------------- 3 files changed, 11 insertions(+), 46 deletions(-) diff --git a/Changes b/Changes index d57da141d2..49a7400e36 100644 --- a/Changes +++ b/Changes @@ -19,6 +19,13 @@ Fixes - 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. + Build ----- diff --git a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp index de9f01c88b..06e74f3830 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp @@ -53,19 +53,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; @@ -121,8 +108,6 @@ IECore::ObjectPtr readPointInstancer( pxr::UsdGeomPointInstancer &pointInstancer // Prototype paths - const static bool g_relativePrototypes = checkEnvFlag( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", false ); - pxr::SdfPathVector targets; Canceller::check( canceller ); pointInstancer.GetPrototypesRel().GetForwardedTargets( &targets ); @@ -134,17 +119,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() ); } } diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index 0a6c86995e..07af43dcb8 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -4614,7 +4614,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 +4623,7 @@ 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' ] ) ) - - def testPointInstancerRelativePrototypes( self ) : - - for relative in [ "0", "1", None ] : - - with self.subTest( relative = relative ) : - - env = os.environ.copy() - if relative is not None : - env["IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES"] = relative - else : - env.pop( "IECOREUSD_POINTINSTANCER_RELATIVE_PROTOTYPES", None ) - - 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( obj["prototypeRoots"].data, IECore.StringVectorData( [ 'Prototypes/sphere', '/cube' ] ) ) @unittest.skipIf( not haveVDB, "No IECoreVDB" ) def testUsdVolVolumeSlashes( self ) : From 5a8f694b57562ce727da2fc1abef100b08175ed9 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Tue, 5 May 2026 16:37:30 +0100 Subject: [PATCH 8/9] USD PointInstancerAlgo : Use IECoreScene::PointInstancer class This allows us to add writing as well, since we can identify PointInstancers from regular PointsPrimitives. --- Changes | 2 + .../src/IECoreUSD/PointInstancerAlgo.cpp | 150 ++++++++++++++++-- contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp | 4 + .../IECoreUSD/test/IECoreUSD/USDSceneTest.py | 41 ++++- 4 files changed, 178 insertions(+), 19 deletions(-) diff --git a/Changes b/Changes index 49a7400e36..34f0a871f5 100644 --- a/Changes +++ b/Changes @@ -10,6 +10,7 @@ Improvements ------------ - PrimitiveVariable : Added Python bindings for `IndexedView` class. +- USDScene : Added support for writing `IECoreScene::PointInstancer` as `UsdGeomPointInstancer`. Fixes ----- @@ -25,6 +26,7 @@ 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/src/IECoreUSD/PointInstancerAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp index 06e74f3830..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; @@ -60,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 @@ -86,26 +89,48 @@ 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 ); + } + } } + newPoints->setInvisibleIDs( invisibleIds ); + // Prototype paths pxr::SdfPathVector targets; @@ -159,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/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 07af43dcb8..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 ) : @@ -4625,6 +4624,38 @@ def testPointInstancerRelativePrototypes( self ) : self.assertEqual( obj["prototypeRoots"].data, IECore.StringVectorData( [ 'Prototypes/sphere', '/cube' ] ) ) + def testPointInstancerRoundTrip( self ) : + + 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" ) + ) + + root = IECoreScene.SceneInterface.create( + os.path.join( self.temporaryDirectory(), "test.usda" ), + IECore.IndexedIO.OpenMode.Write + ) + root.createChild( "instancer" ).writeObject( instancer, 0 ) + del root + + root = IECoreScene.SceneInterface.create( + os.path.join( self.temporaryDirectory(), "test.usda" ), + IECore.IndexedIO.OpenMode.Read + ) + + self.assertEqual( + root.child( "instancer").readObject( 0 ), + instancer + ) + @unittest.skipIf( not haveVDB, "No IECoreVDB" ) def testUsdVolVolumeSlashes( self ) : From d942279a1427aeaabe9f8ab2578f9a00a2c643b3 Mon Sep 17 00:00:00 2001 From: John Haddon Date: Fri, 12 Jun 2026 14:33:03 +0100 Subject: [PATCH 9/9] fixup! PointInstancer : Add first-class PointInstancer type Hopefully fixes linking errors on Windows. --- include/IECoreScene/PointInstancer.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/include/IECoreScene/PointInstancer.h b/include/IECoreScene/PointInstancer.h index f0c7ed514f..c12f0ada2c 100644 --- a/include/IECoreScene/PointInstancer.h +++ b/include/IECoreScene/PointInstancer.h @@ -117,7 +117,7 @@ class IECORESCENE_API PointInstancer : public IECoreScene::PointsPrimitive /// /// Higher-level functionality used to evaluate the instancing. - struct VisibilityQuery + struct IECORESCENE_API VisibilityQuery { VisibilityQuery( const PointInstancer &pointInstancer ) @@ -143,7 +143,7 @@ class IECORESCENE_API PointInstancer : public IECoreScene::PointsPrimitive }; /// Utility class to query properties for individual instances. - struct TransformQuery + struct IECORESCENE_API TransformQuery { TransformQuery( const PointInstancer &pointInstancer );