Skip to content

Commit

Permalink
GaftferVDB : Add VolumeScatter
Browse files Browse the repository at this point in the history
  • Loading branch information
dboogert authored and danieldresser-ie committed Jun 27, 2023
1 parent 7d492ab commit 0cd78d2
Show file tree
Hide file tree
Showing 10 changed files with 835 additions and 0 deletions.
5 changes: 5 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

> Caution : A bug fix in the handling of the `ai:volume:step_scale` attribute may change the appearance of Arnold volume renders.

Features
--------

- VolumeScatter : Added a new node scattering points throughout a volume.

Improvements
------------

Expand Down
1 change: 1 addition & 0 deletions include/GafferVDB/TypeIds.h
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ enum TypeId
PointsGridToPointsTypeId = 110956,
SphereLevelSetTypeId = 110957,
PointsToLevelSetTypeId = 110958,
VolumeScatterTypeId = 110959,
LastTypeId = 110974
};

Expand Down
102 changes: 102 additions & 0 deletions include/GafferVDB/VolumeScatter.h
Original file line number Diff line number Diff line change
@@ -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>() );
~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
275 changes: 275 additions & 0 deletions python/GafferVDBTest/VolumeScatterTest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
##########################################################################
#
# 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 ScatterTest( 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, 18277 )

# 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
self.assertTrue( points.bound().min().equalWithAbsError( imath.V3f(-31.85, -11.02, -25.19 ), 0.01 ) )
self.assertTrue( points.bound().max().equalWithAbsError( imath.V3f(17.34, 91.58, 25.57 ), 0.01 ) )

center = sum( points["P"].data ) / numP
self.assertTrue( center.equalWithRelError( imath.V3f(-4.53, 17.89, -0.57), 0.01 ) )

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.equalWithAbsError( imath.V3f( 7.92, 20.75, 6.94 ), 0.01 ) )

vs["density"].setValue( 2 )
self.assertEqual( len( vs['out'].object( "/test" )["P"].data ), 36915 )

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" )

"""
import IECoreVDB
import GafferVDB
class PointsToLevelSetTest( GafferVDBTest.VDBTestCase ) :
def testSupportedPrimitives( self ) :
sphere = GafferScene.Sphere()
sphereFilter = GafferScene.PathFilter()
sphereFilter["paths"].setValue( IECore.StringVectorData( [ "/sphere" ] ) )
meshToPoints = GafferScene.MeshToPoints()
meshToPoints["in"].setInput( sphere["out"] )
meshToPoints["filter"].setInput( sphereFilter["out"] )
pointsToLevelSet = GafferVDB.PointsToLevelSet()
pointsToLevelSet["in"].setInput( meshToPoints["out"] )
pointsToLevelSet["filter"].setInput( sphereFilter["out"] )
# PointsPrimitives can be converted to level sets.
self.assertIsInstance( pointsToLevelSet["in"].object( "/sphere" ), IECoreScene.PointsPrimitive )
self.assertIsInstance( pointsToLevelSet["out"].object( "/sphere" ), IECoreVDB.VDBObject )
# But so can MeshPrimitives, since our only hard requirement is a primitive variable
# called "P".
meshToPoints["enabled"].setValue( False )
self.assertIsInstance( pointsToLevelSet["in"].object( "/sphere" ), IECoreScene.MeshPrimitive )
self.assertIsInstance( pointsToLevelSet["out"].object( "/sphere" ), IECoreVDB.VDBObject )
# Sphere primitives cannot be converted, because they don't have "P".
sphere["type"].setValue( sphere.Type.Primitive )
self.assertIsInstance( pointsToLevelSet["in"].object( "/sphere" ), IECoreScene.SpherePrimitive )
self.assertIsInstance( pointsToLevelSet["out"].object( "/sphere" ), IECoreScene.SpherePrimitive )
self.assertScenesEqual( pointsToLevelSet["in"], pointsToLevelSet["out"] )
def testWidth( self ) :
plane = GafferScene.Plane()
planeFilter = GafferScene.PathFilter()
planeFilter["paths"].setValue( IECore.StringVectorData( [ "/plane" ] ) )
pointsToLevelSet = GafferVDB.PointsToLevelSet()
pointsToLevelSet["in"].setInput( plane["out"] )
pointsToLevelSet["filter"].setInput( planeFilter["out"] )
pointsToLevelSet["voxelSize"].setValue( 0.025 )
levelSetToMesh = GafferVDB.LevelSetToMesh()
levelSetToMesh["in"].setInput( pointsToLevelSet["out"] )
levelSetToMesh["filter"].setInput( planeFilter["out"] )
w = levelSetToMesh["out"].object( "/plane" ).bound().size().z
pointsToLevelSet["widthScale"].setValue( 2 )
w2 = levelSetToMesh["out"].object( "/plane" ).bound().size().z
self.assertAlmostEqual( w2, w * 2, delta = 0.01 )
primitiveVariables = GafferScene.PrimitiveVariables()
primitiveVariables["in"].setInput( plane["out"] )
primitiveVariables["filter"].setInput( planeFilter["out"] )
primitiveVariables["primitiveVariables"]["width"] = Gaffer.NameValuePlug( "width", 2.0 )
pointsToLevelSet["in"].setInput( primitiveVariables["out"] )
w4 = levelSetToMesh["out"].object( "/plane" ).bound().size().z
self.assertAlmostEqual( w4, w * 4, delta = 0.01 )
def testPointSizeWarnings( self ) :
plane = GafferScene.Plane()
planeFilter = GafferScene.PathFilter()
planeFilter["paths"].setValue( IECore.StringVectorData( [ "/plane" ] ) )
pointsToLevelSet = GafferVDB.PointsToLevelSet()
pointsToLevelSet["in"].setInput( plane["out"] )
pointsToLevelSet["filter"].setInput( planeFilter["out"] )
pointsToLevelSet["voxelSize"].setValue( 4 )
with IECore.CapturingMessageHandler() as mh :
self.assertIsInstance( pointsToLevelSet["out"].object( "/plane" ), IECoreVDB.VDBObject )
self.assertEqual( len( mh.messages ), 1 )
self.assertEqual( mh.messages[0].level, IECore.Msg.Level.Warning )
self.assertEqual( mh.messages[0].context, "PointsToLevelSet" )
self.assertEqual( mh.messages[0].message, "4 points from \"/plane\" were ignored because they were too small" )
def testVelocityTrails( self ) :
points = IECoreScene.PointsPrimitive( IECore.V3fVectorData( [ imath.V3f( 0 ) ] ) )
points["velocity"] = IECoreScene.PrimitiveVariable(
IECoreScene.PrimitiveVariable.Interpolation.Vertex,
IECore.V3fVectorData( [ imath.V3f( 100, 0, 0 ) ] )
)
objectToScene = GafferScene.ObjectToScene()
objectToScene["object"].setValue( points )
objectFilter = GafferScene.PathFilter()
objectFilter["paths"].setValue( IECore.StringVectorData( [ "/object" ] ) )
pointsToLevelSet = GafferVDB.PointsToLevelSet()
pointsToLevelSet["in"].setInput( objectToScene["out"] )
pointsToLevelSet["filter"].setInput( objectFilter["out"] )
self.assertIsInstance( pointsToLevelSet["out"].object( "/object" ), IECoreVDB.VDBObject )
levelSetToMesh = GafferVDB.LevelSetToMesh()
levelSetToMesh["in"].setInput( pointsToLevelSet["out"] )
levelSetToMesh["filter"].setInput( objectFilter["out"] )
b1 = levelSetToMesh["out"].object( "/object" ).bound()
pointsToLevelSet["useVelocity"].setValue( True )
b2 = levelSetToMesh["out"].object( "/object" ).bound()
# Trails are in opposite direction to velocity, so in negative X in this case.
self.assertEqual( b2.max(), b1.max() )
self.assertEqual( b2.min().y, b1.min().y )
self.assertEqual( b2.min().z, b1.min().z )
self.assertLess( b2.min().x, b1.min().x )
# The length of trails can be changed
pointsToLevelSet["velocityScale"].setValue( 2 )
b3 = levelSetToMesh["out"].object( "/object" ).bound()
self.assertEqual( b3.max(), b2.max() )
self.assertEqual( b3.min().y, b2.min().y )
self.assertEqual( b3.min().z, b2.min().z )
self.assertLess( b3.min().x, b2.min().x )
# Trails automatically account for framesPerSecond
with Gaffer.Context() as c :
c.setFramesPerSecond( 1 )
b4 = levelSetToMesh["out"].object( "/object" ).bound()
self.assertLess( b4.min().x, b3.min().x )
if __name__ == "__main__":
unittest.main()
"""
Loading

0 comments on commit 0cd78d2

Please sign in to comment.