Skip to content

Commit b56bb74

Browse files
Merge pull request #1544 from johnhaddon/rmanAttributesFixes
Fix IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES bugs
2 parents a760d07 + c839fd6 commit b56bb74

3 files changed

Lines changed: 169 additions & 83 deletions

File tree

‎Changes‎

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

4+
Fixes
5+
-----
46

7+
- USDScene :
8+
- Fixed bug writing shader assignments with `IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES=1`.
9+
- Fixed bug reading shader assignments with `IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES=1`.
10+
- Fixed bug reading legacy attributes with `IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES=1`.
511

612
10.6.7.0 (relative to 10.6.6.0)
713
========

‎contrib/IECoreUSD/src/IECoreUSD/AttributeAlgo.cpp‎

Lines changed: 119 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@
3636
#include "boost/algorithm/string/erase.hpp"
3737
#include "boost/algorithm/string/replace.hpp"
3838
#include "boost/algorithm/string/predicate.hpp"
39+
#include "boost/container/flat_set.hpp"
40+
#include "boost/container/small_vector.hpp"
3941

4042
IECORE_PUSH_DEFAULT_VISIBILITY
4143
#include "pxr/usd/usdGeom/primvar.h"
@@ -50,14 +52,15 @@ using namespace pxr;
5052
namespace
5153
{
5254

53-
static const pxr::TfToken g_cortexPrimitiveVariableMetadataToken( "cortex_isConstantPrimitiveVariable" );
54-
static const pxr::TfToken g_cortexPrimitiveVariableMetadataTokenDeprecated( "IECOREUSD_CONSTANT_PRIMITIVE_VARIABLE" );
55-
static const std::string g_primVarPrefix = "primvars:";
56-
static const std::string g_primVarUserPrefix = "primvars:user:";
57-
static const std::string g_renderPrefix = "render:";
58-
static const std::string g_riPrefix = "ri:";
59-
static const std::string g_riAttributesPrefix = "ri:attributes:";
60-
static const std::string g_userPrefix = "user:";
55+
const pxr::TfToken g_cortexPrimitiveVariableMetadataToken( "cortex_isConstantPrimitiveVariable" );
56+
const pxr::TfToken g_cortexPrimitiveVariableMetadataTokenDeprecated( "IECOREUSD_CONSTANT_PRIMITIVE_VARIABLE" );
57+
const std::string g_primVarPrefix = "primvars:";
58+
const std::string g_primVarUserPrefix = "primvars:user:";
59+
const std::string g_renderPrefix = "render:";
60+
const std::string g_riPrefix = "ri:";
61+
const std::string g_riAttributesPrefix = "ri:attributes:";
62+
const std::string g_userPrefix = "user:";
63+
const boost::container::flat_set<std::string> g_shaderTypes = { "surface", "displacement", "light", "volume" };
6164

6265
bool writeConformantRenderManAttributes()
6366
{
@@ -68,8 +71,93 @@ bool writeConformantRenderManAttributes()
6871
return false;
6972
}
7073

74+
using Names = boost::container::small_vector<AttributeAlgo::Name, 2>;
75+
76+
// Given a Cortex attribute name, return the ways it might be represented in
77+
// USD. This is a one-to-many mapping, to account for legacy representations
78+
// in old files. The first name returned should be used for writing new files,
79+
// and the other names are used as fallbacks when reading.
80+
Names usdNames( std::string name )
81+
{
82+
if( boost::starts_with( name, g_riPrefix ) )
83+
{
84+
const std::string potentialShaderType = name.substr( g_riPrefix.size() );
85+
if( g_shaderTypes.count( potentialShaderType ) )
86+
{
87+
return { { pxr::TfToken( name ), true } };
88+
}
89+
const AttributeAlgo::Name conformantName = { pxr::TfToken( g_riAttributesPrefix + name.substr( g_riPrefix.size() ) ), true };
90+
const AttributeAlgo::Name legacyName = { pxr::TfToken( name ), false };
91+
if( writeConformantRenderManAttributes() )
92+
{
93+
return { conformantName, legacyName };
94+
}
95+
else
96+
{
97+
return { legacyName, conformantName };
98+
}
99+
}
100+
101+
bool isPrimvar = false;
102+
103+
// The long term plan is to convert only "render:" prefixed attributes to primvars, and it will
104+
// be the client's responsibility to ensure everything important gets prefixed with "render:".
105+
// But for the moment, Gaffer doesn't do this yet, so we support the two most important prefixes
106+
// for Gaffer currently: "user:" and "ai:".
107+
/// \todo I don't think the `render:` plan is working out - it may well be better to just map
108+
/// all Cortex attributes to primvars.
109+
if( boost::starts_with( name, "render:" ) || boost::starts_with( name, "user:" ) || boost::starts_with( name, "ai:" ) )
110+
{
111+
isPrimvar = true;
112+
113+
// Strip the "render:" prefix from when writing attributes as primitive variables
114+
if( boost::starts_with( name, g_renderPrefix ) )
115+
{
116+
name = name.substr( 7 );
117+
}
118+
}
119+
120+
if( name == "ai:disp_map" )
121+
{
122+
// Special case where the whole name is different, not just prefix
123+
name = "arnold:displacement";
124+
}
125+
else
126+
{
127+
size_t colonPos = name.find( ":" );
128+
if( colonPos != std::string::npos )
129+
{
130+
std::string prefix = name.substr( 0, colonPos );
131+
std::string newPrefix;
132+
// Translate prefixes. Currently ai -> arnold is the only mapping supported
133+
if( prefix == "ai" )
134+
{
135+
newPrefix = "arnold";
136+
}
137+
138+
if( newPrefix.size() )
139+
{
140+
name = newPrefix + name.substr( colonPos );
141+
}
142+
}
143+
}
144+
145+
Names result;
146+
if( isPrimvar )
147+
{
148+
result.push_back( { TfToken( name ), true } );
149+
}
150+
if( name.find( ':' ) != std::string::npos )
151+
{
152+
// We add this one even when the primary version is a primvar, as a
153+
// fallback to legacy files from a time when we wrote attributes.
154+
result.push_back( { TfToken( name ), false } );
155+
}
156+
return result;
71157
}
72158

159+
} // namespace
160+
73161
bool IECoreUSD::AttributeAlgo::isCortexAttribute( const pxr::UsdGeomPrimvar &primVar )
74162
{
75163
if( primVar.GetInterpolation() != pxr::UsdGeomTokens->constant )
@@ -133,56 +221,16 @@ pxr::TfToken IECoreUSD::AttributeAlgo::cortexPrimitiveVariableMetadataTokenDepre
133221

134222
IECoreUSD::AttributeAlgo::Name IECoreUSD::AttributeAlgo::nameToUSD( std::string name )
135223
{
136-
if( boost::starts_with( name, g_riPrefix ) && writeConformantRenderManAttributes() )
137-
{
138-
return { pxr::TfToken( g_riAttributesPrefix + name.substr( g_riPrefix.size() ) ), true };
139-
}
140-
141-
bool isPrimvar = false;
142-
143-
// The long term plan is to convert only "render:" prefixed attributes to primvars, and it will
144-
// be the client's responsibility to ensure everything important gets prefixed with "render:".
145-
// But for the moment, Gaffer doesn't do this yet, so we support the two most important prefixes
146-
// for Gaffer currently: "user:" and "ai:".
147-
/// \todo I don't think the `render:` plan is working out - it may well be better to just map
148-
/// all Cortex attributes to primvars.
149-
if( boost::starts_with( name, "render:" ) || boost::starts_with( name, "user:" ) || boost::starts_with( name, "ai:" ) )
224+
const Names names = usdNames( name );
225+
if( names.size() )
150226
{
151-
isPrimvar = true;
152-
153-
// Strip the "render:" prefix from when writing attributes as primitive variables
154-
if( boost::starts_with( name, g_renderPrefix ) )
155-
{
156-
name = name.substr( 7 );
157-
}
227+
return names.front();
158228
}
159-
160-
if( name == "ai:disp_map" )
161-
{
162-
// Special case where the whole name is different, not just prefix
163-
name = "arnold:displacement";
164-
}
165-
else
166-
{
167-
size_t colonPos = name.find( ":" );
168-
if( colonPos != std::string::npos )
169-
{
170-
std::string prefix = name.substr( 0, colonPos );
171-
std::string newPrefix;
172-
// Translate prefixes. Currently ai -> arnold is the only mapping supported
173-
if( prefix == "ai" )
174-
{
175-
newPrefix = "arnold";
176-
}
177-
178-
if( newPrefix.size() )
179-
{
180-
name = newPrefix + name.substr( colonPos );
181-
}
182-
}
183-
}
184-
185-
return { TfToken( name ), isPrimvar };
229+
// This is necessary because `USDScene` calls `nameToUSD()` when writing
230+
// materials, and `surface` etc aren't handled by the above.
231+
/// \todo It would likely be better if the material-writing code wasn't
232+
/// mixed up with this.
233+
return { pxr::TfToken( name ), false };
186234
}
187235

188236
IECore::InternedString IECoreUSD::AttributeAlgo::nameFromUSD( IECoreUSD::AttributeAlgo::Name name )
@@ -234,27 +282,27 @@ IECore::InternedString IECoreUSD::AttributeAlgo::nameFromUSD( IECoreUSD::Attribu
234282

235283
UsdAttribute IECoreUSD::AttributeAlgo::findUSDAttribute( const pxr::UsdPrim &prim, std::string cortexName )
236284
{
237-
AttributeAlgo::Name n = AttributeAlgo::nameToUSD( cortexName );
238-
if( n.isPrimvar )
285+
for( const auto &n : usdNames( cortexName ) )
239286
{
240-
if( pxr::UsdGeomPrimvar primvar = pxr::UsdGeomPrimvarsAPI( prim ).GetPrimvar( n.name ) )
287+
if( n.isPrimvar )
241288
{
242-
if( isCortexAttribute( primvar ) )
289+
if( pxr::UsdGeomPrimvar primvar = pxr::UsdGeomPrimvarsAPI( prim ).GetPrimvar( n.name ) )
243290
{
244-
return primvar.GetAttr();
291+
if( isCortexAttribute( primvar ) )
292+
{
293+
return primvar.GetAttr();
294+
}
245295
}
246296
}
247-
}
248-
249-
// In theory, this should be able to be an else. But for the moment, for attributes that should be written
250-
// to a primvar, we try reading them from an attribute if we can't find them in a primvar. This provides
251-
// some backwards compatibility with files from before we started writing to primvars, and might provide
252-
// compatibility with other USD authors, maybe?
253-
if( pxr::UsdAttribute attribute = prim.GetAttribute( n.name ) )
254-
{
255-
if ( attribute.GetName().GetString().find( ":" ) != std::string::npos && attribute.IsCustom() )
297+
else
256298
{
257-
return attribute;
299+
if( pxr::UsdAttribute attribute = prim.GetAttribute( n.name ) )
300+
{
301+
if( attribute.IsCustom() )
302+
{
303+
return attribute;
304+
}
305+
}
258306
}
259307
}
260308

‎contrib/IECoreUSD/test/IECoreUSD/USDSceneTest.py‎

Lines changed: 44 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4868,28 +4868,42 @@ def testOSLShaderForHDPrman( self ) :
48684868
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).name, "floatAttribute" )
48694869
self.assertEqual( loadedShaderNetwork.getShader( "scale" ).type, "osl:shader" )
48704870

4871-
def testRenderManAttributeRoundTrip( self ) :
4871+
def testWriteConformantRenderManAttributes( self ) :
48724872

48734873
self.addCleanup( os.environ.__delitem__, "IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES" )
48744874

4875-
for conformant in True, False :
4876-
4877-
with self.subTest( conformant = conformant ) :
4875+
for writeConformant in ( True, False ) :
48784876

4879-
os.environ["IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES"] = str( int( conformant ) )
4880-
4881-
fileName = os.path.join( self.temporaryDirectory(), f"renderManAttributes{conformant}.usda" )
4877+
with self.subTest( writeConformant = writeConformant ) :
48824878

48834879
# Test writing to USD.
48844880

4881+
os.environ["IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES"] = str( int( writeConformant ) )
4882+
4883+
fileName = os.path.join( self.temporaryDirectory(), f"renderManAttributes{writeConformant}.usda" )
48854884
scene = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Write )
48864885
child = scene.createChild( "test" )
48874886
child.writeAttribute( "ri:trace:maxdiffusedepth", IECore.IntData( 2 ), 0 )
4887+
4888+
surface = IECoreScene.ShaderNetwork(
4889+
shaders = { "constant" : IECoreScene.Shader( "PxrDiffuse" ) },
4890+
output = ( "constant", "out_bxdf" )
4891+
)
4892+
child.writeAttribute( "ri:surface", surface, 0.0 )
4893+
4894+
surfaceFull = IECoreScene.ShaderNetwork(
4895+
shaders = { "constant" : IECoreScene.Shader( "PxrSurface" ) },
4896+
output = ( "constant", "out_bxdf" )
4897+
)
4898+
child.writeAttribute( "ri:surface:full", surfaceFull, 0.0 )
4899+
48884900
del scene, child
48894901

48904902
stage = pxr.Usd.Stage.Open( fileName )
48914903

4892-
if conformant :
4904+
# The attribute is written differently depending on IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES.
4905+
4906+
if writeConformant :
48934907
primVars = pxr.UsdGeom.PrimvarsAPI( stage.GetPrimAtPath( "/test" ) )
48944908
diffuseDepth = primVars.GetPrimvar( "ri:attributes:trace:maxdiffusedepth" )
48954909
self.assertTrue( diffuseDepth.IsDefined() )
@@ -4900,12 +4914,30 @@ def testRenderManAttributeRoundTrip( self ) :
49004914
diffuseDepth = stage.GetPrimAtPath( "/test" ).GetAttribute( "ri:trace:maxdiffusedepth" )
49014915
self.assertEqual( diffuseDepth.Get( 0 ), 2 )
49024916

4917+
# But the materials should be the same either way, with an `ri:surface` terminal.
4918+
4919+
for purpose in [ "", "full" ] :
4920+
material = pxr.UsdShade.MaterialBindingAPI( stage.GetPrimAtPath( "/test" ) ).ComputeBoundMaterial( purpose )[0]
4921+
output = material.GetOutput( "ri:surface" )
4922+
self.assertTrue( output )
4923+
self.assertEqual( output.GetConnectedSource()[1], "out_bxdf" )
4924+
49034925
# Test loading back to Cortex.
49044926

4905-
scene = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
4906-
child = scene.child( "test" )
4907-
self.assertEqual( child.attributeNames(), [ "ri:trace:maxdiffusedepth" ] )
4908-
self.assertEqual( child.readAttribute( "ri:trace:maxdiffusedepth", 0 ), IECore.IntData( 2 ) )
4927+
for readConformant in ( True, False ) :
4928+
4929+
with self.subTest( readConformant = readConformant ) :
4930+
4931+
os.environ["IECOREUSD_WRITE_CONFORMANT_RENDERMAN_ATTRIBUTES"] = str( int( readConformant ) )
4932+
4933+
fileName = os.path.join( self.temporaryDirectory(), f"renderManAttributes{writeConformant}.usda" )
4934+
4935+
scene = IECoreScene.SceneInterface.create( fileName, IECore.IndexedIO.OpenMode.Read )
4936+
child = scene.child( "test" )
4937+
self.assertEqual( set( child.attributeNames() ), { "ri:trace:maxdiffusedepth", "ri:surface", "ri:surface:full" } )
4938+
self.assertEqual( child.readAttribute( "ri:trace:maxdiffusedepth", 0 ), IECore.IntData( 2 ) )
4939+
self.assertEqual( child.readAttribute( "ri:surface", 0 ), surface )
4940+
self.assertEqual( child.readAttribute( "ri:surface:full", 0 ), surfaceFull )
49094941

49104942
if __name__ == "__main__":
49114943
unittest.main()

0 commit comments

Comments
 (0)