From 1560cefc4f1cdd8a1ac61a5266415a962fe6594a Mon Sep 17 00:00:00 2001 From: Daniel Dresser Date: Thu, 30 Jan 2025 17:35:06 -0800 Subject: [PATCH] IECoreUSD : Option for relative prototype paths inside a PointInstancer If you set the env var IECOREUSD_POINTINSTANCER_RELATIVEPROTOTYPES=1, if an instancer named "/inst" contains an internal prototype path like "/inst/Prototypes/proto", that prototype path will now be loaded as "./Prototypes/proto". This is compatible with how point instancers will now be handled in Gaffer, allowing them to be relocated within the scene hierarchy. --- Changes | 5 +++ .../src/IECoreUSD/PointInstancerAlgo.cpp | 30 +++++++++++++++- .../IECoreUSD/test/IECoreUSD/USDSceneTest.py | 36 +++++++++++++++++++ .../data/pointInstancerWeirdPrototypes.usda | 26 ++++++++++++++ 4 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 contrib/IECoreUSD/test/IECoreUSD/data/pointInstancerWeirdPrototypes.usda diff --git a/Changes b/Changes index fc224a6c6c..14504bf853 100644 --- a/Changes +++ b/Changes @@ -1,6 +1,11 @@ 10.5.x.x (relative to 10.5.12.0) ======== +Features +-------- + +- PointInstancerAlgo : Added support for the env var IECOREUSD_POINTINSTANCER_RELATIVEPROTOTYPES. If this is set to "1", then when USD PointInstancers are loaded as point clouds, if they contain prototype paths beneath themselves in the hierarchy, those prototype paths will be loaded as relative paths, starting with "./". This aligns with how Gaffer will now handle prototype paths, and allows point instancers to be relocated in the hierarchy. + Fixes ----- diff --git a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp index db064e4f13..de9f01c88b 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/PointInstancerAlgo.cpp @@ -53,6 +53,19 @@ 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; @@ -108,16 +121,31 @@ 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 ); + const pxr::SdfPath &primPath = pointInstancer.GetPath(); + IECore::StringVectorDataPtr prototypeRootsData = new IECore::StringVectorData(); auto &prototypeRoots = prototypeRootsData->writable(); prototypeRoots.reserve( targets.size() ); for( const auto &t : targets ) { - prototypeRoots.push_back( t.GetString() ); + if( !g_relativePrototypes || !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() ); + } } newPoints->variables["prototypeRoots"] = IECoreScene::PrimitiveVariable( IECoreScene::PrimitiveVariable::Constant, prototypeRootsData ); diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index d5eda47c4b..c9a52115cf 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -42,6 +42,8 @@ import shutil import tempfile import imath +import subprocess +import sys import threading import time @@ -4486,6 +4488,40 @@ 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 ) : + + root = IECoreScene.SceneInterface.create( + os.path.join( os.path.dirname( __file__ ), "data", "pointInstancerWeirdPrototypes.usda" ), + IECore.IndexedIO.OpenMode.Read + ) + 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 ) + @unittest.skipIf( not haveVDB, "No IECoreVDB" ) def testUsdVolVolumeSlashes( self ) : diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/pointInstancerWeirdPrototypes.usda b/contrib/IECoreUSD/test/IECoreUSD/data/pointInstancerWeirdPrototypes.usda new file mode 100644 index 0000000000..4845af62d6 --- /dev/null +++ b/contrib/IECoreUSD/test/IECoreUSD/data/pointInstancerWeirdPrototypes.usda @@ -0,0 +1,26 @@ +#usda 1.0 +( +) + +def PointInstancer "inst" ( + kind = "group" +) +{ + point3f[] positions = [(0, 0, -20), (0, 0, -16), (0, 0, -12), (0, 0, -8), (0, 0, -4), (0, 0, 0), (0, 0, 4), (0, 0, 8), (0, 0, 12), (0, 0, 16)] + int[] protoIndices = [0, 1, 0, 1, 0, 1, 0, 1, 0, 1] + rel prototypes = [ , ] + + def Scope "Prototypes" ( + kind = "group" + ) + { + def Sphere "sphere" + { + double radius = 1 + } + } +} + +def Cube "cube" +{ +}