diff --git a/Changes.md b/Changes.md index 3ce1e9d9a6a..4e6fc5ee19f 100644 --- a/Changes.md +++ b/Changes.md @@ -5,6 +5,8 @@ Features -------- - MeshNormals : Added a new node for adjusting mesh normals. +- VolumeScatter : Added a new node for scattering points throughout a volume. + Improvements ------------ diff --git a/include/GafferVDB/TypeIds.h b/include/GafferVDB/TypeIds.h index 61f88ab2e75..88eb2f028ae 100644 --- a/include/GafferVDB/TypeIds.h +++ b/include/GafferVDB/TypeIds.h @@ -50,6 +50,7 @@ enum TypeId PointsGridToPointsTypeId = 110956, SphereLevelSetTypeId = 110957, PointsToLevelSetTypeId = 110958, + VolumeScatterTypeId = 110959, LastTypeId = 110974 }; diff --git a/include/GafferVDB/VolumeScatter.h b/include/GafferVDB/VolumeScatter.h new file mode 100644 index 00000000000..6611956932e --- /dev/null +++ b/include/GafferVDB/VolumeScatter.h @@ -0,0 +1,102 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2020, Don Boogert. All rights reserved. +// Copyright (c) 2023, Image Engine Design. 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 Don Boogert 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 "GafferVDB/Export.h" +#include "GafferVDB/TypeIds.h" + +#include "GafferScene/BranchCreator.h" + +#include "Gaffer/NumericPlug.h" +#include "Gaffer/StringPlug.h" + +namespace GafferVDB +{ + +class GAFFERVDB_API VolumeScatter : public GafferScene::BranchCreator +{ + + public : + + VolumeScatter(const std::string &name = defaultName() ); + ~VolumeScatter(); + + IE_CORE_DECLARERUNTIMETYPEDEXTENSION( GafferVDB::VolumeScatter, VolumeScatterTypeId, GafferScene::BranchCreator ); + + Gaffer::StringPlug *namePlug(); + const Gaffer::StringPlug *namePlug() const; + + Gaffer::StringPlug *gridPlug(); + const Gaffer::StringPlug *gridPlug() const; + + Gaffer::FloatPlug *densityPlug(); + const Gaffer::FloatPlug *densityPlug() const; + + Gaffer::StringPlug *pointTypePlug(); + const Gaffer::StringPlug *pointTypePlug() const; + + protected : + + bool affectsBranchBound( const Gaffer::Plug *input ) const override; + void hashBranchBound( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + Imath::Box3f computeBranchBound( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const override; + + bool affectsBranchTransform( const Gaffer::Plug *input ) const override; + void hashBranchTransform( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + Imath::M44f computeBranchTransform( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const override; + + bool affectsBranchAttributes( const Gaffer::Plug *input ) const override; + void hashBranchAttributes( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + IECore::ConstCompoundObjectPtr computeBranchAttributes( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const override; + + bool affectsBranchObject( const Gaffer::Plug *input ) const override; + void hashBranchObject( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + IECore::ConstObjectPtr computeBranchObject( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const override; + + bool affectsBranchChildNames( const Gaffer::Plug *input ) const override; + void hashBranchChildNames( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const override; + IECore::ConstInternedStringVectorDataPtr computeBranchChildNames( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const override; + + private: + + static size_t g_firstPlugIndex; +}; + +IE_CORE_DECLAREPTR( VolumeScatter ) + +} // namespace GafferVDB diff --git a/python/GafferVDBTest/VolumeScatterTest.py b/python/GafferVDBTest/VolumeScatterTest.py new file mode 100644 index 00000000000..d3470ca02ed --- /dev/null +++ b/python/GafferVDBTest/VolumeScatterTest.py @@ -0,0 +1,134 @@ +########################################################################## +# +# Copyright (c) 2023, Image Engine Design Inc. All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# * Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials provided with +# the distribution. +# +# * Neither the name of John Haddon nor the names of +# any other contributors to this software may be used to endorse or +# promote products derived from this software without specific prior +# written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS +# IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, +# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +########################################################################## + +import imath +import unittest +import pathlib + +import IECore +import IECoreScene + +import Gaffer +import GafferScene +import GafferVDB +import GafferVDBTest + +class VolumeScatterTest( GafferVDBTest.VDBTestCase ) : + + def test( self ) : + + reader = GafferScene.SceneReader() + reader["fileName"].setValue( pathlib.Path( __file__ ).parent / "data" / "smoke.vdb" ) + + filter = GafferScene.PathFilter() + filter["paths"].setValue( IECore.StringVectorData( [ "/vdb" ] ) ) + + vs = GafferVDB.VolumeScatter() + vs["in"].setInput( reader["out"] ) + vs["filter"].setInput( filter["out"] ) + + self.assertEqual( vs["out"].childNames( "/" ), IECore.InternedStringVectorData( [ "vdb" ] ) ) + self.assertEqual( vs["out"].childNames( "/vdb" ), IECore.InternedStringVectorData( [ "scatter" ] ) ) + self.assertEqual( vs["out"].childNames( "/vdb/scatter" ), IECore.InternedStringVectorData() ) + + vs["name"].setValue( "test" ) + + self.assertEqual( vs["out"].childNames( "/vdb" ), IECore.InternedStringVectorData( [ "test" ] ) ) + + vs["destination"].setValue( "/" ) + + self.assertEqual( vs["out"].childNames( "/" ), IECore.InternedStringVectorData( [ "vdb", "test" ] ) ) + self.assertEqual( vs["out"].childNames( "/vdb" ), IECore.InternedStringVectorData( ) ) + + + bound = vs['out'].bound( "/test" ) + self.assertTrue( bound.min().equalWithAbsError( imath.V3f( -34.31, -12.88, -26.69 ), 0.01 ) ) + self.assertTrue( bound.max().equalWithAbsError( imath.V3f( 19.55, 93.83, 27.64 ), 0.01 ) ) + + points = vs['out'].object( "/test" ) + + numP = len( points["P"].data ) + self.assertEqual( numP, 18449 ) + + # Characterize the set of points generated in a way that we know approximately matches this smoke vdb. + # These values are derived from the current distribution - if the distribution changes in the future, + # the tolerances will need to loosen, but we should still see approximately these values, which come + # from the shape of the fog ( the tolerances are pretty loose, due to some areas of very low density + # where a change in distribution can have a big impact on the bounding box ) + self.assertTrue( points.bound().min().equalWithAbsError( imath.V3f(-31.85, -11.02, -25.19 ), 1.0 ) ) + self.assertTrue( points.bound().max().equalWithAbsError( imath.V3f(17.34, 91.58, 25.57 ), 1.0 ) ) + + # The center should be fairly close to the true centre, relative to the size of the bound + center = sum( points["P"].data ) / numP + self.assertLess( ( ( center - imath.V3f(-4.50, 17.88, -0.593) ) / points.bound().size() ).length(), 0.003 ) + + diffs = [ ( i - center ) for i in points["P"].data ] + variance = sum( [ ( i - center ) * ( i - center ) for i in points["P"].data ] ) / numP + stdDev = imath.V3f( *[ i ** 0.5 for i in variance ] ) + self.assertTrue( stdDev.equalWithRelError( imath.V3f( 7.92, 20.75, 6.94 ), 0.02 ) ) + + vs["density"].setValue( 2 ) + + self.assertEqual( len( vs['out'].object( "/test" )["P"].data ), 36788 ) + + self.assertEqual( vs['out'].object( "/test" )["type"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, "gl:point" ) ) + + vs["pointType"].setValue( "sphere" ) + + self.assertEqual( vs['out'].object( "/test" )["type"], IECoreScene.PrimitiveVariable( IECoreScene.PrimitiveVariable.Interpolation.Constant, "sphere" ) ) + + def testFail( self ) : + + reader = GafferScene.SceneReader() + reader["fileName"].setValue( pathlib.Path( __file__ ).parent / "data" / "sphere.vdb" ) + + filter = GafferScene.PathFilter() + filter["paths"].setValue( IECore.StringVectorData( [ "/vdb" ] ) ) + + vs = GafferVDB.VolumeScatter() + vs["in"].setInput( reader["out"] ) + vs["filter"].setInput( filter["out"] ) + + # The grid name doesn't match, so we silently return an empty object + self.assertEqual( vs['out'].object( "/vdb/scatter" ), IECore.NullObject() ) + + vs["grid"].setValue( "ls_sphere" ) + + with self.assertRaisesRegex( RuntimeError, "VolumeScatter does not yet support level sets" ) : + vs['out'].object( "/vdb/scatter" ) + +if __name__ == "__main__": + unittest.main() diff --git a/python/GafferVDBTest/__init__.py b/python/GafferVDBTest/__init__.py index 0ea435fdddc..79c93967bb7 100644 --- a/python/GafferVDBTest/__init__.py +++ b/python/GafferVDBTest/__init__.py @@ -42,6 +42,7 @@ from .ModuleTest import ModuleTest from .SphereLevelSetTest import SphereLevelSetTest from .PointsToLevelSetTest import PointsToLevelSetTest +from .VolumeScatterTest import VolumeScatterTest if __name__ == "__main__": import unittest diff --git a/python/GafferVDBUI/VolumeScatterUI.py b/python/GafferVDBUI/VolumeScatterUI.py new file mode 100644 index 00000000000..32b98750215 --- /dev/null +++ b/python/GafferVDBUI/VolumeScatterUI.py @@ -0,0 +1,124 @@ +########################################################################## +# +# Copyright (c) 2020, Don Boogert. All rights reserved. +# Copyright (c) 2023, Image Engine Design. 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 Don Boogert 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 GafferUI +import GafferVDB + +GafferUI.Metadata.registerNode( + + GafferVDB.VolumeScatter, + + "description", + """ + Scatter points according the voxel values of a VDB grid. + """, + + plugs = { + + "name" : [ + + "description", + """ + The name given to the PointsPrimitive - + this will be placed under the location specified by + "destination". + """, + + ], + + "grid" : [ + + "description", + """ + Name of grid in VDBObject in which points will be scattered. + """, + + ], + + "density" : [ + + "description", + """ + This density is multiplied with the value of the grid to produce a number of points per unit volume. + """, + ], + + "pointType" : [ + + "description", + """ + The render type of the points. This defaults to + "gl:point" so that the points are rendered in a + lightweight manner in the viewport. + """, + + "preset:GL Point", "gl:point", + "preset:Particle", "particle", + "preset:Sphere", "sphere", + "preset:Disk", "disk", + "preset:Patch", "patch", + "preset:Blobby", "blobby", + + "plugValueWidget:type", "GafferUI.PresetsPlugValueWidget", + + ], + + "destination" : [ + + "description", + """ + The location where the points primitives will be placed in the output scene. + When the destination is evaluated, the `${scene:path}` variable holds + the location of the source mesh, so the default value parents the points + under the mesh. + + > Tip : `${scene:path}/..` may be used to place the points alongside the + > source mesh. + """, + + ], + + "parent" : [ + + "description", + """ + This plug has been deprecated in favour of using a filter to select the volume. + """ + ], + + } +) diff --git a/python/GafferVDBUI/__init__.py b/python/GafferVDBUI/__init__.py index 7d032f1bfcc..4115b898184 100644 --- a/python/GafferVDBUI/__init__.py +++ b/python/GafferVDBUI/__init__.py @@ -52,5 +52,6 @@ from . import PointsGridToPointsUI from . import SphereLevelSetUI from . import PointsToLevelSetUI +from . import VolumeScatterUI __import__( "IECore" ).loadConfig( "GAFFER_STARTUP_PATHS", subdirectory = "GafferVDBUI" ) diff --git a/src/GafferVDB/VolumeScatter.cpp b/src/GafferVDB/VolumeScatter.cpp new file mode 100644 index 00000000000..3d0702cb0aa --- /dev/null +++ b/src/GafferVDB/VolumeScatter.cpp @@ -0,0 +1,339 @@ +////////////////////////////////////////////////////////////////////////// +// +// Copyright (c) 2020, Don Boogert. All rights reserved. +// Copyright (c) 2023, Image Engine Design. 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 Don Boogert 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 "GafferVDB/VolumeScatter.h" + +#include "GafferVDB/Interrupter.h" + +#include "Gaffer/StringPlug.h" + +#include "IECoreScene/PointsPrimitive.h" +#include "IECoreVDB/VDBObject.h" + +#include "OpenEXR/OpenEXRConfig.h" +#if OPENEXR_VERSION_MAJOR < 3 +#include "OpenEXR/ImathRandom.h" +#else +#include "Imath/ImathRandom.h" +#endif + +#include "openvdb/openvdb.h" +#include "openvdb/tools/PointScatter.h" + +#include "pcg/pcg_random.hpp" + +using namespace std; +using namespace Imath; +using namespace IECore; +using namespace IECoreVDB; +using namespace Gaffer; +using namespace GafferVDB; + +IE_CORE_DEFINERUNTIMETYPED( VolumeScatter ); + +size_t VolumeScatter::g_firstPlugIndex = 0; + +VolumeScatter::VolumeScatter( const std::string &name ) + : BranchCreator( name ) +{ + storeIndexOfNextChild(g_firstPlugIndex); + + addChild( new StringPlug( "name", Plug::In, "scatter" ) ); + addChild( new StringPlug( "grid", Plug::In, "density" ) ); + addChild( new FloatPlug( "density", Plug::In, 1.0f, 0.0f ) ); + addChild( new StringPlug( "pointType", Plug::In, "gl:point" ) ); +} + +VolumeScatter::~VolumeScatter() +{ +} + +Gaffer::StringPlug *VolumeScatter::namePlug() +{ + return getChild( g_firstPlugIndex + 0 ); +} + +const Gaffer::StringPlug *VolumeScatter::namePlug() const +{ + return getChild( g_firstPlugIndex + 0 ); +} + +Gaffer::StringPlug *VolumeScatter::gridPlug() +{ + return getChild( g_firstPlugIndex + 1 ); +} + +const Gaffer::StringPlug *VolumeScatter::gridPlug() const +{ + return getChild( g_firstPlugIndex + 1 ); +} + +Gaffer::FloatPlug *VolumeScatter::densityPlug() +{ + return getChild( g_firstPlugIndex + 2 ); +} + +const Gaffer::FloatPlug *VolumeScatter::densityPlug() const +{ + return getChild( g_firstPlugIndex + 2 ); +} + +Gaffer::StringPlug *VolumeScatter::pointTypePlug() +{ + return getChild( g_firstPlugIndex + 3 ); +} + +const Gaffer::StringPlug *VolumeScatter::pointTypePlug() const +{ + return getChild( g_firstPlugIndex + 3 ); +} + +bool VolumeScatter::affectsBranchBound( const Gaffer::Plug *input ) const +{ + return input == inPlug()->boundPlug(); +} + +void VolumeScatter::hashBranchBound( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + BranchCreator::hashBranchBound( sourcePath, branchPath, context, h ); + h.append( inPlug()->boundHash( sourcePath ) ); +} + +Imath::Box3f VolumeScatter::computeBranchBound( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const +{ + // We could do a potentially more accurate bound by getting the bound for just the grid we're using from + // the vdb, but using the full bound from the vdb loader should be conservative, and it's rare that the grid + // we're using wouldn't fill the whole vdb ( this also matches how other nodes like LevelSetToMesh + // are currently working ) + Box3f b = inPlug()->bound( sourcePath ); + + if( !b.isEmpty() ) + { + // The PointsPrimitive we make has a default point width of 1, + // so we must expand our bounding box to take that into account. + b.min -= V3f( 0.5 ); + b.max += V3f( 0.5 ); + } + return b; +} + +bool VolumeScatter::affectsBranchTransform( const Gaffer::Plug *input ) const +{ + return false; +} + +void VolumeScatter::hashBranchTransform( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + BranchCreator::hashBranchTransform( sourcePath, branchPath, context, h ); +} + +Imath::M44f VolumeScatter::computeBranchTransform( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const +{ + return M44f(); +} + +bool VolumeScatter::affectsBranchAttributes( const Gaffer::Plug *input ) const +{ + return false; +} + +void VolumeScatter::hashBranchAttributes( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + BranchCreator::hashBranchAttributes( sourcePath, branchPath, context, h ); +} + +IECore::ConstCompoundObjectPtr VolumeScatter::computeBranchAttributes( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const +{ + return outPlug()->attributesPlug()->defaultValue(); +} + +bool VolumeScatter::affectsBranchObject( const Gaffer::Plug *input ) const +{ + return + input == inPlug()->objectPlug() || + input == gridPlug() || + input == densityPlug() || + input == pointTypePlug() + ; +} + +void VolumeScatter::hashBranchObject( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + if( branchPath.size() == 1 ) + { + BranchCreator::hashBranchObject( sourcePath, branchPath, context, h ); + + h.append( inPlug()->objectHash( sourcePath ) ); + gridPlug()->hash( h ); + densityPlug()->hash( h ); + pointTypePlug()->hash( h ); + return; + } + + h = outPlug()->objectPlug()->defaultValue()->Object::hash(); +} + +namespace { + +class PointsWriter { +public: + + PointsWriter() + : pointsData(new IECore::V3fVectorData()), points( pointsData->writable() ) + { + } + + void add(const openvdb::Vec3R &pos) + { + points.emplace_back(pos.x(), pos.y(), pos.z()); + } + + IECore::V3fVectorDataPtr pointsData; + std::vector & points; +}; + +} // namespace + +IECore::ConstObjectPtr VolumeScatter::computeBranchObject( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const +{ + if( branchPath.size() != 1 ) + { + return outPlug()->objectPlug()->defaultValue(); + } + + ConstVDBObjectPtr vdbObject = runTimeCast( inPlug()->object( sourcePath ) ); + if( !vdbObject ) + { + return outPlug()->objectPlug()->defaultValue(); + } + + std::string gridName = gridPlug()->getValue(); + + openvdb::GridBase::ConstPtr grid = vdbObject->findGrid( gridName ); + + if ( !grid ) + { + // The classic question: should we raising an exception here? + // It would be much easier to debug failures if we raised errors, but a user might + // also want to run this on a large number of objects with poor QC, and not want + // to fail the whole render because one of them is bad. Maybe we should have a + // toggle for whether to raise exceptions? Currently matching LevelSetToMesh + // and just ignoring missing grids. + return outPlug()->objectPlug()->defaultValue(); + } + + if( grid->getGridClass() == openvdb::GRID_LEVEL_SET ) + { + throw IECore::Exception( "VolumeScatter does not yet support level sets" ); + } + + openvdb::FloatGrid::ConstPtr floatGrid = openvdb::GridBase::constGrid( grid ); + if( !floatGrid ) + { + throw IECore::Exception( "VolumeScatter requires a FloatGrid, does not support : " + grid->type() ); + + } + + PointsWriter pointWriter; + pcg32 generator( 42 ); + const float spread = 1.0f; + Interrupter interrupter( context->canceller() ); + + // This NonUniformScatter built into openvdb is kind of limited. The next step for this node would + // probably be to make a local copy, so we could add features like: + // * a min/max value to remap to 0/1 for when you want to select some of the volume without driving + // the density of points by the volume density. + // * the option to normalize by the volume of the vdb, to produce an approximately constant number of points. + // * support for level sets ( for every region neighbouring active voxels, if all adjacent voxels are under + // threshold, we just generate points as usual, but if some adjacent voxels are over threshold, we + // need to evaluate the interpolated value at each generated point to check if it is under threshold ). + using NonUniformScatter = openvdb::tools::NonUniformPointScatter< + PointsWriter, pcg32, Interrupter + >; + NonUniformScatter densityPointScatter( + pointWriter, densityPlug()->getValue(), generator, spread, &interrupter + ); + + densityPointScatter( *floatGrid ); + + if ( interrupter.wasInterrupted() ) + { + throw IECore::Cancelled(); + } + + IECoreScene::PointsPrimitivePtr result = new IECoreScene::PointsPrimitive( pointWriter.pointsData ); + result->variables["type"] = IECoreScene::PrimitiveVariable( IECoreScene::PrimitiveVariable::Constant, new StringData( pointTypePlug()->getValue() ) ); + return result; +} + +bool VolumeScatter::affectsBranchChildNames( const Gaffer::Plug *input ) const +{ + return input == namePlug(); +} + +void VolumeScatter::hashBranchChildNames( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context, IECore::MurmurHash &h ) const +{ + if( branchPath.size() == 0 ) + { + BranchCreator::hashBranchChildNames( sourcePath, branchPath, context, h ); + namePlug()->hash( h ); + } + else + { + h = outPlug()->childNamesPlug()->defaultValue()->Object::hash(); + } +} + +IECore::ConstInternedStringVectorDataPtr VolumeScatter::computeBranchChildNames( const ScenePath &sourcePath, const ScenePath &branchPath, const Gaffer::Context *context ) const +{ + if( branchPath.size() == 0 ) + { + std::string name = namePlug()->getValue(); + if( name.empty() ) + { + return outPlug()->childNamesPlug()->defaultValue(); + } + InternedStringVectorDataPtr result = new InternedStringVectorData(); + result->writable().push_back( name ); + return result; + } + else + { + return outPlug()->childNamesPlug()->defaultValue(); + } +} diff --git a/src/GafferVDBModule/GafferVDBModule.cpp b/src/GafferVDBModule/GafferVDBModule.cpp index 0b3bf8db35b..085376c8737 100644 --- a/src/GafferVDBModule/GafferVDBModule.cpp +++ b/src/GafferVDBModule/GafferVDBModule.cpp @@ -41,6 +41,7 @@ #include "GafferVDB/MeshToLevelSet.h" #include "GafferVDB/PointsGridToPoints.h" #include "GafferVDB/PointsToLevelSet.h" +#include "GafferVDB/VolumeScatter.h" #include "GafferVDB/SphereLevelSet.h" #include "GafferBindings/DependencyNodeBinding.h" @@ -56,5 +57,6 @@ BOOST_PYTHON_MODULE( _GafferVDB ) GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); GafferBindings::DependencyNodeClass(); + GafferBindings::DependencyNodeClass(); } diff --git a/startup/gui/menus.py b/startup/gui/menus.py index 4313a37c1d5..77b9c717721 100644 --- a/startup/gui/menus.py +++ b/startup/gui/menus.py @@ -507,6 +507,7 @@ def __shaderNodeCreator( nodeName, shaderName ) : nodeMenu.append( "/VDB/Level Set Offset", GafferVDB.LevelSetOffset, searchText = "LevelSetOffset" ) nodeMenu.append( "/VDB/Points Grid To Points", GafferVDB.PointsGridToPoints, searchText = "PointsGridToPoints" ) nodeMenu.append( "/VDB/Sphere Level Set", GafferVDB.SphereLevelSet, searchText="SphereLevelSet") +nodeMenu.append( "/VDB/Volume Scatter", GafferVDB.VolumeScatter, searchText = "VolumeScatter" ) # USD nodes