Skip to content

Commit

Permalink
Merge pull request #5895 from ericmehl/lightEditorBlockers
Browse files Browse the repository at this point in the history
LightEditor support for light filters
  • Loading branch information
ericmehl authored Jun 24, 2024
2 parents e5a5ca1 + b22623d commit e352897
Show file tree
Hide file tree
Showing 13 changed files with 514 additions and 116 deletions.
2 changes: 2 additions & 0 deletions Changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Improvements
- Annotations :
- Added support for `{plug}` value substitutions in node annotations.
- Added <kbd>Ctrl</kbd> + <kbd>Enter</kbd> keyboard shortcut to annotation dialogue. This applies the annotation and closes the dialogue.
- LightEditor : Added support for Arnold light blockers and barndoor, gobo and decay light filters.

Fixes
-----
Expand All @@ -30,6 +31,7 @@ API
- Loop : Added `nextIterationContext()` method.
- AnnotationsGadget : Added `annotationText()` method.
- ParallelAlgoTest : Added `UIThreadCallHandler.receive()` method.
- LightEditor : Added `registerShaderParameter()` method for registering parameters for shader attributes that are not the same as the `rendererKey`.

Build
-----
Expand Down
41 changes: 33 additions & 8 deletions python/GafferArnoldUI/ArnoldShaderUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@
import GafferSceneUI
import GafferArnold

# Arnold shaders to add to the light editor.
lightEditorShaders = {
# "shaderName" : ( "shaderAttributeName", "lightEditorSection" )
"light_blocker" : ( "ai:lightFilter:filter", "Blocker" ),
"barndoor" : ( "ai:lightFilter:barndoor", "Barndoor" ),
"gobo" : ( "ai:lightFilter:gobo", "Gobo" ),
"light_decay" : ( "ai:lightFilter:light_decay", "Decay" ),
}

##########################################################################
# Utilities to make it easier to work with the Arnold API, which has a
# fairly bare wrapping using ctypes.
Expand Down Expand Up @@ -337,14 +346,6 @@ def __translateNodeMetadata( nodeEntry ) :
parent = paramPath.rsplit( '.', 1 )[0]
__metadata[parent]["layout:section:%s:collapsed" % page] = collapsed

if (
arnold.AiNodeEntryGetType( nodeEntry ) == arnold.AI_NODE_LIGHT and
__aiMetadataGetStr( nodeEntry, paramName, "gaffer.plugType" ) != ""
) :
GafferSceneUI.LightEditor.registerParameter(
"ai:light", paramName, page
)

# Label from OSL "label"
label = __aiMetadataGetStr( nodeEntry, paramName, "label" )
if label is None :
Expand All @@ -356,6 +357,27 @@ def __translateNodeMetadata( nodeEntry ) :
__metadata[paramPath]["label"] = label
__metadata[paramPath]["noduleLayout:label"] = label

if (
arnold.AiNodeEntryGetType( nodeEntry ) == arnold.AI_NODE_LIGHT and
__aiMetadataGetStr( nodeEntry, paramName, "gaffer.plugType" ) != ""
) :
GafferSceneUI.LightEditor.registerParameter(
"ai:light", paramName, page
)

if (
nodeName in lightEditorShaders and
__aiMetadataGetStr( nodeEntry, paramName, "gaffer.plugType" ) != ""
) :
attributeName, sectionName = lightEditorShaders[nodeName]
GafferSceneUI.LightEditor.registerShaderParameter(
"ai:light",
paramName,
attributeName,
sectionName,
f"{page} {label}" if page is not None and label is not None else paramName
)

childComponents = {
arnold.AI_TYPE_VECTOR2 : "xy",
arnold.AI_TYPE_VECTOR : "xyz",
Expand Down Expand Up @@ -456,6 +478,9 @@ def addActivator( activator ) :
GafferSceneUI.LightEditor.registerParameter( "ai:light", "width", "Shape" )
GafferSceneUI.LightEditor.registerParameter( "ai:light", "height", "Shape" )

# Manually add the `filteredLights` parameter for `light_blocker`
GafferSceneUI.LightEditor.registerAttribute( "ai:light", "filteredLights", "Blocker" )

##########################################################################
# Gaffer Metadata queries. These are implemented using the preconstructed
# registry above.
Expand Down
2 changes: 1 addition & 1 deletion python/GafferSceneUI/EditScopeUI.py
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ def _summary( processor, linkCreator ) :
summaries[0] = summaries[0][0].upper() + summaries[0][1:]
return " and ".join( summaries )

GafferUI.EditScopeUI.ProcessorWidget.registerProcessorWidget( "AttributeEdits TransformEdits *LightEdits *SurfaceEdits", __LocationEditsWidget )
GafferUI.EditScopeUI.ProcessorWidget.registerProcessorWidget( "AttributeEdits TransformEdits *LightEdits *SurfaceEdits *FilterEdits", __LocationEditsWidget )

class __PruningEditsWidget( _SceneProcessorWidget ) :

Expand Down
116 changes: 107 additions & 9 deletions python/GafferSceneUI/LightEditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def __init__( self, scriptNode, **kw ) :
Gaffer.NodeAlgo.applyUserDefaults( self.__settingsNode )

self.__setFilter = _GafferSceneUI._HierarchyViewSetFilter()
self.__setFilter.setSetNames( [ "__lights" ] )
self.__setFilter.setSetNames( [ "__lights", "__lightFilters" ] )

with column :

Expand Down Expand Up @@ -137,20 +137,27 @@ def scene( self ) :

return self.__plug

# Registers a parameter to be available for editing. `rendererKey` is a pattern
# that will be matched against `self.__settingsNode["attribute"]` to determine if
# the column should be shown.
@classmethod
def registerParameter( cls, rendererKey, parameter, section = None, columnName = None ) :
def __parseParameter( cls, parameter ) :

if isinstance( parameter, str ) :
shader = ""
param = parameter
if "." in parameter :
shader, dot, param = parameter.partition( "." )
parameter = IECoreScene.ShaderNetwork.Parameter( shader, param )
return IECoreScene.ShaderNetwork.Parameter( shader, param )
else :
assert( isinstance( parameter, IECoreScene.ShaderNetwork.Parameter ) )
return parameter

# Registers a parameter to be available for editing. `rendererKey` is a pattern
# that will be matched against `self.__settingsNode["attribute"]` to determine if
# the column should be shown.
# \todo Deprecate in favor of method below.
@classmethod
def registerParameter( cls, rendererKey, parameter, section = None, columnName = None ) :

parameter = cls.__parseParameter( parameter )

GafferSceneUI.LightEditor.registerColumn(
rendererKey,
Expand All @@ -162,6 +169,27 @@ def registerParameter( cls, rendererKey, parameter, section = None, columnName =
section
)

# Registers a parameter to be available for editing. `rendererKey` is a pattern
# that will be matched against `self.__settingsNode["attribute"]` to determine if
# the column should be shown. `attribute` is the attribute holding the shader that
# will be edited. If it is `None`, the attribute will be the same as `rendererKey`.
@classmethod
def registerShaderParameter( cls, rendererKey, parameter, shaderAttribute = None, section = None, columnName = None ) :

parameter = cls.__parseParameter( parameter )

shaderAttribute = shaderAttribute if shaderAttribute is not None else rendererKey

GafferSceneUI.LightEditor.registerColumn(
rendererKey,
".".join( x for x in [ parameter.shader, parameter.name ] if x ),
lambda scene, editScope : _GafferSceneUI._LightEditorInspectorColumn(
GafferSceneUI.Private.ParameterInspector( scene, editScope, shaderAttribute, parameter ),
columnName if columnName is not None else ""
),
section
)

@classmethod
def registerAttribute( cls, rendererKey, attributeName, section = None ) :

Expand Down Expand Up @@ -264,9 +292,9 @@ def __updateColumns( self ) :
sectionColumns += [ c( self.__settingsNode["in"], self.__settingsNode["editScope"] ) for c in section.values() ]

nameColumn = self.__pathListing.getColumns()[0]
muteColumn = self.__pathListing.getColumns()[1]
soloColumn = self.__pathListing.getColumns()[2]
self.__pathListing.setColumns( [ nameColumn, muteColumn, soloColumn ] + sectionColumns )
self.__muteColumn = self.__pathListing.getColumns()[1]
self.__soloColumn = self.__pathListing.getColumns()[2]
self.__pathListing.setColumns( [ nameColumn, self.__muteColumn, self.__soloColumn ] + sectionColumns )

def __settingsPlugSet( self, plug ) :

Expand Down Expand Up @@ -354,11 +382,27 @@ def __editSelectedCells( self, pathListing, quickBoolean = True ) :
inspections = []

with Gaffer.Context( self.getContext() ) as context :
lightSetMembers = self.__settingsNode["in"].set( "__lights" ).value

for selection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) :
if not isinstance( column, _GafferSceneUI._LightEditorInspectorColumn ) :
continue
for pathString in selection.paths() :
path = GafferScene.ScenePlug.stringToPath( pathString )

if (
( column == self.__muteColumn or column == self.__soloColumn ) and
not ( lightSetMembers.match( path ) & (
IECore.PathMatcher.Result.ExactMatch | IECore.PathMatcher.Result.DescendantMatch
) )
) :
with GafferUI.PopupWindow() as self.__popup :
with GafferUI.ListContainer( GafferUI.ListContainer.Orientation.Horizontal, spacing = 4 ) :
GafferUI.Image( "warningSmall.png" )
GafferUI.Label( "<h4>The " + column.headerData().value + " column can only be toggled for lights." )
self.__popup.popup()
return

context["scene:path"] = path
inspection = column.inspector().inspect()

Expand Down Expand Up @@ -560,6 +604,50 @@ def __removeAttributes( self, pathListing ) :
tweak["enabled"].setValue( True )
tweak["mode"].setValue( Gaffer.TweakPlug.Mode.Remove )

def __selectedSetExpressions( self, pathListing ) :

# A dictionary of the form :
# { light1 : set( setExpression1, setExpression2 ), light2 : set( setExpression1 ), ... }
result = {}

lightPath = pathListing.getPath().copy()
for columnSelection, column in zip( pathListing.getSelection(), pathListing.getColumns() ) :
if (
not columnSelection.isEmpty() and (
not isinstance( column, _GafferSceneUI._LightEditorInspectorColumn ) or
not (
Gaffer.Metadata.value( "attribute:" + column.inspector().name(), "ui:scene:acceptsSetName" ) or
Gaffer.Metadata.value( "attribute:" + column.inspector().name(), "ui:scene:acceptsSetNames" ) or
Gaffer.Metadata.value( "attribute:" + column.inspector().name(), "ui:scene:acceptsSetExpression" )
)
)
) :
# We only return set expressions if all selected paths are in
# columns that accept set names or set expressions.
return {}

for path in columnSelection.paths() :
lightPath.setFromString( path )
cellValue = column.cellData( lightPath ).value
if cellValue is not None :
result.setdefault( path, set() ).add( cellValue )
else :
# We only return set expressions if all selected paths are render passes.
return {}

return result

def __selectAffected( self, pathListing ) :

result = IECore.PathMatcher()

with Gaffer.Context( self.getContext() ) as context :
for light, setExpressions in self.__selectedSetExpressions( pathListing ).items() :
for setExpression in setExpressions :
result.addPaths( GafferScene.SetAlgo.evaluateSetExpression( setExpression, self.__settingsNode["in"] ) )

GafferSceneUI.ContextAlgo.setSelectedPaths( self.getContext(), result )

def __buttonPress( self, pathListing, event ) :

if event.button != event.Buttons.Right or event.modifiers != event.Modifiers.None_ :
Expand Down Expand Up @@ -665,6 +753,16 @@ def __buttonPress( self, pathListing, event ) :
"shortCut" : "Backspace, Delete",
}
)
if len( self.__selectedSetExpressions( pathListing ) ) > 0 :
menuDefinition.append(
"SelectAffectedObjectsDivider", { "divider" : True }
)
menuDefinition.append(
"Select Affected Objects",
{
"command" : functools.partial( self.__selectAffected, pathListing ),
}
)

self.__contextMenu = GafferUI.Menu( menuDefinition )
self.__contextMenu.popup( pathListing )
Expand Down
29 changes: 29 additions & 0 deletions python/GafferSceneUITest/AttributeInspectorTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -736,5 +736,34 @@ def testDontEditParentOfInspectedLocation( self ) :

self.assertEqual( row["name"].getValue(), "/parent/child" )

def testLightFilter( self ) :

lightFilter = GafferSceneTest.TestLightFilter()

editScope = Gaffer.EditScope()
editScope.setup( lightFilter["out"] )
editScope["in"].setInput( lightFilter["out"] )

self.__assertExpectedResult(
self.__inspect( editScope["out"], "/lightFilter", "filteredLights" ),
source = lightFilter["filteredLights"],
sourceType = GafferSceneUI.Private.Inspector.Result.SourceType.Other,
editable = True,
edit = lightFilter["filteredLights"]
)

inspection = self.__inspect( editScope["out"], "/lightFilter", "filteredLights", editScope )
edit = inspection.acquireEdit()
edit["enabled"].setValue( True )

self.__assertExpectedResult(
self.__inspect( editScope["out"], "/lightFilter", "filteredLights", editScope ),
source = edit,
sourceType = GafferSceneUI.Private.Inspector.Result.SourceType.EditScope,
editable = True,
edit = edit
)


if __name__ == "__main__" :
unittest.main()
44 changes: 38 additions & 6 deletions python/GafferSceneUITest/LightEditorTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,9 @@ def testLightMuteAttribute( toggleCount, toggleLocation, newStates ) :
editor = GafferSceneUI.LightEditor( script )
editor._LightEditor__settingsNode["editScope"].setInput( script["editScope"]["out"] )

editor._LightEditor__updateColumns()
GafferSceneUI.LightEditor._LightEditor__updateColumns.flush( editor )

widget = editor._LightEditor__pathListing
self.setLightEditorMuteSelection( widget, togglePaths )

Expand Down Expand Up @@ -696,6 +699,9 @@ def testToggleContext( self ) :
self.assertEqual( attr["gl:visualiser:scale"].value, 5.0 )

editor = GafferSceneUI.LightEditor( script )
editor._LightEditor__updateColumns()
GafferSceneUI.LightEditor._LightEditor__updateColumns.flush( editor )

widget = editor._LightEditor__pathListing
editor.setNodeSet( Gaffer.StandardSet( [ script["custAttr"] ] ) )
self.setLightEditorMuteSelection( widget, ["/group/light"] )
Expand All @@ -708,8 +714,8 @@ def testToggleContext( self ) :

def testShaderParameterEditScope( self ) :

GafferSceneUI.LightEditor.registerParameter( "light", "add.a" )
GafferSceneUI.LightEditor.registerParameter( "light", "exposure" )
GafferSceneUI.LightEditor.registerShaderParameter( "light", "add.a" )
GafferSceneUI.LightEditor.registerShaderParameter( "light", "exposure" )

script = Gaffer.ScriptNode()

Expand All @@ -731,12 +737,11 @@ def testShaderParameterEditScope( self ) :
self.assertEqual( attributes["light"].shaders()["add"].parameters["a"].value, imath.Color3f( 0.0 ) )
self.assertEqual( attributes["light"].shaders()["__shader"].parameters["exposure"].value, 0.0 )

with GafferUI.Window() as window :
editor = GafferSceneUI.LightEditor( script )
editor = GafferSceneUI.LightEditor( script )
editor._LightEditor__settingsNode["editScope"].setInput( script["editScope"]["out"] )
window.setVisible( True )

self.waitForIdle( 1000 )
editor._LightEditor__updateColumns()
GafferSceneUI.LightEditor._LightEditor__updateColumns.flush( editor )

editor.setNodeSet( Gaffer.StandardSet( [ script["editScope"] ] ) )

Expand Down Expand Up @@ -863,5 +868,32 @@ def testDeregisterColumn( self ) :
self.assertNotIn( "Y", columnNames )
self.assertNotIn( "Z", columnNames )

def testLightBlockerSoloDisabled( self ) :

script = Gaffer.ScriptNode()

script["blocker"] = GafferScene.Cube()
script["blocker"]["sets"].setValue( "__lightFilters" )

editor = GafferSceneUI.LightEditor( script )
editor._LightEditor__updateColumns()
GafferSceneUI.LightEditor._LightEditor__updateColumns.flush( editor )

editor.setNodeSet( Gaffer.StandardSet( [ script["blocker"] ] ) )

widget = editor._LightEditor__pathListing

columns = widget.getColumns()
for i, c in zip( range( 0, len( columns ) ), columns ) :
if isinstance( c, _GafferSceneUI._LightEditorSetMembershipColumn ) :
selection = [ IECore.PathMatcher() for i in range( 0, len( columns ) ) ]
selection[i].addPath( "/cube" )
widget.setSelection( selection )

editor._LightEditor__editSelectedCells( widget )

self.assertTrue( script["blocker"]["out"].set( "soloLights" ).value.isEmpty() )


if __name__ == "__main__" :
unittest.main()
4 changes: 4 additions & 0 deletions resources/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -371,6 +371,10 @@
"setMemberHighlighted",
"setMemberFaded",
"setMemberFadedHighlighted",
"boxBlocker",
"sphereBlocker",
"planeBlocker",
"cylinderBlocker",
]
},

Expand Down
Loading

0 comments on commit e352897

Please sign in to comment.