Skip to content

Commit 3ee9f33

Browse files
committed
USDScene : Support material purpose (both read and write)
USD allows different materials to be bound for different purposes, with those purposes being `full` or `preview`. See https://graphics.pixar.com/usd/release/wp_usdshade.html. We represent this in Cortex as attributes like `ai:surface:full` or `surface:preview` etc. The intention is not to resolve which of these materials to use during loading in the SceneInterface, but simply to make them available for downstream use. In Gaffer, this might mean using a ShuffleAttributes node to copy `surface:preview` to `surface` so that it gets rendered. Possibly contentious : I removed the explicit management of shader type from `populateMaterial()`, replacing calls like `CreateDisplacementOutput()` with `CreateOutput()`. This is now symmetrical with the reading code, where we happily make attributes from all outputs regardless of their name. It also simplifies `populateMaterial()`, since we already have the exact string to be passed to `CreateOutput()`, rather than splitting it only for `CreateDisplacementOutput()` to join it back together again.
1 parent 2e3e4ea commit 3ee9f33

File tree

4 files changed

+201
-99
lines changed

4 files changed

+201
-99
lines changed

contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp

Lines changed: 100 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,6 @@ IECORE_POP_DEFAULT_VISIBILITY
7676
#include "boost/algorithm/string/predicate.hpp"
7777
#include "boost/algorithm/string/replace.hpp"
7878
#include "boost/algorithm/string/split.hpp"
79-
#include "boost/container/flat_map.hpp"
8079
#include "boost/format.hpp"
8180
#include "boost/functional/hash.hpp"
8281

@@ -358,46 +357,15 @@ SceneInterface::NameList setNamesInternal( const pxr::UsdPrim &prim, bool includ
358357
return result;
359358
}
360359

361-
void populateMaterial( pxr::UsdShadeMaterial &mat, const std::map< const InternedString, ConstShaderNetworkPtr > &shaderTypes )
360+
void populateMaterial( pxr::UsdShadeMaterial &mat, const boost::container::flat_map<pxr::TfToken, IECoreScene::ConstShaderNetworkPtr> &shaders )
362361
{
363-
for( auto &shaderType : shaderTypes )
362+
for( const auto &[output, shaderNetwork] : shaders )
364363
{
365-
std::string type = AttributeAlgo::nameToUSD( shaderType.first.string() ).name.GetString();
366-
std::string prefix;
367-
size_t colonPos = type.rfind( ":" );
368-
if( colonPos != std::string::npos )
369-
{
370-
prefix = type.substr( 0, colonPos );
371-
type = type.substr( colonPos + 1 );
372-
}
373-
374-
pxr::UsdShadeOutput matOutput;
375-
pxr::TfToken renderContext = prefix.size() ? pxr::TfToken( prefix ) : pxr::UsdShadeTokens->universalRenderContext;
376-
if( type == "surface" )
377-
{
378-
matOutput = mat.CreateSurfaceOutput( renderContext );
379-
}
380-
else if( type == "displacement" )
381-
{
382-
matOutput = mat.CreateDisplacementOutput( renderContext );
383-
}
384-
else if( type == "volume" )
385-
{
386-
matOutput = mat.CreateVolumeOutput( renderContext );
387-
}
388-
else
389-
{
390-
IECore::msg(
391-
IECore::Msg::Warning, "IECoreUSD::ShaderAlgo::writeShaderNetwork",
392-
boost::format( "Unrecognized shader type \"%1%\"" ) % type
393-
);
394-
395-
continue;
396-
}
364+
pxr::UsdShadeOutput matOutput = mat.CreateOutput( output, pxr::SdfValueTypeNames->Token );
397365

398-
std::string shaderContainerName = boost::replace_all_copy( shaderType.first.string(), ":", "_" ) + "_shaders";
366+
std::string shaderContainerName = boost::replace_all_copy( output.GetString(), ":", "_" ) + "_shaders";
399367
pxr::UsdGeomScope shaderContainer = pxr::UsdGeomScope::Define( mat.GetPrim().GetStage(), mat.GetPath().AppendChild( pxr::TfToken( shaderContainerName ) ) );
400-
pxr::UsdShadeOutput networkOut = ShaderAlgo::writeShaderNetwork( shaderType.second.get(), shaderContainer.GetPrim() );
368+
pxr::UsdShadeOutput networkOut = ShaderAlgo::writeShaderNetwork( shaderNetwork.get(), shaderContainer.GetPrim() );
401369

402370
if( networkOut.GetPrim().IsValid() )
403371
{
@@ -406,6 +374,28 @@ void populateMaterial( pxr::UsdShadeMaterial &mat, const std::map< const Interne
406374
}
407375
}
408376

377+
std::tuple<pxr::TfToken, pxr::TfToken> materialOutputAndPurpose( const std::string &attributeName )
378+
{
379+
for( const auto &purpose : { pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } )
380+
{
381+
if(
382+
boost::ends_with( attributeName, purpose.GetString() ) &&
383+
attributeName.size() > purpose.GetString().size()
384+
)
385+
{
386+
size_t colonIndex = attributeName.size() - purpose.GetString().size() - 1;
387+
if( attributeName[colonIndex] == ':' )
388+
{
389+
return std::make_tuple(
390+
AttributeAlgo::nameToUSD( attributeName.substr( 0, colonIndex ) ).name,
391+
pxr::TfToken( attributeName.substr( colonIndex + 1 ) )
392+
);
393+
}
394+
}
395+
}
396+
return { AttributeAlgo::nameToUSD( attributeName ).name, pxr::UsdShadeTokens->allPurpose };
397+
}
398+
409399
/// SdfPath is the appropriate cache key for _storage_, but we need a
410400
/// `UsdShadeOutput` for computation. This struct provides the implicit
411401
/// conversion that LRUCache needs to make that possible.
@@ -468,6 +458,14 @@ class USDScene::IO : public RefCounted
468458
m_timeCodesPerSecond( m_stage->GetTimeCodesPerSecond() ),
469459
m_shaderNetworkCache( 10 * 1024 * 1024 ) // 10Mb
470460
{
461+
// Although the USD API implies otherwise, we need a different
462+
// cache per-purpose because `UsdShadeMaterialBindingAPI::ComputeBoundMaterial()`
463+
// gives inconsistent results if the cache is shared. We pre-populate
464+
// `m_usdBindingsCaches` here because it wouldn't be thread-safe to
465+
// make insertions in `computeBoundMaterial()`.
466+
m_usdBindingsCaches[pxr::UsdShadeTokens->allPurpose];
467+
m_usdBindingsCaches[pxr::UsdShadeTokens->full];
468+
m_usdBindingsCaches[pxr::UsdShadeTokens->preview];
471469
}
472470

473471
~IO() override
@@ -534,14 +532,22 @@ class USDScene::IO : public RefCounted
534532
return m_allTags;
535533
}
536534

537-
pxr::UsdShadeMaterial computeBoundMaterial( const pxr::UsdPrim &prim )
535+
pxr::UsdShadeMaterial computeBoundMaterial( const pxr::UsdPrim &prim, const pxr::TfToken &materialPurpose )
538536
{
539537
// This should be thread safe, despite using caches, because
540538
// BindingsCache and CollectionQueryCache are implemented by USD as
541539
// tbb::concurrent_unordered_map
542-
return pxr::UsdShadeMaterialBindingAPI( prim ).ComputeBoundMaterial(
543-
&m_usdBindingsCache, &m_usdCollectionQueryCache
540+
pxr::UsdRelationship bindingRelationship;
541+
pxr::UsdShadeMaterial material = pxr::UsdShadeMaterialBindingAPI( prim ).ComputeBoundMaterial(
542+
&m_usdBindingsCaches.at( materialPurpose ), &m_usdCollectionQueryCache, materialPurpose, &bindingRelationship
544543
);
544+
if( material && materialPurpose != pxr::UsdShadeTokens->allPurpose && bindingRelationship.GetBaseName() != materialPurpose )
545+
{
546+
// Ignore USD fallback to the all purpose binding. We want to load only the bindings that actually exist,
547+
// and then allow people to manage them after loading.
548+
return pxr::UsdShadeMaterial();
549+
}
550+
return material;
545551
}
546552

547553
IECoreScene::ConstShaderNetworkPtr readShaderNetwork( const pxr::UsdShadeOutput &output )
@@ -579,7 +585,7 @@ class USDScene::IO : public RefCounted
579585
std::once_flag m_allTagsFlag;
580586
SceneInterface::NameList m_allTags;
581587

582-
pxr::UsdShadeMaterialBindingAPI::BindingsCache m_usdBindingsCache;
588+
boost::container::flat_map<pxr::TfToken, pxr::UsdShadeMaterialBindingAPI::BindingsCache> m_usdBindingsCaches;
583589
pxr::UsdShadeMaterialBindingAPI::CollectionQueryCache m_usdCollectionQueryCache;
584590

585591
ShaderNetworkCache m_shaderNetworkCache;
@@ -605,7 +611,7 @@ USDScene::USDScene( IOPtr io, LocationPtr location )
605611

606612
USDScene::~USDScene()
607613
{
608-
if( m_shaders.size() )
614+
if( m_materials.size() )
609615
{
610616
try
611617
{
@@ -626,24 +632,29 @@ USDScene::~USDScene()
626632
materialContainer.GetPrim().SetMetadata( g_metadataAutoMaterials, true );
627633
}
628634

629-
// Use a hash to identify the combination of shaders in this material
630-
IECore::MurmurHash materialHash;
631-
for( auto &shaderType : m_shaders )
635+
for( const auto &[purpose, material] : m_materials )
632636
{
633-
materialHash.append( shaderType.first );
634-
materialHash.append( shaderType.second->Object::hash() );
635-
}
636-
pxr::TfToken matName( "material_" + materialHash.toString() );
637+
// Use a hash to identify the combination of shaders in this material.
638+
IECore::MurmurHash materialHash;
639+
for( const auto &[output, shaderNetwork] : material )
640+
{
641+
materialHash.append( output );
642+
materialHash.append( shaderNetwork->Object::hash() );
643+
}
644+
pxr::TfToken matName( "material_" + materialHash.toString() );
637645

638-
pxr::SdfPath matPath = materialContainer.GetPrim().GetPath().AppendChild( matName );
639-
pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( materialContainer.GetPrim().GetStage(), matPath );
640-
if( !mat )
641-
{
642-
// Another location has not yet defined this material
643-
mat = pxr::UsdShadeMaterial::Define( materialContainer.GetPrim().GetStage(), matPath );
644-
populateMaterial( mat, m_shaders );
646+
// Write the material if it hasn't been written already.
647+
pxr::SdfPath matPath = materialContainer.GetPrim().GetPath().AppendChild( matName );
648+
pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( materialContainer.GetPrim().GetStage(), matPath );
649+
if( !mat )
650+
{
651+
mat = pxr::UsdShadeMaterial::Define( materialContainer.GetPrim().GetStage(), matPath );
652+
populateMaterial( mat, material );
653+
}
654+
655+
// Bind the material to this location
656+
pxr::UsdShadeMaterialBindingAPI( m_location->prim ).Bind( mat, pxr::UsdShadeTokens->fallbackStrength, purpose );
645657
}
646-
pxr::UsdShadeMaterialBindingAPI( m_location->prim ).Bind( mat );
647658
}
648659
catch( std::exception &e )
649660
{
@@ -839,19 +850,14 @@ bool USDScene::hasAttribute( const SceneInterface::Name &name ) const
839850
}
840851
else
841852
{
842-
pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim );
843-
pxr::UsdPrim matPrim = mat.GetPrim();
844-
845-
if( matPrim.IsValid() )
853+
const auto &[output, purpose] = materialOutputAndPurpose( name.string() );
854+
if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) )
846855
{
847-
pxr::TfToken n = AttributeAlgo::nameToUSD( name.string() ).name;
848-
pxr::UsdShadeOutput o = mat.GetOutput( n );
849-
if( o && pxr::UsdAttribute( o ).IsAuthored() )
856+
if( pxr::UsdShadeOutput o = mat.GetOutput( output ) )
850857
{
851-
return true;
858+
return o.GetAttr().IsAuthored();
852859
}
853860
}
854-
855861
return false;
856862
}
857863
}
@@ -907,15 +913,18 @@ void USDScene::attributeNames( SceneInterface::NameList &attrs ) const
907913
}
908914
}
909915

910-
pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim );
911-
912-
if( mat.GetPrim().IsValid() )
916+
for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } )
913917
{
914-
for( pxr::UsdShadeOutput &o : mat.GetOutputs() )
918+
if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) )
915919
{
916-
if( o && pxr::UsdAttribute( o ).IsAuthored() )
920+
for( pxr::UsdShadeOutput &o : mat.GetOutputs( /* onlyAuthored = */ true ) )
917921
{
918-
attrs.push_back( AttributeAlgo::nameFromUSD( { o.GetBaseName(), false } ) );
922+
InternedString attrName = AttributeAlgo::nameFromUSD( { o.GetBaseName() , false } );
923+
if( !purpose.IsEmpty() )
924+
{
925+
attrName = attrName.string() + ":" + purpose.GetString();
926+
}
927+
attrs.push_back( attrName );
919928
}
920929
}
921930
}
@@ -1000,24 +1009,17 @@ ConstObjectPtr USDScene::readAttribute( const SceneInterface::Name &name, double
10001009
}
10011010
else
10021011
{
1003-
pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim );
1004-
1005-
if( mat.GetPrim().IsValid() )
1012+
const auto &[output, purpose] = materialOutputAndPurpose( name.string() );
1013+
if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) )
10061014
{
1007-
pxr::TfToken n = AttributeAlgo::nameToUSD( name.string() ).name;
1008-
1009-
// If there's no output declared, then we will return nullptr, versus
1010-
// having an output with no source connected, which will return an
1011-
// empty shader network
1012-
pxr::UsdShadeOutput o = mat.GetOutput( n );
1013-
if( o && pxr::UsdAttribute( o ).IsAuthored() )
1015+
pxr::UsdShadeOutput o = mat.GetOutput( output );
1016+
if( o && o.GetAttr().IsAuthored() )
10141017
{
10151018
return m_root->readShaderNetwork( o );
10161019
}
10171020
}
1021+
return nullptr;
10181022
}
1019-
1020-
return nullptr;
10211023
}
10221024

10231025
void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *attribute, double time )
@@ -1079,7 +1081,8 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a
10791081
}
10801082
else if( const IECoreScene::ShaderNetwork *shaderNetwork = runTimeCast<const ShaderNetwork>( attribute ) )
10811083
{
1082-
m_shaders[name] = shaderNetwork;
1084+
const auto &[output, purpose] = materialOutputAndPurpose( name.string() );
1085+
m_materials[purpose][output] = shaderNetwork;
10831086
}
10841087
else if( name.string() == "gaffer:globals" )
10851088
{
@@ -1432,9 +1435,19 @@ void USDScene::attributesHash( double time, IECore::MurmurHash &h ) const
14321435
}
14331436
}
14341437

1435-
pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim );
1438+
bool haveMaterials = false;
1439+
for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } )
1440+
{
1441+
if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) )
1442+
{
1443+
// \todo - This does not consider the possibility that the material could contain time-varying
1444+
// attributes
1445+
append( mat.GetPrim().GetPath(), h );
1446+
haveMaterials = true;
1447+
}
1448+
}
14361449

1437-
if( haveAttributes || mat.GetPrim().IsValid() )
1450+
if( haveAttributes || haveMaterials )
14381451
{
14391452
h.append( m_root->fileName() );
14401453

@@ -1446,13 +1459,6 @@ void USDScene::attributesHash( double time, IECore::MurmurHash &h ) const
14461459
appendPrimOrMasterPath( m_location->prim, h );
14471460
}
14481461

1449-
if( mat.GetPrim().IsValid() )
1450-
{
1451-
// \todo - This does not consider the possibility that the material could contain time-varying
1452-
// attributes
1453-
append( mat.GetPrim().GetPath(), h );
1454-
}
1455-
14561462
if( mightBeTimeVarying )
14571463
{
14581464
h.append( time );

contrib/IECoreUSD/src/IECoreUSD/USDScene.h

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242

4343
#include "IECore/PathMatcherData.h"
4444

45+
#include "boost/container/flat_map.hpp"
4546
// Included here to avoid it being included indirectly via `stage.h`, inside the
4647
// scope of IECORE_PUSH_DEFAULT_VISIBILITY.
4748
#include "boost/function/function_base.hpp"
@@ -119,7 +120,13 @@ class USDScene : public IECoreScene::SceneInterface
119120
IOPtr m_root;
120121
LocationPtr m_location;
121122

122-
std::map< const IECore::InternedString, IECoreScene::ConstShaderNetworkPtr > m_shaders;
123+
// Contains all the shader networks for a single material, mapping from material output
124+
// (e.g. "surface", "displacement" etc) to the shading network that drives that output.
125+
using MaterialNetworks = boost::container::flat_map<pxr::TfToken, IECoreScene::ConstShaderNetworkPtr>;
126+
// Contains the materials to be bound for this location, indexed by purpose.
127+
using Materials = boost::container::flat_map<pxr::TfToken, MaterialNetworks>;
128+
Materials m_materials;
129+
123130
};
124131

125132
IE_CORE_DECLAREPTR( USDScene )

contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2686,10 +2686,6 @@ def testShaders( self ) :
26862686

26872687
shaderLocation.writeAttribute( "volume", oneShaderNetwork, 0 ) # USD supports shaders without a prefix
26882688

2689-
# A shader type that doesn't correspond to anything in USD won't be written out,
2690-
# but make sure it doesn't crash anything
2691-
shaderLocation.writeAttribute( "testBad:badShaderType", oneShaderNetwork, 0 )
2692-
26932689
del writerRoot, shaderLocation
26942690

26952691
# Read via USD API
@@ -3154,5 +3150,41 @@ def testColor4fShaderParameterComponentConnections( self ) :
31543150
root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
31553151
self.assertEqual( root.child( "object" ).readAttribute( "ai:surface", 0 ), network )
31563152

3153+
def testMaterialPurpose( self ) :
3154+
3155+
def assertExpected( root ) :
3156+
3157+
sphere = root.child( "model" ).child( "sphere" )
3158+
3159+
self.assertEqual( set( sphere.attributeNames() ), { "surface", "surface:full", "surface:preview" } )
3160+
for n in ( "surface", "surface:full", "surface:preview" ) :
3161+
self.assertTrue( sphere.hasAttribute( n ) )
3162+
3163+
self.assertEqual(
3164+
sphere.readAttribute( "surface", 0 ).getShader( "surface" ).parameters["base"],
3165+
IECore.FloatData( 0 )
3166+
)
3167+
3168+
self.assertEqual(
3169+
sphere.readAttribute( "surface:full", 0 ).getShader( "surface" ).parameters["base"],
3170+
IECore.FloatData( 0.5 )
3171+
)
3172+
3173+
self.assertEqual(
3174+
sphere.readAttribute( "surface:preview", 0 ).getShader( "surface" ).parameters["base"],
3175+
IECore.FloatData( 1 )
3176+
)
3177+
3178+
inRoot = IECoreScene.SceneInterface.create( os.path.dirname( __file__ ) + "/data/materialPurpose.usda", IECore.IndexedIO.OpenMode.Read )
3179+
assertExpected( inRoot )
3180+
3181+
roundTripFileName = os.path.join( self.temporaryDirectory(), "materialPurpose.usda" )
3182+
outRoot = IECoreScene.SceneInterface.create( roundTripFileName, IECore.IndexedIO.OpenMode.Write )
3183+
3184+
IECoreScene.SceneAlgo.copy( inRoot, outRoot, 0, 0, 24, IECoreScene.SceneAlgo.ProcessFlags.All )
3185+
3186+
roundTripRoot = IECoreScene.SceneInterface.create( roundTripFileName, IECore.IndexedIO.OpenMode.Read )
3187+
assertExpected( roundTripRoot )
3188+
31573189
if __name__ == "__main__":
31583190
unittest.main()

0 commit comments

Comments
 (0)