diff --git a/pype/hosts/maya/attributes.py b/pype/hosts/maya/attributes.py new file mode 100644 index 00000000000..a98548301a7 --- /dev/null +++ b/pype/hosts/maya/attributes.py @@ -0,0 +1,334 @@ +# -*- coding: utf-8 -*- +"""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 Pype + +""" + +from maya import cmds +import maya.api.OpenMaya as om +import pymel.core as pm + +import maya.app.renderSetup.model.utils as utils +from maya.app.renderSetup.model import renderSetup +from maya.app.renderSetup.model.override import ( + AbsOverride, + RelOverride, + UniqueOverride +) + +EXACT_MATCH = 0 +PARENT_MATCH = 1 +CLIENT_MATCH = 2 + +DEFAULT_RENDER_LAYER = "defaultRenderLayer" + + +def get_rendersetup_layer(layer): + """Return render setup layer name. + + This also converts names from legacy renderLayer node name to render setup + name. + + Note: `DEFAULT_RENDER_LAYER` 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: + # DEFAULT_RENDER_LAYER 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_` 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) + + +def get_attr_in_layer(node_attr, layer): + """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, + 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 pm.getAttr(node_attr_, asString=True) + + 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 pm.getAttr(node_attr, asString=True) + + 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 = pm.getAttr(layer_override.name() + ".attrValue") + if match == EXACT_MATCH: + # value = value + pass + if match == PARENT_MATCH: + value = value[index] + if match == CLIENT_MATCH: + value[index] = value + + elif isinstance(layer_override, RelOverride): + # Relative override + # Value = Original * Multiply + Offset + multiply = pm.getAttr(layer_override.name() + ".multiply") + offset = pm.getAttr(layer_override.name() + ".offset") + + if match == EXACT_MATCH: + value = value * multiply + offset + if match == PARENT_MATCH: + value = value * multiply[index] + offset[index] + if match == CLIENT_MATCH: + value[index] = value[index] * multiply + offset + + else: + raise TypeError("Unsupported override: %s" % layer_override) + + return value + + +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) + print("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) diff --git a/pype/plugins/maya/publish/collect_renderable_camera.py b/pype/plugins/maya/publish/collect_renderable_camera.py index 6db819a656e..8ce56786615 100644 --- a/pype/plugins/maya/publish/collect_renderable_camera.py +++ b/pype/plugins/maya/publish/collect_renderable_camera.py @@ -2,7 +2,7 @@ from maya import cmds -from pype.hosts.maya import lib +from pype.hosts.maya.attributes import get_attr_in_layer class CollectRenderableCamera(pyblish.api.InstancePlugin): @@ -24,7 +24,7 @@ def process(self, instance): self.log.info("layer: {}".format(layer)) cameras = cmds.ls(type="camera", long=True) renderable = [c for c in cameras if - lib.get_attr_in_layer("%s.renderable" % c, layer=layer)] + get_attr_in_layer("%s.renderable" % c, layer)] self.log.info("Found cameras %s: %s" % (len(renderable), renderable))