Skip to content

Commit 161f015

Browse files
committed
IECoreUSD : Add basic UsdVolVolume <-> VDBObject interoperability
1 parent f418931 commit 161f015

File tree

6 files changed

+296
-3
lines changed

6 files changed

+296
-3
lines changed

Changes

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
10.5.x.x (relative to 10.5.0.0)
22
========
33

4+
Improvements
5+
------------
6+
7+
- USDScene : Added limited support for reading and writing `Volume` prims via `IECoreVDB::VDBObject`.
8+
49
Fixes
510
-----
611

SConstruct

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3041,6 +3041,18 @@ if doConfigure :
30413041
]
30423042
)
30433043

3044+
if haveVDB :
3045+
usdEnv.Append(
3046+
LIBS = [
3047+
os.path.basename( vdbEnv.subst( "$INSTALL_LIB_NAME" ) ),
3048+
"${USD_LIB_PREFIX}usdVol",
3049+
vdbEnv.subst( "openvdb" + env["VDB_LIB_SUFFIX"] )
3050+
],
3051+
CPPDEFINES = [ "IECOREUSD_WITH_OPENVDB" ]
3052+
)
3053+
else :
3054+
usdSources = [ f for f in usdSources if os.path.basename( f ) != "VolumeAlgo.cpp" ]
3055+
30443056
# library
30453057
usdLibrary = usdEnv.SharedLibrary( "lib/" + os.path.basename( usdEnv.subst( "$INSTALL_USDLIB_NAME" ) ), usdSources )
30463058
usdLibraryInstall = usdEnv.Install( os.path.dirname( usdEnv.subst( "$INSTALL_USDLIB_NAME" ) ), usdLibrary )

contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,9 @@ IECORE_PUSH_DEFAULT_VISIBILITY
7070
#include "pxr/usd/usdShade/material.h"
7171
#include "pxr/usd/usdShade/materialBindingAPI.h"
7272
#include "pxr/usd/usdShade/connectableAPI.h"
73+
#ifdef IECOREUSD_WITH_OPENVDB
74+
#include "pxr/usd/usdVol/fieldBase.h"
75+
#endif
7376
IECORE_POP_DEFAULT_VISIBILITY
7477

7578
#include "boost/algorithm/string/classification.hpp"
@@ -165,6 +168,14 @@ bool isSceneChild( const pxr::UsdPrim &prim )
165168
return false;
166169
}
167170

171+
#ifdef IECOREUSD_WITH_OPENVDB
172+
if( prim.IsA<pxr::UsdVolFieldBase>() )
173+
{
174+
// This will be absorbed into the VBDObject loaded by VDBAlgo.
175+
return false;
176+
}
177+
#endif
178+
168179
bool autoMaterials = false;
169180
prim.GetMetadata( g_metadataAutoMaterials, &autoMaterials );
170181

@@ -516,7 +527,7 @@ Imath::M44d localTransform( const pxr::UsdPrim &prim, pxr::UsdTimeCode time )
516527
return result;
517528
}
518529

519-
// Used to assign a unique hash to each USD file. Using a global counter rather than the file name
530+
// Used to assign a unique hash to each USD file. Using a global counter rather than the file name
520531
// means that we treat the same file as separate if it is closed and reopened. This means it's not
521532
// a problem if USD changes things when a file is reopened. USD appears to not in general guarantee
522533
// that anything is the same when reopening an unchanged file - things we're aware of that could
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
//////////////////////////////////////////////////////////////////////////
2+
//
3+
// Copyright (c) 2023, Cinesite VFX Ltd. All rights reserved.
4+
//
5+
// Redistribution and use in source and binary forms, with or without
6+
// modification, are permitted provided that the following conditions are
7+
// met:
8+
//
9+
// * Redistributions of source code must retain the above copyright
10+
// notice, this list of conditions and the following disclaimer.
11+
//
12+
// * Redistributions in binary form must reproduce the above copyright
13+
// notice, this list of conditions and the following disclaimer in the
14+
// documentation and/or other materials provided with the distribution.
15+
//
16+
// * Neither the name of Image Engine Design nor the names of any
17+
// other contributors to this software may be used to endorse or
18+
// promote products derived from this software without specific prior
19+
// written permission.
20+
//
21+
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
22+
// IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
23+
// THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
24+
// PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
25+
// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
26+
// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
27+
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
28+
// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
29+
// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
30+
// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
31+
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32+
//
33+
//////////////////////////////////////////////////////////////////////////
34+
35+
#include "IECoreUSD/ObjectAlgo.h"
36+
37+
#include "IECoreVDB/VDBObject.h"
38+
39+
#include "IECore/MessageHandler.h"
40+
41+
IECORE_PUSH_DEFAULT_VISIBILITY
42+
#include "pxr/usd/usdVol/volume.h"
43+
#include "pxr/usd/usdVol/openVDBAsset.h"
44+
IECORE_POP_DEFAULT_VISIBILITY
45+
46+
using namespace pxr;
47+
using namespace IECore;
48+
using namespace IECoreScene;
49+
using namespace IECoreUSD;
50+
51+
// Overview
52+
// ========
53+
//
54+
// The closest analogue to `UsdVolVolume` in Cortex is `IECoreVDB::VDBObject`,
55+
// but the two classes have significant differences.
56+
//
57+
// - UsdVol provides schemas for referencing volume files on disk, but currently
58+
// provides no access to the volume data itself. On the other hand, VDBObject
59+
// provides direct access to data via `openvdb::GridBase::Ptr`, making it
60+
// suitable for use in live volume generation and processing. We take
61+
// advantage of this in Gaffer by providing various nodes for manipulating
62+
// volumes.
63+
// - UsdVolVolume allows the referencing of multiple fields (grids in VDB
64+
// parlance) from multiple different files, and allows the fields to be given
65+
// names that are distinct from the grid names themselves. While a VDBObject
66+
// _can_ be composed of multiple grids from arbitrary sources, this is done by
67+
// calling `insertGrid()`, and doesn't track the file of origin (if there even
68+
// was one). VDBObject is only guaranteed to reference file-backed data when
69+
// constructed from a single filename and when the loaded grids have not been
70+
// modified subsequently.
71+
//
72+
// While there is scope for extending VDBObject to provide a cleaner mapping, at
73+
// present this would have limited benefit. All our supported VDB-consuming
74+
// renderers have volume objects that reference only a single file, so we would
75+
// just be pushing the mismatch further down the pipeline. So for now we provide
76+
// the closest mapping we can, and issue warnings for any loss of data.
77+
78+
// Reading
79+
// =======
80+
81+
namespace
82+
{
83+
84+
IECore::ObjectPtr readVolume( pxr::UsdVolVolume &volume, pxr::UsdTimeCode time, const Canceller *canceller )
85+
{
86+
std::string fileName;
87+
for( const auto &[fieldName, fieldPath] : volume.GetFieldPaths() )
88+
{
89+
UsdVolOpenVDBAsset fieldAsset( volume.GetPrim().GetPrimAtPath( fieldPath ) );
90+
if( !fieldAsset )
91+
{
92+
IECore::msg(
93+
IECore::Msg::Warning, "IECoreVDB::VolumeAlgo::readVolume",
94+
"Ignoring \"" + fieldPath.GetAsString() + "\" because it is not an OpenVDBAsset"
95+
);
96+
continue;
97+
}
98+
99+
SdfAssetPath fieldAssetPath;
100+
fieldAsset.GetFilePathAttr().Get( &fieldAssetPath, time );
101+
const std::string fieldFileName = fieldAssetPath.GetResolvedPath();
102+
103+
if( fileName.empty() )
104+
{
105+
fileName = fieldFileName;
106+
}
107+
else if( fieldFileName != fileName )
108+
{
109+
IECore::msg(
110+
IECore::Msg::Warning, "IECoreVDB::VolumeAlgo::readVolume",
111+
"Ignoring file \"" + fieldFileName + "\" from field \"" + fieldPath.GetAsString() + "\""
112+
);
113+
}
114+
}
115+
116+
if( fileName.empty() )
117+
{
118+
IECore::msg(
119+
IECore::Msg::Warning, "IECoreVDB::VolumeAlgo::readVolume",
120+
"No file found for \"" + volume.GetPrim().GetPath().GetAsString() + "\""
121+
);
122+
return nullptr;
123+
}
124+
125+
return new IECoreVDB::VDBObject( fileName );
126+
}
127+
128+
bool volumeMightBeTimeVarying( pxr::UsdVolVolume &volume )
129+
{
130+
for( const auto &[fieldName, fieldPath] : volume.GetFieldPaths() )
131+
{
132+
UsdVolOpenVDBAsset fieldAsset( volume.GetPrim().GetPrimAtPath( fieldPath ) );
133+
if( !fieldAsset )
134+
{
135+
continue;
136+
}
137+
138+
if( fieldAsset.GetFilePathAttr().ValueMightBeTimeVarying() )
139+
{
140+
return true;
141+
}
142+
}
143+
return false;
144+
}
145+
146+
ObjectAlgo::ReaderDescription<pxr::UsdVolVolume> g_volumeReaderDescription( pxr::TfToken( "Volume" ), readVolume, volumeMightBeTimeVarying );
147+
148+
} // namespace
149+
150+
//////////////////////////////////////////////////////////////////////////
151+
// Writing
152+
//////////////////////////////////////////////////////////////////////////
153+
154+
namespace
155+
{
156+
157+
pxr::TfToken gridClass( const openvdb::GridBase *grid )
158+
{
159+
switch( grid->getGridClass() )
160+
{
161+
case openvdb::GRID_LEVEL_SET :
162+
return pxr::TfToken( "GRID_LEVEL_SET" );
163+
case openvdb::GRID_FOG_VOLUME :
164+
return pxr::TfToken( "GRID_FOG_VOLUME" );
165+
case openvdb::GRID_STAGGERED :
166+
return pxr::TfToken( "GRID_STAGGERED" );
167+
default :
168+
return pxr::TfToken( "GRID_UNKNOWN" );
169+
}
170+
}
171+
172+
bool writeVolume( const IECoreVDB::VDBObject *object, const pxr::UsdStagePtr &stage, const pxr::SdfPath &path, pxr::UsdTimeCode time )
173+
{
174+
if( !object->unmodifiedFromFile() )
175+
{
176+
IECore::msg(
177+
IECore::Msg::Warning, "IECoreVDB::VolumeAlgo::writeVolume",
178+
"Not writing \"" + path.GetAsString() + "\"because VDBObject is not backed by a file"
179+
);
180+
return false;
181+
}
182+
183+
auto volume = UsdVolVolume::Define( stage, path );
184+
185+
for( const auto &gridName : object->gridNames() )
186+
{
187+
const TfToken gridNameToken( gridName );
188+
auto fieldPath = path.AppendChild( gridNameToken );
189+
auto fieldAsset = UsdVolOpenVDBAsset::Define( stage, fieldPath );
190+
fieldAsset.CreateFilePathAttr().Set( SdfAssetPath( object->fileName() ), time );
191+
fieldAsset.CreateFieldNameAttr().Set( gridNameToken );
192+
fieldAsset.CreateFieldClassAttr().Set( gridClass( object->findGrid( gridName ).get() ) );
193+
volume.CreateFieldRelationship( gridNameToken, fieldPath );
194+
}
195+
196+
return true;
197+
}
198+
199+
ObjectAlgo::WriterDescription<IECoreVDB::VDBObject> g_volumeWriterDescription( writeVolume );
200+
201+
} // namespace

contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@
3232
#
3333
##########################################################################
3434

35+
import importlib
3536
import os
3637
import math
3738
import unittest
39+
import pathlib
3840
import shutil
3941
import tempfile
4042
import imath
@@ -51,6 +53,8 @@
5153
if pxr.Usd.GetVersion() < ( 0, 19, 3 ) :
5254
pxr.Usd.Attribute.HasAuthoredValue = pxr.Usd.Attribute.HasAuthoredValueOpinion
5355

56+
haveVDB = importlib.util.find_spec( "IECoreVDB" ) is not None
57+
5458
class USDSceneTest( unittest.TestCase ) :
5559

5660
def setUp( self ) :
@@ -482,7 +486,7 @@ def testInstancePrototypeHashesNotReused( self ) :
482486
# The solution we've ended up with is instead of assigning consistent hashes, each instance of the
483487
# file gets it's own unique hashes, and is basically treated separately. This allows us to just
484488
# use the prototype names in the hash, since we force the hash to be unique anyway.
485-
489+
486490
usedHashes = set()
487491

488492
for i in range( 100 ):
@@ -506,7 +510,7 @@ def testInstancePrototypeHashesNotReused( self ) :
506510
instanceJ = scene.child( "instance%i" % j )
507511
self.assertEqual( h1, instanceJ.child( "world" ).hash( scene.HashType.TransformHash, 1 ) )
508512
del instanceJ
509-
513+
510514
self.assertNotIn( h2, usedHashes )
511515
for j in range( 11, 20 ):
512516
instanceJ = scene.child( "instance%i" % j )
@@ -3667,5 +3671,52 @@ def testMaterialBindingInsideInstance( self ) :
36673671
shader2 = instance2.readAttribute( "surface", 0.0, _copy = False )
36683672
self.assertTrue( shader1.isSame( shader2 ) )
36693673

3674+
@unittest.skipIf( not haveVDB, "No IECoreVDB" )
3675+
def testReadUsdVolVolume( self ) :
3676+
3677+
import IECoreVDB
3678+
3679+
fileName = os.path.dirname( __file__ ) + "/data/volume.usda"
3680+
root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
3681+
child = root.child( "volume" )
3682+
self.assertEqual( child.childNames(), [] )
3683+
3684+
vdbObject = child.readObject( 0 )
3685+
self.assertIsInstance( vdbObject, IECoreVDB.VDBObject )
3686+
self.assertTrue( pathlib.Path( vdbObject.fileName() ).samefile( "test/IECoreVDB/data/smoke.vdb" ) )
3687+
self.assertTrue( vdbObject.unmodifiedFromFile() )
3688+
3689+
@unittest.skipIf( not haveVDB, "No IECoreVDB" )
3690+
def testWriteVDBObject( self ) :
3691+
3692+
import IECoreVDB
3693+
3694+
vdbFileName = "./test/IECoreVDB/data/smoke.vdb"
3695+
vdbObject = IECoreVDB.VDBObject( vdbFileName )
3696+
3697+
# Write via SceneInterface
3698+
3699+
fileName = os.path.join( self.temporaryDirectory(), "volume.usda" )
3700+
root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
3701+
child = root.createChild( "smoke" )
3702+
child.writeObject( vdbObject, 0 )
3703+
3704+
del root, child
3705+
3706+
# Verify via USD API
3707+
3708+
stage = pxr.Usd.Stage.Open( fileName )
3709+
volume = pxr.UsdVol.Volume( stage.GetPrimAtPath( "/smoke" ) )
3710+
self.assertTrue( volume )
3711+
self.assertTrue( volume.HasFieldRelationship( "density" ) )
3712+
self.assertEqual( volume.GetFieldPath( "density" ), "/smoke/density" )
3713+
self.assertEqual( volume.GetPrim().GetChildrenNames(), [ "density" ] )
3714+
3715+
field = pxr.UsdVol.OpenVDBAsset( stage.GetPrimAtPath( "/smoke/density" ) )
3716+
self.assertTrue( field )
3717+
self.assertEqual( field.GetFieldNameAttr().Get( 0 ), "density" )
3718+
self.assertEqual( field.GetFilePathAttr().Get( 0 ), vdbFileName )
3719+
self.assertEqual( field.GetFieldClassAttr().Get( 0 ), "GRID_FOG_VOLUME" )
3720+
36703721
if __name__ == "__main__":
36713722
unittest.main()
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
#usda 1.0
2+
3+
def Volume "volume"
4+
{
5+
custom rel field:density = </volume/density>
6+
7+
def OpenVDBAsset "density"
8+
{
9+
int fieldIndex = 0
10+
token fieldName = "density"
11+
asset filePath = @test/IECoreVDB/data/smoke.vdb@
12+
}
13+
}

0 commit comments

Comments
 (0)