Skip to content

Commit e66958e

Browse files
Merge pull request #1540 from johnhaddon/usdOSLShaderCompatibility
IECoreUSD : Add option to write OSL shaders conformant with RenderMan
2 parents 8b4f718 + c1f8aca commit e66958e

4 files changed

Lines changed: 188 additions & 46 deletions

File tree

Changes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
10.6.x.x (relative to 10.6.5.0)
22
========
33

4+
Improvements
5+
------------
46

7+
- USDScene : Added `IECOREUSD_WRITE_CONFORMANT_OSL_SHADERS` environment variable. If set to a value of `1`, this causes OSL shaders to be written in the format expected by RenderMan. Set the `RMAN_SHADERPATH` environment variable appropriately to ensure RenderMan can find the shaders.
58

69
10.6.5.0 (relative to 10.6.4.0)
710
========

contrib/IECoreUSD/src/IECoreUSD/ShaderAlgo.cpp

Lines changed: 112 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
#include "boost/algorithm/string/replace.hpp"
5757
#include "boost/pointer_cast.hpp"
5858

59+
#include <filesystem>
5960
#include <regex>
6061

6162
#if PXR_VERSION < 2102
@@ -66,26 +67,68 @@ namespace
6667
{
6768

6869
const pxr::TfToken g_blindDataToken( "cortex:blindData" );
70+
const pxr::TfToken g_shaderNameToken( "cortex:shaderName" );
71+
const pxr::TfToken g_shaderTypeToken( "cortex:shaderType" );
6972
pxr::TfToken g_legacyAdapterLabelToken( IECoreScene::ShaderNetworkAlgo::componentConnectionAdapterLabel().string() );
7073

71-
std::pair<pxr::TfToken, std::string> shaderIdAndType( const pxr::UsdShadeConnectableAPI &connectable )
74+
bool writeConformantOSLShaders()
75+
{
76+
if( const char *e = getenv( "IECOREUSD_WRITE_CONFORMANT_OSL_SHADERS" ) )
77+
{
78+
return strcmp( e, "0" );
79+
}
80+
return false;
81+
}
82+
83+
// Recover a Cortex-style shader name and type, essentially reversing the
84+
// transformation done by `createShaderPrim()`.
85+
std::pair<std::string, std::string> shaderNameAndType( const pxr::UsdShadeConnectableAPI &connectable )
7286
{
73-
pxr::TfToken id;
74-
std::string type;
7587
if( auto shader = pxr::UsdShadeShader( connectable ) )
7688
{
77-
shader.GetShaderId( &id );
78-
type = "surface";
89+
std::string name;
90+
std::string type;
91+
92+
pxr::VtValue nameVtValue = shader.GetPrim().GetCustomDataByKey( g_shaderNameToken );
93+
if( !nameVtValue.IsEmpty() )
94+
{
95+
name = nameVtValue.Get<std::string>();
96+
}
97+
else
98+
{
99+
pxr::TfToken id;
100+
shader.GetShaderId( &id );
101+
name = id.GetString();
102+
type = "surface";
103+
const size_t colonPos = name.find( ":" );
104+
if( colonPos != std::string::npos )
105+
{
106+
std::string prefix = name.substr( 0, colonPos );
107+
name = name.substr( colonPos + 1 );
108+
if( prefix == "arnold" )
109+
{
110+
prefix = "ai";
111+
}
112+
type = prefix + ":shader";
113+
}
114+
}
115+
116+
pxr::VtValue typeVtValue = shader.GetPrim().GetCustomDataByKey( g_shaderTypeToken );
117+
if( !typeVtValue.IsEmpty() )
118+
{
119+
type = typeVtValue.Get<std::string>();
120+
}
121+
122+
return { name, type };
79123
}
80124
#if PXR_VERSION >= 2111
81125
else if( auto light = pxr::UsdLuxLightAPI( connectable ) )
82126
{
83-
light.GetShaderIdAttr().Get( &id );
84-
type = "light";
127+
return { light.GetShaderId( {} ).GetString(), "light" };
85128
}
86129
#endif
87130

88-
return std::make_pair( id, type );
131+
return { "", "" };
89132
}
90133

91134
bool writeNonStandardLightParameter( const std::string &name, const IECore::Data *value, pxr::UsdShadeConnectableAPI usdShader )
@@ -232,24 +275,7 @@ IECore::InternedString readShaderNetworkWalk( const pxr::SdfPath &anchorPath, co
232275
return handle;
233276
}
234277

235-
auto [id, shaderType] = shaderIdAndType( usdShader );
236-
std::string shaderName = "defaultsurface";
237-
if( id.size() )
238-
{
239-
std::string name = id.GetString();
240-
size_t colonPos = name.find( ":" );
241-
if( colonPos != std::string::npos )
242-
{
243-
std::string prefix = name.substr( 0, colonPos );
244-
name = name.substr( colonPos + 1 );
245-
if( prefix == "arnold" )
246-
{
247-
prefix = "ai";
248-
}
249-
shaderType = prefix + ":shader";
250-
}
251-
shaderName = name;
252-
}
278+
// Read parameter values and connections.
253279

254280
IECore::CompoundDataPtr parametersData = new IECore::CompoundData();
255281
IECore::CompoundDataMap &parameters = parametersData->writable();
@@ -295,6 +321,9 @@ IECore::InternedString readShaderNetworkWalk( const pxr::SdfPath &anchorPath, co
295321

296322
readNonStandardLightParameters( usdShader.GetPrim(), parameters );
297323

324+
// Create shader.
325+
326+
auto [shaderName, shaderType] = shaderNameAndType( usdShader );
298327
IECoreScene::ShaderPtr newShader = new IECoreScene::Shader( shaderName, shaderType, parametersData );
299328

300329
// General purpose support for any Cortex blind data.
@@ -395,35 +424,73 @@ pxr::UsdShadeConnectableAPI createShaderPrim( const IECoreScene::Shader *shader,
395424
throw IECore::Exception( "Could not create shader at " + path.GetAsString() );
396425
}
397426

398-
const std::string type = shader->getType();
427+
// We need to declare the shader in a form corresponding to entries in USD's
428+
// Sdr registry, so that if rendered in `usdview` or another Hydra-based
429+
// app, the render delegates can find the shaders. We could potentially do
430+
// that done by finding a shader in the Sdr registry by name and then using
431+
// `SdrShaderNode.GetIdentifier()` or
432+
// `SdrShaderNode.GetResolvedImplementationURI()`, although it's not clear
433+
// how you'd decide which to use. Anyway, for now at least, we want to be
434+
// able to operate in environments without renderer-specific USD plugins
435+
// installed. So instead of querying the registry we use some heuristics of
436+
// our own.
399437

400-
std::string typePrefix;
401438
if( boost::starts_with( shader->getName(), "Pxr" ) || boost::starts_with( shader->getName(), "Lama" ) )
402439
{
403-
// Leave the type prefix empty. This should be the default, but we are currently only doing this
404-
// for a small number of shaders that we can be completely confident require it, in order to
405-
// preserve backwards compatibility.
440+
// Could be either an OSL or a C++ shader, but either way, RenderMan
441+
// registers it in the SdrRegistry by name.
442+
usdShader.SetShaderId( pxr::TfToken( shader->getName() ) );
443+
}
444+
else if( boost::starts_with( shader->getType(), "ai:" ) )
445+
{
446+
// Arnold registers all plugins at startup, so also only needs the name
447+
// to be able to create a shader. It uses an `arnold:` prefix to avoid
448+
// clashes with the names of other shaders.
449+
usdShader.SetShaderId( pxr::TfToken( "arnold:" + shader->getName() ) );
450+
}
451+
else if(
452+
boost::starts_with( shader->getType(), "osl:" ) &&
453+
writeConformantOSLShaders()
454+
)
455+
{
456+
// Arbitrary OSL shader. Arnold would want that written as an
457+
// `arnold:osl` shader with `input:shadername` pointing to the shader.
458+
// But that will never work in another renderer. RenderMan takes a
459+
// slightly more generic approach by registering each OSL shader from
460+
// `RMAN_SHADERPATH` into the Sdr registry, so we follow that in the hope
461+
// that Arnold and other renderers might fall in line in future.
462+
if( shader->getName().find( '/' ) != std::string::npos )
463+
{
464+
// Unfortunately, RenderMan's SdrDiscoveryPlugin uses only the leaf
465+
// name, even though the Riley API will accept `{directory}/{file}`.
466+
// So we write the leaf name, and accept that we can no longer
467+
// round-trip exactly (we are also losing the shader type).
468+
//
469+
// Our plan is that in future we'll flatten our shader libraries
470+
// into a single directory, and remove the few remaining spots in
471+
// the codebase that rely on `Shader::getType()`.
472+
usdShader.SetShaderId( pxr::TfToken( std::filesystem::path( shader->getName() ).stem().string() ) );
473+
}
474+
else
475+
{
476+
usdShader.SetShaderId( pxr::TfToken( shader->getName() ) );
477+
}
406478
}
407479
else
408480
{
409-
size_t typeColonPos = type.find( ":" );
481+
const std::string &type = shader->getType();
482+
const size_t typeColonPos = type.find( ":" );
410483
if( typeColonPos != std::string::npos )
411484
{
412-
// According to our current understanding, this is almost completely wrong. Renderer's like
413-
// PRMan won't accept shaders with type prefixes, and Arnold apparently requires all shaders
414-
// to be prefixed with "arnold:", including OSL. This code prefixes OSL shaders with "osl:",
415-
// which fails in all renderers we're aware of - we're keeping this behaviour for now for
416-
// backwards compatibility reasons.
417-
typePrefix = type.substr( 0, typeColonPos ) + ":";
418-
419-
// This is the one case that actually works
420-
if( typePrefix == "ai:" )
421-
{
422-
typePrefix = "arnold:";
423-
}
485+
// We don't currently know of any renderers where this is right, but
486+
// it is our historic behaviour, which we are keeping until we know better.
487+
usdShader.SetShaderId( pxr::TfToken( type.substr( 0, typeColonPos ) + ":" + shader->getName() ) );
488+
}
489+
else
490+
{
491+
usdShader.SetShaderId( pxr::TfToken( shader->getName() ) );
424492
}
425493
}
426-
usdShader.SetShaderId( pxr::TfToken( typePrefix + shader->getName() ) );
427494

428495
return usdShader.ConnectableAPI();
429496
}

contrib/IECoreUSD/src/IECoreUSD/USDScene.cpp

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -497,7 +497,6 @@ void populateMaterial( pxr::UsdShadeMaterial &mat, const boost::container::flat_
497497
for( const auto &[output, shaderNetwork] : shaders )
498498
{
499499
pxr::UsdShadeOutput matOutput = mat.CreateOutput( output, pxr::SdfValueTypeNames->Token );
500-
501500
std::string shaderContainerName = boost::replace_all_copy( output.GetString(), ":", "_" ) + "_shaders";
502501
pxr::UsdGeomScope shaderContainer = pxr::UsdGeomScope::Define( mat.GetPrim().GetStage(), mat.GetPath().AppendChild( pxr::TfToken( shaderContainerName ) ) );
503502
pxr::UsdShadeOutput networkOut = ShaderAlgo::writeShaderNetwork( shaderNetwork.get(), shaderContainer.GetPrim() );

contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4795,5 +4795,78 @@ def testMaterialTerminalFromSubgraph( self ) :
47954795

47964796
self.assertEqual( shaderNetwork.outputShader().name, "LamaSurface" )
47974797

4798+
def testOSLShaderForHDPrman( self ) :
4799+
4800+
self.addCleanup( os.environ.__delitem__, "IECOREUSD_WRITE_CONFORMANT_OSL_SHADERS" )
4801+
4802+
for conformant in True, False :
4803+
4804+
with self.subTest( conformant = conformant ) :
4805+
4806+
os.environ["IECOREUSD_WRITE_CONFORMANT_OSL_SHADERS"] = str( int( conformant ) )
4807+
4808+
fileName = os.path.join( self.temporaryDirectory(), f"testConformance{conformant}.usda" )
4809+
4810+
shaderNetwork = IECoreScene.ShaderNetwork(
4811+
shaders = {
4812+
"output" : IECoreScene.Shader( "PxrDiffuse", "ri:surface" ),
4813+
"texture" : IECoreScene.Shader( "Pattern/Noise", "osl:shader" ),
4814+
"scale" : IECoreScene.Shader( "floatAttribute", "osl:shader" ),
4815+
},
4816+
connections = [
4817+
( ( "scale", "out" ), ( "texture", "scale" ) ),
4818+
( ( "texture", "out" ), ( "output", "diffuseColor" ) ),
4819+
],
4820+
output = ( "output", "bxdf_out" ),
4821+
)
4822+
4823+
# Test writing to USD.
4824+
4825+
scene = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
4826+
scene.createChild( "test" ).writeAttribute( "ri:surface", shaderNetwork, 0 )
4827+
del scene
4828+
4829+
stage = pxr.Usd.Stage.Open( fileName )
4830+
4831+
outputShader = pxr.UsdShade.Shader(
4832+
next( p for p in stage.Traverse() if p.IsA( pxr.UsdShade.Shader ) and p.GetName() == "output" )
4833+
)
4834+
self.assertEqual( outputShader.GetShaderId(), "PxrDiffuse" )
4835+
4836+
textureShader = pxr.UsdShade.Shader(
4837+
next( p for p in stage.Traverse() if p.IsA( pxr.UsdShade.Shader ) and p.GetName() == "texture" )
4838+
)
4839+
if conformant :
4840+
self.assertEqual( textureShader.GetShaderId(), "Noise" )
4841+
else :
4842+
self.assertEqual( textureShader.GetShaderId(), "osl:Pattern/Noise" )
4843+
4844+
scaleShader = pxr.UsdShade.Shader(
4845+
next( p for p in stage.Traverse() if p.IsA( pxr.UsdShade.Shader ) and p.GetName() == "scale" )
4846+
)
4847+
if conformant :
4848+
self.assertEqual( scaleShader.GetShaderId(), "floatAttribute" )
4849+
else :
4850+
self.assertEqual( scaleShader.GetShaderId(), "osl:floatAttribute" )
4851+
4852+
# Test loading back to Cortex.
4853+
4854+
scene = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
4855+
loadedShaderNetwork = scene.child( "test" ).readAttribute( "ri:surface", 0 )
4856+
4857+
if conformant :
4858+
# Can't round trip. We will need to adjust our usage to avoid shaders
4859+
# nested in directories.
4860+
self.assertEqual( loadedShaderNetwork.getShader( "texture" ).name, "Noise" )
4861+
self.assertEqual( loadedShaderNetwork.getShader( "texture" ).type, "surface" )
4862+
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).name, "floatAttribute" )
4863+
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).type, "surface" )
4864+
else :
4865+
# Round tripped exactly.
4866+
self.assertEqual( loadedShaderNetwork.getShader( "texture" ).name, "Pattern/Noise" )
4867+
self.assertEqual( loadedShaderNetwork.getShader( "texture" ).type, "osl:shader" )
4868+
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).name, "floatAttribute" )
4869+
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).type, "osl:shader" )
4870+
47984871
if __name__ == "__main__":
47994872
unittest.main()

0 commit comments

Comments
 (0)