From 2a34c900c470bc1f303a91ff609a56c7d1b4955d Mon Sep 17 00:00:00 2001 From: Alex Fuller Date: Tue, 24 Feb 2026 15:35:36 +1100 Subject: [PATCH] IECoreUSD : Round-trip Material prims with binds. --- Changes | 7 + contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp | 384 +++++++++++++++++- contrib/IECoreUSD/src/IECoreUSD/USDScene.h | 3 + .../IECoreUSD/test/IECoreUSD/USDSceneTest.py | 192 ++++++++- .../IECoreUSD/data/exposedShaderInput.usda | 4 +- .../test/IECoreUSD/data/materialPurpose.usda | 4 +- .../IECoreUSD/data/shaderNameConflict.usda | 4 +- .../test/IECoreUSD/data/shaderParentLoc.usda | 4 +- .../IECoreUSD/data/textureParameters.usda | 4 +- .../data/unconnectedMaterialOutput.usda | 11 +- 10 files changed, 594 insertions(+), 23 deletions(-) diff --git a/Changes b/Changes index 2e102b1959..a030059b46 100644 --- a/Changes +++ b/Changes @@ -1,7 +1,14 @@ 10.7.x.x (relative to 10.7.0.0a6) ======== +Features +-------- +- IECoreUSD : + - Added the ability to round-trip Scope and Material schema types via `usd:schemaType` attribute. + - Create `__materials` set for locations acting as materials. + - Added `usd:material:binding` as well as `usd:material:binding:full` and `usd:material:binding:preview` attributes to round-trip material bindings. + - If the shader networks assigned to the material location matches the shader networks assigned to the location with the binding attribute, a `cortex_autoMaterials` scope won't be created on the location and instead will bind to the material location only to improve round-tripping consistency with other DCCs. 10.7.0.0a6 (relative to 10.7.0.0a5) ========== diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp index 9a6177fdfc..442f305904 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp @@ -220,6 +220,11 @@ bool isSceneChild( const pxr::UsdPrim &prim ) return false; } + if( prim.GetParent().IsA() ) + { + return false; + } + #ifdef IECOREUSD_WITH_OPENVDB if( prim.IsA() ) { @@ -233,7 +238,8 @@ bool isSceneChild( const pxr::UsdPrim &prim ) return (!autoMaterials) && ( prim.GetTypeName().IsEmpty() || - pxr::UsdGeomImageable( prim ) + pxr::UsdGeomImageable( prim ) || + prim.IsA() ); } @@ -282,7 +288,8 @@ boost::container::flat_map g_schemaTypeSetPredicate #if PXR_VERSION >= 2111 { pxr::TfToken( "__lights" ), &pxr::UsdPrim::HasAPI }, #endif - { pxr::TfToken( "usd:pointInstancers" ), &pxr::UsdPrim::IsA } + { pxr::TfToken( "usd:pointInstancers" ), &pxr::UsdPrim::IsA }, + { pxr::TfToken( "__materials" ), &pxr::UsdPrim::IsA }, }; bool collectionIsSet( const pxr::UsdCollectionAPI &collection ) @@ -840,6 +847,44 @@ class USDScene::IO : public RefCounted } } + // Get SdfPath's hash impl for tbb::concurrent_hash_map + struct SdfPathHashCompare + { + static size_t hash( const pxr::SdfPath &path ) + { + return path.GetHash(); + } + + static bool equal( const pxr::SdfPath &first, const pxr::SdfPath &second ) + { + return first == second; + } + }; + + using MaterialHash = boost::container::flat_map; + using MaterialHashes = tbb::concurrent_hash_map; + + void setMaterialHash( const pxr::SdfPath &path, const pxr::TfToken &purpose, const IECore::MurmurHash &hash ) + { + MaterialHashes::accessor writeAccessor; + m_materialHashes.insert( writeAccessor, path ); + writeAccessor->second[purpose] = hash; + writeAccessor.release(); + } + + const IECore::MurmurHash getMaterialHash( const pxr::SdfPath &path, const pxr::TfToken &purpose ) + { + MaterialHashes::const_accessor readAccessor; + if( m_materialHashes.find( readAccessor, path ) ) + { + if( readAccessor->second.contains( purpose ) ) + { + return readAccessor->second.at( purpose ); + } + } + return IECore::MurmurHash(); + } + inline int uniqueId() { return m_uniqueId; @@ -898,6 +943,9 @@ class USDScene::IO : public RefCounted ShaderNetworkCache m_shaderNetworkCache; ShaderNetworkMightBeTimeVaryingCache m_shaderNetworkMightBeTimeVaryingCache; + // Store the material hash to compare and potentially reference. + MaterialHashes m_materialHashes; + // Used to identify a file uniquely ( including between different openings of the same filename, // since closing and reopening a file may cause USD to shuffle the contents ). const int m_uniqueId; @@ -923,7 +971,7 @@ USDScene::USDScene( IOPtr io, LocationPtr location ) USDScene::~USDScene() { - if( m_materials.size() ) + if( m_materials.size() || m_materialBinds.size() ) { try { @@ -934,6 +982,106 @@ USDScene::~USDScene() topAncestor = topAncestor.GetParentPath(); } + // Already converted global materials + if( m_location->prim.IsA() ) + { + return; + } + + if( m_materialBinds.size() ) + { + bool isUnique = false; + + for( const auto &[purpose, material] : m_materials ) + { + // Use a hash to identify the combination of shaders in this material. + IECore::MurmurHash materialHash; + for( const auto &[output, shaderNetwork] : material ) + { + materialHash.append( output ); + materialHash.append( shaderNetwork->Object::hash() ); + } + + if( m_materialBinds.contains( purpose ) ) + { + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( m_root->getStage(), m_materialBinds[purpose] ); + if( !mat ) + { + IECore::msg( + IECore::Msg::Warning, "USDScene::~USDScene", + fmt::format( "Unable to find material \"{}\" to bind to \"{}\"", + m_materialBinds[purpose].GetString(), m_location->prim.GetPath().GetString() ) + ); + isUnique = true; + break; + } + if( m_root->getMaterialHash( m_materialBinds[purpose], purpose ) != materialHash ) + { + IECore::msg( + IECore::Msg::Warning, "USDScene::~USDScene", + fmt::format( "Unable to bind material \"{}\" to \"{}\" as they do not match.", + m_materialBinds[purpose].GetString(), m_location->prim.GetPath().GetString() ) + ); + isUnique = true; + break; + } + // Bind the material to this location + pxr::UsdShadeMaterialBindingAPI::Apply( m_location->prim ).Bind( + mat, pxr::UsdShadeTokens->fallbackStrength, purpose + ); + } + else + { + IECore::msg( + IECore::Msg::Warning, "USDScene::~USDScene", + fmt::format( "Unable to find the correct purpose \"{}\" on material \"{}\" to bind to \"{}\".", + purpose.GetString(), m_materialBinds[purpose].GetString(), m_location->prim.GetPath().GetString() ) + ); + isUnique = true; + break; + } + } + + if( !m_materials.size() ) + { + // If there is a material bind but no materials assigned directly, ensure we apply the bind anyways. + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) + { + if( m_materialBinds.contains( purpose ) ) + { + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( m_root->getStage(), m_materialBinds[purpose] ); + if ( mat ) + { + pxr::UsdShadeMaterialBindingAPI::Apply( m_location->prim ).Bind( + mat, pxr::UsdShadeTokens->fallbackStrength, purpose + ); + } + else + { + IECore::msg( + IECore::Msg::Warning, "USDScene::~USDScene", + fmt::format( "Unable to find material \"{}\" to bind to \"{}\"", + m_materialBinds[purpose].GetString(), m_location->prim.GetPath().GetString() ) + ); + } + } + } + return; + } + else if( !isUnique ) + { + // Return early if the shaders are bound to material prims. + return; + } + else + { + IECore::msg( + IECore::Msg::Warning, "USDScene::~USDScene", + fmt::format( "Generating unique materials for \"{}\" (see warnings above).", m_location->prim.GetPath().GetString() ) + ); + } + } + pxr::UsdGeomScope materialContainer = pxr::UsdGeomScope::Get( m_root->getStage(), topAncestor.AppendChild( pxr::TfToken( "materials" ) ) ); if( !materialContainer ) { @@ -1090,6 +1238,8 @@ namespace const IECore::InternedString g_purposeAttributeName( "usd:purpose" ); const IECore::InternedString g_kindAttributeName( "usd:kind" ); +const IECore::InternedString g_schemaTypeAttributeName( "usd:schemaType" ); +const IECore::InternedString g_materialBindingAttributeName( "usd:material:binding" ); const IECore::InternedString g_lightAttributeName( "light" ); const IECore::InternedString g_doubleSidedAttributeName( "doubleSided" ); @@ -1127,6 +1277,44 @@ bool USDScene::hasAttribute( const SceneInterface::Name &name ) const { return pxr::UsdGeomGprim( m_location->prim ).GetDoubleSidedAttr().HasAuthoredValue(); } + else if( name == g_schemaTypeAttributeName ) + { + if( m_location->prim.IsA() ) + { + return true; + } + else if( m_location->prim.IsA() ) + { + return true; + } + return false; + } + else if( boost::starts_with( name.string(), g_materialBindingAttributeName.string() ) ) + { + if( !m_location->prim.HasAPI() ) + { + return false; + } + + pxr::UsdShadeMaterialBindingAPI binding = pxr::UsdShadeMaterialBindingAPI( m_location->prim ); + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) + { + if( binding.GetDirectBinding( purpose ).IsBound() && + isSceneChild( binding.GetDirectBinding( purpose ).GetMaterial().GetPrim().GetParent() ) ) + { + InternedString attrName = g_materialBindingAttributeName; + if( !purpose.IsEmpty() ) + { + attrName = attrName.string() + ":" + purpose.GetString(); + } + if( name == attrName ) + { + return true; + } + } + } + return false; + } else if( auto attribute = AttributeAlgo::findUSDAttribute( m_location->prim, name.string() ) ) { return attribute.HasAuthoredValue(); @@ -1134,6 +1322,16 @@ bool USDScene::hasAttribute( const SceneInterface::Name &name ) const else { const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + + if( m_location->prim.IsA() ) + { + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( m_root->getStage(), m_location->prim.GetPath() ); + if( pxr::UsdShadeOutput o = mat.GetOutput( output ) ) + { + return ShaderAlgo::canReadShaderNetwork( o ); + } + } + if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) ) { if( pxr::UsdShadeOutput o = mat.GetOutput( output ) ) @@ -1182,6 +1380,29 @@ void USDScene::attributeNames( SceneInterface::NameList &attrs ) const attrs.push_back( g_doubleSidedAttributeName ); } + if( m_location->prim.IsA() || m_location->prim.IsA() ) + { + attrs.push_back( g_schemaTypeAttributeName ); + } + + if( m_location->prim.HasAPI() ) + { + pxr::UsdShadeMaterialBindingAPI binding = pxr::UsdShadeMaterialBindingAPI( m_location->prim ); + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) + { + if( binding.GetDirectBinding( purpose ).IsBound() && + isSceneChild( binding.GetDirectBinding( purpose ).GetMaterial().GetPrim().GetParent() ) ) + { + InternedString attrName = g_materialBindingAttributeName; + if( !purpose.IsEmpty() ) + { + attrName = attrName.string() + ":" + purpose.GetString(); + } + attrs.push_back( attrName ); + } + } + } + std::vector attributes = m_location->prim.GetAuthoredAttributes(); for( const auto &attribute : attributes ) { @@ -1196,6 +1417,20 @@ void USDScene::attributeNames( SceneInterface::NameList &attrs ) const } } + if ( m_location->prim.IsA() ) + { + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( m_root->getStage(), m_location->prim.GetPath() ); + for( pxr::UsdShadeOutput &o : mat.GetOutputs( /* onlyAuthored = */ true ) ) + { + if( !ShaderAlgo::canReadShaderNetwork( o ) ) + { + continue; + } + InternedString attrName = AttributeAlgo::nameFromUSD( { o.GetBaseName() , false } ); + attrs.push_back( attrName ); + } + } + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) { if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) ) @@ -1290,6 +1525,38 @@ ConstObjectPtr USDScene::readAttribute( const SceneInterface::Name &name, double } return nullptr; } + else if( name == g_schemaTypeAttributeName ) + { + if( m_location->prim.IsA() ) + { + return new StringData( "Material" ); + } + else if( m_location->prim.IsA() ) + { + return new StringData( "Scope" ); + } + return nullptr; + } + else if( boost::starts_with( name.string(), g_materialBindingAttributeName.string() ) ) + { + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) + { + InternedString attrName = g_materialBindingAttributeName; + if( !purpose.IsEmpty() ) + { + attrName = attrName.string() + ":" + purpose.GetString(); + } + if( name == attrName ) + { + pxr::UsdShadeMaterialBindingAPI binding = pxr::UsdShadeMaterialBindingAPI( m_location->prim ); + if( isSceneChild( binding.GetDirectBinding( purpose ).GetMaterial().GetPrim().GetParent() ) ) + { + return new StringData( binding.GetDirectBinding( purpose ).GetMaterialPath().GetString() ); + } + } + } + return nullptr; + } else if( pxr::UsdAttribute attribute = AttributeAlgo::findUSDAttribute( m_location->prim, name.string() ) ) { return DataAlgo::fromUSD( attribute, m_root->timeCode( time ) ); @@ -1297,6 +1564,16 @@ ConstObjectPtr USDScene::readAttribute( const SceneInterface::Name &name, double else { const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + + if( m_location->prim.IsA() ) + { + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( m_root->getStage(), m_location->prim.GetPath() ); + if( pxr::UsdShadeOutput o = mat.GetOutput( output ) ) + { + return m_root->readShaderNetwork( o, m_root->timeCode( time ) ); + } + } + if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) ) { if( pxr::UsdShadeOutput o = mat.GetOutput( output ) ) @@ -1365,22 +1642,72 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a } } } - else if( const IECoreScene::ShaderNetwork *shaderNetwork = runTimeCast( attribute ) ) + else if( name == g_schemaTypeAttributeName ) { -#if PXR_VERSION >= 2111 - if( name == g_lightAttributeName ) + if( auto *data = reportedCast( attribute, "USDScene::writeAttribute", name.c_str() ) ) { - ShaderAlgo::writeLight( shaderNetwork, m_location->prim ); + pxr::TfType schemaType = pxr::UsdSchemaRegistry::GetInstance().GetTypeFromName( pxr::TfToken( data->readable() ) ); + if( schemaType.IsA() ) + { + pxr::UsdGeomScope scope = pxr::UsdGeomScope::Define( m_root->getStage(), m_location->prim.GetPath() ); + pxr::UsdGeomXformable( scope.GetPrim() ).ClearXformOpOrder(); + } + else if( schemaType.IsA() ) + { + pxr::UsdShadeMaterial material = pxr::UsdShadeMaterial::Define( m_root->getStage(), m_location->prim.GetPath() ); + pxr::UsdGeomXformable( material.GetPrim() ).ClearXformOpOrder(); + } } - else + } + else if( const IECoreScene::ShaderNetwork *shaderNetwork = runTimeCast( attribute ) ) + { + // TODO : Just check if the parent is a Scope to determine if this is a material container? + if( m_location->prim.GetParent().IsA() ) { const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); m_materials[purpose][output] = shaderNetwork; + + pxr::SdfPath matPath = m_location->prim.GetPath(); + + // Re-calculate each time we come across a new ShaderNetwork + for( const auto &[purpose, material] : m_materials ) + { + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( m_root->getStage(), matPath ); + if( !mat ) + { + mat = pxr::UsdShadeMaterial::Define( m_root->getStage(), matPath ); + } + populateMaterial( mat, material ); + + // Use a hash to identify the combination of shaders in this material. + IECore::MurmurHash materialHash; + for( const auto &[output, shaderNetwork] : material ) + { + materialHash.append( output ); + materialHash.append( shaderNetwork->Object::hash() ); + } + // Store the material hash to compare with other materials + // eg. allow referencing if they match. + m_root->setMaterialHash( matPath, purpose, materialHash ); + } } + else + { +#if PXR_VERSION >= 2111 + if( name == g_lightAttributeName ) + { + ShaderAlgo::writeLight( shaderNetwork, m_location->prim ); + } + else + { + const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + m_materials[purpose][output] = shaderNetwork; + } #else - const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); - m_materials[purpose][output] = shaderNetwork; + const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + m_materials[purpose][output] = shaderNetwork; #endif + } } else if( name.string() == "gaffer:globals" ) { @@ -1403,6 +1730,14 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a } } } + else if( boost::starts_with( name.string(), g_materialBindingAttributeName.string() ) ) + { + if( const IECore::StringData *stringData = runTimeCast( attribute ) ) + { + const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + m_materialBinds[purpose] = pxr::SdfPath( stringData->readable() ); + } + } else if( name.string().find( ':' ) != std::string::npos ) { if( const Data *data = runTimeCast( attribute ) ) @@ -1765,6 +2100,25 @@ void USDScene::attributesHash( double time, IECore::MurmurHash &h ) const mightBeTimeVarying |= doubleSidedAttr.ValueMightBeTimeVarying(); } + if( m_location->prim.IsA() || m_location->prim.IsA() ) + { + haveAttributes = true; + } + + if( m_location->prim.HasAPI() ) + { + pxr::UsdShadeMaterialBindingAPI binding = pxr::UsdShadeMaterialBindingAPI( m_location->prim ); + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) + { + if( binding.GetDirectBinding( purpose ).IsBound() && + isSceneChild( binding.GetDirectBinding( purpose ).GetMaterial().GetPrim().GetParent() ) ) + { + haveAttributes = true; + break; + } + } + } + std::vector attributes = m_location->prim.GetAuthoredAttributes(); for( const auto &attribute : attributes ) { @@ -1793,6 +2147,16 @@ void USDScene::attributesHash( double time, IECore::MurmurHash &h ) const } } + if( m_location->prim.IsA() ) + { + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( m_root->getStage(), m_location->prim.GetPath() ); + for( pxr::UsdShadeOutput &o : mat.GetOutputs( /* onlyAuthored = */ true ) ) + { + mightBeTimeVarying = mightBeTimeVarying || m_root->shaderNetworkMightBeTimeVarying( o ); + } + haveMaterials = true; + } + if( haveAttributes || haveMaterials ) { h.append( m_root->uniqueId() ); diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.h b/contrib/IECoreUSD/src/IECoreUSD/USDScene.h index cee778e1ec..4022dc6d5d 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.h +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.h @@ -126,6 +126,9 @@ class USDScene : public IECoreScene::SceneInterface // Contains the materials to be bound for this location, indexed by purpose. using Materials = boost::container::flat_map; Materials m_materials; + // Use a material bind if the attribute usd:material:binding was found. + using MaterialBinds = boost::container::flat_map; + MaterialBinds m_materialBinds; }; diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index 235ae71e11..1d93b042f7 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -2025,7 +2025,8 @@ def testScope( self ) : self.assertEqual( root.childNames(), [ "scope" ] ) scope = root.child( "scope" ) - self.assertEqual( scope.attributeNames(), [] ) + self.assertEqual( scope.attributeNames(), [ "usd:schemaType" ] ) + self.assertEqual( scope.readAttribute( "usd:schemaType", 0.0 ), IECore.StringData( "Scope" ) ) self.assertEqual( scope.readTransformAsMatrix( 0 ), imath.M44d() ) self.assertFalse( scope.hasObject() ) @@ -2217,7 +2218,7 @@ def checkHasTag( scene ) : self.assertSetNamesEqual( root.readTags( root.AncestorTag ), [] ) self.assertSetNamesEqual( root.readTags( root.LocalTag ), [] ) - self.assertSetNamesEqual( root.readTags( root.DescendantTag ), allTags + [ "__cameras", "usd:pointInstancers" ] + self.__expectedLightSets() ) + self.assertSetNamesEqual( root.readTags( root.DescendantTag ), allTags + [ "__cameras", "usd:pointInstancers", "__materials" ] + self.__expectedLightSets() ) checkHasTag( root ) a = root.child( "a" ) @@ -2265,7 +2266,7 @@ def testSchemaTypeSetsAndTags( self ) : instancerGroup = group.child( "instancerGroup" ) instancer = instancerGroup.child( "instancer" ) - self.assertSetNamesEqual( root.setNames(), [ "__cameras", "usd:pointInstancers" ] + self.__expectedLightSets() ) + self.assertSetNamesEqual( root.setNames(), [ "__cameras", "usd:pointInstancers", "__materials" ] + self.__expectedLightSets() ) self.assertSetNamesEqual( group.setNames(), [] ) self.assertSetNamesEqual( camera.setNames(), [] ) self.assertSetNamesEqual( instancerGroup.setNames(), [] ) @@ -2287,7 +2288,7 @@ def testSchemaTypeSetsAndTags( self ) : self.assertSetNamesEqual( root.readTags( root.AncestorTag ), [] ) self.assertSetNamesEqual( root.readTags( root.LocalTag ), [] ) - self.assertSetNamesEqual( root.readTags( root.DescendantTag ), [ "__cameras", "usd:pointInstancers" ] + self.__expectedLightSets() ) + self.assertSetNamesEqual( root.readTags( root.DescendantTag ), [ "__cameras", "usd:pointInstancers", "__materials" ] + self.__expectedLightSets() ) self.assertSetNamesEqual( group.readTags( root.AncestorTag ), [] ) self.assertSetNamesEqual( group.readTags( root.LocalTag ), [] ) @@ -4555,7 +4556,7 @@ def testSetNameValidation( self ) : self.assertEqual( set( root.setNames() ), - set( expectedSetNames.values() ) | { "__lights", "usd:pointInstancers", "__cameras" } + set( expectedSetNames.values() ) | { "__lights", "usd:pointInstancers", "__cameras", "__materials" } ) for setIndex, setName in enumerate( expectedSetNames.values() ) : @@ -4770,5 +4771,186 @@ def testMaterialTerminalFromSubgraph( self ) : self.assertEqual( shaderNetwork.outputShader().name, "LamaSurface" ) + def testWriteSchemaType( self ) : + + # Write via SceneInterface + + fileName = os.path.join( self.temporaryDirectory(), "schemaType.usda" ) + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write ) + + # We are only interested in Scope and Material, and "None" would be the default XForm. + schemaTypes = ( None, "Scope", "Material" ) + + # Use SceneInterface to write values for Schema Type. + + for schemaType in schemaTypes : + child = root.createChild( schemaType or "none" ) + if schemaType is not None : + child.writeAttribute( "usd:schemaType", IECore.StringData( schemaType ), 0 ) + del child + del root + + # Verify by reading via USD API. + + stage = pxr.Usd.Stage.Open( fileName ) + for schemaType in schemaTypes : + child = stage.GetPrimAtPath( "/{}".format( schemaType or "none" ) ) + schemaTypeName = child.GetPrimTypeInfo().GetSchemaTypeName() + if schemaType is None : + self.assertEqual( schemaTypeName, "Xform" ) + else : + self.assertEqual( schemaTypeName, schemaType ) + + # Verify by reading via SceneInterface. + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + for schemaType in schemaTypes : + child = root.child( schemaType or "none" ) + if schemaType is not None : + self.assertTrue( child.hasAttribute( "usd:schemaType" ) ) + self.assertIn( "usd:schemaType", child.attributeNames() ) + self.assertIsInstance( child.readAttribute( "usd:schemaType", 0 ), IECore.StringData ) + self.assertEqual( child.readAttribute( "usd:schemaType", 0 ).value, schemaType ) + else : + self.assertFalse( child.hasAttribute( "usd:schemaType" ) ) + self.assertNotIn( "usd:schemaType", child.attributeNames() ) + self.assertEqual( child.readAttribute( "usd:schemaType", 0 ), None ) + + def testWriteMaterialBind( self ) : + + # Write via SceneInterface + + fileName = os.path.join( self.temporaryDirectory(), "materialBind.usda" ) + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write ) + + # Use SceneInterface to write values for Schema Type. + + materials = root.createChild( "materials" ) + materials.writeAttribute( "usd:schemaType", IECore.StringData( "Scope" ), 0 ) + material = materials.createChild( "material" ) + material.writeAttribute( "usd:schemaType", IECore.StringData( "Material" ), 0 ) + + # Create a ShaderNetwork onto the Material. + + network1 = IECoreScene.ShaderNetwork( + shaders = { + "output" : IECoreScene.Shader( + "mySurface", "surface", + { + "color3fParameter" : imath.Color3f( 1, 2, 3 ), + } + ), + }, + output = "output" + ) + material.writeAttribute( "surface", network1, 0 ) + + # Now create a prim with a bind to the Material prim. + + sphere1 = root.createChild( "sphere1" ) + sphere1.writeObject( IECoreScene.SpherePrimitive( 3.0 ), 0 ) + sphere1.writeAttribute( "usd:material:binding", IECore.StringData( "/materials/material" ), 0 ) + + # A bind and the shader directly assigned, matching hashes should collapse + # into a single bind to the original material prim. + + sphere2 = root.createChild( "sphere2" ) + sphere2.writeObject( IECoreScene.SpherePrimitive( 3.0 ), 0 ) + sphere2.writeAttribute( "usd:material:binding", IECore.StringData( "/materials/material" ), 0 ) + sphere2.writeAttribute( "surface", network1, 0 ) + + # The shader network doesn't match, so it should reject the assignment + # and store a unique material inside a Scope with cortex_autoMaterials set to true. + + network2 = IECoreScene.ShaderNetwork( + shaders = { + "output" : IECoreScene.Shader( + "mySurface", "surface", + { + "color3fParameter" : imath.Color3f( 9, 9, 9 ), + } + ), + }, + output = "output" + ) + + sphere3 = root.createChild( "sphere3" ) + sphere3.writeObject( IECoreScene.SpherePrimitive( 3.0 ), 0 ) + sphere3.writeAttribute( "usd:material:binding", IECore.StringData( "/materials/material" ), 0 ) + sphere3.writeAttribute( "surface", network2, 0 ) + + del material, materials, sphere1, sphere2, sphere3, root + + # Verify by reading via USD API. + + stage = pxr.Usd.Stage.Open( fileName ) + + binding = pxr.UsdShade.MaterialBindingAPI( stage.GetPrimAtPath( "/sphere1" ) ) + self.assertEqual( binding.GetDirectBinding( "" ).GetMaterialPath(), "/materials/material" ) + self.assertFalse( stage.GetPrimAtPath( "/sphere1/materials" ).IsValid() ) + + self.assertEqual( binding.GetDirectBinding( "" ).GetMaterial().GetPrim().GetPrimTypeInfo().GetSchemaTypeName(), "Material" ) + self.assertEqual( stage.GetPrimAtPath( "/materials" ).GetPrimTypeInfo().GetSchemaTypeName(), "Scope" ) + self.assertEqual( stage.GetPrimAtPath( "/materials/material" ).GetPrimTypeInfo().GetSchemaTypeName(), "Material" ) + + mat1 = pxr.UsdShade.MaterialBindingAPI.ComputeBoundMaterials( [ stage.GetPrimAtPath( "/sphere1" ) ] )[0][0] + oneShaderSource = mat1.GetOutput( "surface" ).GetConnectedSource() + self.assertEqual( oneShaderSource[1], "DEFAULT_OUTPUT" ) + + binding = pxr.UsdShade.MaterialBindingAPI( stage.GetPrimAtPath( "/sphere2" ) ) + self.assertEqual( binding.GetDirectBinding( "" ).GetMaterialPath(), "/materials/material" ) + self.assertFalse( stage.GetPrimAtPath( "/sphere2/materials" ).IsValid() ) + + mat2 = pxr.UsdShade.MaterialBindingAPI.ComputeBoundMaterials( [ stage.GetPrimAtPath( "/sphere2" ) ] )[0][0] + oneShaderSource = mat2.GetOutput( "surface" ).GetConnectedSource() + self.assertEqual( oneShaderSource[1], "DEFAULT_OUTPUT" ) + + binding = pxr.UsdShade.MaterialBindingAPI( stage.GetPrimAtPath( "/sphere3" ) ) + self.assertIn( "/sphere3/materials/material_", str( binding.GetDirectBinding( "" ).GetMaterialPath() ) ) + self.assertTrue( stage.GetPrimAtPath( "/sphere3/materials" ).IsValid() ) + + mat3 = pxr.UsdShade.MaterialBindingAPI.ComputeBoundMaterials( [ stage.GetPrimAtPath( "/sphere3" ) ] )[0][0] + oneShaderSource = mat3.GetOutput( "surface" ).GetConnectedSource() + self.assertEqual( oneShaderSource[1], "DEFAULT_OUTPUT" ) + + self.assertEqual( mat1.GetPrim().GetPath(), mat2.GetPrim().GetPath() ) + self.assertNotEqual( mat1.GetPrim().GetPath(), mat3.GetPrim().GetPath() ) + self.assertNotEqual( mat2.GetPrim().GetPath(), mat3.GetPrim().GetPath() ) + + # Verify by reading via SceneInterface. + + root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) + + for sphereName in ( "sphere1", "sphere2" ) : + + sphere = root.child( sphereName ) + self.assertTrue( sphere.hasAttribute( "usd:material:binding" ) ) + self.assertTrue( sphere.hasAttribute( "surface" ) ) + self.assertIn( "usd:material:binding", sphere.attributeNames() ) + self.assertIsInstance( sphere.readAttribute( "usd:material:binding", 0 ), IECore.StringData ) + self.assertEqual( sphere.readAttribute( "usd:material:binding", 0 ).value, "/materials/material" ) + + # Sphere3 should be using its own unique shader without binding attributes + + sphere = root.child( "sphere3" ) + self.assertFalse( sphere.hasAttribute( "usd:material:binding" ) ) + self.assertTrue( sphere.hasAttribute( "surface" ) ) + self.assertNotIn( "usd:material:binding", sphere.attributeNames() ) + + materials = root.child( "materials" ) + self.assertTrue( materials.hasAttribute( "usd:schemaType" ) ) + self.assertIn( "usd:schemaType", materials.attributeNames() ) + self.assertIsInstance( materials.readAttribute( "usd:schemaType", 0 ), IECore.StringData ) + self.assertEqual( materials.readAttribute( "usd:schemaType", 0 ).value, "Scope" ) + + material = materials.child( "material" ) + self.assertTrue( material.hasAttribute( "usd:schemaType" ) ) + self.assertIn( "usd:schemaType", material.attributeNames() ) + self.assertIsInstance( material.readAttribute( "usd:schemaType", 0 ), IECore.StringData ) + self.assertEqual( material.readAttribute( "usd:schemaType", 0 ).value, "Material" ) + self.assertFalse( material.hasAttribute( "usd:material:binding" ) ) + self.assertTrue( material.hasAttribute( "surface" ) ) + + if __name__ == "__main__": unittest.main() diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/exposedShaderInput.usda b/contrib/IECoreUSD/test/IECoreUSD/data/exposedShaderInput.usda index baeb410ca3..7c0f60ea51 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/data/exposedShaderInput.usda +++ b/contrib/IECoreUSD/test/IECoreUSD/data/exposedShaderInput.usda @@ -10,7 +10,9 @@ def "model" rel material:binding = } - def Scope "materials" + def Scope "materials" ( + cortex_autoMaterials = true + ) { def Material "material1" diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/materialPurpose.usda b/contrib/IECoreUSD/test/IECoreUSD/data/materialPurpose.usda index 3380c199f0..aa3016599c 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/data/materialPurpose.usda +++ b/contrib/IECoreUSD/test/IECoreUSD/data/materialPurpose.usda @@ -12,7 +12,9 @@ def "model" rel material:binding:preview = } - def Scope "materials" + def Scope "materials" ( + cortex_autoMaterials = true + ) { def Material "material" diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/shaderNameConflict.usda b/contrib/IECoreUSD/test/IECoreUSD/data/shaderNameConflict.usda index 5c342ae638..5fa4de7abf 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/data/shaderNameConflict.usda +++ b/contrib/IECoreUSD/test/IECoreUSD/data/shaderNameConflict.usda @@ -10,7 +10,9 @@ def Xform "shaderLocation" ( custom float arnold:surface = 7 rel material:binding = - def Scope "materials" + def Scope "materials" ( + cortex_autoMaterials = true + ) { def Material "testMat" { diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/shaderParentLoc.usda b/contrib/IECoreUSD/test/IECoreUSD/data/shaderParentLoc.usda index 2695146269..7e9519786e 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/data/shaderParentLoc.usda +++ b/contrib/IECoreUSD/test/IECoreUSD/data/shaderParentLoc.usda @@ -6,7 +6,9 @@ def Xform "shaderLocation" ( { rel material:binding = - def Scope "materials" + def Scope "materials" ( + cortex_autoMaterials = true + ) { def Material "testMat" { diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/textureParameters.usda b/contrib/IECoreUSD/test/IECoreUSD/data/textureParameters.usda index 6ddcb1d561..64707534f3 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/data/textureParameters.usda +++ b/contrib/IECoreUSD/test/IECoreUSD/data/textureParameters.usda @@ -10,7 +10,9 @@ def "model" rel material:binding = } - def Scope "materials" + def Scope "materials" ( + cortex_autoMaterials = true + ) { def Material "textureParameterTest" diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/unconnectedMaterialOutput.usda b/contrib/IECoreUSD/test/IECoreUSD/data/unconnectedMaterialOutput.usda index 97a7511f87..1c786c1c98 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/data/unconnectedMaterialOutput.usda +++ b/contrib/IECoreUSD/test/IECoreUSD/data/unconnectedMaterialOutput.usda @@ -4,10 +4,15 @@ def Sphere "sphere" ( prepend apiSchemas = ["MaterialBindingAPI"] ) { - rel material:binding = + rel material:binding = } -def Material "myMaterial" +def Scope "materials" ( + cortex_autoMaterials = true +) { - token outputs:cycles:surface + def Material "myMaterial" + { + token outputs:cycles:surface + } }