Skip to content

lib_rendersetup

Code to get attributes from render layer without switching to it.

https://github.com/Colorbleed/colorbleed-config/blob/acre/colorbleed/maya/lib_rendersetup.py Credits: Roy Nieterau (BigRoy) / Colorbleed Modified for use in AYON

get_attr_in_layer(node_attr, layer, as_string=True)

Return attribute value in Render Setup layer.

This will only work for attributes which can be retrieved with maya.cmds.getAttr and for which Relative and Absolute overrides are applicable.

Examples:

>>> get_attr_in_layer("defaultResolution.width", layer="layer1")
>>> get_attr_in_layer("defaultRenderGlobals.startFrame", layer="layer")
>>> get_attr_in_layer("transform.translate", layer="layer3")

Parameters:

Name Type Description Default
attr str

attribute name as 'node.attribute'

required
layer str

layer name

required

Returns:

Name Type Description
object

attribute value in layer

Source code in client/ayon_maya/api/lib_rendersetup.py
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
def get_attr_in_layer(node_attr, layer, as_string=True):
    """Return attribute value in Render Setup layer.

    This will only work for attributes which can be
    retrieved with `maya.cmds.getAttr` and for which
    Relative and Absolute overrides are applicable.

    Examples:
        >>> get_attr_in_layer("defaultResolution.width", layer="layer1")
        >>> get_attr_in_layer("defaultRenderGlobals.startFrame", layer="layer")
        >>> get_attr_in_layer("transform.translate", layer="layer3")

    Args:
        attr (str): attribute name as 'node.attribute'
        layer (str): layer name

    Returns:
        object: attribute value in layer

    """

    def _layer_needs_update(layer):
        """Return whether layer needs updating."""
        # Use `getattr` as e.g. DEFAULT_RENDER_LAYER does not have
        # the attribute
        return getattr(layer, "needsMembershipUpdate", False) or \
            getattr(layer, "needsApplyUpdate", False)

    def get_default_layer_value(node_attr_):
        """Return attribute value in `DEFAULT_RENDER_LAYER`."""
        inputs = cmds.listConnections(node_attr_,
                                      source=True,
                                      destination=False,
                                      # We want to skip conversion nodes since
                                      # an override to `endFrame` could have
                                      # a `unitToTimeConversion` node
                                      # in-between
                                      skipConversionNodes=True,
                                      type="applyOverride") or []
        if inputs:
            override = inputs[0]
            history_overrides = cmds.ls(cmds.listHistory(override,
                                                         pruneDagObjects=True),
                                        type="applyOverride")
            node = history_overrides[-1] if history_overrides else override
            node_attr_ = node + ".original"

        return get_attribute(node_attr_, asString=as_string)

    layer = get_rendersetup_layer(layer)
    rs = renderSetup.instance()
    current_layer = rs.getVisibleRenderLayer()
    if current_layer.name() == layer:

        # Ensure layer is up-to-date
        if _layer_needs_update(current_layer):
            try:
                rs.switchToLayer(current_layer)
            except RuntimeError:
                # Some cases can cause errors on switching
                # the first time with Render Setup layers
                # e.g. different overrides to compounds
                # and its children plugs. So we just force
                # it another time. If it then still fails
                # we will let it error out.
                rs.switchToLayer(current_layer)

        return get_attribute(node_attr, asString=as_string)

    overrides = get_attr_overrides(node_attr, layer)
    default_layer_value = get_default_layer_value(node_attr)
    if not overrides:
        return default_layer_value

    value = default_layer_value
    for match, layer_override, index in overrides:
        if isinstance(layer_override, AbsOverride):
            # Absolute override
            value = get_attribute(layer_override.name() + ".attrValue")
            if match == EXACT_MATCH:
                # value = value
                pass
            elif match == PARENT_MATCH:
                value = value[index]
            elif match == CLIENT_MATCH:
                value[index] = value

        elif isinstance(layer_override, RelOverride):
            # Relative override
            # Value = Original * Multiply + Offset
            multiply = get_attribute(layer_override.name() + ".multiply")
            offset = get_attribute(layer_override.name() + ".offset")

            if match == EXACT_MATCH:
                value = value * multiply + offset
            elif match == PARENT_MATCH:
                value = value * multiply[index] + offset[index]
            elif match == CLIENT_MATCH:
                value[index] = value[index] * multiply + offset

        else:
            raise TypeError("Unsupported override: %s" % layer_override)

    return value

get_attr_overrides(node_attr, layer, skip_disabled=True, skip_local_render=True, stop_at_absolute_override=True)

Return all Overrides applicable to the attribute.

Overrides are returned as a 3-tuple

(Match, Override, Index)

Match

This is any of EXACT_MATCH, PARENT_MATCH, CLIENT_MATCH and defines whether the override is exactly on the plug, on the parent or on a child plug.

Override

This is the RenderSetup Override instance.

Index

This is the Plug index under the parent or for the child that matches. The EXACT_MATCH index will always be None. For PARENT_MATCH the index is which index the plug is under the parent plug. For CLIENT_MATCH the index is which child index matches the plug.

Parameters:

Name Type Description Default
node_attr str

attribute name as 'node.attribute'

required
layer str

layer name

required
skip_disabled bool

exclude disabled overrides

True
skip_local_render bool

exclude overrides marked as local render.

True
stop_at_absolute_override

exclude overrides prior to the last absolute override as they have no influence on the resulting value.

True

Returns:

Name Type Description
list

Ordered Overrides in order of strength

Source code in client/ayon_maya/api/lib_rendersetup.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
def get_attr_overrides(node_attr, layer,
                       skip_disabled=True,
                       skip_local_render=True,
                       stop_at_absolute_override=True):
    """Return all Overrides applicable to the attribute.

    Overrides are returned as a 3-tuple:
        (Match, Override, Index)

    Match:
        This is any of EXACT_MATCH, PARENT_MATCH, CLIENT_MATCH
        and defines whether the override is exactly on the
        plug, on the parent or on a child plug.

    Override:
        This is the RenderSetup Override instance.

    Index:
        This is the Plug index under the parent or for
        the child that matches. The EXACT_MATCH index will
        always be None. For PARENT_MATCH the index is which
        index the plug is under the parent plug. For CLIENT_MATCH
        the index is which child index matches the plug.

    Args:
        node_attr (str): attribute name as 'node.attribute'
        layer (str): layer name
        skip_disabled (bool): exclude disabled overrides
        skip_local_render (bool): exclude overrides marked
            as local render.
        stop_at_absolute_override: exclude overrides prior
            to the last absolute override as they have
            no influence on the resulting value.

    Returns:
        list: Ordered Overrides in order of strength

    """

    def get_mplug_children(plug):
        """Return children MPlugs of compound `MPlug`."""
        children = []
        if plug.isCompound:
            for i in range(plug.numChildren()):
                children.append(plug.child(i))
        return children

    def get_mplug_names(mplug):
        """Return long and short name of `MPlug`."""
        long_name = mplug.partialName(useLongNames=True)
        short_name = mplug.partialName(useLongNames=False)
        return {long_name, short_name}

    def iter_override_targets(override):
        try:
            for target in override._targets():
                yield target
        except AssertionError:
            # Workaround: There is a bug where the private `_targets()` method
            #             fails on some attribute plugs. For example overrides
            #             to the defaultRenderGlobals.endFrame
            #             (Tested in Maya 2020.2)
            log.debug("Workaround for %s" % override)
            from maya.app.renderSetup.common.utils import findPlug

            attr = override.attributeName()
            if isinstance(override, UniqueOverride):
                node = override.targetNodeName()
                yield findPlug(node, attr)
            else:
                nodes = override.parent().selector().nodes()
                for node in nodes:
                    if cmds.attributeQuery(attr, node=node, exists=True):
                        yield findPlug(node, attr)

    # Get the MPlug for the node.attr
    sel = om.MSelectionList()
    sel.add(node_attr)
    plug = sel.getPlug(0)

    layer = get_rendersetup_layer(layer)
    if layer == DEFAULT_RENDER_LAYER:
        # DEFAULT_RENDER_LAYER will never have overrides
        # since it's the default layer
        return []

    rs_layer = renderSetup.instance().getRenderLayer(layer)
    if rs_layer is None:
        # Renderlayer does not exist
        return

    # Get any parent or children plugs as we also
    # want to include them in the attribute match
    # for overrides
    parent = plug.parent() if plug.isChild else None
    parent_index = None
    if parent:
        parent_index = get_mplug_children(parent).index(plug)

    children = get_mplug_children(plug)

    # Create lookup for the attribute by both long
    # and short names
    attr_names = get_mplug_names(plug)
    for child in children:
        attr_names.update(get_mplug_names(child))
    if parent:
        attr_names.update(get_mplug_names(parent))

        # Get all overrides of the layer
    # And find those that are relevant to the attribute
    plug_overrides = []

    # Iterate over the overrides in reverse so we get the last
    # overrides first and can "break" whenever an absolute
    # override is reached
    layer_overrides = list(utils.getOverridesRecursive(rs_layer))
    for layer_override in reversed(layer_overrides):

        if skip_disabled and not layer_override.isEnabled():
            # Ignore disabled overrides
            continue

        if skip_local_render and layer_override.isLocalRender():
            continue

        # The targets list can be very large so we'll do
        # a quick filter by attribute name to detect whether
        # it matches the attribute name, or its parent or child
        if layer_override.attributeName() not in attr_names:
            continue

        override_match = None
        for override_plug in iter_override_targets(layer_override):

            override_match = None
            if plug == override_plug:
                override_match = (EXACT_MATCH, layer_override, None)

            elif parent and override_plug == parent:
                override_match = (PARENT_MATCH, layer_override, parent_index)

            elif children and override_plug in children:
                child_index = children.index(override_plug)
                override_match = (CLIENT_MATCH, layer_override, child_index)

            if override_match:
                plug_overrides.append(override_match)
                break

        if (
                override_match and
                stop_at_absolute_override and
                isinstance(layer_override, AbsOverride) and
                # When the override is only on a child plug then it doesn't
                # override the entire value so we not stop at this override
                not override_match[0] == CLIENT_MATCH
        ):
            # If override is absolute override, then BREAK out
            # of parent loop we don't need to look any further as
            # this is the absolute override
            break

    return reversed(plug_overrides)

get_rendersetup_layer(layer)

Return render setup layer name.

This also converts names from legacy renderLayer node name to render setup name.

defaultRenderLayer is not a renderSetupLayer node but it is however

the valid layer name for Render Setup - so we return that as is.

Example

for legacy_layer in cmds.ls(type="renderLayer"): layer = get_rendersetup_layer(legacy_layer)

Returns:

Type Description

str or None: Returns renderSetupLayer node name if layer is a valid layer name in legacy renderlayers or render setup layers. Returns None if the layer can't be found or Render Setup is currently disabled.

Source code in client/ayon_maya/api/lib_rendersetup.py
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
def get_rendersetup_layer(layer):
    """Return render setup layer name.

    This also converts names from legacy renderLayer node name to render setup
    name.

    Note: `defaultRenderLayer` is not a renderSetupLayer node but it is however
          the valid layer name for Render Setup - so we return that as is.

    Example:
        >>> for legacy_layer in cmds.ls(type="renderLayer"):
        >>>    layer = get_rendersetup_layer(legacy_layer)

    Returns:
        str or None: Returns renderSetupLayer node name if `layer` is a valid
            layer name in legacy renderlayers or render setup layers.
            Returns None if the layer can't be found or Render Setup is
            currently disabled.


    """
    if layer == DEFAULT_RENDER_LAYER:
        # defaultRenderLayer doesn't have a `renderSetupLayer`
        return layer

    if not cmds.mayaHasRenderSetup():
        return None

    if not cmds.objExists(layer):
        return None

    if cmds.nodeType(layer) == "renderSetupLayer":
        return layer

    # By default Render Setup renames the legacy renderlayer
    # to `rs_<layername>` but lets not rely on that as the
    # layer node can be renamed manually
    connections = cmds.listConnections(layer + ".message",
                                       type="renderSetupLayer",
                                       exactType=True,
                                       source=False,
                                       destination=True,
                                       plugs=True) or []
    return next((conn.split(".", 1)[0] for conn in connections
                 if conn.endswith(".legacyRenderLayer")), None)

get_shader_in_layer(node, layer)

Return the assigned shader in a renderlayer without switching layers.

This has been developed and tested for Legacy Renderlayers and not for Render Setup.

This will also return the shader for any face assignments, however

it will not return the components they are assigned to. This could be implemented, but since Maya's renderlayers are famous for breaking with face assignments there has been no need for this function to support that.

Returns:

Name Type Description
list

The list of assigned shaders in the given layer.

Source code in client/ayon_maya/api/lib_rendersetup.py
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
def get_shader_in_layer(node, layer):
    """Return the assigned shader in a renderlayer without switching layers.

    This has been developed and tested for Legacy Renderlayers and *not* for
    Render Setup.

    Note: This will also return the shader for any face assignments, however
        it will *not* return the components they are assigned to. This could
        be implemented, but since Maya's renderlayers are famous for breaking
        with face assignments there has been no need for this function to
        support that.

    Returns:
        list: The list of assigned shaders in the given layer.

    """

    def _get_connected_shader(plug):
        """Return current shader"""
        return cmds.listConnections(plug,
                                    source=False,
                                    destination=True,
                                    plugs=False,
                                    connections=False,
                                    type="shadingEngine") or []

    # We check the instObjGroups (shader connection) for layer overrides.
    plug = node + ".instObjGroups"

    # Ignore complex query if we're in the layer anyway (optimization)
    current_layer = cmds.editRenderLayerGlobals(query=True,
                                                currentRenderLayer=True)
    if layer == current_layer:
        return _get_connected_shader(plug)

    connections = cmds.listConnections(plug,
                                       plugs=True,
                                       source=False,
                                       destination=True,
                                       type="renderLayer") or []
    connections = filter(lambda x: x.endswith(".outPlug"), connections)
    if not connections:
        # If no overrides anywhere on the shader, just get the current shader
        return _get_connected_shader(plug)

    def _get_override(connections, layer):
        """Return the overridden connection for that layer in connections"""
        # If there's an override on that layer, return that.
        for connection in connections:
            if (connection.startswith(layer + ".outAdjustments") and
                    connection.endswith(".outPlug")):

                # This is a shader override on that layer so get the shader
                # connected to .outValue of the .outAdjustment[i]
                out_adjustment = connection.rsplit(".", 1)[0]
                connection_attr = out_adjustment + ".outValue"
                override = cmds.listConnections(connection_attr) or []

                return override

    override_shader = _get_override(connections, layer)
    if override_shader is not None:
        return override_shader
    else:
        # Get the override for "defaultRenderLayer" (=masterLayer)
        return _get_override(connections, layer="defaultRenderLayer")