diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp index d1b7dd250a..c21b2d5035 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp @@ -76,7 +76,6 @@ IECORE_POP_DEFAULT_VISIBILITY #include "boost/algorithm/string/predicate.hpp" #include "boost/algorithm/string/replace.hpp" #include "boost/algorithm/string/split.hpp" -#include "boost/container/flat_map.hpp" #include "boost/format.hpp" #include "boost/functional/hash.hpp" @@ -358,46 +357,15 @@ SceneInterface::NameList setNamesInternal( const pxr::UsdPrim &prim, bool includ return result; } -void populateMaterial( pxr::UsdShadeMaterial &mat, const std::map< const InternedString, ConstShaderNetworkPtr > &shaderTypes ) +void populateMaterial( pxr::UsdShadeMaterial &mat, const boost::container::flat_map &shaders ) { - for( auto &shaderType : shaderTypes ) + for( const auto &[output, shaderNetwork] : shaders ) { - std::string type = AttributeAlgo::nameToUSD( shaderType.first.string() ).name.GetString(); - std::string prefix; - size_t colonPos = type.rfind( ":" ); - if( colonPos != std::string::npos ) - { - prefix = type.substr( 0, colonPos ); - type = type.substr( colonPos + 1 ); - } - - pxr::UsdShadeOutput matOutput; - pxr::TfToken renderContext = prefix.size() ? pxr::TfToken( prefix ) : pxr::UsdShadeTokens->universalRenderContext; - if( type == "surface" ) - { - matOutput = mat.CreateSurfaceOutput( renderContext ); - } - else if( type == "displacement" ) - { - matOutput = mat.CreateDisplacementOutput( renderContext ); - } - else if( type == "volume" ) - { - matOutput = mat.CreateVolumeOutput( renderContext ); - } - else - { - IECore::msg( - IECore::Msg::Warning, "IECoreUSD::ShaderAlgo::writeShaderNetwork", - boost::format( "Unrecognized shader type \"%1%\"" ) % type - ); - - continue; - } + pxr::UsdShadeOutput matOutput = mat.CreateOutput( output, pxr::SdfValueTypeNames->Token ); - std::string shaderContainerName = boost::replace_all_copy( shaderType.first.string(), ":", "_" ) + "_shaders"; + std::string shaderContainerName = boost::replace_all_copy( output.GetString(), ":", "_" ) + "_shaders"; pxr::UsdGeomScope shaderContainer = pxr::UsdGeomScope::Define( mat.GetPrim().GetStage(), mat.GetPath().AppendChild( pxr::TfToken( shaderContainerName ) ) ); - pxr::UsdShadeOutput networkOut = ShaderAlgo::writeShaderNetwork( shaderType.second.get(), shaderContainer.GetPrim() ); + pxr::UsdShadeOutput networkOut = ShaderAlgo::writeShaderNetwork( shaderNetwork.get(), shaderContainer.GetPrim() ); if( networkOut.GetPrim().IsValid() ) { @@ -406,6 +374,28 @@ void populateMaterial( pxr::UsdShadeMaterial &mat, const std::map< const Interne } } +std::tuple materialOutputAndPurpose( const std::string &attributeName ) +{ + for( const auto &purpose : { pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) + { + if( + boost::ends_with( attributeName, purpose.GetString() ) && + attributeName.size() > purpose.GetString().size() + ) + { + size_t colonIndex = attributeName.size() - purpose.GetString().size() - 1; + if( attributeName[colonIndex] == ':' ) + { + return std::make_tuple( + AttributeAlgo::nameToUSD( attributeName.substr( 0, colonIndex ) ).name, + pxr::TfToken( attributeName.substr( colonIndex + 1 ) ) + ); + } + } + } + return { AttributeAlgo::nameToUSD( attributeName ).name, pxr::UsdShadeTokens->allPurpose }; +} + /// SdfPath is the appropriate cache key for _storage_, but we need a /// `UsdShadeOutput` for computation. This struct provides the implicit /// conversion that LRUCache needs to make that possible. @@ -468,6 +458,14 @@ class USDScene::IO : public RefCounted m_timeCodesPerSecond( m_stage->GetTimeCodesPerSecond() ), m_shaderNetworkCache( 10 * 1024 * 1024 ) // 10Mb { + // Although the USD API implies otherwise, we need a different + // cache per-purpose because `UsdShadeMaterialBindingAPI::ComputeBoundMaterial()` + // gives inconsistent results if the cache is shared. We pre-populate + // `m_usdBindingsCaches` here because it wouldn't be thread-safe to + // make insertions in `computeBoundMaterial()`. + m_usdBindingsCaches[pxr::UsdShadeTokens->allPurpose]; + m_usdBindingsCaches[pxr::UsdShadeTokens->full]; + m_usdBindingsCaches[pxr::UsdShadeTokens->preview]; } ~IO() override @@ -534,14 +532,31 @@ class USDScene::IO : public RefCounted return m_allTags; } - pxr::UsdShadeMaterial computeBoundMaterial( const pxr::UsdPrim &prim ) + /// \todo This "flattens" material assignment, so that materials assigned at `/root` are loaded as attributes + /// on `/root/child` as well. This is not really what we want - we want to load sparsely and let attribute + /// inheritance do the rest. This would be complicated by two factors : + /// + /// - USD's collection-based bindings. A collection-based binding on an ancestor prim would need to be transformed + /// into a Cortex attribute on the prim, if the collection includes the prim. + /// - USD's `bindingStrength` concept, where `UsdShadeTokens->strongerThanDescendants` allows an ancestor's + /// binding to clobber descendant bindings during resolution. It is not clear how to represent that in Cortex - + /// perhaps by not loading the descendant attributes at all? + pxr::UsdShadeMaterial computeBoundMaterial( const pxr::UsdPrim &prim, const pxr::TfToken &materialPurpose ) { // This should be thread safe, despite using caches, because // BindingsCache and CollectionQueryCache are implemented by USD as // tbb::concurrent_unordered_map - return pxr::UsdShadeMaterialBindingAPI( prim ).ComputeBoundMaterial( - &m_usdBindingsCache, &m_usdCollectionQueryCache + pxr::UsdRelationship bindingRelationship; + pxr::UsdShadeMaterial material = pxr::UsdShadeMaterialBindingAPI( prim ).ComputeBoundMaterial( + &m_usdBindingsCaches.at( materialPurpose ), &m_usdCollectionQueryCache, materialPurpose, &bindingRelationship ); + if( material && materialPurpose != pxr::UsdShadeTokens->allPurpose && bindingRelationship.GetBaseName() != materialPurpose ) + { + // Ignore USD fallback to the all purpose binding. We want to load only the bindings that actually exist, + // and then allow people to manage them after loading. + return pxr::UsdShadeMaterial(); + } + return material; } IECoreScene::ConstShaderNetworkPtr readShaderNetwork( const pxr::UsdShadeOutput &output ) @@ -579,7 +594,7 @@ class USDScene::IO : public RefCounted std::once_flag m_allTagsFlag; SceneInterface::NameList m_allTags; - pxr::UsdShadeMaterialBindingAPI::BindingsCache m_usdBindingsCache; + boost::container::flat_map m_usdBindingsCaches; pxr::UsdShadeMaterialBindingAPI::CollectionQueryCache m_usdCollectionQueryCache; ShaderNetworkCache m_shaderNetworkCache; @@ -605,7 +620,7 @@ USDScene::USDScene( IOPtr io, LocationPtr location ) USDScene::~USDScene() { - if( m_shaders.size() ) + if( m_materials.size() ) { try { @@ -626,24 +641,29 @@ USDScene::~USDScene() materialContainer.GetPrim().SetMetadata( g_metadataAutoMaterials, true ); } - // Use a hash to identify the combination of shaders in this material - IECore::MurmurHash materialHash; - for( auto &shaderType : m_shaders ) + for( const auto &[purpose, material] : m_materials ) { - materialHash.append( shaderType.first ); - materialHash.append( shaderType.second->Object::hash() ); - } - pxr::TfToken matName( "material_" + materialHash.toString() ); + // 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() ); + } + pxr::TfToken matName( "material_" + materialHash.toString() ); - pxr::SdfPath matPath = materialContainer.GetPrim().GetPath().AppendChild( matName ); - pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( materialContainer.GetPrim().GetStage(), matPath ); - if( !mat ) - { - // Another location has not yet defined this material - mat = pxr::UsdShadeMaterial::Define( materialContainer.GetPrim().GetStage(), matPath ); - populateMaterial( mat, m_shaders ); + // Write the material if it hasn't been written already. + pxr::SdfPath matPath = materialContainer.GetPrim().GetPath().AppendChild( matName ); + pxr::UsdShadeMaterial mat = pxr::UsdShadeMaterial::Get( materialContainer.GetPrim().GetStage(), matPath ); + if( !mat ) + { + mat = pxr::UsdShadeMaterial::Define( materialContainer.GetPrim().GetStage(), matPath ); + populateMaterial( mat, material ); + } + + // Bind the material to this location + pxr::UsdShadeMaterialBindingAPI( m_location->prim ).Bind( mat, pxr::UsdShadeTokens->fallbackStrength, purpose ); } - pxr::UsdShadeMaterialBindingAPI( m_location->prim ).Bind( mat ); } catch( std::exception &e ) { @@ -839,19 +859,14 @@ bool USDScene::hasAttribute( const SceneInterface::Name &name ) const } else { - pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim ); - pxr::UsdPrim matPrim = mat.GetPrim(); - - if( matPrim.IsValid() ) + const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) ) { - pxr::TfToken n = AttributeAlgo::nameToUSD( name.string() ).name; - pxr::UsdShadeOutput o = mat.GetOutput( n ); - if( o && pxr::UsdAttribute( o ).IsAuthored() ) + if( pxr::UsdShadeOutput o = mat.GetOutput( output ) ) { - return true; + return o.GetAttr().IsAuthored(); } } - return false; } } @@ -907,15 +922,18 @@ void USDScene::attributeNames( SceneInterface::NameList &attrs ) const } } - pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim ); - - if( mat.GetPrim().IsValid() ) + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) { - for( pxr::UsdShadeOutput &o : mat.GetOutputs() ) + if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) ) { - if( o && pxr::UsdAttribute( o ).IsAuthored() ) + for( pxr::UsdShadeOutput &o : mat.GetOutputs( /* onlyAuthored = */ true ) ) { - attrs.push_back( AttributeAlgo::nameFromUSD( { o.GetBaseName(), false } ) ); + InternedString attrName = AttributeAlgo::nameFromUSD( { o.GetBaseName() , false } ); + if( !purpose.IsEmpty() ) + { + attrName = attrName.string() + ":" + purpose.GetString(); + } + attrs.push_back( attrName ); } } } @@ -1000,24 +1018,17 @@ ConstObjectPtr USDScene::readAttribute( const SceneInterface::Name &name, double } else { - pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim ); - - if( mat.GetPrim().IsValid() ) + const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) ) { - pxr::TfToken n = AttributeAlgo::nameToUSD( name.string() ).name; - - // If there's no output declared, then we will return nullptr, versus - // having an output with no source connected, which will return an - // empty shader network - pxr::UsdShadeOutput o = mat.GetOutput( n ); - if( o && pxr::UsdAttribute( o ).IsAuthored() ) + pxr::UsdShadeOutput o = mat.GetOutput( output ); + if( o && o.GetAttr().IsAuthored() ) { return m_root->readShaderNetwork( o ); } } + return nullptr; } - - return nullptr; } void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *attribute, double time ) @@ -1079,7 +1090,8 @@ void USDScene::writeAttribute( const SceneInterface::Name &name, const Object *a } else if( const IECoreScene::ShaderNetwork *shaderNetwork = runTimeCast( attribute ) ) { - m_shaders[name] = shaderNetwork; + const auto &[output, purpose] = materialOutputAndPurpose( name.string() ); + m_materials[purpose][output] = shaderNetwork; } else if( name.string() == "gaffer:globals" ) { @@ -1432,9 +1444,19 @@ void USDScene::attributesHash( double time, IECore::MurmurHash &h ) const } } - pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim ); + bool haveMaterials = false; + for( const auto &purpose : { pxr::UsdShadeTokens->allPurpose, pxr::UsdShadeTokens->preview, pxr::UsdShadeTokens->full } ) + { + if( pxr::UsdShadeMaterial mat = m_root->computeBoundMaterial( m_location->prim, purpose ) ) + { + // \todo - This does not consider the possibility that the material could contain time-varying + // attributes + append( mat.GetPrim().GetPath(), h ); + haveMaterials = true; + } + } - if( haveAttributes || mat.GetPrim().IsValid() ) + if( haveAttributes || haveMaterials ) { h.append( m_root->fileName() ); @@ -1446,13 +1468,6 @@ void USDScene::attributesHash( double time, IECore::MurmurHash &h ) const appendPrimOrMasterPath( m_location->prim, h ); } - if( mat.GetPrim().IsValid() ) - { - // \todo - This does not consider the possibility that the material could contain time-varying - // attributes - append( mat.GetPrim().GetPath(), h ); - } - if( mightBeTimeVarying ) { h.append( time ); diff --git a/contrib/IECoreUSD/src/IECoreUSD/USDScene.h b/contrib/IECoreUSD/src/IECoreUSD/USDScene.h index 5c6d4e685a..cee778e1ec 100644 --- a/contrib/IECoreUSD/src/IECoreUSD/USDScene.h +++ b/contrib/IECoreUSD/src/IECoreUSD/USDScene.h @@ -42,6 +42,7 @@ #include "IECore/PathMatcherData.h" +#include "boost/container/flat_map.hpp" // Included here to avoid it being included indirectly via `stage.h`, inside the // scope of IECORE_PUSH_DEFAULT_VISIBILITY. #include "boost/function/function_base.hpp" @@ -119,7 +120,13 @@ class USDScene : public IECoreScene::SceneInterface IOPtr m_root; LocationPtr m_location; - std::map< const IECore::InternedString, IECoreScene::ConstShaderNetworkPtr > m_shaders; + // Contains all the shader networks for a single material, mapping from material output + // (e.g. "surface", "displacement" etc) to the shading network that drives that output. + using MaterialNetworks = boost::container::flat_map; + // Contains the materials to be bound for this location, indexed by purpose. + using Materials = boost::container::flat_map; + Materials m_materials; + }; IE_CORE_DECLAREPTR( USDScene ) diff --git a/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py b/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py index 56e379dc7b..1a67c30303 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/SceneCacheFileFormatTest.py @@ -38,8 +38,8 @@ import tempfile import unittest - -import pxr +import pxr.Usd +import pxr.Sdf import imath @@ -91,7 +91,7 @@ def _writeScene( self, fileName, transformRootChildren=False, writeInvalidUSDNam if transformRootChildren: t.writeTransform( IECore.M44dData(imath.M44d().translate(imath.V3d( 2, 0, 0 ))), 2.0 ) - + boxA = IECoreScene.MeshPrimitive.createBox( imath.Box3f( imath.V3f( 0 ), imath.V3f( 1 ) ) ) if writeCs: boxA["Cs"] = IECoreScene.PrimitiveVariable( @@ -278,7 +278,7 @@ def testAnimatedVisibility( self ): # invisible self.assertEqual( parentVisibility.Get( 1012 ), "invisible" ) - + # still invisible self.assertEqual( parentVisibility.Get( 1021 ), "invisible" ) @@ -304,7 +304,7 @@ def testAnimatedVisibility( self ): # invisible self.assertEqual( parent.readAttribute( IECoreScene.SceneInterface.visibilityName, 1012 / fps ).value, False ) - + # still invisible self.assertEqual( parent.readAttribute( IECoreScene.SceneInterface.visibilityName, 1021 / fps ).value, False ) @@ -326,7 +326,7 @@ def testTagsLoadedAsCollections( self ): t.writeTags( ["t1", "all", "asset-(12)"] ) s.writeTags( ["s1", "all", "asset-(12)"] ) del m, t, s - + # root stage = pxr.Usd.Stage.Open( fileName ) root = stage.GetPseudoRoot() @@ -431,17 +431,17 @@ def testDefaultPrimVars( self ): normalsData[0], pxr.Gf.Vec3f(0.0, 0.0, -1.0) ) - + self.assertEqual( normalsData[8], pxr.Gf.Vec3f(0.0, 0.0, 1.0) ) - + self.assertEqual( normalsData[16], pxr.Gf.Vec3f(0.0, 1.0, 0.0) ) - + self.assertEqual( normalsData[23], pxr.Gf.Vec3f(0.0, -1.0, 0.0) @@ -454,12 +454,12 @@ def testDefaultPrimVars( self ): uvsData[0], pxr.Gf.Vec2f(0.375, 0.0) ) - + self.assertEqual( uvsData[8], pxr.Gf.Vec2f(0.375, 1.0) ) - + self.assertEqual( uvsData[12], pxr.Gf.Vec2f(0.125, .25) @@ -544,7 +544,7 @@ def testCustomPrimvars( self ): # interpolation self.assertEqual( customPoint.GetMetadata( "interpolation" ), "vertex" ) - + # value self.assertEqual( customPoint.Get( 24.0 ), pxr.Vt.Vec3fArray( 1, [ pxr.Gf.Vec3f( 12 ) ] ) ) @@ -645,7 +645,7 @@ def testCornersAndCreases( self ): # crease indices self.assertEqual( mesh.creaseIds(), IECore.IntVectorData( [ 0, 1, 2 ] ) ) - + # crease sharpness self.assertEqual( mesh.creaseSharpnesses()[0], 4.0 ) @@ -693,7 +693,7 @@ def testPointsPrimitive( self ): points = location.readObject( 1.0 ) pointsData = points["P"].data - + self.assertEqual( pointsData[0], imath.V3f( 1, 2, 3 ) ) self.assertEqual( pointsData[1], imath.V3f( 12, 13, 14 ) ) @@ -712,7 +712,7 @@ def testCurvesPrimitive( self ): "catmullRom" : IECore.CubicBasisf.catmullRom(), "linear" : IECore.CubicBasisf.linear(), "bezier" : IECore.CubicBasisf.bezier(), - + } children = [] @@ -1046,7 +1046,7 @@ def testLinksTimeOffset( self ): fps = 24.0 fileName = os.path.join( self.temporaryDirectory(), "testUSDTimeOffsetLinked.scc" ) self._writeScene( fileName, transformRootChildren=True ) - + for start, end in ( ( 0.5, 2.0 ), ( 1.5, 2.5 ), ( 1.0, 1.5) ): linkedScene = IECoreScene.SharedSceneInterfaces.get( fileName ) linkFileName = "{}/testUSDTimeOffsetLink_{}_{}.lscc".format( self.temporaryDirectory(), start, end ) @@ -1057,7 +1057,7 @@ def testLinksTimeOffset( self ): linkedOffset.writeAttribute( IECoreScene.LinkedScene.linkAttribute, linkDataA, start ) linkedOffset.writeAttribute( IECoreScene.LinkedScene.linkAttribute, linkDataB, end ) - + noTimeOffset = l.createChild( "linked" ) noTimeOffset.writeLink( linkedScene ) @@ -1134,7 +1134,7 @@ def testWriteTimeSamplesWitinFrameRange( self ): } stage.Export( fileName, args=args ) - + scene = IECoreScene.SharedSceneInterfaces.get( fileName ) sphere = scene.child( "Sphere" ) diff --git a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py index 3632792240..a604f2cd45 100644 --- a/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py +++ b/contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py @@ -2686,10 +2686,6 @@ def testShaders( self ) : shaderLocation.writeAttribute( "volume", oneShaderNetwork, 0 ) # USD supports shaders without a prefix - # A shader type that doesn't correspond to anything in USD won't be written out, - # but make sure it doesn't crash anything - shaderLocation.writeAttribute( "testBad:badShaderType", oneShaderNetwork, 0 ) - del writerRoot, shaderLocation # Read via USD API @@ -3154,5 +3150,41 @@ def testColor4fShaderParameterComponentConnections( self ) : root = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read ) self.assertEqual( root.child( "object" ).readAttribute( "ai:surface", 0 ), network ) + def testMaterialPurpose( self ) : + + def assertExpected( root ) : + + sphere = root.child( "model" ).child( "sphere" ) + + self.assertEqual( set( sphere.attributeNames() ), { "surface", "surface:full", "surface:preview" } ) + for n in ( "surface", "surface:full", "surface:preview" ) : + self.assertTrue( sphere.hasAttribute( n ) ) + + self.assertEqual( + sphere.readAttribute( "surface", 0 ).getShader( "surface" ).parameters["base"], + IECore.FloatData( 0 ) + ) + + self.assertEqual( + sphere.readAttribute( "surface:full", 0 ).getShader( "surface" ).parameters["base"], + IECore.FloatData( 0.5 ) + ) + + self.assertEqual( + sphere.readAttribute( "surface:preview", 0 ).getShader( "surface" ).parameters["base"], + IECore.FloatData( 1 ) + ) + + inRoot = IECoreScene.SceneInterface.create( os.path.dirname( __file__ ) + "/data/materialPurpose.usda", IECore.IndexedIO.OpenMode.Read ) + assertExpected( inRoot ) + + roundTripFileName = os.path.join( self.temporaryDirectory(), "materialPurpose.usda" ) + outRoot = IECoreScene.SceneInterface.create( roundTripFileName, IECore.IndexedIO.OpenMode.Write ) + + IECoreScene.SceneAlgo.copy( inRoot, outRoot, 0, 0, 24, IECoreScene.SceneAlgo.ProcessFlags.All ) + + roundTripRoot = IECoreScene.SceneInterface.create( roundTripFileName, IECore.IndexedIO.OpenMode.Read ) + assertExpected( roundTripRoot ) + if __name__ == "__main__": unittest.main() diff --git a/contrib/IECoreUSD/test/IECoreUSD/data/materialPurpose.usda b/contrib/IECoreUSD/test/IECoreUSD/data/materialPurpose.usda new file mode 100644 index 0000000000..b6c3887670 --- /dev/null +++ b/contrib/IECoreUSD/test/IECoreUSD/data/materialPurpose.usda @@ -0,0 +1,57 @@ +#usda 1.0 + +def "model" +{ + + def Sphere "sphere" + { + rel material:binding = + rel material:binding:full = + rel material:binding:preview = + } + + def Scope "materials" + { + + def Material "material" + { + + token outputs:surface.connect = + + def Shader "surface" + { + uniform token info:id = "arnold:standard_surface" + float inputs:base = 0 + token outputs:DEFAULT_OUTPUT + } + } + + def Material "fullMaterial" + { + + token outputs:surface.connect = + + def Shader "surface" + { + uniform token info:id = "arnold:standard_surface" + float inputs:base = 0.5 + token outputs:DEFAULT_OUTPUT + } + } + + def Material "previewMaterial" + { + + token outputs:surface.connect = + + def Shader "surface" + { + uniform token info:id = "arnold:standard_surface" + float inputs:base = 1 + token outputs:DEFAULT_OUTPUT + } + } + + } + +}