diff --git a/openpype/hosts/maya/api/expected_files.py b/openpype/hosts/maya/api/expected_files.py deleted file mode 100644 index 15e0dc598c..0000000000 --- a/openpype/hosts/maya/api/expected_files.py +++ /dev/null @@ -1,945 +0,0 @@ -# -*- coding: utf-8 -*- -"""Module handling expected render output from Maya. - -This module is used in :mod:`collect_render` and :mod:`collect_vray_scene`. - -Note: - To implement new renderer, just create new class inheriting from - :class:`AExpectedFiles` and add it to :func:`ExpectedFiles.get()`. - -Attributes: - R_SINGLE_FRAME (:class:`re.Pattern`): Find single frame number. - R_FRAME_RANGE (:class:`re.Pattern`): Find frame range. - R_FRAME_NUMBER (:class:`re.Pattern`): Find frame number in string. - R_LAYER_TOKEN (:class:`re.Pattern`): Find layer token in image prefixes. - R_AOV_TOKEN (:class:`re.Pattern`): Find AOV token in image prefixes. - R_SUBSTITUTE_AOV_TOKEN (:class:`re.Pattern`): Find and substitute AOV token - in image prefixes. - R_REMOVE_AOV_TOKEN (:class:`re.Pattern`): Find and remove AOV token in - image prefixes. - R_CLEAN_FRAME_TOKEN (:class:`re.Pattern`): Find and remove unfilled - Renderman frame token in image prefix. - R_CLEAN_EXT_TOKEN (:class:`re.Pattern`): Find and remove unfilled Renderman - extension token in image prefix. - R_SUBSTITUTE_LAYER_TOKEN (:class:`re.Pattern`): Find and substitute render - layer token in image prefixes. - R_SUBSTITUTE_SCENE_TOKEN (:class:`re.Pattern`): Find and substitute scene - token in image prefixes. - R_SUBSTITUTE_CAMERA_TOKEN (:class:`re.Pattern`): Find and substitute camera - token in image prefixes. - RENDERER_NAMES (dict): Renderer names mapping between reported name and - *human readable* name. - IMAGE_PREFIXES (dict): Mapping between renderers and their respective - image prefix attribute names. - -Todo: - Determine `multipart` from render instance. - -""" - -import types -import re -import os -from abc import ABCMeta, abstractmethod - -import six -import attr - -import openpype.hosts.maya.api.lib as lib - -from maya import cmds -import maya.app.renderSetup.model.renderSetup as renderSetup - - -R_SINGLE_FRAME = re.compile(r"^(-?)\d+$") -R_FRAME_RANGE = re.compile(r"^(?P(-?)\d+)-(?P(-?)\d+)$") -R_FRAME_NUMBER = re.compile(r".+\.(?P[0-9]+)\..+") -R_LAYER_TOKEN = re.compile( - r".*((?:%l)|(?:)|(?:)).*", re.IGNORECASE -) -R_AOV_TOKEN = re.compile(r".*%a.*|.*.*|.*.*", re.IGNORECASE) -R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a||", re.IGNORECASE) -R_REMOVE_AOV_TOKEN = re.compile( - r"_%a|\.%a|_|\.|_|\.", re.IGNORECASE) -# to remove unused renderman tokens -R_CLEAN_FRAME_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) -R_CLEAN_EXT_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) - -R_SUBSTITUTE_LAYER_TOKEN = re.compile( - r"%l||", re.IGNORECASE -) -R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r"%c|", re.IGNORECASE) -R_SUBSTITUTE_SCENE_TOKEN = re.compile(r"%s|", re.IGNORECASE) - -RENDERER_NAMES = { - "mentalray": "MentalRay", - "vray": "V-Ray", - "arnold": "Arnold", - "renderman": "Renderman", - "redshift": "Redshift", -} - -# not sure about the renderman image prefix -IMAGE_PREFIXES = { - "mentalray": "defaultRenderGlobals.imageFilePrefix", - "vray": "vraySettings.fileNamePrefix", - "arnold": "defaultRenderGlobals.imageFilePrefix", - "renderman": "rmanGlobals.imageFileFormat", - "redshift": "defaultRenderGlobals.imageFilePrefix", -} - - -@attr.s -class LayerMetadata(object): - """Data class for Render Layer metadata.""" - frameStart = attr.ib() - frameEnd = attr.ib() - cameras = attr.ib() - sceneName = attr.ib() - layerName = attr.ib() - renderer = attr.ib() - defaultExt = attr.ib() - filePrefix = attr.ib() - enabledAOVs = attr.ib() - frameStep = attr.ib(default=1) - padding = attr.ib(default=4) - - -class ExpectedFiles: - """Class grouping functionality for all supported renderers. - - Attributes: - multipart (bool): Flag if multipart exrs are used. - - """ - multipart = False - - def __init__(self, render_instance): - """Constructor.""" - self._render_instance = render_instance - - def get(self, renderer, layer): - """Get expected files for given renderer and render layer. - - Args: - renderer (str): Name of renderer - layer (str): Name of render layer - - Returns: - dict: Expected rendered files by AOV - - Raises: - :exc:`UnsupportedRendererException`: If requested renderer - is not supported. It needs to be implemented by extending - :class:`AExpectedFiles` and added to this methods ``if`` - statement. - - """ - renderSetup.instance().switchToLayerUsingLegacyName(layer) - - if renderer.lower() == "arnold": - return self._get_files(ExpectedFilesArnold(layer, - self._render_instance)) - if renderer.lower() == "vray": - return self._get_files(ExpectedFilesVray( - layer, self._render_instance)) - if renderer.lower() == "redshift": - return self._get_files(ExpectedFilesRedshift( - layer, self._render_instance)) - if renderer.lower() == "mentalray": - return self._get_files(ExpectedFilesMentalray( - layer, self._render_instance)) - if renderer.lower() == "renderman": - return self._get_files(ExpectedFilesRenderman( - layer, self._render_instance)) - - raise UnsupportedRendererException( - "unsupported {}".format(renderer) - ) - - def _get_files(self, renderer): - # type: (AExpectedFiles) -> list - files = renderer.get_files() - self.multipart = renderer.multipart - return files - - -@six.add_metaclass(ABCMeta) -class AExpectedFiles: - """Abstract class with common code for all renderers. - - Attributes: - renderer (str): name of renderer. - layer (str): name of render layer. - multipart (bool): flag for multipart exrs. - - """ - - renderer = None - layer = None - multipart = False - - def __init__(self, layer, render_instance): - """Constructor.""" - self.layer = layer - self.render_instance = render_instance - - @abstractmethod - def get_aovs(self): - """To be implemented by renderer class.""" - - @staticmethod - def sanitize_camera_name(camera): - """Sanitize camera name. - - Remove Maya illegal characters from camera name. - - Args: - camera (str): Maya camera name. - - Returns: - (str): sanitized camera name - - Example: - >>> AExpectedFiles.sanizite_camera_name('test:camera_01') - test_camera_01 - - """ - return re.sub('[^0-9a-zA-Z_]+', '_', camera) - - def get_renderer_prefix(self): - """Return prefix for specific renderer. - - This is for most renderers the same and can be overridden if needed. - - Returns: - str: String with image prefix containing tokens - - Raises: - :exc:`UnsupportedRendererException`: If we requested image - prefix for renderer we know nothing about. - See :data:`IMAGE_PREFIXES` for mapping of renderers and - image prefixes. - - """ - try: - file_prefix = cmds.getAttr(IMAGE_PREFIXES[self.renderer]) - except KeyError: - raise UnsupportedRendererException( - "Unsupported renderer {}".format(self.renderer) - ) - return file_prefix - - def _get_layer_data(self): - # type: () -> LayerMetadata - # ______________________________________________ - # ____________________/ ____________________________________________/ - # 1 - get scene name /__________________/ - # ____________________/ - _, scene_basename = os.path.split(cmds.file(q=True, loc=True)) - scene_name, _ = os.path.splitext(scene_basename) - - file_prefix = self.get_renderer_prefix() - - if not file_prefix: - raise RuntimeError("Image prefix not set") - - layer_name = self.layer - if self.layer.startswith("rs_"): - layer_name = self.layer[3:] - - return LayerMetadata( - frameStart=int(self.get_render_attribute("startFrame")), - frameEnd=int(self.get_render_attribute("endFrame")), - frameStep=int(self.get_render_attribute("byFrameStep")), - padding=int(self.get_render_attribute("extensionPadding")), - # if we have token in prefix path we'll expect output for - # every renderable camera in layer. - cameras=self.get_renderable_cameras(), - sceneName=scene_name, - layerName=layer_name, - renderer=self.renderer, - defaultExt=cmds.getAttr("defaultRenderGlobals.imfPluginKey"), - filePrefix=file_prefix, - enabledAOVs=self.get_aovs() - ) - - def _generate_single_file_sequence( - self, layer_data, force_aov_name=None): - # type: (LayerMetadata, str) -> list - expected_files = [] - for cam in layer_data.cameras: - file_prefix = layer_data.filePrefix - mappings = ( - (R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName), - (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), - # this is required to remove unfilled aov token, for example - # in Redshift - (R_REMOVE_AOV_TOKEN, "") if not force_aov_name \ - else (R_SUBSTITUTE_AOV_TOKEN, force_aov_name), - - (R_CLEAN_FRAME_TOKEN, ""), - (R_CLEAN_EXT_TOKEN, ""), - ) - - for regex, value in mappings: - file_prefix = re.sub(regex, value, file_prefix) - - for frame in range( - int(layer_data.frameStart), - int(layer_data.frameEnd) + 1, - int(layer_data.frameStep), - ): - expected_files.append( - "{}.{}.{}".format( - file_prefix, - str(frame).rjust(layer_data.padding, "0"), - layer_data.defaultExt, - ) - ) - return expected_files - - def _generate_aov_file_sequences(self, layer_data): - # type: (LayerMetadata) -> list - expected_files = [] - aov_file_list = {} - for aov in layer_data.enabledAOVs: - for cam in layer_data.cameras: - file_prefix = layer_data.filePrefix - - mappings = ( - (R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName), - (R_SUBSTITUTE_CAMERA_TOKEN, - self.sanitize_camera_name(cam)), - (R_SUBSTITUTE_AOV_TOKEN, aov[0]), - (R_CLEAN_FRAME_TOKEN, ""), - (R_CLEAN_EXT_TOKEN, ""), - ) - - for regex, value in mappings: - file_prefix = re.sub(regex, value, file_prefix) - - aov_files = [] - for frame in range( - int(layer_data.frameStart), - int(layer_data.frameEnd) + 1, - int(layer_data.frameStep), - ): - aov_files.append( - "{}.{}.{}".format( - file_prefix, - str(frame).rjust(layer_data.padding, "0"), - aov[1], - ) - ) - - # if we have more then one renderable camera, append - # camera name to AOV to allow per camera AOVs. - aov_name = aov[0] - if len(layer_data.cameras) > 1: - aov_name = "{}_{}".format(aov[0], - self.sanitize_camera_name(cam)) - - aov_file_list[aov_name] = aov_files - file_prefix = layer_data.filePrefix - - expected_files.append(aov_file_list) - return expected_files - - def get_files(self): - """Return list of expected files. - - It will translate render token strings ('', etc.) to - their values. This task is tricky as every renderer deals with this - differently. It depends on `get_aovs()` abstract method implemented - for every supported renderer. - - """ - layer_data = self._get_layer_data() - - expected_files = [] - if layer_data.enabledAOVs: - return self._generate_aov_file_sequences(layer_data) - else: - return self._generate_single_file_sequence(layer_data) - - def get_renderable_cameras(self): - # type: () -> list - """Get all renderable cameras. - - Returns: - list: list of renderable cameras. - - """ - cam_parents = [ - cmds.listRelatives(x, ap=True)[-1] for x in cmds.ls(cameras=True) - ] - - return [ - cam - for cam in cam_parents - if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam))) - ] - - @staticmethod - def maya_is_true(attr_val): - """Whether a Maya attr evaluates to True. - - When querying an attribute value from an ambiguous object the - Maya API will return a list of values, which need to be properly - handled to evaluate properly. - - Args: - attr_val (mixed): Maya attribute to be evaluated as bool. - - Returns: - bool: cast Maya attribute to Pythons boolean value. - - """ - if isinstance(attr_val, types.BooleanType): - return attr_val - if isinstance(attr_val, (types.ListType, types.GeneratorType)): - return any(attr_val) - - return bool(attr_val) - - @staticmethod - def get_layer_overrides(attribute): - """Get overrides for attribute on current render layer. - - Args: - attribute (str): Maya attribute name. - - Returns: - Value of attribute override. - - """ - connections = cmds.listConnections(attribute, plugs=True) - if connections: - for connection in connections: - if connection: - # node_name = connection.split(".")[0] - - attr_name = "%s.value" % ".".join( - connection.split(".")[:-1] - ) - yield cmds.getAttr(attr_name) - - def get_render_attribute(self, attribute): - """Get attribute from render options. - - Args: - attribute (str): name of attribute to be looked up. - - Returns: - Attribute value - - """ - return lib.get_attr_in_layer( - "defaultRenderGlobals.{}".format(attribute), layer=self.layer - ) - - -class ExpectedFilesArnold(AExpectedFiles): - """Expected files for Arnold renderer. - - Attributes: - aiDriverExtension (dict): Arnold AOV driver extension mapping. - Is there a better way? - renderer (str): name of renderer. - - """ - - aiDriverExtension = { - "jpeg": "jpg", - "exr": "exr", - "deepexr": "exr", - "png": "png", - "tiff": "tif", - "mtoa_shaders": "ass", # TODO: research what those last two should be - "maya": "", - } - - def __init__(self, layer, render_instance): - """Constructor.""" - super(ExpectedFilesArnold, self).__init__(layer, render_instance) - self.renderer = "arnold" - - def get_aovs(self): - """Get all AOVs. - - See Also: - :func:`AExpectedFiles.get_aovs()` - - Raises: - :class:`AOVError`: If AOV cannot be determined. - - """ - enabled_aovs = [] - try: - if not ( - cmds.getAttr("defaultArnoldRenderOptions.aovMode") - and not cmds.getAttr("defaultArnoldDriver.mergeAOVs") # noqa: W503, E501 - ): - # AOVs are merged in mutli-channel file - self.multipart = True - return enabled_aovs - except ValueError: - # this occurs when Render Setting windows was not opened yet. In - # such case there are no Arnold options created so query for AOVs - # will fail. We terminate here as there are no AOVs specified then. - # This state will most probably fail later on some Validator - # anyway. - return enabled_aovs - - # AOVs are set to be rendered separately. We should expect - # token in path. - - # handle aovs from references - use_ref_aovs = self.render_instance.data.get( - "useReferencedAovs", False) or False - - ai_aovs = cmds.ls(type="aiAOV") - if not use_ref_aovs: - ref_aovs = cmds.ls(type="aiAOV", referencedNodes=True) - ai_aovs = list(set(ai_aovs) - set(ref_aovs)) - - for aov in ai_aovs: - enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) - ai_driver = cmds.listConnections("{}.outputs".format(aov))[0] - ai_translator = cmds.getAttr("{}.aiTranslator".format(ai_driver)) - try: - aov_ext = self.aiDriverExtension[ai_translator] - except KeyError: - msg = ( - "Unrecognized arnold " "driver format for AOV - {}" - ).format(cmds.getAttr("{}.name".format(aov))) - raise AOVError(msg) - - for override in self.get_layer_overrides( - "{}.enabled".format(aov) - ): - enabled = self.maya_is_true(override) - if enabled: - # If aov RGBA is selected, arnold will translate it to `beauty` - aov_name = cmds.getAttr("%s.name" % aov) - if aov_name == "RGBA": - aov_name = "beauty" - enabled_aovs.append((aov_name, aov_ext)) - # Append 'beauty' as this is arnolds - # default. If token is specified and no AOVs are - # defined, this will be used. - enabled_aovs.append( - (u"beauty", cmds.getAttr("defaultRenderGlobals.imfPluginKey")) - ) - return enabled_aovs - - -class ExpectedFilesVray(AExpectedFiles): - """Expected files for V-Ray renderer.""" - - def __init__(self, layer, render_instance): - """Constructor.""" - super(ExpectedFilesVray, self).__init__(layer, render_instance) - self.renderer = "vray" - - def get_renderer_prefix(self): - """Get image prefix for V-Ray. - - This overrides :func:`AExpectedFiles.get_renderer_prefix()` as - we must add `` token manually. - - See also: - :func:`AExpectedFiles.get_renderer_prefix()` - - """ - prefix = super(ExpectedFilesVray, self).get_renderer_prefix() - prefix = "{}_".format(prefix) - return prefix - - def _get_layer_data(self): - # type: () -> LayerMetadata - """Override to get vray specific extension.""" - layer_data = super(ExpectedFilesVray, self)._get_layer_data() - default_ext = cmds.getAttr("vraySettings.imageFormatStr") - if default_ext in ["exr (multichannel)", "exr (deep)"]: - default_ext = "exr" - layer_data.defaultExt = default_ext - layer_data.padding = cmds.getAttr("vraySettings.fileNamePadding") - return layer_data - - def get_files(self): - """Get expected files. - - This overrides :func:`AExpectedFiles.get_files()` as we - we need to add one sequence for plain beauty if AOVs are enabled - as vray output beauty without 'beauty' in filename. - - """ - expected_files = super(ExpectedFilesVray, self).get_files() - - layer_data = self._get_layer_data() - # remove 'beauty' from filenames as vray doesn't output it - update = {} - if layer_data.enabledAOVs: - for aov, seqs in expected_files[0].items(): - if aov.startswith("beauty"): - new_list = [] - for seq in seqs: - new_list.append(seq.replace("_beauty", "")) - update[aov] = new_list - - expected_files[0].update(update) - return expected_files - - def get_aovs(self): - """Get all AOVs. - - See Also: - :func:`AExpectedFiles.get_aovs()` - - """ - enabled_aovs = [] - - try: - # really? do we set it in vray just by selecting multichannel exr? - if ( - cmds.getAttr("vraySettings.imageFormatStr") - == "exr (multichannel)" # noqa: W503 - ): - # AOVs are merged in mutli-channel file - self.multipart = True - return enabled_aovs - except ValueError: - # this occurs when Render Setting windows was not opened yet. In - # such case there are no VRay options created so query for AOVs - # will fail. We terminate here as there are no AOVs specified then. - # This state will most probably fail later on some Validator - # anyway. - return enabled_aovs - - default_ext = cmds.getAttr("vraySettings.imageFormatStr") - if default_ext in ["exr (multichannel)", "exr (deep)"]: - default_ext = "exr" - - # add beauty as default - enabled_aovs.append( - (u"beauty", default_ext) - ) - - # handle aovs from references - use_ref_aovs = self.render_instance.data.get( - "useReferencedAovs", False) or False - - # this will have list of all aovs no matter if they are coming from - # reference or not. - vr_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"]) or [] - if not use_ref_aovs: - ref_aovs = cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"], - referencedNodes=True) or [] - # get difference - vr_aovs = list(set(vr_aovs) - set(ref_aovs)) - - for aov in vr_aovs: - enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) - for override in self.get_layer_overrides( - "{}.enabled".format(aov) - ): - enabled = self.maya_is_true(override) - - if enabled: - enabled_aovs.append( - (self._get_vray_aov_name(aov), default_ext)) - - return enabled_aovs - - @staticmethod - def _get_vray_aov_name(node): - """Get AOVs name from Vray. - - Args: - node (str): aov node name. - - Returns: - str: aov name. - - """ - vray_name = None - vray_explicit_name = None - vray_file_name = None - for node_attr in cmds.listAttr(node): - if node_attr.startswith("vray_filename"): - vray_file_name = cmds.getAttr("{}.{}".format(node, node_attr)) - elif node_attr.startswith("vray_name"): - vray_name = cmds.getAttr("{}.{}".format(node, node_attr)) - elif node_attr.startswith("vray_explicit_name"): - vray_explicit_name = cmds.getAttr( - "{}.{}".format(node, node_attr)) - - if vray_file_name is not None and vray_file_name != "": - final_name = vray_file_name - elif vray_explicit_name is not None and vray_explicit_name != "": - final_name = vray_explicit_name - elif vray_name is not None and vray_name != "": - final_name = vray_name - else: - continue - # special case for Material Select elements - these are named - # based on the materia they are connected to. - if "vray_mtl_mtlselect" in cmds.listAttr(node): - connections = cmds.listConnections( - "{}.vray_mtl_mtlselect".format(node)) - if connections: - final_name += '_{}'.format(str(connections[0])) - - return final_name - - -class ExpectedFilesRedshift(AExpectedFiles): - """Expected files for Redshift renderer. - - Attributes: - - unmerged_aovs (list): Name of aovs that are not merged into resulting - exr and we need them specified in expectedFiles output. - - """ - - unmerged_aovs = ["Cryptomatte"] - - def __init__(self, layer, render_instance): - """Construtor.""" - super(ExpectedFilesRedshift, self).__init__(layer, render_instance) - self.renderer = "redshift" - - def get_renderer_prefix(self): - """Get image prefix for Redshift. - - This overrides :func:`AExpectedFiles.get_renderer_prefix()` as - we must add `` token manually. - - See also: - :func:`AExpectedFiles.get_renderer_prefix()` - - """ - prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix() - prefix = "{}.".format(prefix) - return prefix - - def get_files(self): - """Get expected files. - - This overrides :func:`AExpectedFiles.get_files()` as we - we need to add one sequence for plain beauty if AOVs are enabled - as vray output beauty without 'beauty' in filename. - - """ - expected_files = super(ExpectedFilesRedshift, self).get_files() - layer_data = self._get_layer_data() - - # Redshift doesn't merge Cryptomatte AOV to final exr. We need to check - # for such condition and add it to list of expected files. - - for aov in layer_data.enabledAOVs: - if aov[0].lower() == "cryptomatte": - aov_name = aov[0] - expected_files.append( - {aov_name: self._generate_single_file_sequence(layer_data)} - ) - - if layer_data.get("enabledAOVs"): - # because if Beauty is added manually, it will be rendered as - # 'Beauty_other' in file name and "standard" beauty will have - # 'Beauty' in its name. When disabled, standard output will be - # without `Beauty`. - if expected_files[0].get(u"Beauty"): - expected_files[0][u"Beauty_other"] = expected_files[0].pop( - u"Beauty") - new_list = [ - seq.replace(".Beauty", ".Beauty_other") - for seq in expected_files[0][u"Beauty_other"] - ] - - expected_files[0][u"Beauty_other"] = new_list - expected_files[0][u"Beauty"] = self._generate_single_file_sequence( # noqa: E501 - layer_data, force_aov_name="Beauty" - ) - else: - expected_files[0][u"Beauty"] = self._generate_single_file_sequence( # noqa: E501 - layer_data - ) - - return expected_files - - def get_aovs(self): - """Get all AOVs. - - See Also: - :func:`AExpectedFiles.get_aovs()` - - """ - enabled_aovs = [] - - try: - if self.maya_is_true( - cmds.getAttr("redshiftOptions.exrForceMultilayer") - ): - # AOVs are merged in mutli-channel file - self.multipart = True - return enabled_aovs - except ValueError: - # this occurs when Render Setting windows was not opened yet. In - # such case there are no Redshift options created so query for AOVs - # will fail. We terminate here as there are no AOVs specified then. - # This state will most probably fail later on some Validator - # anyway. - return enabled_aovs - - default_ext = cmds.getAttr( - "redshiftOptions.imageFormat", asString=True) - rs_aovs = cmds.ls(type="RedshiftAOV", referencedNodes=False) - - for aov in rs_aovs: - enabled = self.maya_is_true(cmds.getAttr("{}.enabled".format(aov))) - for override in self.get_layer_overrides( - "{}.enabled".format(aov) - ): - enabled = self.maya_is_true(override) - - if enabled: - # If AOVs are merged into multipart exr, append AOV only if it - # is in the list of AOVs that renderer cannot (or will not) - # merge into final exr. - if self.maya_is_true( - cmds.getAttr("redshiftOptions.exrForceMultilayer") - ): - if cmds.getAttr("%s.name" % aov) in self.unmerged_aovs: - enabled_aovs.append( - (cmds.getAttr("%s.name" % aov), default_ext) - ) - else: - enabled_aovs.append( - (cmds.getAttr("%s.name" % aov), default_ext) - ) - - if self.maya_is_true( - cmds.getAttr("redshiftOptions.exrForceMultilayer") - ): - # AOVs are merged in mutli-channel file - self.multipart = True - - return enabled_aovs - - -class ExpectedFilesRenderman(AExpectedFiles): - """Expected files for Renderman renderer. - - Warning: - This is very rudimentary and needs more love and testing. - """ - - def __init__(self, layer, render_instance): - """Constructor.""" - super(ExpectedFilesRenderman, self).__init__(layer, render_instance) - self.renderer = "renderman" - - def get_aovs(self): - """Get all AOVs. - - See Also: - :func:`AExpectedFiles.get_aovs()` - - """ - enabled_aovs = [] - - default_ext = "exr" - displays = cmds.listConnections("rmanGlobals.displays") - for aov in displays: - aov_name = str(aov) - if aov_name == "rmanDefaultDisplay": - aov_name = "beauty" - - enabled = self.maya_is_true(cmds.getAttr("{}.enable".format(aov))) - for override in self.get_layer_overrides( - "{}.enable".format(aov) - ): - enabled = self.maya_is_true(override) - - if enabled: - enabled_aovs.append((aov_name, default_ext)) - - return enabled_aovs - - def get_files(self): - """Get expected files. - - This overrides :func:`AExpectedFiles.get_files()` as we - we need to add one sequence for plain beauty if AOVs are enabled - as vray output beauty without 'beauty' in filename. - - In renderman we hack it with prepending path. This path would - normally be translated from `rmanGlobals.imageOutputDir`. We skip - this and hardcode prepend path we expect. There is no place for user - to mess around with this settings anyway and it is enforced in - render settings validator. - """ - layer_data = self._get_layer_data() - new_aovs = {} - - expected_files = super(ExpectedFilesRenderman, self).get_files() - # we always get beauty - for aov, files in expected_files[0].items(): - new_files = [] - for file in files: - new_file = "{}/{}/{}".format( - layer_data["sceneName"], layer_data["layerName"], file - ) - new_files.append(new_file) - new_aovs[aov] = new_files - - return [new_aovs] - - -class ExpectedFilesMentalray(AExpectedFiles): - """Skeleton unimplemented class for Mentalray renderer.""" - - def __init__(self, layer, render_instance): - """Constructor. - - Raises: - :exc:`UnimplementedRendererException`: as it is not implemented. - - """ - super(ExpectedFilesMentalray, self).__init__(layer, render_instance) - raise UnimplementedRendererException("Mentalray not implemented") - - def get_aovs(self): - """Get all AOVs. - - See Also: - :func:`AExpectedFiles.get_aovs()` - - """ - return [] - - -class AOVError(Exception): - """Custom exception for determining AOVs.""" - - -class UnsupportedRendererException(Exception): - """Custom exception. - - Raised when requesting data from unsupported renderer. - """ - - -class UnimplementedRendererException(Exception): - """Custom exception. - - Raised when requesting data from renderer that is not implemented yet. - """ diff --git a/openpype/hosts/maya/api/lib.py b/openpype/hosts/maya/api/lib.py index b87e106865..b24235447f 100644 --- a/openpype/hosts/maya/api/lib.py +++ b/openpype/hosts/maya/api/lib.py @@ -2252,10 +2252,8 @@ def get_attr_in_layer(attr, layer): try: if cmds.mayaHasRenderSetup(): - log.debug("lib.get_attr_in_layer is not " - "optimized for render setup") - with renderlayer(layer): - return cmds.getAttr(attr) + from . import lib_rendersetup + return lib_rendersetup.get_attr_in_layer(attr, layer) except AttributeError: pass diff --git a/openpype/hosts/maya/api/lib_renderproducts.py b/openpype/hosts/maya/api/lib_renderproducts.py new file mode 100644 index 0000000000..fb99584c5d --- /dev/null +++ b/openpype/hosts/maya/api/lib_renderproducts.py @@ -0,0 +1,1039 @@ +# -*- coding: utf-8 -*- +"""Module handling expected render output from Maya. + +This module is used in :mod:`collect_render` and :mod:`collect_vray_scene`. + +Note: + To implement new renderer, just create new class inheriting from + :class:`ARenderProducts` and add it to :func:`RenderProducts.get()`. + +Attributes: + R_SINGLE_FRAME (:class:`re.Pattern`): Find single frame number. + R_FRAME_RANGE (:class:`re.Pattern`): Find frame range. + R_FRAME_NUMBER (:class:`re.Pattern`): Find frame number in string. + R_LAYER_TOKEN (:class:`re.Pattern`): Find layer token in image prefixes. + R_AOV_TOKEN (:class:`re.Pattern`): Find AOV token in image prefixes. + R_SUBSTITUTE_AOV_TOKEN (:class:`re.Pattern`): Find and substitute AOV token + in image prefixes. + R_REMOVE_AOV_TOKEN (:class:`re.Pattern`): Find and remove AOV token in + image prefixes. + R_CLEAN_FRAME_TOKEN (:class:`re.Pattern`): Find and remove unfilled + Renderman frame token in image prefix. + R_CLEAN_EXT_TOKEN (:class:`re.Pattern`): Find and remove unfilled Renderman + extension token in image prefix. + R_SUBSTITUTE_LAYER_TOKEN (:class:`re.Pattern`): Find and substitute render + layer token in image prefixes. + R_SUBSTITUTE_SCENE_TOKEN (:class:`re.Pattern`): Find and substitute scene + token in image prefixes. + R_SUBSTITUTE_CAMERA_TOKEN (:class:`re.Pattern`): Find and substitute camera + token in image prefixes. + IMAGE_PREFIXES (dict): Mapping between renderers and their respective + image prefix attribute names. + +Thanks: + Roy Nieterau (BigRoy) / Colorbleed for overhaul of original + *expected_files*. + +""" + +import logging +import re +import os +from abc import ABCMeta, abstractmethod + +import six +import attr + +from . import lib +from . import lib_rendersetup + +from maya import cmds, mel + +log = logging.getLogger(__name__) + +R_SINGLE_FRAME = re.compile(r"^(-?)\d+$") +R_FRAME_RANGE = re.compile(r"^(?P(-?)\d+)-(?P(-?)\d+)$") +R_FRAME_NUMBER = re.compile(r".+\.(?P[0-9]+)\..+") +R_LAYER_TOKEN = re.compile( + r".*((?:%l)|(?:)|(?:)).*", re.IGNORECASE +) +R_AOV_TOKEN = re.compile(r".*%a.*|.*.*|.*.*", re.IGNORECASE) +R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a||", re.IGNORECASE) +R_REMOVE_AOV_TOKEN = re.compile( + r"_%a|\.%a|_|\.|_|\.", re.IGNORECASE) +# to remove unused renderman tokens +R_CLEAN_FRAME_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) +R_CLEAN_EXT_TOKEN = re.compile(r"\.?\.?", re.IGNORECASE) + +R_SUBSTITUTE_LAYER_TOKEN = re.compile( + r"%l||", re.IGNORECASE +) +R_SUBSTITUTE_CAMERA_TOKEN = re.compile(r"%c|", re.IGNORECASE) +R_SUBSTITUTE_SCENE_TOKEN = re.compile(r"%s|", re.IGNORECASE) + +# not sure about the renderman image prefix +IMAGE_PREFIXES = { + "vray": "vraySettings.fileNamePrefix", + "arnold": "defaultRenderGlobals.imageFilePrefix", + "renderman": "rmanGlobals.imageFileFormat", + "redshift": "defaultRenderGlobals.imageFilePrefix", +} + + +@attr.s +class LayerMetadata(object): + """Data class for Render Layer metadata.""" + frameStart = attr.ib() + frameEnd = attr.ib() + cameras = attr.ib() + sceneName = attr.ib() + layerName = attr.ib() + renderer = attr.ib() + defaultExt = attr.ib() + filePrefix = attr.ib() + frameStep = attr.ib(default=1) + padding = attr.ib(default=4) + + # Render Products + products = attr.ib(init=False, default=attr.Factory(list)) + + +@attr.s +class RenderProduct(object): + """Describes an image or other file-like artifact produced by a render. + + Warning: + This currently does NOT return as a product PER render camera. + A single Render Product will generate files per camera. E.g. with two + cameras each render product generates two sequences on disk assuming + the file path prefix correctly uses the tokens. + + """ + productName = attr.ib() + ext = attr.ib() # extension + aov = attr.ib(default=None) # source aov + driver = attr.ib(default=None) # source driver + multipart = attr.ib(default=False) # multichannel file + + +def get(layer, render_instance=None): + # type: (str, object) -> ARenderProducts + """Get render details and products for given renderer and render layer. + + Args: + layer (str): Name of render layer + render_instance (pyblish.api.Instance): Publish instance. + If not provided an empty mock instance is used. + + Returns: + ARenderProducts: The correct RenderProducts instance for that + renderlayer. + + Raises: + :exc:`UnsupportedRendererException`: If requested renderer + is not supported. It needs to be implemented by extending + :class:`ARenderProducts` and added to this methods ``if`` + statement. + + """ + + if render_instance is None: + # For now produce a mock instance + class Instance(object): + data = {} + render_instance = Instance() + + renderer_name = lib.get_attr_in_layer( + "defaultRenderGlobals.currentRenderer", + layer=layer + ) + + renderer = { + "arnold": RenderProductsArnold, + "vray": RenderProductsVray, + "redshift": RenderProductsRedshift, + "renderman": RenderProductsRenderman + }.get(renderer_name.lower(), None) + if renderer is None: + raise UnsupportedRendererException( + "unsupported {}".format(renderer_name) + ) + + return renderer(layer, render_instance) + + +@six.add_metaclass(ABCMeta) +class ARenderProducts: + """Abstract class with common code for all renderers. + + Attributes: + renderer (str): name of renderer. + + """ + + renderer = None + + def __init__(self, layer, render_instance): + """Constructor.""" + self.layer = layer + self.render_instance = render_instance + self.multipart = False + + # Initialize + self.layer_data = self._get_layer_data() + self.layer_data.products = self.get_render_products() + + @abstractmethod + def get_render_products(self): + """To be implemented by renderer class. + + This should return a list of RenderProducts. + + Returns: + list: List of RenderProduct + + """ + + @staticmethod + def sanitize_camera_name(camera): + # type: (str) -> str + """Sanitize camera name. + + Remove Maya illegal characters from camera name. + + Args: + camera (str): Maya camera name. + + Returns: + (str): sanitized camera name + + Example: + >>> ARenderProducts.sanizite_camera_name('test:camera_01') + test_camera_01 + + """ + return re.sub('[^0-9a-zA-Z_]+', '_', camera) + + def get_renderer_prefix(self): + # type: () -> str + """Return prefix for specific renderer. + + This is for most renderers the same and can be overridden if needed. + + Returns: + str: String with image prefix containing tokens + + Raises: + :exc:`UnsupportedRendererException`: If we requested image + prefix for renderer we know nothing about. + See :data:`IMAGE_PREFIXES` for mapping of renderers and + image prefixes. + + """ + try: + file_prefix_attr = IMAGE_PREFIXES[self.renderer] + except KeyError: + raise UnsupportedRendererException( + "Unsupported renderer {}".format(self.renderer) + ) + + file_prefix = self._get_attr(file_prefix_attr) + + if not file_prefix: + # Fall back to scene name by default + log.debug("Image prefix not set, using ") + file_prefix = "" + + return file_prefix + + def get_render_attribute(self, attribute): + """Get attribute from render options. + + Args: + attribute (str): name of attribute to be looked up. + + Returns: + Attribute value + + """ + return self._get_attr("defaultRenderGlobals", attribute) + + def _get_attr(self, node_attr, attribute=None): + """Return the value of the attribute in the renderlayer + + For readability this allows passing in the attribute in two ways. + + As a single argument: + _get_attr("node.attr") + Or as two arguments: + _get_attr("node", "attr") + + Returns: + Value of the attribute inside the layer this instance is set to. + + """ + + if attribute is None: + plug = node_attr + else: + plug = "{}.{}".format(node_attr, attribute) + + return lib.get_attr_in_layer(plug, layer=self.layer) + + def _get_layer_data(self): + # type: () -> LayerMetadata + # ______________________________________________ + # ____________________/ ____________________________________________/ + # 1 - get scene name /__________________/ + # ____________________/ + _, scene_basename = os.path.split(cmds.file(q=True, loc=True)) + scene_name, _ = os.path.splitext(scene_basename) + + file_prefix = self.get_renderer_prefix() + + # If the Render Layer belongs to a Render Setup layer then the + # output name is based on the Render Setup Layer name without + # the `rs_` prefix. + layer_name = self.layer + rs_layer = lib_rendersetup.get_rendersetup_layer(layer_name) + if rs_layer: + layer_name = rs_layer + + if self.layer == "defaultRenderLayer": + # defaultRenderLayer renders as masterLayer + layer_name = "masterLayer" + + # todo: Support Custom Frames sequences 0,5-10,100-120 + # Deadline allows submitting renders with a custom frame list + # to support those cases we might want to allow 'custom frames' + # to be overridden to `ExpectFiles` class? + layer_data = LayerMetadata( + frameStart=int(self.get_render_attribute("startFrame")), + frameEnd=int(self.get_render_attribute("endFrame")), + frameStep=int(self.get_render_attribute("byFrameStep")), + padding=int(self.get_render_attribute("extensionPadding")), + # if we have token in prefix path we'll expect output for + # every renderable camera in layer. + cameras=self.get_renderable_cameras(), + sceneName=scene_name, + layerName=layer_name, + renderer=self.renderer, + defaultExt=self._get_attr("defaultRenderGlobals.imfPluginKey"), + filePrefix=file_prefix + ) + return layer_data + + def _generate_file_sequence( + self, layer_data, + force_aov_name=None, + force_ext=None, + force_cameras=None): + # type: (LayerMetadata, str, str, list) -> list + expected_files = [] + cameras = force_cameras if force_cameras else layer_data.cameras + ext = force_ext or layer_data.defaultExt + for cam in cameras: + file_prefix = layer_data.filePrefix + mappings = ( + (R_SUBSTITUTE_SCENE_TOKEN, layer_data.sceneName), + (R_SUBSTITUTE_LAYER_TOKEN, layer_data.layerName), + (R_SUBSTITUTE_CAMERA_TOKEN, self.sanitize_camera_name(cam)), + # this is required to remove unfilled aov token, for example + # in Redshift + (R_REMOVE_AOV_TOKEN, "") if not force_aov_name \ + else (R_SUBSTITUTE_AOV_TOKEN, force_aov_name), + + (R_CLEAN_FRAME_TOKEN, ""), + (R_CLEAN_EXT_TOKEN, ""), + ) + + for regex, value in mappings: + file_prefix = re.sub(regex, value, file_prefix) + + for frame in range( + int(layer_data.frameStart), + int(layer_data.frameEnd) + 1, + int(layer_data.frameStep), + ): + frame_str = str(frame).rjust(layer_data.padding, "0") + expected_files.append( + "{}.{}.{}".format(file_prefix, frame_str, ext) + ) + return expected_files + + def get_files(self, product, camera): + # type: (RenderProduct, str) -> list + """Return list of expected files. + + It will translate render token strings ('', etc.) to + their values. This task is tricky as every renderer deals with this + differently. That's why we expose `get_files` as a method on the + Renderer class so it can be overridden for complex cases. + + Args: + product (RenderProduct): Render product to be used for file + generation. + camera (str): Camera name. + + Returns: + List of files + + """ + return self._generate_file_sequence( + self.layer_data, + force_aov_name=product.productName, + force_ext=product.ext, + force_cameras=[camera] + ) + + def get_renderable_cameras(self): + # type: () -> list + """Get all renderable camera transforms. + + Returns: + list: list of renderable cameras. + + """ + + renderable_cameras = [ + cam for cam in cmds.ls(cameras=True) + if self._get_attr(cam, "renderable") + ] + + # The output produces a sanitized name for using its + # shortest unique path of the transform so we'll return + # at least that unique path. This could include a parent + # name too when two cameras have the same name but are + # in a different hierarchy, e.g. "group1|cam" and "group2|cam" + def get_name(camera): + return cmds.ls(cmds.listRelatives(camera, + parent=True, + fullPath=True))[0] + + return [get_name(cam) for cam in renderable_cameras] + + +class RenderProductsArnold(ARenderProducts): + """Render products for Arnold renderer. + + References: + mtoa.utils.getFileName() + mtoa.utils.ui.common.updateArnoldTargetFilePreview() + + Notes: + - Output Denoising AOVs are not currently included. + - Only Frame/Animation ext: name.#.ext is supported. + - Use Custom extension is not supported. + - and tokens not tested + - With Merge AOVs but in File Name Prefix Arnold + will still NOT merge the aovs. This class correctly resolves + it - but user should be aware. + - File Path Prefix overrides per AOV driver are not implemented + + Attributes: + aiDriverExtension (dict): Arnold AOV driver extension mapping. + Is there a better way? + renderer (str): name of renderer. + + """ + renderer = "arnold" + aiDriverExtension = { + "jpeg": "jpg", + "exr": "exr", + "deepexr": "exr", + "png": "png", + "tiff": "tif", + "mtoa_shaders": "ass", # TODO: research what those last two should be + "maya": "", + } + + def get_renderer_prefix(self): + + prefix = super(RenderProductsArnold, self).get_renderer_prefix() + merge_aovs = self._get_attr("defaultArnoldDriver.mergeAOVs") + if not merge_aovs and "" not in prefix.lower(): + # When Merge AOVs is disabled and token not present + # then Arnold prepends / to the output path. + # todo: It's untested what happens if AOV driver has an + # an explicit override path prefix. + prefix = "/" + prefix + + return prefix + + def _get_aov_render_products(self, aov): + """Return all render products for the AOV""" + + products = list() + aov_name = self._get_attr(aov, "name") + ai_drivers = cmds.listConnections("{}.outputs".format(aov), + source=True, + destination=False, + type="aiAOVDriver") or [] + + for ai_driver in ai_drivers: + # todo: check aiAOVDriver.prefix as it could have + # a custom path prefix set for this driver + + # Skip Drivers set only for GUI + # 0: GUI, 1: Batch, 2: GUI and Batch + output_mode = self._get_attr(ai_driver, "outputMode") + if output_mode == 0: # GUI only + log.warning("%s has Output Mode set to GUI, " + "skipping...", ai_driver) + continue + + ai_translator = self._get_attr(ai_driver, "aiTranslator") + try: + ext = self.aiDriverExtension[ai_translator] + except KeyError: + raise AOVError( + "Unrecognized arnold driver format " + "for AOV - {}".format(aov_name) + ) + + # If aov RGBA is selected, arnold will translate it to `beauty` + name = aov_name + if name == "RGBA": + name = "beauty" + + # Support Arnold light groups for AOVs + # Global AOV: When disabled the main layer is not written: `{pass}` + # All Light Groups: When enabled, a `{pass}_lgroups` file is + # written and is always merged into a single file + # Light Groups List: When set, a product per light group is written + # e.g. {pass}_front, {pass}_rim + global_aov = self._get_attr(aov, "globalAov") + if global_aov: + product = RenderProduct(productName=name, + ext=ext, + aov=aov_name, + driver=ai_driver) + products.append(product) + + all_light_groups = self._get_attr(aov, "lightGroups") + if all_light_groups: + # All light groups is enabled. A single multipart + # Render Product + product = RenderProduct(productName=name + "_lgroups", + ext=ext, + aov=aov_name, + driver=ai_driver, + # Always multichannel output + multipart=True) + products.append(product) + else: + value = self._get_attr(aov, "lightGroupsList") + if not value: + continue + selected_light_groups = value.strip().split() + for light_group in selected_light_groups: + # Render Product per selected light group + aov_light_group_name = "{}_{}".format(name, light_group) + product = RenderProduct(productName=aov_light_group_name, + aov=aov_name, + driver=ai_driver, + ext=ext) + products.append(product) + + return products + + def get_render_products(self): + """Get all AOVs. + + See Also: + :func:`ARenderProducts.get_render_products()` + + Raises: + :class:`AOVError`: If AOV cannot be determined. + + """ + + if not cmds.ls("defaultArnoldRenderOptions", type="aiOptions"): + # this occurs when Render Setting windows was not opened yet. In + # such case there are no Arnold options created so query for AOVs + # will fail. We terminate here as there are no AOVs specified then. + # This state will most probably fail later on some Validator + # anyway. + return [] + + default_ext = self._get_attr("defaultRenderGlobals.imfPluginKey") + beauty_product = RenderProduct(productName="beauty", + ext=default_ext, + driver="defaultArnoldDriver") + + # AOVs > Legacy > Maya Render View > Mode + aovs_enabled = bool( + self._get_attr("defaultArnoldRenderOptions.aovMode") + ) + if not aovs_enabled: + return [beauty_product] + + # Common > File Output > Merge AOVs or + # We don't need to check for Merge AOVs due to overridden + # `get_renderer_prefix()` behavior which forces + has_renderpass_token = ( + "" in self.layer_data.filePrefix.lower() + ) + if not has_renderpass_token: + beauty_product.multipart = True + return [beauty_product] + + # AOVs are set to be rendered separately. We should expect + # token in path. + # handle aovs from references + use_ref_aovs = self.render_instance.data.get( + "useReferencedAovs", False) or False + + aovs = cmds.ls(type="aiAOV") + if not use_ref_aovs: + ref_aovs = cmds.ls(type="aiAOV", referencedNodes=True) + aovs = list(set(aovs) - set(ref_aovs)) + + products = [] + + # Append the AOV products + for aov in aovs: + enabled = self._get_attr(aov, "enabled") + if not enabled: + continue + + # For now stick to the legacy output format. + aov_products = self._get_aov_render_products(aov) + products.extend(aov_products) + + if not any(product.aov == "RGBA" for product in products): + # Append default 'beauty' as this is arnolds default. + # However, it is excluded whenever a RGBA pass is enabled. + # For legibility add the beauty layer as first entry + products.insert(0, beauty_product) + + # TODO: Output Denoising AOVs? + + return products + + +class RenderProductsVray(ARenderProducts): + """Expected files for V-Ray renderer. + + Notes: + - "Disabled" animation incorrectly returns frames in filename + - "Renumber Frames" is not supported + + Reference: + vrayAddRenderElementImpl() in vrayCreateRenderElementsTab.mel + + """ + # todo: detect whether rendering with V-Ray GPU + whether AOV is supported + + renderer = "vray" + + def get_renderer_prefix(self): + # type: () -> str + """Get image prefix for V-Ray. + + This overrides :func:`ARenderProducts.get_renderer_prefix()` as + we must add `` token manually. + + See also: + :func:`ARenderProducts.get_renderer_prefix()` + + """ + prefix = super(RenderProductsVray, self).get_renderer_prefix() + prefix = "{}.".format(prefix) + return prefix + + def _get_layer_data(self): + # type: () -> LayerMetadata + """Override to get vray specific extension.""" + layer_data = super(RenderProductsVray, self)._get_layer_data() + + default_ext = self._get_attr("vraySettings.imageFormatStr") + if default_ext in ["exr (multichannel)", "exr (deep)"]: + default_ext = "exr" + layer_data.defaultExt = default_ext + layer_data.padding = self._get_attr("vraySettings.fileNamePadding") + + return layer_data + + def get_render_products(self): + """Get all AOVs. + + See Also: + :func:`ARenderProducts.get_render_products()` + + """ + if not cmds.ls("vraySettings", type="VRaySettingsNode"): + # this occurs when Render Setting windows was not opened yet. In + # such case there are no VRay options created so query for AOVs + # will fail. We terminate here as there are no AOVs specified then. + # This state will most probably fail later on some Validator + # anyway. + return [] + + image_format_str = self._get_attr("vraySettings.imageFormatStr") + default_ext = image_format_str + if default_ext in {"exr (multichannel)", "exr (deep)"}: + default_ext = "exr" + + products = [] + + # add beauty as default when not disabled + dont_save_rgb = self._get_attr("vraySettings.dontSaveRgbChannel") + if not dont_save_rgb: + products.append(RenderProduct(productName="", ext=default_ext)) + + # separate alpha file + separate_alpha = self._get_attr("vraySettings.separateAlpha") + if separate_alpha: + products.append(RenderProduct(productName="Alpha", + ext=default_ext)) + + if image_format_str == "exr (multichannel)": + # AOVs are merged in m-channel file, only main layer is rendered + self.multipart = True + return products + + # handle aovs from references + use_ref_aovs = self.render_instance.data.get( + "useReferencedAovs", False) or False + + # this will have list of all aovs no matter if they are coming from + # reference or not. + aov_types = ["VRayRenderElement", "VRayRenderElementSet"] + aovs = cmds.ls(type=aov_types) + if not use_ref_aovs: + ref_aovs = cmds.ls(type=aov_types, referencedNodes=True) or [] + aovs = list(set(aovs) - set(ref_aovs)) + + for aov in aovs: + enabled = self._get_attr(aov, "enabled") + if not enabled: + continue + + class_type = self._get_attr(aov + ".vrayClassType") + if class_type == "LightMixElement": + # Special case which doesn't define a name by itself but + # instead seems to output multiple Render Products, + # specifically "Self_Illumination" and "Environment" + product_names = ["Self_Illumination", "Environment"] + for name in product_names: + product = RenderProduct(productName=name, + ext=default_ext, + aov=aov) + products.append(product) + # Continue as we've processed this special case AOV + continue + + aov_name = self._get_vray_aov_name(aov) + product = RenderProduct(productName=aov_name, + ext=default_ext, + aov=aov) + products.append(product) + + return products + + def _get_vray_aov_attr(self, node, prefix): + """Get value for attribute that starts with key in name + + V-Ray AOVs have attribute names that include the type + of AOV in the attribute name, for example: + - vray_filename_rawdiffuse + - vray_filename_velocity + - vray_name_gi + - vray_explicit_name_extratex + + To simplify querying the "vray_filename" or "vray_name" + attributes we just find the first attribute that has + that particular "{prefix}_" in the attribute name. + + Args: + node (str): AOV node name + prefix (str): Prefix of the attribute name. + + Returns: + Value of the attribute if it exists, else None + + """ + attrs = cmds.listAttr(node, string="{}_*".format(prefix)) + if not attrs: + return None + + assert len(attrs) == 1, "Found more than one attribute: %s" % attrs + attr = attrs[0] + + return self._get_attr(node, attr) + + def _get_vray_aov_name(self, node): + """Get AOVs name from Vray. + + Args: + node (str): aov node name. + + Returns: + str: aov name. + + """ + + vray_explicit_name = self._get_vray_aov_attr(node, + "vray_explicit_name") + vray_filename = self._get_vray_aov_attr(node, "vray_filename") + vray_name = self._get_vray_aov_attr(node, "vray_name") + final_name = vray_explicit_name or vray_filename or vray_name or None + + class_type = self._get_attr(node, "vrayClassType") + if not vray_explicit_name: + # Explicit name takes precedence and overrides completely + # otherwise add the connected node names to the special cases + # Any namespace colon ':' gets replaced to underscore '_' + # so we sanitize using `sanitize_camera_name` + def _get_source_name(node, attr): + """Return sanitized name of input connection to attribute""" + plug = "{}.{}".format(node, attr) + connections = cmds.listConnections(plug, + source=True, + destination=False) + if connections: + return self.sanitize_camera_name(connections[0]) + + if class_type == "MaterialSelectElement": + # Name suffix is based on the connected material or set + attrs = [ + "vray_mtllist_mtlselect", + "vray_mtl_mtlselect" + ] + for attribute in attrs: + name = _get_source_name(node, attribute) + if name: + final_name += '_{}'.format(name) + break + else: + log.warning("Material Select Element has no " + "selected materials: %s", node) + + elif class_type == "ExtraTexElement": + # Name suffix is based on the connected textures + extratex_type = self._get_attr(node, "vray_type_extratex") + attr = { + 0: "vray_texture_extratex", + 1: "vray_float_texture_extratex", + 2: "vray_int_texture_extratex", + }.get(extratex_type) + name = _get_source_name(node, attr) + if name: + final_name += '_{}'.format(name) + else: + log.warning("Extratex Element has no incoming texture") + + assert final_name, "Output filename not defined for AOV: %s" % node + + return final_name + + +class RenderProductsRedshift(ARenderProducts): + """Expected files for Redshift renderer. + + Notes: + - `get_files()` only supports rendering with frames, like "animation" + + Attributes: + + unmerged_aovs (list): Name of aovs that are not merged into resulting + exr and we need them specified in Render Products output. + + """ + + renderer = "redshift" + unmerged_aovs = {"Cryptomatte"} + + def get_renderer_prefix(self): + """Get image prefix for Redshift. + + This overrides :func:`ARenderProducts.get_renderer_prefix()` as + we must add `` token manually. + + See also: + :func:`ARenderProducts.get_renderer_prefix()` + + """ + prefix = super(RenderProductsRedshift, self).get_renderer_prefix() + prefix = "{}.".format(prefix) + return prefix + + def get_render_products(self): + """Get all AOVs. + + See Also: + :func:`ARenderProducts.get_render_products()` + + """ + + if not cmds.ls("redshiftOptions", type="RedshiftOptions"): + # this occurs when Render Setting windows was not opened yet. In + # such case there are no Redshift options created so query for AOVs + # will fail. We terminate here as there are no AOVs specified then. + # This state will most probably fail later on some Validator + # anyway. + return [] + + # For Redshift we don't directly return upon forcing multilayer + # due to some AOVs still being written into separate files, + # like Cryptomatte. + # AOVs are merged in multi-channel file + multipart = bool(self._get_attr("redshiftOptions.exrForceMultilayer")) + + # Get Redshift Extension from image format + image_format = self._get_attr("redshiftOptions.imageFormat") # integer + ext = mel.eval("redshiftGetImageExtension(%i)" % image_format) + + use_ref_aovs = self.render_instance.data.get( + "useReferencedAovs", False) or False + + aovs = cmds.ls(type="RedshiftAOV") + if not use_ref_aovs: + ref_aovs = cmds.ls(type="RedshiftAOV", referencedNodes=True) + aovs = list(set(aovs) - set(ref_aovs)) + + products = [] + light_groups_enabled = False + has_beauty_aov = False + for aov in aovs: + enabled = self._get_attr(aov, "enabled") + if not enabled: + continue + + aov_type = self._get_attr(aov, "aovType") + if multipart and aov_type not in self.unmerged_aovs: + continue + + # Any AOVs that still get processed, like Cryptomatte + # by themselves are not multipart files. + aov_multipart = not multipart + + # Redshift skips rendering of masterlayer without AOV suffix + # when a Beauty AOV is rendered. It overrides the main layer. + if aov_type == "Beauty": + has_beauty_aov = True + + aov_name = self._get_attr(aov, "name") + + # Support light Groups + light_groups = [] + if self._get_attr(aov, "supportsLightGroups"): + all_light_groups = self._get_attr(aov, "allLightGroups") + if all_light_groups: + # All light groups is enabled + light_groups = self._get_redshift_light_groups() + else: + value = self._get_attr(aov, "lightGroupList") + # note: string value can return None when never set + if value: + selected_light_groups = value.strip().split() + light_groups = selected_light_groups + + for light_group in light_groups: + aov_light_group_name = "{}_{}".format(aov_name, + light_group) + product = RenderProduct(productName=aov_light_group_name, + aov=aov_name, + ext=ext, + multipart=aov_multipart) + products.append(product) + + if light_groups: + light_groups_enabled = True + + # Redshift AOV Light Select always renders the global AOV + # even when light groups are present so we don't need to + # exclude it when light groups are active + product = RenderProduct(productName=aov_name, + aov=aov_name, + ext=ext, + multipart=aov_multipart) + products.append(product) + + # When a Beauty AOV is added manually, it will be rendered as + # 'Beauty_other' in file name and "standard" beauty will have + # 'Beauty' in its name. When disabled, standard output will be + # without `Beauty`. Except when using light groups. + if light_groups_enabled: + return products + + beauty_name = "Beauty_other" if has_beauty_aov else "" + products.insert(0, + RenderProduct(productName=beauty_name, + ext=ext, + multipart=multipart)) + + return products + + @staticmethod + def _get_redshift_light_groups(): + return sorted(mel.eval("redshiftAllAovLightGroups")) + + +class RenderProductsRenderman(ARenderProducts): + """Expected files for Renderman renderer. + + Warning: + This is very rudimentary and needs more love and testing. + """ + + renderer = "renderman" + + def get_render_products(self): + """Get all AOVs. + + See Also: + :func:`ARenderProducts.get_render_products()` + + """ + products = [] + + default_ext = "exr" + displays = cmds.listConnections("rmanGlobals.displays") + for aov in displays: + enabled = self._get_attr(aov, "enabled") + if not enabled: + continue + + aov_name = str(aov) + if aov_name == "rmanDefaultDisplay": + aov_name = "beauty" + + product = RenderProduct(productName=aov_name, + ext=default_ext) + products.append(product) + + return products + + def get_files(self, product, camera): + """Get expected files. + + In renderman we hack it with prepending path. This path would + normally be translated from `rmanGlobals.imageOutputDir`. We skip + this and hardcode prepend path we expect. There is no place for user + to mess around with this settings anyway and it is enforced in + render settings validator. + """ + files = super(RenderProductsRenderman, self).get_files(product, camera) + + layer_data = self.layer_data + new_files = [] + for file in files: + new_file = "{}/{}/{}".format( + layer_data["sceneName"], layer_data["layerName"], file + ) + new_files.append(new_file) + + return new_files + + +class AOVError(Exception): + """Custom exception for determining AOVs.""" + + +class UnsupportedRendererException(Exception): + """Custom exception. + + Raised when requesting data from unsupported renderer. + """ diff --git a/openpype/hosts/maya/api/lib_rendersetup.py b/openpype/hosts/maya/api/lib_rendersetup.py new file mode 100644 index 0000000000..0736febe9c --- /dev/null +++ b/openpype/hosts/maya/api/lib_rendersetup.py @@ -0,0 +1,343 @@ +# -*- coding: utf-8 -*- +"""Library for handling Render Setup in Maya.""" +from maya import cmds +import maya.api.OpenMaya as om +import logging + +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 +) + +ExactMatch = 0 +ParentMatch = 1 +ChildMatch = 2 + +DefaultRenderLayer = "defaultRenderLayer" + +log = logging.getLogger(__name__) + + +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 == DefaultRenderLayer: + # 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_` 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 + + """ + + # Delay pymel import to here because it's slow to load + import pymel.core as pm + + def _layer_needs_update(layer): + """Return whether layer needs updating.""" + # Use `getattr` as e.g. DefaultRenderLayer 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 defaultRenderLayer""" + 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 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 == ExactMatch: + value = value + if match == ParentMatch: + value = value[index] + if match == ChildMatch: + 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 == ExactMatch: + value = value * multiply + offset + if match == ParentMatch: + value = value * multiply[index] + offset[index] + if match == ChildMatch: + 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 ExactMatch, ParentMatch, ChildMatch + 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 ExactMatch index will + always be None. For ParentMatch the index is which + index the plug is under the parent plug. For ChildMatch + 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 == DefaultRenderLayer: + # DefaultRenderLayer 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 = (ExactMatch, layer_override, None) + + elif parent and override_plug == parent: + override_match = (ParentMatch, layer_override, parent_index) + + elif children and override_plug in children: + child_index = children.index(override_plug) + override_match = (ChildMatch, 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] == ChildMatch + ): + # 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/openpype/hosts/maya/plugins/publish/collect_render.py b/openpype/hosts/maya/plugins/publish/collect_render.py index e90efbc64d..5049647ff9 100644 --- a/openpype/hosts/maya/plugins/publish/collect_render.py +++ b/openpype/hosts/maya/plugins/publish/collect_render.py @@ -49,7 +49,7 @@ import maya.app.renderSetup.model.renderSetup as renderSetup import pyblish.api from avalon import maya, api -from openpype.hosts.maya.api.expected_files import ExpectedFiles +from openpype.hosts.maya.api.lib_renderproducts import get as get_layer_render_products # noqa: E501 from openpype.hosts.maya.api import lib @@ -168,10 +168,21 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # return all expected files for all cameras and aovs in given # frame range - ef = ExpectedFiles(render_instance) - exp_files = ef.get(renderer, layer_name) - self.log.info("multipart: {}".format(ef.multipart)) + layer_render_products = get_layer_render_products( + layer_name, render_instance) + render_products = layer_render_products.layer_data.products + assert render_products, "no render products generated" + exp_files = [] + for product in render_products: + for camera in layer_render_products.layer_data.cameras: + exp_files.append( + {product.productName: layer_render_products.get_files( + product, camera)}) + + self.log.info("multipart: {}".format( + layer_render_products.multipart)) assert exp_files, "no file names were generated, this is bug" + self.log.info(exp_files) # if we want to attach render to subset, check if we have AOV's # in expectedFiles. If so, raise error as we cannot attach AOV @@ -186,24 +197,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin): full_exp_files = [] aov_dict = {} - # we either get AOVs or just list of files. List of files can - # mean two things - there are no AOVs enabled or multipass EXR - # is produced. In either case we treat those as `beauty`. - if isinstance(exp_files[0], dict): - for aov, files in exp_files[0].items(): - full_paths = [] - for e in files: - full_path = os.path.join(workspace, "renders", e) - full_path = full_path.replace("\\", "/") - full_paths.append(full_path) - aov_dict[aov] = full_paths - else: + # replace relative paths with absolute. Render products are + # returned as list of dictionaries. + for aov in exp_files: full_paths = [] - for e in exp_files: - full_path = os.path.join(workspace, "renders", e) + for file in aov[aov.keys()[0]]: + full_path = os.path.join(workspace, "renders", file) full_path = full_path.replace("\\", "/") full_paths.append(full_path) - aov_dict["beauty"] = full_paths + aov_dict[aov.keys()[0]] = full_paths frame_start_render = int(self.get_render_attribute( "startFrame", layer=layer_name)) @@ -235,7 +237,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): "subset": expected_layer_name, "attachTo": attach_to, "setMembers": layer_name, - "multipartExr": ef.multipart, + "multipartExr": layer_render_products.multipart, "review": render_instance.data.get("review") or False, "publish": True, @@ -320,10 +322,6 @@ class CollectMayaRender(pyblish.api.ContextPlugin): instance.data.update(data) self.log.debug("data: {}".format(json.dumps(data, indent=4))) - # Restore current layer. - self.log.info("Restoring to {}".format(current_layer.name())) - self._rs.switchToLayer(current_layer) - def parse_options(self, render_globals): """Get all overrides with a value, skip those without.