Skip to content

Commit

Permalink
Merge pull request #6060 from danieldresser-ie/vdbMerge
Browse files Browse the repository at this point in the history
MeshToLevelSet : Support Merging Inputs
  • Loading branch information
johnhaddon authored Oct 9, 2024
2 parents 0587d11 + e60801d commit 41cbfd5
Show file tree
Hide file tree
Showing 20 changed files with 661 additions and 158 deletions.
7 changes: 7 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Features
- `staticComponent` : A single character string for the component to use as the static component for the color field. The other two components in the "RGB", "HSV" and "TMI" triplets will be controllable in the widget.
- `colorFieldVisible` : A boolean indicating if the color field should be visible or not.
- Added a menu item to the color chooser settings to save the UI configuration for the inline color chooser and the dialogue color chooser as a startup script to persist the configuration across Gaffer restarts.
- MeshToLevelSet : Added `destination` plug, allowing multiple input meshes to be merged into a single level set at an arbitrary location.
- LevelSetToMesh : Added `destination` plug, allowing multiple input level sets to be merged into a single mesh at an arbitrary location.

Improvements
------------
Expand All @@ -31,6 +33,11 @@ Breaking Changes
----------------

- Cycles : Removed custom handling of unnormalized lights. We now rely on Cycles' inbuilt behaviour which results in a brightness difference for unnormalized point, spot and disk lights.
- MeshToLevelSet : Objects which are not meshes are now converted to an empty VDB grid, instead of being left unchanged.
- LevelSetToMesh :
- Objects which are not level sets are now converted to an empty mesh, instead of being left unchanged.
- Removed the `adjustBounds` plug. In the rare case where it is important to recompute slightly tighter bounds, one workaround is to use ShufflePrimitiveVariables to shuffle from "P" to "P" with `adjustBounds` checked.
- Removed support for grid types other than `FloatGrid`. If other types are required, please request them.

1.5.0.0a2 (relative to 1.5.0.0a1)
=========
Expand Down
2 changes: 1 addition & 1 deletion include/GafferScene/MergeCurves.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class GAFFERSCENE_API MergeCurves : public MergeObjects

protected :

IECore::ConstObjectPtr mergeObjects( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;
IECore::ConstObjectPtr computeMergedObject( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;

private :

Expand Down
2 changes: 1 addition & 1 deletion include/GafferScene/MergeMeshes.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class GAFFERSCENE_API MergeMeshes : public MergeObjects

protected :

IECore::ConstObjectPtr mergeObjects( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;
IECore::ConstObjectPtr computeMergedObject( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;

private :

Expand Down
19 changes: 17 additions & 2 deletions include/GafferScene/MergeObjects.h
Original file line number Diff line number Diff line change
Expand Up @@ -107,15 +107,30 @@ class GAFFERSCENE_API MergeObjects : public FilteredSceneProcessor
Gaffer::ValuePlug::CachePolicy computeCachePolicy( const Gaffer::ValuePlug *output ) const override;


/// @name Actual object merge function
/// If there are any additional plugs that affect the merge, this should be implemented
/// to call the base class, and then return true for those extra plugs.
virtual bool affectsMergedObject( const Gaffer::Plug *input ) const;

/// If there are any additional plugs that affect the merge, this should be implemented
/// to call the base class, and then add those plugs to the hash.
virtual void hashMergedObject(
const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h
) const;

/// Actual object merge function
/// This must be implemented by derived classes. It receives a vector of pairs of objects
/// and the transform that maps each object into the shared space of the output location.
///
virtual IECore::ConstObjectPtr mergeObjects(
virtual IECore::ConstObjectPtr computeMergedObject(
const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources,
const Gaffer::Context *context
) const = 0;






// \todo - should we offer alternate ways to merge bounds? Can we think of any use cases for this?
//virtual Imath::Box3f mergeBounds( const std::vector< ScenePath > &sourcePaths, const Gaffer::Context *context ) const;

Expand Down
2 changes: 1 addition & 1 deletion include/GafferScene/MergePoints.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class GAFFERSCENE_API MergePoints : public MergeObjects

protected :

IECore::ConstObjectPtr mergeObjects( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;
IECore::ConstObjectPtr computeMergedObject( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;

private :

Expand Down
17 changes: 10 additions & 7 deletions include/GafferVDB/LevelSetToMesh.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,23 +39,23 @@
#include "GafferVDB/Export.h"
#include "GafferVDB/TypeIds.h"

#include "GafferScene/Deformer.h"
#include "GafferScene/MergeObjects.h"

#include "Gaffer/NumericPlug.h"
#include "Gaffer/StringPlug.h"

namespace GafferVDB
{

class GAFFERVDB_API LevelSetToMesh : public GafferScene::Deformer
class GAFFERVDB_API LevelSetToMesh : public GafferScene::MergeObjects
{

public :

explicit LevelSetToMesh( const std::string &name=defaultName<LevelSetToMesh>() );
~LevelSetToMesh() override;

GAFFER_NODE_DECLARE_TYPE( GafferVDB::LevelSetToMesh, LevelSetToMeshTypeId, GafferScene::Deformer );
GAFFER_NODE_DECLARE_TYPE( GafferVDB::LevelSetToMesh, LevelSetToMeshTypeId, GafferScene::MergeObjects );

Gaffer::StringPlug *gridPlug();
const Gaffer::StringPlug *gridPlug() const;
Expand All @@ -68,10 +68,13 @@ class GAFFERVDB_API LevelSetToMesh : public GafferScene::Deformer

protected :

bool affectsProcessedObject( const Gaffer::Plug *input ) const override;
void hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const override;
IECore::ConstObjectPtr computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const override;
Gaffer::ValuePlug::CachePolicy processedObjectComputeCachePolicy() const override;
bool affectsMergedObject( const Gaffer::Plug *input ) const override;

void hashMergedObject(
const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h
) const override;

IECore::ConstObjectPtr computeMergedObject( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;

private :

Expand Down
17 changes: 10 additions & 7 deletions include/GafferVDB/MeshToLevelSet.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
#include "GafferVDB/Export.h"
#include "GafferVDB/TypeIds.h"

#include "GafferScene/ObjectProcessor.h"
#include "GafferScene/MergeObjects.h"

#include "Gaffer/NumericPlug.h"

Expand All @@ -51,15 +51,15 @@ class StringPlug;
namespace GafferVDB
{

class GAFFERVDB_API MeshToLevelSet : public GafferScene::ObjectProcessor
class GAFFERVDB_API MeshToLevelSet : public GafferScene::MergeObjects
{

public :

explicit MeshToLevelSet( const std::string &name=defaultName<MeshToLevelSet>() );
~MeshToLevelSet() override;

GAFFER_NODE_DECLARE_TYPE( GafferVDB::MeshToLevelSet, MeshToLevelSetTypeId, GafferScene::ObjectProcessor );
GAFFER_NODE_DECLARE_TYPE( GafferVDB::MeshToLevelSet, MeshToLevelSetTypeId, GafferScene::MergeObjects );

Gaffer::StringPlug *gridPlug();
const Gaffer::StringPlug *gridPlug() const;
Expand All @@ -75,10 +75,13 @@ class GAFFERVDB_API MeshToLevelSet : public GafferScene::ObjectProcessor

protected :

bool affectsProcessedObject( const Gaffer::Plug *plug ) const override;
void hashProcessedObject( const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h ) const override;
IECore::ConstObjectPtr computeProcessedObject( const ScenePath &path, const Gaffer::Context *context, const IECore::Object *inputObject ) const override;
Gaffer::ValuePlug::CachePolicy processedObjectComputeCachePolicy() const override;
bool affectsMergedObject( const Gaffer::Plug *input ) const override;

void hashMergedObject(
const ScenePath &path, const Gaffer::Context *context, IECore::MurmurHash &h
) const override;

IECore::ConstObjectPtr computeMergedObject( const std::vector< std::pair< IECore::ConstObjectPtr, Imath::M44f > > &sources, const Gaffer::Context *context ) const override;

private :

Expand Down
19 changes: 19 additions & 0 deletions python/GafferSceneTest/IECoreScenePreviewTest/PrimitiveAlgoTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,25 @@ def testMergeFewPerf( self ) :
with GafferTest.TestRunner.PerformanceScope() :
PrimitiveAlgo.mergePrimitives( meshes )

@GafferTest.TestRunner.PerformanceTestMethod()
def testSingleMeshPerf( self ) :

# Calling mergePrimitives with a single source should use transformPrimitives
# and not pay the cost of accumulating topology

mesh = IECoreScene.MeshPrimitive.createPlane(
imath.Box2f( imath.V2f( -2 ), imath.V2f( 2 ) ),
divisions = imath.V2i( 2000, 2000 )
)

m = imath.M44f()
m.setTranslation( imath.V3f( 0, 1, 0 ) )

meshes = [ ( mesh, m ) ]

with GafferTest.TestRunner.PerformanceScope() :
PrimitiveAlgo.mergePrimitives( meshes )


if __name__ == "__main__":
unittest.main()
2 changes: 1 addition & 1 deletion python/GafferSceneTest/SceneTestCase.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,7 @@ def assertParallelGetValueComputesObjectOnce( self, scene, path ) :
c["scene:path"] = GafferScene.ScenePlug.stringToPath( path )
GafferTest.parallelGetValue( scene["object"], 100 )

if isinstance( scene.node(), GafferScene.ObjectProcessor ) :
if isinstance( scene.node(), GafferScene.ObjectProcessor ) or isinstance( scene.node(), GafferScene.MergeObjects ) :
self.assertEqual( pm.plugStatistics( scene.node()["__processedObject"] ).computeCount, 1 )
elif isinstance( scene.node(), GafferScene.ObjectSource ) :
self.assertEqual( pm.plugStatistics( scene.node()["__source"] ).computeCount, 1 )
Expand Down
79 changes: 59 additions & 20 deletions python/GafferVDBTest/LevelSetToMeshTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#
##########################################################################

import imath
import pathlib

import IECore
Expand Down Expand Up @@ -68,7 +69,7 @@ def testCanConvertLevelSetToMesh( self ) :
mesh = levelSetToMesh["out"].object( "sphere" )
self.assertTrue( isinstance( mesh, IECoreScene.MeshPrimitive) )

def testChangingIsoValueUpdatesBounds ( self ) :
def testChangingIsoValueDoesntUpdateBounds ( self ) :

sphere = GafferScene.Sphere()
sphere["radius"].setValue( 5 )
Expand All @@ -84,27 +85,8 @@ def testChangingIsoValueUpdatesBounds ( self ) :
levelSetToMesh["in"].setInput( meshToLevelSet["out"] )

self.assertSceneValid( levelSetToMesh["out"] )
self.assertEqual( levelSetToMesh["adjustBounds"].getValue(), False )
self.assertEqual( levelSetToMesh["out"].bound( "/sphere" ), levelSetToMesh["in"].bound( "/sphere" ) )

levelSetToMesh["adjustBounds"].setValue( True )
self.assertSceneValid( levelSetToMesh["out"] )
self.assertEqual(
levelSetToMesh["out"].bound( "/sphere" ),
levelSetToMesh["out"].object( "/sphere" ).bound()
)
bound = levelSetToMesh["out"].bound( "/sphere" )

levelSetToMesh["isoValue"].setValue( -0.5 ) # Shrinks the output mesh

self.assertSceneValid( levelSetToMesh["out"] )
self.assertEqual(
levelSetToMesh["out"].bound( "/sphere" ),
levelSetToMesh["out"].object( "/sphere" ).bound()
)
self.assertTrue( bound.intersects( levelSetToMesh["out"].bound( "/sphere" ).min() ) )
self.assertTrue( bound.intersects( levelSetToMesh["out"].bound( "/sphere" ).max() ) )

def testIncreasingAdapativityDecreasesPolyCount( self ) :

sphere = GafferScene.Sphere()
Expand Down Expand Up @@ -141,3 +123,60 @@ def testParallelGetValueComputesObjectOnce( self ) :
levelSetToMesh["grid"].setValue( "ls_sphere" )

self.assertParallelGetValueComputesObjectOnce( levelSetToMesh["out"], "/vdb" )

def testMerging( self ):

# Quick test of merging a ring of spheres into a torus.
# This test checks the number of faces produced, which is quick way to check that we're
# getting the right amount of overlap, but relies on the specific algorithm used by OpenVDB.
# A future update to OpenVDB might cause this test to fail and require updating, but it would
# be easy enough to validate these results and update the numbers if that happens. ( An OpenVDB
# update that changes this algorithm seems possible, but unlikely ).

sphere = GafferScene.Sphere( "sphere" )

duplicate = GafferScene.Duplicate( "duplicate" )
duplicate["in"].setInput( sphere["out"] )
duplicate["copies"].setValue( 11 )
duplicate["transform"]["rotate"].setValue( imath.V3f( 0, 30, 0 ) )
duplicate["transform"]["pivot"].setValue( imath.V3f( -2, 0, 0 ) )
self.setFilter( duplicate, path='/sphere' )

freezeTransform = GafferScene.FreezeTransform( "freezeTransform" )
freezeTransform["enabled"].setValue( False )
freezeTransform["in"].setInput( duplicate["out"] )
self.setFilter( freezeTransform, path='/*' )

meshToLevelSet = GafferVDB.MeshToLevelSet( "meshToLevelSet" )
meshToLevelSet["in"].setInput( freezeTransform["out"] )
self.setFilter( meshToLevelSet, path='/*' )

levelSetToMesh = GafferVDB.LevelSetToMesh( "levelSetToMesh" )
levelSetToMesh["in"].setInput( meshToLevelSet["out"] )
self.setFilter( levelSetToMesh, path='/*' )

# We're not yet merging, so the spheres all get converted to the same mesh
self.assertEqual( levelSetToMesh["out"].object( "/sphere" ).numFaces(), 1854 )
self.assertEqual( levelSetToMesh["out"].object( "/sphere6" ).numFaces(), 1854 )

# Merge into a big donut
levelSetToMesh["destination"].setValue( '/merged' )

unfrozen = levelSetToMesh["out"].object( "/merged" )
self.assertEqual( unfrozen.numFaces(), 11336 )

# Now try freezing the transform to make sure we get matching results
freezeTransform["enabled"].setValue( True )

# The individual spheres are now each a bit different
levelSetToMesh["destination"].setValue( '${scene:path}' )
self.assertEqual( levelSetToMesh["out"].object( "/sphere" ).numFaces(), 1854 )
self.assertEqual( levelSetToMesh["out"].object( "/sphere5" ).numFaces(), 1842 )
levelSetToMesh["destination"].setValue( '/merged' )

# The combined mesh is very slightly different ( only because of resampling error
# when rotating the grids ). The result is different, but not enough to change the
# face count.
frozen = levelSetToMesh["out"].object( "/merged" )
self.assertNotEqual( frozen["P"], unfrozen["P"] )
self.assertEqual( frozen.numFaces(), 11336 )
58 changes: 58 additions & 0 deletions python/GafferVDBTest/MeshToLevelSetTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
#
##########################################################################

import imath
import time

import IECore
Expand Down Expand Up @@ -233,3 +234,60 @@ def testRecursionViaIntermediateQuery( self ) :
# we'll get deadlock.

meshToLevelSet2["out"].object( "/cube" )

def testMerging( self ):

# Create two non-overlapping spheres
sphere = GafferScene.Sphere()
sphere["radius"].setValue( 1.0 )

sphere2 = GafferScene.Sphere()
sphere2["name"].setValue( "sphere2" )
sphere2["radius"].setValue( 1.0 )
sphere2["transform"]["translate"]["x"].setValue( 5 )

freezeTransform = GafferScene.FreezeTransform()
freezeTransform["in"].setInput( sphere2["out"] )
self.setFilter( freezeTransform, '/sphere2' )

parent = GafferScene.Parent()
parent["parent"].setValue( "/" )
parent["in"].setInput( sphere["out"] )
parent["children"][0].setInput( freezeTransform["out"] )


meshToLevelSet = GafferVDB.MeshToLevelSet()
meshToLevelSet["in"].setInput( parent["out"] )
self.setFilter( meshToLevelSet, '/*' )

voxelCountA = meshToLevelSet["out"].object( "/sphere" ).findGrid( "surface" ).activeVoxelCount()
voxelCountB = meshToLevelSet["out"].object( "/sphere2" ).findGrid( "surface" ).activeVoxelCount()

# Maybe this could change if OpenVDB's algorithm changes, but I would expect it to be constant
# unless something weird changes, so might as well check the actual numbers
self.assertEqual( voxelCountA, 7712 )
self.assertEqual( voxelCountB, 7712 )

meshToLevelSet["destination"].setValue( "/merged" )

# If we write both locations to the same destination, they get merged
self.assertEqual(
meshToLevelSet["out"].object( "/merged" ).findGrid( "surface" ).activeVoxelCount(),
voxelCountA + voxelCountB
)

@GafferTest.TestRunner.PerformanceTestMethod()
def testBasicPerf( self ):
sphere = GafferScene.Sphere()
sphere["radius"].setValue( 2.0 )
sphere["divisions"].setValue( imath.V2i( 1000, 1000 ) )

meshToLevelSet = GafferVDB.MeshToLevelSet()
self.setFilter( meshToLevelSet, '/sphere' )
meshToLevelSet["voxelSize"].setValue( 0.05 )
meshToLevelSet["in"].setInput( sphere["out"] )

meshToLevelSet["in"].object( "/sphere" )

with GafferTest.TestRunner.PerformanceScope() :
meshToLevelSet["out"].object( "/sphere" )
Loading

0 comments on commit 41cbfd5

Please sign in to comment.