Skip to content

LocaliseAttributes / AttributeQuery / AttributeTweaks : Inherit global attributes #6502

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ Improvements
- Nodes can be pinned for comparison by dropping them onto the green "B" side of the "Scene" comparison header [^1].
- The current expansion is now preserved when enabling or disabling comparison mode [^1].
- Viewer : Added `Add` image comparison mode.
- LocaliseAttributes : Added support for localising global attributes, controlled by the new `includeGlobalAttributes` plug.
- AttributeTweaks, ShaderTweaks : Global attributes are now localised when `localise` is enabled and no matching attribute is found at the target location or any of its ancestors.
- AttributeQuery, ShaderQuery : Global attributes are now queried when `inherit` is enabled and no matching attribute is found at the target location or any of its ancestors.

Fixes
-----
Expand All @@ -21,10 +24,17 @@ Fixes

- CyclesShader : Moved the `principled_bsdf.diffuse_roughness` parameter to a new "Diffuse" section in the Node Editor [^1].

API
---

- ScenePlug : Added optional `withGlobalAttributes` arguments to `fullAttributes()` and `fullAttributesHash()`.

Breaking Changes
----------------

- StandardLightVisualiser : Removed protected methods for drawing visualiser elements. These are now part of `GafferSceneUI::Private::LightVisualiserAlgo`. This namespace can be used by light visualisers, but is currently `Private` while the API details are being resolved.
- AttributeTweaks : Tweaks with `localise` enabled and a mode of `CreateIfMissing` will now not create an attribute if it is missing from the scene hierarchy, but exists in the globals.
- AttributeQuery : Queries with `inherit` enabled will now return a result when querying an attribute that does not exist in the scene hierarchy, but does exist in the globals.

[^1]: To be omitted from the notes for the final 1.6.0.0 release.

Expand Down
3 changes: 3 additions & 0 deletions include/GafferScene/LocaliseAttributes.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ class GAFFERSCENE_API LocaliseAttributes : public AttributeProcessor
Gaffer::StringPlug *attributesPlug();
const Gaffer::StringPlug *attributesPlug() const;

Gaffer::BoolPlug *includeGlobalAttributesPlug();
const Gaffer::BoolPlug *includeGlobalAttributesPlug() const;

protected :

bool affectsProcessedAttributes( const Gaffer::Plug *input ) const override;
Expand Down
5 changes: 3 additions & 2 deletions include/GafferScene/ScenePlug.h
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ class GAFFERSCENE_API ScenePlug : public Gaffer::ValuePlug
/// Returns just the attributes set at the specific scene path.
IECore::ConstCompoundObjectPtr attributes( const ScenePath &scenePath ) const;
/// Returns the full set of inherited attributes at the specified scene path.
IECore::CompoundObjectPtr fullAttributes( const ScenePath &scenePath ) const;
/// Attributes are also inherited from the globals when `withGlobalAttributes` is true.
IECore::CompoundObjectPtr fullAttributes( const ScenePath &scenePath, bool withGlobalAttributes = false ) const;
IECore::ConstObjectPtr object( const ScenePath &scenePath ) const;
IECore::ConstInternedStringVectorDataPtr childNames( const ScenePath &scenePath ) const;
/// Returns true if the specified location exists.
Expand All @@ -249,7 +250,7 @@ class GAFFERSCENE_API ScenePlug : public Gaffer::ValuePlug
IECore::MurmurHash transformHash( const ScenePath &scenePath ) const;
IECore::MurmurHash fullTransformHash( const ScenePath &scenePath ) const;
IECore::MurmurHash attributesHash( const ScenePath &scenePath ) const;
IECore::MurmurHash fullAttributesHash( const ScenePath &scenePath ) const;
IECore::MurmurHash fullAttributesHash( const ScenePath &scenePath, bool withGlobalAttributes = false ) const;
IECore::MurmurHash objectHash( const ScenePath &scenePath ) const;
IECore::MurmurHash childNamesHash( const ScenePath &scenePath ) const;
IECore::MurmurHash childBoundsHash( const ScenePath &scenePath ) const;
Expand Down
88 changes: 88 additions & 0 deletions python/GafferSceneTest/AttributeQueryTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@

import IECore
import Gaffer
import GafferTest
import GafferScene
import GafferSceneTest

Expand Down Expand Up @@ -1337,5 +1338,92 @@ def testQueryDoubleData( self ) :
self.assertTrue( query["exists"].getValue() )
self.assertEqual( query["value"].getValue(), 2.5 )

def testInheritedGlobalAttribute( self ) :

sphere = GafferScene.Sphere()

globalAttributes = GafferScene.CustomAttributes()
globalAttributes["in"].setInput( sphere["out"] )
globalAttributes["global"].setValue( True )
globalAttributes["extraAttributes"].setValue( IECore.CompoundObject( { "test" : IECore.DoubleData( 5.0 ) } ) )

attributes = GafferScene.CustomAttributes()
attributes["in"].setInput( globalAttributes["out"] )
attributes["extraAttributes"].setValue( IECore.CompoundObject( { "test" : IECore.DoubleData( 2.5 ) } ) )
attributes["enabled"].setValue( False )

query = GafferScene.AttributeQuery()
query.setup( Gaffer.FloatPlug() )
query["scene"].setInput( attributes["out"] )
query["location"].setValue( "/sphere" )
query["attribute"].setValue( "test" )

self.assertFalse( query["exists"].getValue() )
self.assertEqual( query["value"].getValue(), 0.0 )

query["inherit"].setValue( True )

self.assertTrue( query["exists"].getValue() )
self.assertEqual( query["value"].getValue(), 5.0 )

globalAttributes["extraAttributes"].setValue( IECore.CompoundObject( { "test" : IECore.DoubleData( 10.0 ) } ) )

self.assertTrue( query["exists"].getValue() )
self.assertEqual( query["value"].getValue(), 10.0 )

attributes["enabled"].setValue( True )

self.assertTrue( query["exists"].getValue() )
self.assertEqual( query["value"].getValue(), 2.5 )

query["inherit"].setValue( False )

self.assertTrue( query["exists"].getValue() )
self.assertEqual( query["value"].getValue(), 2.5 )

def testDirtyPropagation( self ) :

sphere = GafferScene.Sphere()

globalAttributes = GafferScene.StandardAttributes()
globalAttributes["in"].setInput( sphere["out"] )
globalAttributes["global"].setValue( True )

standardAttributes = GafferScene.StandardAttributes()
standardAttributes["in"].setInput( globalAttributes["out"] )

query = GafferScene.AttributeQuery()
query.setup( Gaffer.BoolPlug() )
query["scene"].setInput( standardAttributes["out"] )
query["location"].setValue( "/sphere" )

cs = GafferTest.CapturingSlot( query.plugDirtiedSignal() )

standardAttributes["attributes"]["scene:visible"]["enabled"].setValue( True )
self.assertIn( query["value"], { x[0] for x in cs } )

standardAttributes["attributes"]["scene:visible"]["enabled"].setValue( False )
del cs[:]
query["default"].setValue( True )
self.assertIn( query["value"], { x[0] for x in cs } )

# modifying the globals with the `inherit` plug disabled
# should not dirty query["value"]
del cs[:]
globalAttributes["attributes"]["scene:visible"]["enabled"].setValue( True )
self.assertNotIn( query["value"], { x[0] for x in cs } )

query["inherit"].setValue( True )
self.assertIn( query["value"], { x[0] for x in cs } )

globalAttributes["attributes"]["scene:visible"]["enabled"].setValue( False )
del cs[:]
globalAttributes["attributes"]["scene:visible"]["enabled"].setValue( True )
self.assertIn( query["value"], { x[0] for x in cs } )

del cs[:]
standardAttributes["attributes"]["scene:visible"]["enabled"].setValue( True )
self.assertIn( query["value"], { x[0] for x in cs } )

if __name__ == "__main__":
unittest.main()
26 changes: 22 additions & 4 deletions python/GafferSceneTest/AttributeTweaksTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,14 +142,20 @@ def testLocalise( self ) :
group = GafferScene.Group()
group["in"][0].setInput( plane["out"] )

globalAttributes = GafferScene.StandardAttributes()
globalAttributes["global"].setValue( True )
globalAttributes["attributes"]["gaffer:transformBlurSegments"]["enabled"].setValue( True )
globalAttributes["attributes"]["gaffer:transformBlurSegments"]["value"].setValue( 4 )
globalAttributes["in"].setInput( group["out"] )

attributes = GafferScene.StandardAttributes()
attributes["attributes"]["gaffer:transformBlurSegments"]["enabled"].setValue( True )
attributes["attributes"]["gaffer:transformBlurSegments"]["value"].setValue( 5 )

groupFilter = GafferScene.PathFilter()
groupFilter["paths"].setValue( IECore.StringVectorData( [ "/group" ] ) )

attributes["in"].setInput( group["out"] )
attributes["in"].setInput( globalAttributes["out"] )
attributes["filter"].setInput( groupFilter["out"] )

self.assertIn( "gaffer:transformBlurSegments", attributes["out"].attributes( "/group" ) )
Expand All @@ -162,11 +168,11 @@ def testLocalise( self ) :
tweaks["in"].setInput( attributes["out"] )
tweaks["filter"].setInput( planeFilter["out"] )

segmentsTweak = Gaffer.TweakPlug( "gaffer:transformBlurSegments", 2 )
segmentsTweak = Gaffer.TweakPlug( "gaffer:transformBlurSegments", 2, Gaffer.TweakPlug.Mode.Multiply )
tweaks["tweaks"].addChild( segmentsTweak )

self.assertEqual( tweaks["localise"].getValue(), False )
with self.assertRaisesRegex( RuntimeError, "Cannot apply tweak with mode Replace to \"gaffer:transformBlurSegments\" : This parameter does not exist" ) :
with self.assertRaisesRegex( RuntimeError, "Cannot apply tweak with mode Multiply to \"gaffer:transformBlurSegments\" : This parameter does not exist" ) :
tweaks["out"].attributes( "/group/plane" )

tweaks["localise"].setValue( True )
Expand All @@ -176,7 +182,19 @@ def testLocalise( self ) :

planeAttr = tweaks["out"].attributes( "/group/plane" )
self.assertIn( "gaffer:transformBlurSegments", planeAttr )
self.assertEqual( planeAttr["gaffer:transformBlurSegments"], IECore.IntData( 2 ) )
self.assertEqual( planeAttr["gaffer:transformBlurSegments"], IECore.IntData( 10 ) )

attributes["enabled"].setValue( False )
groupAttr = tweaks["out"].fullAttributes( "/group", withGlobalAttributes = True )
self.assertEqual( groupAttr["gaffer:transformBlurSegments"], IECore.IntData( 4 ) )

planeAttr = tweaks["out"].attributes( "/group/plane" )
self.assertIn( "gaffer:transformBlurSegments", planeAttr )
self.assertEqual( planeAttr["gaffer:transformBlurSegments"], IECore.IntData( 8 ) )

globalAttributes["attributes"]["gaffer:transformBlurSegments"]["value"].setValue( 6 )
planeAttr = tweaks["out"].attributes( "/group/plane" )
self.assertEqual( planeAttr["gaffer:transformBlurSegments"], IECore.IntData( 12 ) )

# Test disabling tweak results in no localisation

Expand Down
57 changes: 56 additions & 1 deletion python/GafferSceneTest/LocaliseAttributesTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,14 @@ def test( self ) :
outerGroupFilter = GafferScene.PathFilter()
outerGroupFilter["paths"].setValue( IECore.StringVectorData( [ "/outerGroup" ] ) )

globalAttributes = GafferScene.CustomAttributes()
globalAttributes["in"].setInput( outerGroup["out"] )
globalAttributes["global"].setValue( True )
globalAttributes["attributes"].addChild( Gaffer.NameValuePlug( "a", "globalA" ) )
globalAttributes["attributes"].addChild( Gaffer.NameValuePlug( "d", "globalD" ) )

planeAttributes = GafferScene.CustomAttributes()
planeAttributes["in"].setInput( outerGroup["out"] )
planeAttributes["in"].setInput( globalAttributes["out"] )
planeAttributes["filter"].setInput( planeFilter["out"] )
planeAttributes["attributes"].addChild( Gaffer.NameValuePlug( "a", "planeA" ) )

Expand Down Expand Up @@ -120,6 +126,11 @@ def test( self ) :
localiseAttributes["out"].attributes( "/outerGroup/innerGroup/plane" ),
localiseAttributes["in"].fullAttributes( "/outerGroup/innerGroup/plane" )
)
localiseAttributes["includeGlobalAttributes"].setValue( True )
self.assertEqual(
localiseAttributes["out"].attributes( "/outerGroup/innerGroup/plane" ),
localiseAttributes["in"].fullAttributes( "/outerGroup/innerGroup/plane", withGlobalAttributes = True )
)

# Localise a subset of the attributes.

Expand All @@ -143,14 +154,58 @@ def test( self ) :
} )
)

localiseAttributes["attributes"].setValue( "c d" )
self.assertEqual(
localiseAttributes["out"].attributes( "/outerGroup/innerGroup/plane" ),
IECore.CompoundObject( {
"a" : IECore.StringData( "planeA" ),
"c" : IECore.StringData( "outerGroupC" ),
"d" : IECore.StringData( "globalD" ),
} )
)

globalAttributes["attributes"]["NameValuePlug1"]["value"].setValue( "anotherGlobalD" )
self.assertEqual(
localiseAttributes["out"].attributes( "/outerGroup/innerGroup/plane" ),
IECore.CompoundObject( {
"a" : IECore.StringData( "planeA" ),
"c" : IECore.StringData( "outerGroupC" ),
"d" : IECore.StringData( "anotherGlobalD" ),
} )
)

localiseAttributes["includeGlobalAttributes"].setValue( False )
self.assertEqual(
localiseAttributes["out"].attributes( "/outerGroup/innerGroup/plane" ),
IECore.CompoundObject( {
"a" : IECore.StringData( "planeA" ),
"c" : IECore.StringData( "outerGroupC" ),
} )
)

def testDirtyPropagation( self ) :

globalAttributes = GafferScene.StandardAttributes()
globalAttributes["global"].setValue( True )
standardAttributes = GafferScene.StandardAttributes()
standardAttributes["in"].setInput( globalAttributes["out"] )
localiseAttributes = GafferScene.LocaliseAttributes()
localiseAttributes["in"].setInput( standardAttributes["out"] )

cs = GafferTest.CapturingSlot( localiseAttributes.plugDirtiedSignal() )

globalAttributes["attributes"]["doubleSided"]["enabled"].setValue( True )
self.assertNotIn( localiseAttributes["out"]["attributes"], { x[0] for x in cs } )

localiseAttributes["includeGlobalAttributes"].setValue( True )
self.assertIn( localiseAttributes["out"]["attributes"], { x[0] for x in cs } )

globalAttributes["attributes"]["doubleSided"]["enabled"].setValue( False )
del cs[:]
globalAttributes["attributes"]["doubleSided"]["enabled"].setValue( True )
self.assertIn( localiseAttributes["out"]["attributes"], { x[0] for x in cs } )

del cs[:]
standardAttributes["attributes"]["scene:visible"]["enabled"].setValue( True )
self.assertIn( localiseAttributes["out"]["attributes"], { x[0] for x in cs } )

Expand Down
38 changes: 38 additions & 0 deletions python/GafferSceneTest/ScenePlugTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ def testFullAttributes( self ) :
n = GafferSceneTest.CompoundObjectSource()
n["in"].setValue(
IECore.CompoundObject( {
"globals" : {
"attribute:a" : IECore.StringData( "aGlobal" ),
"attribute:d" : IECore.StringData( "dGlobal" ),
"attribute:e" : IECore.StringData( "eGlobal" ),
},
"children" : {
"group" : {
"attributes" : {
Expand All @@ -116,6 +121,7 @@ def testFullAttributes( self ) :
"attributes" : {
"b" : IECore.StringData( "bOverride" ),
"c" : IECore.StringData( "c" ),
"d" : IECore.StringData( "d" ),
},
}
}
Expand All @@ -132,15 +138,47 @@ def testFullAttributes( self ) :
} )
)

self.assertEqual(
n["out"].fullAttributes( "/group", withGlobalAttributes = True ),
IECore.CompoundObject( {
"a" : IECore.StringData( "a" ),
"b" : IECore.StringData( "b" ),
"d" : IECore.StringData( "dGlobal" ),
"e" : IECore.StringData( "eGlobal" ),
} )
)

self.assertNotEqual(
n["out"].fullAttributesHash( "/group" ),
n["out"].fullAttributesHash( "/group", withGlobalAttributes = True )
)

self.assertEqual(
n["out"].fullAttributes( "/group/ball" ),
IECore.CompoundObject( {
"a" : IECore.StringData( "a" ),
"b" : IECore.StringData( "bOverride" ),
"c" : IECore.StringData( "c" ),
"d" : IECore.StringData( "d" ),
} )
)

self.assertEqual(
n["out"].fullAttributes( "/group/ball", withGlobalAttributes = True ),
IECore.CompoundObject( {
"a" : IECore.StringData( "a" ),
"b" : IECore.StringData( "bOverride" ),
"c" : IECore.StringData( "c" ),
"d" : IECore.StringData( "d" ),
"e" : IECore.StringData( "eGlobal" ),
} )
)

self.assertNotEqual(
n["out"].fullAttributesHash( "/group/ball" ),
n["out"].fullAttributesHash( "/group/ball", withGlobalAttributes = True )
)

def testCreateCounterpart( self ) :

s1 = GafferScene.ScenePlug( "a", Gaffer.Plug.Direction.Out )
Expand Down
Loading
Loading