Skip to content

Commit efe1c67

Browse files
committed
IECoreUSD : Add option to write OSL shaders conformant with RenderMan
This means writing the `info:id` attribute without our `osl:` prefix, and omitting any directories in the shader name. Since this is a breaking change, it is gated behind an IECOREUSD_WRITE_CONFORMANT_OSL_SHADERS environment variable for folks to opt in via. Unfortunately Arnold doesn't support shaders exported in this format, but it didn't support the old format either. Since the new format seems to be "blessed" by Pixar, perhaps we can contribute support for it to `arnold-usd` in the future. Alternatively, we could write OSL shaders as `arnold:osl` shaders, but only when assigned via an `arnold:` prefixed material binding. This is left as future work though. Adopting the RenderMan-style formatting for OSL shaders implies a few things for our own small shader library in Gaffer : - We should move to a flat directory structure, so we don't need to save the full name in the `cortex:shaderName` sidecar metadata. - We should prefix the shader names with "Gaffer", since the likelihood of collision is much greater without the directory names. This is also left as future work though.
1 parent 8b4f718 commit efe1c67

4 files changed

Lines changed: 182 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: 115 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,76 @@ 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, but also record the full name so we
467+
// can round-trip back to Cortex exactly.
468+
usdShader.SetShaderId( pxr::TfToken( std::filesystem::path( shader->getName() ).stem() ) );
469+
usdShader.GetPrim().SetCustomDataByKey( g_shaderNameToken, pxr::VtValue( shader->getName() ) );
470+
}
471+
else
472+
{
473+
usdShader.SetShaderId( pxr::TfToken( shader->getName() ) );
474+
}
475+
// We've lost the fact that it was an OSL shader, so store that information
476+
// as custom data for use when loading.
477+
usdShader.GetPrim().SetCustomDataByKey( g_shaderTypeToken, pxr::VtValue( shader->getType() ) );
478+
/// \todo We don't want to be resorting to storing custom data for the name or
479+
/// the type. Rejig things so that we only use leaf names for shaders, and don't
480+
/// rely on the type anywhere, and then remove the custom data.
406481
}
407482
else
408483
{
409-
size_t typeColonPos = type.find( ":" );
484+
const std::string &type = shader->getType();
485+
const size_t typeColonPos = type.find( ":" );
410486
if( typeColonPos != std::string::npos )
411487
{
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-
}
488+
// We don't currently know of any renderers where this is right, but
489+
// it is our historic behaviour, which we are keeping until we know better.
490+
usdShader.SetShaderId( pxr::TfToken( type.substr( 0, typeColonPos ) + ":" + shader->getName() ) );
491+
}
492+
else
493+
{
494+
usdShader.SetShaderId( pxr::TfToken( shader->getName() ) );
424495
}
425496
}
426-
usdShader.SetShaderId( pxr::TfToken( typePrefix + shader->getName() ) );
427497

428498
return usdShader.ConnectableAPI();
429499
}

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: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4795,5 +4795,69 @@ 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+
self.assertEqual( loadedShaderNetwork.getShader( "texture" ).name, "Pattern/Noise" )
4858+
self.assertEqual( loadedShaderNetwork.getShader( "texture" ).type, "osl:shader" )
4859+
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).name, "floatAttribute" )
4860+
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).type, "osl:shader" )
4861+
47984862
if __name__ == "__main__":
47994863
unittest.main()

0 commit comments

Comments
 (0)