From 52413591062291dab1e5f3ec05448970bb797971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Tue, 19 May 2020 18:54:55 +0200 Subject: [PATCH 01/22] ExpectedFiles as standalone package --- pype/maya/expected_files.py | 837 ++++++++++++++++++ .../global/publish/submit_publish_job.py | 3 +- pype/plugins/maya/publish/collect_render.py | 644 +------------- .../maya/publish/collect_vray_scene.py | 8 +- 4 files changed, 868 insertions(+), 624 deletions(-) create mode 100644 pype/maya/expected_files.py diff --git a/pype/maya/expected_files.py b/pype/maya/expected_files.py new file mode 100644 index 0000000000..d103c1bc27 --- /dev/null +++ b/pype/maya/expected_files.py @@ -0,0 +1,837 @@ +# -*- 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. + ImagePrefixes (dict): Mapping between renderers and their respective + image prefix atrribute names. + +""" + +import types +import re +import os +from abc import ABCMeta, abstractmethod + +import six + +import pype.maya.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|_|_", 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 +ImagePrefixes = { + "mentalray": "defaultRenderGlobals.imageFilePrefix", + "vray": "vraySettings.fileNamePrefix", + "arnold": "defaultRenderGlobals.imageFilePrefix", + "renderman": "rmanGlobals.imageFileFormat", + "redshift": "defaultRenderGlobals.imageFilePrefix", +} + + +class ExpectedFiles: + """Class grouping functionality for all supported renderers. + + Attributes: + multipart (bool): Flag if multipart exrs are used. + + """ + + multipart = False + + 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)) + elif renderer.lower() == "vray": + return self._get_files(ExpectedFilesVray(layer)) + elif renderer.lower() == "redshift": + return self._get_files(ExpectedFilesRedshift(layer)) + elif renderer.lower() == "mentalray": + return self._get_files(ExpectedFilesMentalray(layer)) + elif renderer.lower() == "renderman": + return self._get_files(ExpectedFilesRenderman(layer)) + else: + raise UnsupportedRendererException( + "unsupported {}".format(renderer) + ) + + def _get_files(self, renderer): + 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): + """Constructor.""" + self.layer = layer + + @abstractmethod + def get_aovs(self): + """To be implemented by renderer class.""" + pass + + def get_renderer_prefix(self): + """Return prefix for specific renderer. + + This is for most renderers the same and can be overriden 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:`ImagePrefixes` for mapping of renderers and + image prefixes. + + """ + try: + file_prefix = cmds.getAttr(ImagePrefixes[self.renderer]) + except KeyError: + raise UnsupportedRendererException( + "Unsupported renderer {}".format(self.renderer) + ) + return file_prefix + + def _get_layer_data(self): + # ______________________________________________ + # ____________________/ ____________________________________________/ + # 1 - get scene name /__________________/ + # ____________________/ + scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True)) + scene_name, _ = os.path.splitext(scene_basename) + + # ______________________________________________ + # ____________________/ ____________________________________________/ + # 2 - detect renderer /__________________/ + # ____________________/ + renderer = self.renderer + + # ________________________________________________ + # __________________/ ______________________________________________/ + # 3 - image prefix /__________________/ + # __________________/ + file_prefix = self.get_renderer_prefix() + + if not file_prefix: + raise RuntimeError("Image prefix not set") + + default_ext = cmds.getAttr("defaultRenderGlobals.imfPluginKey") + + # ________________________________________________ + # __________________/ ______________________________________________/ + # 4 - get renderable cameras_____________/ + # __________________/ + + # if we have token in prefix path we'll expect output for + # every renderable camera in layer. + + renderable_cameras = self.get_renderable_cameras() + # ________________________________________________ + # __________________/ ______________________________________________/ + # 5 - get AOVs /____________________/ + # __________________/ + + enabled_aovs = self.get_aovs() + + layer_name = self.layer + if self.layer.startswith("rs_"): + layer_name = self.layer[3:] + start_frame = int(self.get_render_attribute("startFrame")) + end_frame = int(self.get_render_attribute("endFrame")) + frame_step = int(self.get_render_attribute("byFrameStep")) + padding = int(self.get_render_attribute("extensionPadding")) + + scene_data = { + "frameStart": start_frame, + "frameEnd": end_frame, + "frameStep": frame_step, + "padding": padding, + "cameras": renderable_cameras, + "sceneName": scene_name, + "layerName": layer_name, + "renderer": renderer, + "defaultExt": default_ext, + "filePrefix": file_prefix, + "enabledAOVs": enabled_aovs, + } + return scene_data + + def _generate_single_file_sequence(self, layer_data): + expected_files = [] + file_prefix = layer_data["filePrefix"] + for cam in layer_data["cameras"]: + mappings = ( + (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), + (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), + (R_SUBSTITUTE_CAMERA_TOKEN, cam), + # this is required to remove unfilled aov token, for example + # in Redshift + (R_REMOVE_AOV_TOKEN, ""), + (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): + expected_files = [] + aov_file_list = {} + file_prefix = layer_data["filePrefix"] + for aov in layer_data["enabledAOVs"]: + for cam in layer_data["cameras"]: + + mappings = ( + (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), + (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), + (R_SUBSTITUTE_CAMERA_TOKEN, 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], 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.get("enabledAOVs"): + expected_files = self._generate_aov_file_sequences(layer_data) + else: + expected_files = self._generate_single_file_sequence(layer_data) + + return expected_files + + def get_renderable_cameras(self): + """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) + ] + + renderable_cameras = [] + for cam in cam_parents: + renderable = False + if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam))): + renderable = True + + for override in self.get_layer_overrides( + "{}.renderable".format(cam), self.layer + ): + renderable = self.maya_is_true(override) + + if renderable: + renderable_cameras.append(cam) + return renderable_cameras + + def maya_is_true(self, 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 + elif isinstance(attr_val, (types.ListType, types.GeneratorType)): + return any(attr_val) + else: + return bool(attr_val) + + def get_layer_overrides(self, attr, layer): + """Get overrides for attribute on given render layer. + + Args: + attr (str): Maya attribute name. + layer (str): Maya render layer name. + + Returns: + Value of attribute override. + + """ + connections = cmds.listConnections(attr, plugs=True) + if connections: + for connection in connections: + if connection: + node_name = connection.split(".")[0] + if cmds.nodeType(node_name) == "renderLayer": + attr_name = "%s.value" % ".".join( + connection.split(".")[:-1] + ) + if node_name == layer: + yield cmds.getAttr(attr_name) + + def get_render_attribute(self, attr): + """Get attribute from render options. + + Args: + attr (str): name of attribute to be looked up. + + Returns: + Attribute value + + """ + return lib.get_attr_in_layer( + "defaultRenderGlobals.{}".format(attr), 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): + """Constructor.""" + super(ExpectedFilesArnold, self).__init__(layer) + 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. + + ai_aovs = [n for n in cmds.ls(type="aiAOV")] + + 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), self.layer + ): + 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): + """Constructor.""" + super(ExpectedFilesVray, self).__init__(layer) + 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_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() + if layer_data.get("enabledAOVs"): + expected_files[0][u"beauty"] = self._generate_single_file_sequence( + layer_data + ) # noqa: E501 + + 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 == "exr (multichannel)" or default_ext == "exr (deep)": + default_ext = "exr" + + vr_aovs = [ + n + for n in cmds.ls( + type=["VRayRenderElement", "VRayRenderElementSet"] + ) + ] + + 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), "rs_{}".format(self.layer) + ): + enabled = self.maya_is_true(override) + + if enabled: + # todo: find how vray set format for AOVs + enabled_aovs.append( + (self._get_vray_aov_name(aov), default_ext)) + return enabled_aovs + + def _get_vray_aov_name(self, node): + + # Get render element pass type + vray_node_attr = next( + attr + for attr in cmds.listAttr(node) + if attr.startswith("vray_name") + ) + pass_type = vray_node_attr.rsplit("_", 1)[-1] + + # Support V-Ray extratex explicit name (if set by user) + if pass_type == "extratex": + explicit_attr = "{}.vray_explicit_name_extratex".format(node) + explicit_name = cmds.getAttr(explicit_attr) + if explicit_name: + return explicit_name + + # Node type is in the attribute name but we need to check if value + # of the attribute as it can be changed + return cmds.getAttr("{}.{}".format(node, vray_node_attr)) + + +class ExpectedFilesRedshift(AExpectedFiles): + """Expected files for Redshift renderer. + + Attributes: + ext_mapping (list): Mapping redshift extension dropdown values + to strings. + + """ + + ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] + + def __init__(self, layer): + """Construtor.""" + super(ExpectedFilesRedshift, self).__init__(layer) + 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() + + # we need to add one sequence for plain beauty if AOVs are enabled. + # as redshift output beauty without 'beauty' in filename. + + layer_data = self._get_layer_data() + if layer_data.get("enabledAOVs"): + expected_files[0][u"beauty"] = self._generate_single_file_sequence( + layer_data + ) # noqa: E501 + + 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 = self.ext_mapping[ + cmds.getAttr("redshiftOptions.imageFormat") + ] + rs_aovs = [n for n in cmds.ls(type="RedshiftAOV")] + + # todo: find out how to detect multichannel exr for redshift + 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), self.layer + ): + enabled = self.maya_is_true(override) + + if enabled: + enabled_aovs.append( + (cmds.getAttr("%s.name" % aov), default_ext) + ) + + 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): + """Constructor.""" + super(ExpectedFilesRenderman, self).__init__(layer) + 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), self.layer + ): + 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): + """Constructor. + + Raises: + :exc:`UnimplementedRendererException`: as it is not implemented. + + """ + 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.""" + + pass + + +class UnsupportedRendererException(Exception): + """Custom exception. + + Raised when requesting data from unsupported renderer. + """ + + pass + + +class UnimplementedRendererException(Exception): + """Custom exception. + + Raised when requesting data from renderer that is not implemented yet. + """ + + pass diff --git a/pype/plugins/global/publish/submit_publish_job.py b/pype/plugins/global/publish/submit_publish_job.py index 2f4e0a5ae1..7a5657044b 100644 --- a/pype/plugins/global/publish/submit_publish_job.py +++ b/pype/plugins/global/publish/submit_publish_job.py @@ -147,7 +147,8 @@ class ProcessSubmittedJobOnFarm(pyblish.api.InstancePlugin): hosts = ["fusion", "maya", "nuke"] - families = ["render.farm", "prerener", "renderlayer", "imagesequence"] + families = ["render.farm", "prerener", + "renderlayer", "imagesequence", "vrayscene"] aov_filter = {"maya": ["beauty"]} diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index dbc0594c7c..32e572a166 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -1,4 +1,6 @@ -""" +# -*- coding: utf-8 -*- +"""Collect render data. + This collector will go through render layers in maya and prepare all data needed to create instances and their representations for submition and publishing on farm. @@ -39,10 +41,7 @@ Provides: import re import os -import types -import six import json -from abc import ABCMeta, abstractmethod from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup @@ -51,53 +50,18 @@ import pyblish.api from avalon import maya, api import pype.maya.lib as lib - - -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|_|_", 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 -ImagePrefixes = { - "mentalray": "defaultRenderGlobals.imageFilePrefix", - "vray": "vraySettings.fileNamePrefix", - "arnold": "defaultRenderGlobals.imageFilePrefix", - "renderman": "rmanGlobals.imageFileFormat", - "redshift": "defaultRenderGlobals.imageFilePrefix", -} +from pype.maya.expected_files import ExpectedFiles class CollectMayaRender(pyblish.api.ContextPlugin): - """Gather all publishable render layers from renderSetup""" + """Gather all publishable render layers from renderSetup.""" order = pyblish.api.CollectorOrder + 0.01 hosts = ["maya"] label = "Collect Render Layers" def process(self, context): + """Entry point to collector.""" render_instance = None for instance in context: if "rendering" in instance.data["families"]: @@ -155,7 +119,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # test if there are sets (subsets) to attach render to sets = cmds.sets(layer, query=True) or [] - attachTo = [] + attach_to = [] if sets: for s in sets: if "family" not in cmds.listAttr(s): @@ -182,15 +146,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # return all expected files for all cameras and aovs in given # frame range - exf = ExpectedFiles() - exp_files = exf.get(renderer, layer_name) - self.log.info("multipart: {}".format(exf.multipart)) + ef = ExpectedFiles() + exp_files = ef.get(renderer, layer_name) + self.log.info("multipart: {}".format(ef.multipart)) assert exp_files, "no file names were generated, this is bug" # 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 # (considered to be subset on its own) to another subset - if attachTo: + if attach_to: assert len(exp_files[0].keys()) == 1, ( "attaching multiple AOVs or renderable cameras to " "subset is not supported" @@ -247,9 +211,9 @@ class CollectMayaRender(pyblish.api.ContextPlugin): # Get layer specific settings, might be overrides data = { "subset": expected_layer_name, - "attachTo": attachTo, + "attachTo": attach_to, "setMembers": layer_name, - "multipartExr": exf.multipart, + "multipartExr": ef.multipart, "publish": True, "handleStart": handle_start, @@ -312,7 +276,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): self._rs.switchToLayer(current_layer) def parse_options(self, render_globals): - """Get all overrides with a value, skip those without + """Get all overrides with a value, skip those without. Here's the kicker. These globals override defaults in the submission integrator, but an empty value means no overriding is made. @@ -323,8 +287,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): Returns: dict: only overrides with values - """ + """ attributes = maya.read(render_globals) options = {"renderGlobals": {}} @@ -396,575 +360,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin): return rset.getOverrides() def get_render_attribute(self, attr, layer): + """Get attribute from render options. + + Args: + attr (str): name of attribute to be looked up. + + Returns: + Attribute value + + """ return lib.get_attr_in_layer( "defaultRenderGlobals.{}".format(attr), layer=layer ) - - -class ExpectedFiles: - multipart = False - - def get(self, renderer, layer): - renderSetup.instance().switchToLayerUsingLegacyName(layer) - - if renderer.lower() == "arnold": - return self._get_files(ExpectedFilesArnold(layer)) - elif renderer.lower() == "vray": - return self._get_files(ExpectedFilesVray(layer)) - elif renderer.lower() == "redshift": - return self._get_files(ExpectedFilesRedshift(layer)) - elif renderer.lower() == "mentalray": - return self._get_files(ExpectedFilesMentalray(layer)) - elif renderer.lower() == "renderman": - return self._get_files(ExpectedFilesRenderman(layer)) - else: - raise UnsupportedRendererException( - "unsupported {}".format(renderer) - ) - - def _get_files(self, renderer): - files = renderer.get_files() - self.multipart = renderer.multipart - return files - - -@six.add_metaclass(ABCMeta) -class AExpectedFiles: - renderer = None - layer = None - multipart = False - - def __init__(self, layer): - self.layer = layer - - @abstractmethod - def get_aovs(self): - pass - - def get_renderer_prefix(self): - try: - file_prefix = cmds.getAttr(ImagePrefixes[self.renderer]) - except KeyError: - raise UnsupportedRendererException( - "Unsupported renderer {}".format(self.renderer) - ) - return file_prefix - - def _get_layer_data(self): - # ______________________________________________ - # ____________________/ ____________________________________________/ - # 1 - get scene name /__________________/ - # ____________________/ - scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True)) - scene_name, _ = os.path.splitext(scene_basename) - - # ______________________________________________ - # ____________________/ ____________________________________________/ - # 2 - detect renderer /__________________/ - # ____________________/ - renderer = self.renderer - - # ________________________________________________ - # __________________/ ______________________________________________/ - # 3 - image prefix /__________________/ - # __________________/ - file_prefix = self.get_renderer_prefix() - - if not file_prefix: - raise RuntimeError("Image prefix not set") - - default_ext = cmds.getAttr("defaultRenderGlobals.imfPluginKey") - - # ________________________________________________ - # __________________/ ______________________________________________/ - # 4 - get renderable cameras_____________/ - # __________________/ - - # if we have token in prefix path we'll expect output for - # every renderable camera in layer. - - renderable_cameras = self.get_renderable_cameras() - # ________________________________________________ - # __________________/ ______________________________________________/ - # 5 - get AOVs /____________________/ - # __________________/ - - enabled_aovs = self.get_aovs() - - layer_name = self.layer - if self.layer.startswith("rs_"): - layer_name = self.layer[3:] - start_frame = int(self.get_render_attribute("startFrame")) - end_frame = int(self.get_render_attribute("endFrame")) - frame_step = int(self.get_render_attribute("byFrameStep")) - padding = int(self.get_render_attribute("extensionPadding")) - - scene_data = { - "frameStart": start_frame, - "frameEnd": end_frame, - "frameStep": frame_step, - "padding": padding, - "cameras": renderable_cameras, - "sceneName": scene_name, - "layerName": layer_name, - "renderer": renderer, - "defaultExt": default_ext, - "filePrefix": file_prefix, - "enabledAOVs": enabled_aovs, - } - return scene_data - - def _generate_single_file_sequence(self, layer_data): - expected_files = [] - file_prefix = layer_data["filePrefix"] - for cam in layer_data["cameras"]: - mappings = ( - (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), - # this is required to remove unfilled aov token, for example - # in Redshift - (R_REMOVE_AOV_TOKEN, ""), - (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): - expected_files = [] - aov_file_list = {} - file_prefix = layer_data["filePrefix"] - for aov in layer_data["enabledAOVs"]: - for cam in layer_data["cameras"]: - - mappings = ( - (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, 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], 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): - """ - This method will 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.get("enabledAOVs"): - expected_files = self._generate_aov_file_sequences(layer_data) - else: - expected_files = self._generate_single_file_sequence(layer_data) - - return expected_files - - def get_renderable_cameras(self): - cam_parents = [ - cmds.listRelatives(x, ap=True)[-1] for x in cmds.ls(cameras=True) - ] - - renderable_cameras = [] - for cam in cam_parents: - renderable = False - if self.maya_is_true(cmds.getAttr("{}.renderable".format(cam))): - renderable = True - - for override in self.get_layer_overrides( - "{}.renderable".format(cam), self.layer - ): - renderable = self.maya_is_true(override) - - if renderable: - renderable_cameras.append(cam) - return renderable_cameras - - def maya_is_true(self, 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. - """ - if isinstance(attr_val, types.BooleanType): - return attr_val - elif isinstance(attr_val, (types.ListType, types.GeneratorType)): - return any(attr_val) - else: - return bool(attr_val) - - def get_layer_overrides(self, attr, layer): - connections = cmds.listConnections(attr, plugs=True) - if connections: - for connection in connections: - if connection: - node_name = connection.split(".")[0] - if cmds.nodeType(node_name) == "renderLayer": - attr_name = "%s.value" % ".".join( - connection.split(".")[:-1] - ) - if node_name == layer: - yield cmds.getAttr(attr_name) - - def get_render_attribute(self, attr): - return lib.get_attr_in_layer( - "defaultRenderGlobals.{}".format(attr), layer=self.layer - ) - - -class ExpectedFilesArnold(AExpectedFiles): - - # Arnold AOV driver extension mapping - # Is there a better way? - 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): - super(ExpectedFilesArnold, self).__init__(layer) - self.renderer = "arnold" - - def get_aovs(self): - 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. - - ai_aovs = [n for n in cmds.ls(type="aiAOV")] - - 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), self.layer - ): - 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): - - # V-ray file extension mapping - # 5 - exr - # 6 - multichannel exr - # 13 - deep exr - - def __init__(self, layer): - super(ExpectedFilesVray, self).__init__(layer) - self.renderer = "vray" - - def get_renderer_prefix(self): - prefix = super(ExpectedFilesVray, self).get_renderer_prefix() - prefix = "{}_".format(prefix) - return prefix - - def get_files(self): - expected_files = super(ExpectedFilesVray, self).get_files() - - # we need to add one sequence for plain beauty if AOVs are enabled. - # as vray output beauty without 'beauty' in filename. - - layer_data = self._get_layer_data() - if layer_data.get("enabledAOVs"): - expected_files[0][u"beauty"] = self._generate_single_file_sequence( - layer_data - ) # noqa: E501 - - return expected_files - - def get_aovs(self): - 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 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 - - default_ext = cmds.getAttr("vraySettings.imageFormatStr") - if default_ext == "exr (multichannel)" or default_ext == "exr (deep)": - default_ext = "exr" - - vr_aovs = [ - n - for n in cmds.ls( - type=["VRayRenderElement", "VRayRenderElementSet"] - ) - ] - - # todo: find out how to detect multichannel exr for vray - 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), "rs_{}".format(self.layer) - ): - enabled = self.maya_is_true(override) - - if enabled: - # todo: find how vray set format for AOVs - enabled_aovs.append( - (self._get_vray_aov_name(aov), default_ext)) - return enabled_aovs - - def _get_vray_aov_name(self, node): - - # Get render element pass type - vray_node_attr = next( - attr - for attr in cmds.listAttr(node) - if attr.startswith("vray_name") - ) - pass_type = vray_node_attr.rsplit("_", 1)[-1] - - # Support V-Ray extratex explicit name (if set by user) - if pass_type == "extratex": - explicit_attr = "{}.vray_explicit_name_extratex".format(node) - explicit_name = cmds.getAttr(explicit_attr) - if explicit_name: - return explicit_name - - # Node type is in the attribute name but we need to check if value - # of the attribute as it can be changed - return cmds.getAttr("{}.{}".format(node, vray_node_attr)) - - -class ExpectedFilesRedshift(AExpectedFiles): - - # mapping redshift extension dropdown values to strings - ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] - - def __init__(self, layer): - super(ExpectedFilesRedshift, self).__init__(layer) - self.renderer = "redshift" - - def get_renderer_prefix(self): - prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix() - prefix = "{}_".format(prefix) - return prefix - - def get_files(self): - expected_files = super(ExpectedFilesRedshift, self).get_files() - - # we need to add one sequence for plain beauty if AOVs are enabled. - # as redshift output beauty without 'beauty' in filename. - - layer_data = self._get_layer_data() - if layer_data.get("enabledAOVs"): - expected_files[0][u"beauty"] = self._generate_single_file_sequence( - layer_data - ) # noqa: E501 - - return expected_files - - def get_aovs(self): - 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 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 - - default_ext = self.ext_mapping[ - cmds.getAttr("redshiftOptions.imageFormat") - ] - rs_aovs = [n for n in cmds.ls(type="RedshiftAOV")] - - # todo: find out how to detect multichannel exr for redshift - 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), self.layer - ): - enabled = self.maya_is_true(override) - - if enabled: - enabled_aovs.append( - (cmds.getAttr("%s.name" % aov), default_ext) - ) - - return enabled_aovs - - -class ExpectedFilesRenderman(AExpectedFiles): - def __init__(self, layer): - super(ExpectedFilesRenderman, self).__init__(layer) - self.renderer = "renderman" - - def get_aovs(self): - 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), self.layer - ): - enabled = self.maya_is_true(override) - - if enabled: - enabled_aovs.append((aov_name, default_ext)) - - return enabled_aovs - - def get_files(self): - """ - In renderman we hack it with prepending path. This path would - normally be translated from `rmanGlobals.imageOutputDir`. We skip - this and harcode 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): - def __init__(self, layer): - raise UnimplementedRendererException("Mentalray not implemented") - - def get_aovs(self): - return [] - - -class AOVError(Exception): - pass - - -class UnsupportedRendererException(Exception): - pass - - -class UnimplementedRendererException(Exception): - pass diff --git a/pype/plugins/maya/publish/collect_vray_scene.py b/pype/plugins/maya/publish/collect_vray_scene.py index 211b212a76..e144e72bdb 100644 --- a/pype/plugins/maya/publish/collect_vray_scene.py +++ b/pype/plugins/maya/publish/collect_vray_scene.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Collect .vrayscene instance.""" import os import pyblish.api @@ -5,18 +7,18 @@ import pyblish.api from maya import cmds from avalon import api +from pype.maya.expected_files import ExpectedFiles class CollectVRayScene(pyblish.api.ContextPlugin): - """Collect all information prior for exporting vrscenes - """ + """Collect all information prior for exporting vrscenes.""" order = pyblish.api.CollectorOrder label = "Collect VRay Scene" hosts = ["maya"] def process(self, context): - + """Collector entry point.""" # Sort by displayOrder def sort_by_display_order(layer): return cmds.getAttr("%s.displayOrder" % layer) From be9302580fc67f83da295a6b4f1745eedbf84942 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Thu, 21 May 2020 18:40:50 +0200 Subject: [PATCH 02/22] submitting vray export job to deadline --- pype/plugins/maya/create/create_render.py | 88 ++++- pype/plugins/maya/publish/collect_render.py | 17 +- .../maya/publish/collect_vray_scene.py | 2 +- .../maya/publish/submit_maya_deadline.py | 374 +++++++++++++----- .../maya/publish/submit_vray_deadline.py | 6 +- 5 files changed, 350 insertions(+), 137 deletions(-) diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index 080c6bd55d..8e32cd4041 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- +"""Create ``Render`` instance in Maya.""" import os import json import appdirs @@ -11,7 +13,38 @@ import avalon.maya class CreateRender(avalon.maya.Creator): - """Create render layer for export""" + """Create *render* instance. + + Render instances are not actually published, they hold options for + collecting of render data. It render instance is present, it will trigger + collection of render layers, AOVs, cameras for either direct submission + to render farm or export as various standalone formats (like V-Rays + ``vrscenes`` or Arnolds ``ass`` files) and then submitting them to render + farm. + + Instance has following attributes:: + + primaryPool (list of str): Primary list of slave machine pool to use. + secondaryPool (list of str): Optional secondary list of slave pools. + suspendPublishJob (bool): Suspend the job after it is submitted. + extendFrames (bool): Use already existing frames from previous version + to extend current render. + overrideExistingFrame (bool): Overwrite already existing frames. + priority (int): Submitted job priority + framesPerTask (int): How many frames per task to render. This is + basically job division on render farm. + whitelist (list of str): White list of slave machines + machineList (list of str): Specific list of slave machines to use + useMayaBatch (bool): Use Maya batch mode to render as opposite to + Maya interactive mode. This consumes different licenses. + vrscene (bool): Submit as ``vrscene`` file for standalone V-Ray + renderer. + ass (bool): Submit as ``ass`` file for standalone Arnold renderer. + + See Also: + https://pype.club/docs/artist_hosts_maya#creating-basic-render-setup + + """ label = "Render" family = "rendering" @@ -42,9 +75,11 @@ class CreateRender(avalon.maya.Creator): } def __init__(self, *args, **kwargs): + """Constructor.""" super(CreateRender, self).__init__(*args, **kwargs) def process(self): + """Entry point.""" exists = cmds.ls(self.name) if exists: return cmds.warning("%s already exists." % exists[0]) @@ -145,17 +180,21 @@ class CreateRender(avalon.maya.Creator): self.data["whitelist"] = False self.data["machineList"] = "" self.data["useMayaBatch"] = True + self.data["vrayScene"] = False + self.data["assScene"] = False self.options = {"useSelection": False} # Force no content def _load_credentials(self): - """ - Load Muster credentials from file and set `MUSTER_USER`, - `MUSTER_PASSWORD`, `MUSTER_REST_URL` is loaded from presets. + """Load Muster credentials. - .. todo:: + Load Muster credentials from file and set ```MUSTER_USER``, + ```MUSTER_PASSWORD``, ``MUSTER_REST_URL`` is loaded from presets. + + Raises: + RuntimeError: If loaded credentials are invalid. + AttributeError: If ``MUSTER_REST_URL`` is not set. - Show login dialog if access token is invalid or missing. """ app_dir = os.path.normpath(appdirs.user_data_dir("pype-app", "pype")) file_name = "muster_cred.json" @@ -172,8 +211,11 @@ class CreateRender(avalon.maya.Creator): raise AttributeError("Muster REST API url not set") def _get_muster_pools(self): - """ - Get render pools from muster + """Get render pools from Muster. + + Raises: + Exception: If pool list cannot be obtained from Muster. + """ params = {"authToken": self._token} api_entry = "/api/pools/list" @@ -209,14 +251,17 @@ class CreateRender(avalon.maya.Creator): raise Exception("Cannot show login form to Muster") def _requests_post(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. + """Wrap request post method. - WARNING: disabling SSL certificate validation is defeating one line + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line of defense SSL is providing and it is not recommended. + """ if "verify" not in kwargs: kwargs["verify"] = ( @@ -225,14 +270,17 @@ class CreateRender(avalon.maya.Creator): return requests.post(*args, **kwargs) def _requests_get(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. + """Wrap request get method. - WARNING: disabling SSL certificate validation is defeating one line + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line of defense SSL is providing and it is not recommended. + """ if "verify" not in kwargs: kwargs["verify"] = ( diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 32e572a166..5cdae580f5 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -125,7 +125,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if "family" not in cmds.listAttr(s): continue - attachTo.append( + attach_to.append( { "version": None, # we need integrator for that "subset": s, @@ -170,15 +170,15 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if isinstance(exp_files[0], dict): for aov, files in exp_files[0].items(): full_paths = [] - for ef in files: - full_path = os.path.join(workspace, "renders", ef) + 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: full_paths = [] - for ef in exp_files: - full_path = os.path.join(workspace, "renders", ef) + for e in exp_files: + full_path = os.path.join(workspace, "renders", e) full_path = full_path.replace("\\", "/") full_paths.append(full_path) aov_dict["beauty"] = full_paths @@ -255,6 +255,13 @@ class CollectMayaRender(pyblish.api.ContextPlugin): data[attr] = value + # handle standalone renderers + if render_instance.data.get("vrayScene") is True: + data["families"].append("vrayscene") + + if render_instance.data.get("ass") is True: + data["families"].append("assScene") + # Include (optional) global settings # Get global overrides and translate to Deadline values overrides = self.parse_options(str(render_globals)) diff --git a/pype/plugins/maya/publish/collect_vray_scene.py b/pype/plugins/maya/publish/collect_vray_scene.py index e144e72bdb..df7e5c1ab6 100644 --- a/pype/plugins/maya/publish/collect_vray_scene.py +++ b/pype/plugins/maya/publish/collect_vray_scene.py @@ -15,7 +15,7 @@ class CollectVRayScene(pyblish.api.ContextPlugin): order = pyblish.api.CollectorOrder label = "Collect VRay Scene" - hosts = ["maya"] + hosts = ["foo"] def process(self, context): """Collector entry point.""" diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 89e7393fe5..e67e0ec50f 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -3,26 +3,60 @@ This module is taking care of submitting job from Maya to Deadline. It creates job and set correct environments. Its behavior is controlled by -`DEADLINE_REST_URL` environment variable - pointing to Deadline Web Service -and `MayaSubmitDeadline.use_published (bool)` property telling Deadline to +``DEADLINE_REST_URL`` environment variable - pointing to Deadline Web Service +and :data:`MayaSubmitDeadline.use_published` property telling Deadline to use published scene workfile or not. + +If ``vrscene`` or ``assscene`` are detected in families, it will first +submit job to export these files and then dependent job to render them. + +Attributes: + payload_skeleton (dict): Skeleton payload data sent as job to Deadline. + Default values are for ``MayaBatch`` plugin. + """ import os import json import getpass -import re +import copy + import clique +import requests from maya import cmds from avalon import api -from avalon.vendor import requests - import pyblish.api - import pype.maya.lib as lib +# Documentation for keys available at: +# https://docs.thinkboxsoftware.com +# /products/deadline/8.0/1_User%20Manual/manual +# /manual-submission.html#job-info-file-options + +payload_skeleton = { + "JobInfo": { + "BatchName": None, # Top-level group name + "Name": None, # Job name, as seen in Monitor + "UserName": None, + "Plugin": "MayaBatch", + "Frames": "{start}-{end}x{step}", + "Comment": None, + }, + "PluginInfo": { + "SceneFile": None, # Input + "OutputFilePath": None, # Output directory and filename + "OutputFilePrefix": None, + "Version": cmds.about(version=True), # Mandatory for Deadline + "UsingRenderLayers": True, + "RenderLayer": None, # Render only this layer + "Renderer": None, + "ProjectPath": None, # Resolve relative references + }, + "AuxFiles": [] # Mandatory for Deadline, may be empty +} + def get_renderer_variables(renderlayer=None): """Retrieve the extension which has been set in the VRay settings. @@ -91,7 +125,15 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): """Submit available render layers to Deadline. Renders are submitted to a Deadline Web Service as - supplied via the environment variable DEADLINE_REST_URL + supplied via the environment variable ``DEADLINE_REST_URL``. + + Note: + If Deadline configuration is not detected, this plugin will + be disabled. + + Attributes: + use_published (bool): Use published scene to render instead of the + one in work area. """ @@ -108,10 +150,11 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): use_published = True def process(self, instance): - - DEADLINE_REST_URL = os.environ.get("DEADLINE_REST_URL", - "http://localhost:8082") - assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" + """Plugin entry point.""" + self._instance = instance + self._deadline_url = os.environ.get( + "DEADLINE_REST_URL", "http://localhost:8082") + assert self._deadline_url, "Requires DEADLINE_REST_URL" context = instance.context workspace = context.data["workspaceDir"] @@ -119,6 +162,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): filepath = None + # Handle render/export from published scene or not ------------------ if self.use_published: for i in context: if "workfile" in i.data["families"]: @@ -166,11 +210,11 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): orig_scene, new_scene )) - allInstances = [] + all_instances = [] for result in context.data["results"]: if (result["instance"] is not None and - result["instance"] not in allInstances): - allInstances.append(result["instance"]) + result["instance"] not in all_instances): # noqa: E128 + all_instances.append(result["instance"]) # fallback if nothing was set if not filepath: @@ -179,6 +223,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.log.debug(filepath) + # Gather needed data ------------------------------------------------ filename = os.path.basename(filepath) comment = context.data.get("comment", "") dirname = os.path.join(workspace, "renders") @@ -198,91 +243,49 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): output_filename_0 = filename_0 + # Create render folder ---------------------------------------------- try: # Ensure render folder exists os.makedirs(dirname) except OSError: pass - # Documentation for keys available at: - # https://docs.thinkboxsoftware.com - # /products/deadline/8.0/1_User%20Manual/manual - # /manual-submission.html#job-info-file-options - payload = { - "JobInfo": { - # Top-level group name - "BatchName": filename, + # Fill in common data to payload ------------------------------------ + payload_data = {} + payload_data["filename"] = filename + payload_data["filepath"] = filepath + payload_data["jobname"] = jobname + payload_data["deadline_user"] = deadline_user + payload_data["comment"] = comment + payload_data["output_filename_0"] = output_filename_0 + payload_data["render_variables"] = render_variables + payload_data["renderlayer"] = renderlayer + payload_data["workspace"] = workspace - # Asset dependency to wait for at least the scene file to sync. - "AssetDependency0": filepath, + frame_pattern = payload_skeleton["JobInfo"]["Frames"] + payload_skeleton["JobInfo"]["Frames"] = frame_pattern.format( + start=int(self._instance.data["frameStartHandle"]), + end=int(self._instance.data["frameEndHandle"]), + step=int(self._instance.data["byFrameStep"])) - # Job name, as seen in Monitor - "Name": jobname, + payload_skeleton["JobInfo"]["Plugin"] = self._instance.data.get( + "mayaRenderPlugin", "MayaBatch") - # Arbitrary username, for visualisation in Monitor - "UserName": deadline_user, + payload_skeleton["JobInfo"]["BatchName"] = filename + # Job name, as seen in Monitor + payload_skeleton["JobInfo"]["Name"] = jobname + # Arbitrary username, for visualisation in Monitor + payload_skeleton["JobInfo"]["UserName"] = deadline_user + # Optional, enable double-click to preview rendered + # frames from Deadline Monitor + payload_skeleton["JobInfo"]["OutputDirectory0"] = \ + os.path.dirname(output_filename_0) + payload_skeleton["JobInfo"]["OutputFilename0"] = \ + output_filename_0.replace("\\", "/") - "Plugin": instance.data.get("mayaRenderPlugin", "MayaBatch"), - "Frames": "{start}-{end}x{step}".format( - start=int(instance.data["frameStartHandle"]), - end=int(instance.data["frameEndHandle"]), - step=int(instance.data["byFrameStep"]), - ), - - "Comment": comment, - - # Optional, enable double-click to preview rendered - # frames from Deadline Monitor - "OutputDirectory0": os.path.dirname(output_filename_0), - "OutputFilename0": output_filename_0.replace("\\", "/") - }, - "PluginInfo": { - # Input - "SceneFile": filepath, - - # Output directory and filename - "OutputFilePath": dirname.replace("\\", "/"), - "OutputFilePrefix": render_variables["filename_prefix"], - - # Mandatory for Deadline - "Version": cmds.about(version=True), - - # Only render layers are considered renderable in this pipeline - "UsingRenderLayers": True, - - # Render only this layer - "RenderLayer": renderlayer, - - # Determine which renderer to use from the file itself - "Renderer": instance.data["renderer"], - - # Resolve relative references - "ProjectPath": workspace, - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - exp = instance.data.get("expectedFiles") - - OutputFilenames = {} - expIndex = 0 - - if isinstance(exp[0], dict): - # we have aovs and we need to iterate over them - for aov, files in exp[0].items(): - col = clique.assemble(files)[0][0] - outputFile = col.format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(expIndex)] = outputFile # noqa: E501 - OutputFilenames[expIndex] = outputFile - expIndex += 1 - else: - col = clique.assemble(files)[0][0] - outputFile = col.format('{head}{padding}{tail}') - payload['JobInfo']['OutputFilename' + str(expIndex)] = outputFile - # OutputFilenames[expIndex] = outputFile + payload_skeleton["JobInfo"]["Comment"] = comment + # Handle environments ----------------------------------------------- # We need those to pass them to pype for it to set correct context keys = [ "FTRACK_API_KEY", @@ -298,27 +301,67 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) - payload["JobInfo"].update({ + payload_skeleton["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, value=environment[key] ) for index, key in enumerate(environment) }) - - # Include optional render globals + # Add options from RenderGlobals------------------------------------- render_globals = instance.data.get("renderGlobals", {}) - payload["JobInfo"].update(render_globals) + payload_skeleton["JobInfo"].update(render_globals) + + # Submit preceeding export jobs ------------------------------------- + export_job = None + if "vrayscene" in instance.data["families"]: + export_job = self._submit_export(payload_data, "vray") + + if "assscene" in instance.data["families"]: + export_job = self._submit_export(payload_data, "arnold") + + # Prepare main render job ------------------------------------------- + if "vrayscene" in instance.data["families"]: + payload = self._get_vray_render_payload(payload_data) + elif "assscene" in instance.data["families"]: + pass + else: + payload = self._get_maya_payload(payload_data) + + # Add export job as dependency -------------------------------------- + if export_job: + payload["JobInfo"]["JobDependency0"] = export_job + + # Add list of expected files to job --------------------------------- + exp = instance.data.get("expectedFiles") + + output_filenames = {} + exp_index = 0 + + if isinstance(exp[0], dict): + # we have aovs and we need to iterate over them + for aov, files in exp[0].items(): + col = clique.assemble(files)[0][0] + output_file = col.format('{head}{padding}{tail}') + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 + output_filenames[exp_index] = output_file + exp_index += 1 + else: + col = clique.assemble(files)[0][0] + output_file = col.format('{head}{padding}{tail}') + payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file + # OutputFilenames[exp_index] = output_file plugin = payload["JobInfo"]["Plugin"] self.log.info("using render plugin : {}".format(plugin)) self.preflight_check(instance) + # Submit job to farm ------------------------------------------------ self.log.info("Submitting ...") - self.log.info(json.dumps(payload, indent=4, sort_keys=True)) + self.log.debug(json.dumps(payload, indent=4, sort_keys=True)) # E.g. http://192.168.0.1:8082/api/jobs - url = "{}/api/jobs".format(DEADLINE_REST_URL) + url = "{}/api/jobs".format(self._deadline_url) response = self._requests_post(url, json=payload) if not response.ok: raise Exception(response.text) @@ -327,9 +370,118 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): instance.data["outputDir"] = os.path.dirname(filename_0) instance.data["deadlineSubmissionJob"] = response.json() - def preflight_check(self, instance): - """Ensure the startFrame, endFrame and byFrameStep are integers""" + def _get_maya_payload(self, data): + payload = copy.deepcopy(payload_skeleton) + job_info_ext = { + # Asset dependency to wait for at least the scene file to sync. + "AssetDependency0": data["filepath"], + } + + plugin_info = { + "SceneFile": data["filepath"], + # Output directory and filename + "OutputFilePath": data["dirname"].replace("\\", "/"), + "OutputFilePrefix": data["render_variables"]["filename_prefix"], # noqa: E501 + + # Only render layers are considered renderable in this pipeline + "UsingRenderLayers": True, + + # Render only this layer + "RenderLayer": data["renderlayer"], + + # Determine which renderer to use from the file itself + "Renderer": self._instance.data["renderer"], + + # Resolve relative references + "ProjectPath": data["workspace"], + } + payload["JobInfo"].update(job_info_ext) + payload["PluginInfo"].update(plugin_info) + return payload + + def _get_vray_export_payload(self, data): + payload = copy.deepcopy(payload_skeleton) + + job_info_ext = { + # Job name, as seen in Monitor + "Name": "Export {} [{}-{}]".format( + data["jobname"], + int(self._instance.data["frameStartHandle"]), + int(self._instance.data["frameEndHandle"])), + + "Plugin": "MayaBatch", + "FramesPerTask": self._instance.data.get("framesPerTask", 1) + } + + plugin_info = { + # Renderer + "Renderer": "vray", + # Input + "SceneFile": data["filepath"], + "SkipExistingFrames": True, + "UsingRenderLayers": True, + "UseLegacyRenderLayers": True + } + + payload["JobInfo"].update(job_info_ext) + payload["PluginInfo"].update(plugin_info) + return payload + + def _get_vray_render_payload(self, data): + payload = copy.deepcopy(payload_skeleton) + + first_file = data["output_filename_0"] + ext, _ = os.path.splitext(first_file) + first_file = first_file.replace(ext, "vrscene") + first_file = first_file.replace( + "#" * data["render_variables"]["padding"], + "{:04d}".format(int(self._instance.data["frameStartHandle"]))) + job_info_ext = { + "Name": "Render {} [{}-{}]".format( + data["jobname"], + int(self._instance.data["frameStartHandle"]), + int(self._instance.data["frameEndHandle"])), + + "Plugin": "Vray", + "OverrideTaskExtraInfoNames": False, + } + + plugin_info = { + "InputFilename": first_file, + "SeparateFilesPerFrame": True, + "VRayEngine": "V-Ray", + + "Width": self._instance.data["resolutionWidth"], + "Height": self._instance.data["resolutionHeight"], + } + + payload["JobInfo"].update(job_info_ext) + payload["PluginInfo"].update(plugin_info) + return payload + + def _submit_export(self, data, format): + if format == "vray": + payload = self._get_vray_export_payload(data) + self.log.info("Submitting vrscene export job.") + elif format == "ass": + payload = self._get_arnold_export_payload(data) + self.log.info("Submitting ass export job.") + + url = "{}/api/jobs".format(self._deadline_url) + response = self._requests_post(url, json=payload) + if not response.ok: + self.log.error("Submition failed!") + self.log.error(response.status_code) + self.log.error(response.content) + self.log.debug(payload) + raise RuntimeError(response.text) + + dependency = response.json() + return dependency["_id"] + + def preflight_check(self, instance): + """Ensure the startFrame, endFrame and byFrameStep are integers.""" for key in ("frameStartHandle", "frameEndHandle", "byFrameStep"): value = instance.data[key] @@ -342,14 +494,17 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): ) def _requests_post(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. + """Wrap request post method. - WARNING: disabling SSL certificate validation is defeating one line + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line of defense SSL is providing and it is not recommended. + """ if 'verify' not in kwargs: kwargs['verify'] = False if os.getenv("PYPE_DONT_VERIFY_SSL", True) else True # noqa @@ -358,14 +513,17 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): return requests.post(*args, **kwargs) def _requests_get(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. + """Wrap request get method. - WARNING: disabling SSL certificate validation is defeating one line + Disabling SSL certificate validation if ``DONT_VERIFY_SSL`` environment + variable is found. This is useful when Deadline or Muster server are + running with self-signed certificates and their certificate is not + added to trusted certificates on client machines. + + Warning: + Disabling SSL certificate validation is defeating one line of defense SSL is providing and it is not recommended. + """ if 'verify' not in kwargs: kwargs['verify'] = False if os.getenv("PYPE_DONT_VERIFY_SSL", True) else True # noqa diff --git a/pype/plugins/maya/publish/submit_vray_deadline.py b/pype/plugins/maya/publish/submit_vray_deadline.py index e9bdb7e377..4a8955942a 100644 --- a/pype/plugins/maya/publish/submit_vray_deadline.py +++ b/pype/plugins/maya/publish/submit_vray_deadline.py @@ -24,7 +24,7 @@ class VraySubmitDeadline(pyblish.api.InstancePlugin): label = "Submit to Deadline ( vrscene )" order = pyblish.api.IntegratorOrder hosts = ["maya"] - families = ["vrayscene"] + families = ["vrayscene_foo"] if not os.environ.get("DEADLINE_REST_URL"): optional = False active = False @@ -130,7 +130,7 @@ class VraySubmitDeadline(pyblish.api.InstancePlugin): start_frame = int(instance.data["frameStart"]) end_frame = int(instance.data["frameEnd"]) - ext = instance.data.get("ext", "exr") + ext = instance.data.get("ext") or "exr" # Create output directory for renders render_ouput = self.format_output_filename(instance, @@ -238,7 +238,7 @@ class VraySubmitDeadline(pyblish.api.InstancePlugin): for index, k in enumerate(env)} def format_output_filename(self, instance, filename, template, dir=False): - """Format the expected output file of the Export job + """Format the expected output file of the Export job. Example: /_/ From 64c31c01e31fba0c58e896a3933ba947f608324c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ond=C5=99ej=20Samohel?= Date: Fri, 22 May 2020 19:23:58 +0200 Subject: [PATCH 03/22] correct path for vrscene input --- .../maya/publish/submit_maya_deadline.py | 69 +++++++++++++++---- 1 file changed, 57 insertions(+), 12 deletions(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index e67e0ec50f..00e3c6ae62 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -284,6 +284,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): output_filename_0.replace("\\", "/") payload_skeleton["JobInfo"]["Comment"] = comment + payload_skeleton["PluginInfo"]["RenderLayer"] = renderlayer # Handle environments ----------------------------------------------- # We need those to pass them to pype for it to set correct context @@ -295,12 +296,13 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "AVALON_ASSET", "AVALON_TASK", "PYPE_USERNAME", - "PYPE_DEV" + "PYPE_DEV", + "PYPE_LOG_NO_COLORS" ] environment = dict({key: os.environ[key] for key in keys if key in os.environ}, **api.Session) - + environment["PYPE_LOG_NO_COLORS"] = "1" payload_skeleton["JobInfo"].update({ "EnvironmentKeyValue%d" % index: "{key}={value}".format( key=key, @@ -374,8 +376,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload = copy.deepcopy(payload_skeleton) job_info_ext = { - # Asset dependency to wait for at least the scene file to sync. - "AssetDependency0": data["filepath"], + # Asset dependency to wait for at least the scene file to sync. + "AssetDependency0": data["filepath"], } plugin_info = { @@ -402,7 +404,6 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): def _get_vray_export_payload(self, data): payload = copy.deepcopy(payload_skeleton) - job_info_ext = { # Job name, as seen in Monitor "Name": "Export {} [{}-{}]".format( @@ -421,7 +422,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "SceneFile": data["filepath"], "SkipExistingFrames": True, "UsingRenderLayers": True, - "UseLegacyRenderLayers": True + "UseLegacyRenderLayers": True, + "RenderLayer": data["renderlayer"], + "ProjectPath": data["workspace"] } payload["JobInfo"].update(job_info_ext) @@ -430,13 +433,14 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): def _get_vray_render_payload(self, data): payload = copy.deepcopy(payload_skeleton) + vray_settings = cmds.ls(type="VRaySettingsNode") + node = vray_settings[0] + template = cmds.getAttr("{}.vrscene_filename".format(node)) + # "vrayscene//_/" - first_file = data["output_filename_0"] - ext, _ = os.path.splitext(first_file) - first_file = first_file.replace(ext, "vrscene") - first_file = first_file.replace( - "#" * data["render_variables"]["padding"], - "{:04d}".format(int(self._instance.data["frameStartHandle"]))) + scene, _ = os.path.splitext(data["filename"]) + first_file = self.format_output_filename(scene, template) + first_file = "{}/{}".format(data["workspace"], first_file) job_info_ext = { "Name": "Render {} [{}-{}]".format( data["jobname"], @@ -530,3 +534,44 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # add 10sec timeout before bailing out kwargs['timeout'] = 10 return requests.get(*args, **kwargs) + + def format_output_filename(self, filename, template, dir=False): + """Format the expected output file of the Export job. + + Example: + /_/ + "shot010_v006/shot010_v006_CHARS/CHARS" + + Args: + instance: + filename(str): + dir(bool): + + Returns: + str + + """ + def smart_replace(string, key_values): + new_string = string + for key, value in key_values.items(): + new_string = new_string.replace(key, value) + return new_string + + # Ensure filename has no extension + file_name, _ = os.path.splitext(filename) + + # Reformat without tokens + output_path = smart_replace( + template, + {"": file_name, + "": self._instance.data['setMembers']}) + + if dir: + return output_path.replace("\\", "/") + + start_frame = int(self._instance.data["frameStartHandle"]) + filename_zero = "{}_{:04d}.vrscene".format(output_path, start_frame) + + result = filename_zero.replace("\\", "/") + + return result From e1f29399a4cf116267d1e61818c5fc4e9669e9f5 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Thu, 28 May 2020 13:53:58 +0200 Subject: [PATCH 04/22] arnold ass export wip --- pype/plugins/maya/publish/collect_render.py | 4 +- .../maya/publish/submit_maya_deadline.py | 131 +++++++++++++++--- pype/scripts/export_maya_ass_job.py | 101 ++++++++++++++ pype/scripts/export_maya_ass_sequence.mel | 67 +++++++++ 4 files changed, 283 insertions(+), 20 deletions(-) create mode 100644 pype/scripts/export_maya_ass_job.py create mode 100644 pype/scripts/export_maya_ass_sequence.mel diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 5cdae580f5..df1d3f4bd5 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -259,8 +259,8 @@ class CollectMayaRender(pyblish.api.ContextPlugin): if render_instance.data.get("vrayScene") is True: data["families"].append("vrayscene") - if render_instance.data.get("ass") is True: - data["families"].append("assScene") + if render_instance.data.get("assScene") is True: + data["families"].append("assscene") # Include (optional) global settings # Get global overrides and translate to Deadline values diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 00e3c6ae62..b5962bf2c8 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -179,6 +179,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): self.log.info("Using published scene for render {}".format( filepath)) + if not os.path.exists(filepath): + self.log.error("published scene does not exist!") + raise # now we need to switch scene in expected files # because token will now point to published # scene file and that might differ from current one @@ -261,6 +264,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload_data["render_variables"] = render_variables payload_data["renderlayer"] = renderlayer payload_data["workspace"] = workspace + payload_data["dirname"] = dirname frame_pattern = payload_skeleton["JobInfo"]["Frames"] payload_skeleton["JobInfo"]["Frames"] = frame_pattern.format( @@ -315,6 +319,9 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # Submit preceeding export jobs ------------------------------------- export_job = None + assert not all(x in instance.data["families"] + for x in ['vrayscene', 'assscene']), ( + "Vray Scene and Ass Scene options are mutually exclusive") if "vrayscene" in instance.data["families"]: export_job = self._submit_export(payload_data, "vray") @@ -325,7 +332,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if "vrayscene" in instance.data["families"]: payload = self._get_vray_render_payload(payload_data) elif "assscene" in instance.data["families"]: - pass + payload = self._get_arnold_render_payload(payload_data) else: payload = self._get_maya_payload(payload_data) @@ -381,22 +388,22 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): } plugin_info = { - "SceneFile": data["filepath"], - # Output directory and filename - "OutputFilePath": data["dirname"].replace("\\", "/"), - "OutputFilePrefix": data["render_variables"]["filename_prefix"], # noqa: E501 + "SceneFile": data["filepath"], + # Output directory and filename + "OutputFilePath": data["dirname"].replace("\\", "/"), + "OutputFilePrefix": data["render_variables"]["filename_prefix"], # noqa: E501 - # Only render layers are considered renderable in this pipeline - "UsingRenderLayers": True, + # Only render layers are considered renderable in this pipeline + "UsingRenderLayers": True, - # Render only this layer - "RenderLayer": data["renderlayer"], + # Render only this layer + "RenderLayer": data["renderlayer"], - # Determine which renderer to use from the file itself - "Renderer": self._instance.data["renderer"], + # Determine which renderer to use from the file itself + "Renderer": self._instance.data["renderer"], - # Resolve relative references - "ProjectPath": data["workspace"], + # Resolve relative references + "ProjectPath": data["workspace"], } payload["JobInfo"].update(job_info_ext) payload["PluginInfo"].update(plugin_info) @@ -415,7 +422,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): "FramesPerTask": self._instance.data.get("framesPerTask", 1) } - plugin_info = { + plugin_info_ext = { # Renderer "Renderer": "vray", # Input @@ -428,7 +435,73 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): } payload["JobInfo"].update(job_info_ext) - payload["PluginInfo"].update(plugin_info) + payload["PluginInfo"].update(plugin_info_ext) + return payload + + def _get_arnold_export_payload(self, data): + + try: + from pype.scripts import export_maya_ass_job + except Exception: + assert False, ( + "Expected module 'export_maya_ass_job' to be available") + + module_path = export_maya_ass_job.__file__ + if module_path.endswith(".pyc"): + module_path = module_path[: -len(".pyc")] + ".py" + + script = os.path.normpath(module_path) + + payload = copy.deepcopy(payload_skeleton) + job_info_ext = { + # Job name, as seen in Monitor + "Name": "Export {} [{}-{}]".format( + data["jobname"], + int(self._instance.data["frameStartHandle"]), + int(self._instance.data["frameEndHandle"])), + + "Plugin": "Python", + "FramesPerTask": self._instance.data.get("framesPerTask", 1) + } + + plugin_info_ext = { + "Version": "3.6", + "ScriptFile": script, + "Arguments": "", + "SingleFrameOnly": "True", + } + payload["JobInfo"].update(job_info_ext) + payload["PluginInfo"].update(plugin_info_ext) + + envs = [] + for k, v in payload["JobInfo"].items(): + if k.startswith("EnvironmentKeyValue"): + envs.append(v) + + # add app name to environment + envs.append( + "AVALON_APP_NAME={}".format(os.environ.get("AVALON_APP_NAME"))) + envs.append( + "PYPE_ASS_EXPORT_RENDER_LAYER={}".format(data["renderlayer"])) + envs.append( + "PYPE_ASS_EXPORT_SCENE_FILE={}".format(data["filepath"])) + envs.append( + "PYPE_ASS_EXPORT_OUTPUT={}".format( + payload['JobInfo']['OutputFilename0'])) + envs.append( + "PYPE_ASS_EXPORT_START={}".format( + int(self._instance.data["frameStartHandle"]))) + envs.append( + "PYPE_ASS_EXPORT_END={}".format( + int(self._instance.data["frameEndHandle"]))) + envs.append( + "PYPE_ASS_EXPORT_STEP={}".format(1)) + + i = 0 + for e in envs: + payload["JobInfo"]["EnvironmentKeyValue{}".format(i)] = e + i += 1 + return payload def _get_vray_render_payload(self, data): @@ -439,7 +512,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): # "vrayscene//_/" scene, _ = os.path.splitext(data["filename"]) - first_file = self.format_output_filename(scene, template) + first_file = self.format_vray_output_filename(scene, template) first_file = "{}/{}".format(data["workspace"], first_file) job_info_ext = { "Name": "Render {} [{}-{}]".format( @@ -464,11 +537,33 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): payload["PluginInfo"].update(plugin_info) return payload + def _get_arnold_render_payload(self, data): + payload = copy.deepcopy(payload_skeleton) + ass_file, _ = os.path.splitext(data["output_filename_0"]) + first_file = ass_file + ".ass" + job_info_ext = { + "Name": "Render {} [{}-{}]".format( + data["jobname"], + int(self._instance.data["frameStartHandle"]), + int(self._instance.data["frameEndHandle"])), + + "Plugin": "Arnold", + "OverrideTaskExtraInfoNames": False, + } + + plugin_info = { + "ArnoldFile": first_file, + } + + payload["JobInfo"].update(job_info_ext) + payload["PluginInfo"].update(plugin_info) + return payload + def _submit_export(self, data, format): if format == "vray": payload = self._get_vray_export_payload(data) self.log.info("Submitting vrscene export job.") - elif format == "ass": + elif format == "arnold": payload = self._get_arnold_export_payload(data) self.log.info("Submitting ass export job.") @@ -535,7 +630,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): kwargs['timeout'] = 10 return requests.get(*args, **kwargs) - def format_output_filename(self, filename, template, dir=False): + def format_vray_output_filename(self, filename, template, dir=False): """Format the expected output file of the Export job. Example: diff --git a/pype/scripts/export_maya_ass_job.py b/pype/scripts/export_maya_ass_job.py new file mode 100644 index 0000000000..d343eec131 --- /dev/null +++ b/pype/scripts/export_maya_ass_job.py @@ -0,0 +1,101 @@ +"""This module is used for command line exporting of ASS files.""" + +import os +import argparse +import logging +import subprocess +import platform + +try: + from shutil import which +except ImportError: + # we are in python < 3.3 + def which(command): + path = os.getenv('PATH') + for p in path.split(os.path.pathsep): + p = os.path.join(p, command) + if os.path.exists(p) and os.access(p, os.X_OK): + return p + +handler = logging.basicConfig() +log = logging.getLogger("Publish Image Sequences") +log.setLevel(logging.DEBUG) + +error_format = "Failed {plugin.__name__}: {error} -- {error.traceback}" + + +def __main__(): + parser = argparse.ArgumentParser() + parser.add_argument("--paths", + nargs="*", + default=[], + help="The filepaths to publish. This can be a " + "directory or a path to a .json publish " + "configuration.") + parser.add_argument("--gui", + default=False, + action="store_true", + help="Whether to run Pyblish in GUI mode.") + + parser.add_argument("--pype", help="Pype root") + + kwargs, args = parser.parse_known_args() + + print("Running pype ...") + auto_pype_root = os.path.dirname(os.path.abspath(__file__)) + auto_pype_root = os.path.abspath(auto_pype_root + "../../../../..") + + auto_pype_root = os.environ.get('PYPE_SETUP_PATH') or auto_pype_root + if os.environ.get('PYPE_SETUP_PATH'): + print("Got Pype location from environment: {}".format( + os.environ.get('PYPE_SETUP_PATH'))) + + pype_command = "pype.ps1" + if platform.system().lower() == "linux": + pype_command = "pype" + elif platform.system().lower() == "windows": + pype_command = "pype.bat" + + if kwargs.pype: + pype_root = kwargs.pype + else: + # test if pype.bat / pype is in the PATH + # if it is, which() will return its path and we use that. + # if not, we use auto_pype_root path. Caveat of that one is + # that it can be UNC path and that will not work on windows. + + pype_path = which(pype_command) + + if pype_path: + pype_root = os.path.dirname(pype_path) + else: + pype_root = auto_pype_root + + print("Set pype root to: {}".format(pype_root)) + print("Paths: {}".format(kwargs.paths or [os.getcwd()])) + + # paths = kwargs.paths or [os.environ.get("PYPE_METADATA_FILE")] or [os.getcwd()] # noqa + + mayabatch = os.environ.get("AVALON_APP_NAME").replace("maya", "mayabatch") + args = [ + os.path.join(pype_root, pype_command), + "launch", + "--app", + mayabatch, + "-script", + os.path.join(pype_root, "repos", "pype", + "pype", "scripts", "export_maya_ass_sequence.mel") + ] + + print("Pype command: {}".format(" ".join(args))) + # Forcing forwaring the environment because environment inheritance does + # not always work. + # Cast all values in environment to str to be safe + env = {k: str(v) for k, v in os.environ.items()} + exit_code = subprocess.call(args, env=env) + if exit_code != 0: + raise RuntimeError("Publishing failed.") + + +if __name__ == '__main__': + __main__() diff --git a/pype/scripts/export_maya_ass_sequence.mel b/pype/scripts/export_maya_ass_sequence.mel new file mode 100644 index 0000000000..83d1d010ac --- /dev/null +++ b/pype/scripts/export_maya_ass_sequence.mel @@ -0,0 +1,67 @@ +/* + Script to export specified layer as ass files. + +Attributes: + + scene_file (str): Name of the scene to load. + start (int): Start frame. + end (int): End frame. + step (int): Step size. + output_path (str): File output path. + render_layer (str): Name of render layer. + +*/ + +$scene_file=`getenv "PYPE_ASS_EXPORT_SCENE_FILE"`; +$step=`getenv "PYPE_ASS_EXPORT_STEP"`; +$start=`getenv "PYPE_ASS_EXPORT_START"`; +$end=`getenv "PYPE_ASS_EXPORT_END"`; +$file_path=`getenv "PYPE_ASS_EXPORT_OUTPUT"`; +$render_layer = `getenv "PYPE_ASS_EXPORT_RENDER_LAYER"`; + +print("*** ASS Export Plugin\n"); + +if ($scene_file == "") { + print("!!! cannot determine scene file\n"); + quit -a -ex -1; +} + +if ($step == "") { + print("!!! cannot determine step size\n"); + quit -a -ex -1; +} + +if ($start == "") { + print("!!! cannot determine start frame\n"); + quit -a -ex -1; +} + +if ($end == "") { + print("!!! cannot determine end frame\n"); + quit -a -ex -1; +} + +if ($file_path == "") { + print("!!! cannot determine output file\n"); + quit -a -ex -1; +} + +if ($render_layer == "") { + print("!!! cannot determine render layer\n"); + quit -a -ex -1; +} + + +print(">>> Opening Scene [ " + $scene_file + " ]\n"); + +// open scene +file -o -f $scene_file; + +// switch to render layer +print(">>> Switching layer [ "+ $render_layer + " ]\n"); +editRenderLayerGlobals -currentRenderLayer $render_layer; + +// export +print(">>> Exporting to [ " + $file_path + " ]\n"); +arnoldExportAss -mask 255 -sl 1 -ll 1 -bb 1 -sf $start -se $end -b -fs $step; +print("--- Done\n"); From 97a01aec4b192e7907021dd964ef13b9884fbfa6 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Fri, 29 May 2020 16:41:31 +0200 Subject: [PATCH 05/22] limit ass export job frame range on deadline --- pype/plugins/maya/publish/submit_maya_deadline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index b5962bf2c8..5441438b1a 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -461,7 +461,8 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): int(self._instance.data["frameEndHandle"])), "Plugin": "Python", - "FramesPerTask": self._instance.data.get("framesPerTask", 1) + "FramesPerTask": self._instance.data.get("framesPerTask", 1), + "Frames": 1 } plugin_info_ext = { From 7ad9d4d087316d764541f632eb232a8edb0fcf03 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Sun, 31 May 2020 17:53:18 +0200 Subject: [PATCH 06/22] redshift fixes --- pype/maya/expected_files.py | 42 ++++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/pype/maya/expected_files.py b/pype/maya/expected_files.py index d103c1bc27..a737268354 100644 --- a/pype/maya/expected_files.py +++ b/pype/maya/expected_files.py @@ -51,7 +51,7 @@ 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".*((?:%l)|(?:)|(?:)).*", re.IGNORECASE ) R_AOV_TOKEN = re.compile(r".*%a.*|.*.*|.*.*", re.IGNORECASE) R_SUBSTITUTE_AOV_TOKEN = re.compile(r"%a||", re.IGNORECASE) @@ -633,8 +633,13 @@ class ExpectedFilesRedshift(AExpectedFiles): ext_mapping (list): Mapping redshift extension dropdown values to strings. + unmerged_aovs (list): Name of aovs that are not merged into resulting + exr and we need them specified in expectedFiles output. + """ + unmerged_aovs = ["Cryptomatte"] + ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] def __init__(self, layer): @@ -673,7 +678,17 @@ class ExpectedFilesRedshift(AExpectedFiles): if layer_data.get("enabledAOVs"): expected_files[0][u"beauty"] = self._generate_single_file_sequence( layer_data - ) # noqa: E501 + ) + + # 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.get("enabledAOVs"): + if aov[0].lower() == "cryptomatte": + aov_name = aov[0] + expected_files.append( + {aov_name: self._generate_single_file_sequence( + layer_data, aov_name=aov_name)}) return expected_files @@ -715,9 +730,26 @@ class ExpectedFilesRedshift(AExpectedFiles): enabled = self.maya_is_true(override) if enabled: - enabled_aovs.append( - (cmds.getAttr("%s.name" % aov), default_ext) - ) + # 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 From 3259438b94b615fce8b176ad365550302d296fad Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Tue, 2 Jun 2020 12:02:20 +0200 Subject: [PATCH 07/22] hound fixes and removal of unused files --- .../maya/publish/collect_vray_scene.py | 112 ------- .../maya/publish/submit_maya_deadline.py | 4 +- .../maya/publish/submit_vray_deadline.py | 293 ------------------ 3 files changed, 2 insertions(+), 407 deletions(-) delete mode 100644 pype/plugins/maya/publish/collect_vray_scene.py delete mode 100644 pype/plugins/maya/publish/submit_vray_deadline.py diff --git a/pype/plugins/maya/publish/collect_vray_scene.py b/pype/plugins/maya/publish/collect_vray_scene.py deleted file mode 100644 index df7e5c1ab6..0000000000 --- a/pype/plugins/maya/publish/collect_vray_scene.py +++ /dev/null @@ -1,112 +0,0 @@ -# -*- coding: utf-8 -*- -"""Collect .vrayscene instance.""" -import os - -import pyblish.api - -from maya import cmds - -from avalon import api -from pype.maya.expected_files import ExpectedFiles - - -class CollectVRayScene(pyblish.api.ContextPlugin): - """Collect all information prior for exporting vrscenes.""" - - order = pyblish.api.CollectorOrder - label = "Collect VRay Scene" - hosts = ["foo"] - - def process(self, context): - """Collector entry point.""" - # Sort by displayOrder - def sort_by_display_order(layer): - return cmds.getAttr("%s.displayOrder" % layer) - - host = api.registered_host() - - asset = api.Session["AVALON_ASSET"] - work_dir = context.data["workspaceDir"] - - # Get VRay Scene instance - vray_scenes = host.lsattr("family", "vrayscene") - if not vray_scenes: - self.log.info("Skipping vrayScene collection, no " - "vrayscene instance found..") - return - - assert len(vray_scenes) == 1, "Multiple vrayscene instances found!" - vray_scene = vray_scenes[0] - - vrscene_data = host.read(vray_scene) - - assert cmds.ls("vraySettings", type="VRaySettingsNode"), ( - "VRay Settings node does not exists. " - "Please ensure V-Ray is the current renderer." - ) - - # Output data - start_frame = int(cmds.getAttr("defaultRenderGlobals.startFrame")) - end_frame = int(cmds.getAttr("defaultRenderGlobals.endFrame")) - - # Create output file path with template - file_name = context.data["currentFile"].replace("\\", "/") - vrscene = ("vrayscene", "", "_", "") - vrscene_output = os.path.join(work_dir, *vrscene) - - # Check and create render output template for render job - # outputDir is required for submit_publish_job - if not vrscene_data.get("suspendRenderJob", False): - renders = ("renders", "", "_", "") - output_renderpath = os.path.join(work_dir, *renders) - vrscene_data["outputDir"] = output_renderpath - - # Get resolution - resolution = (cmds.getAttr("defaultResolution.width"), - cmds.getAttr("defaultResolution.height")) - - # Get format extension - extension = cmds.getAttr("vraySettings.imageFormatStr") - - # Get render layers - render_layers = [i for i in cmds.ls(type="renderLayer") if - cmds.getAttr("{}.renderable".format(i)) and not - cmds.referenceQuery(i, isNodeReferenced=True)] - - render_layers = sorted(render_layers, key=sort_by_display_order) - for layer in render_layers: - - subset = layer - if subset == "defaultRenderLayer": - subset = "masterLayer" - - data = { - "subset": subset, - "setMembers": layer, - - "frameStart": start_frame, - "frameEnd": end_frame, - "renderer": "vray", - "resolution": resolution, - "ext": ".{}".format(extension), - - # instance subset - "family": "VRay Scene", - "families": ["vrayscene"], - "asset": asset, - "time": api.time(), - "author": context.data["user"], - - # Add source to allow tracing back to the scene from - # which was submitted originally - "source": file_name, - - # Store VRay Scene additional data - "vrsceneOutput": vrscene_output - } - - data.update(vrscene_data) - - instance = context.create_instance(subset) - self.log.info("Created: %s" % instance.name) - instance.data.update(data) diff --git a/pype/plugins/maya/publish/submit_maya_deadline.py b/pype/plugins/maya/publish/submit_maya_deadline.py index 1e58215ed8..5a8b2f6e5a 100644 --- a/pype/plugins/maya/publish/submit_maya_deadline.py +++ b/pype/plugins/maya/publish/submit_maya_deadline.py @@ -358,7 +358,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): if isinstance(exp[0], dict): # we have aovs and we need to iterate over them - for aov, files in exp[0].items(): + for _aov, files in exp[0].items(): col = clique.assemble(files)[0][0] output_file = col.format('{head}{padding}{tail}') payload['JobInfo']['OutputFilename' + str(exp_index)] = output_file # noqa: E501 @@ -453,7 +453,7 @@ class MayaSubmitDeadline(pyblish.api.InstancePlugin): try: from pype.scripts import export_maya_ass_job except Exception: - assert False, ( + raise AssertionError( "Expected module 'export_maya_ass_job' to be available") module_path = export_maya_ass_job.__file__ diff --git a/pype/plugins/maya/publish/submit_vray_deadline.py b/pype/plugins/maya/publish/submit_vray_deadline.py deleted file mode 100644 index 4a8955942a..0000000000 --- a/pype/plugins/maya/publish/submit_vray_deadline.py +++ /dev/null @@ -1,293 +0,0 @@ -import getpass -import json -import os -from copy import deepcopy - -import pyblish.api - -from avalon import api -from avalon.vendor import requests - -from maya import cmds - - -class VraySubmitDeadline(pyblish.api.InstancePlugin): - """Export the scene to `.vrscene` files per frame per render layer - - vrscene files will be written out based on the following template: - /vrayscene//_/ - - A dependency job will be added for each layer to render the framer - through VRay Standalone - - """ - label = "Submit to Deadline ( vrscene )" - order = pyblish.api.IntegratorOrder - hosts = ["maya"] - families = ["vrayscene_foo"] - if not os.environ.get("DEADLINE_REST_URL"): - optional = False - active = False - else: - optional = True - - def process(self, instance): - - DEADLINE_REST_URL = api.Session.get("DEADLINE_REST_URL", - "http://localhost:8082") - assert DEADLINE_REST_URL, "Requires DEADLINE_REST_URL" - - context = instance.context - - deadline_url = "{}/api/jobs".format(DEADLINE_REST_URL) - deadline_user = context.data.get("deadlineUser", getpass.getuser()) - - filepath = context.data["currentFile"] - filename = os.path.basename(filepath) - task_name = "{} - {}".format(filename, instance.name) - - batch_name = "{} - (vrscene)".format(filename) - - # Get the output template for vrscenes - vrscene_output = instance.data["vrsceneOutput"] - - # This is also the input file for the render job - first_file = self.format_output_filename(instance, - filename, - vrscene_output) - - start_frame = int(instance.data["frameStart"]) - end_frame = int(instance.data["frameEnd"]) - - # Primary job - self.log.info("Submitting export job ..") - - payload = { - "JobInfo": { - # Top-level group name - "BatchName": batch_name, - - # Job name, as seen in Monitor - "Name": "Export {} [{}-{}]".format(task_name, - start_frame, - end_frame), - - # Arbitrary username, for visualisation in Monitor - "UserName": deadline_user, - - "Plugin": "MayaBatch", - "Frames": "{}-{}".format(start_frame, end_frame), - "FramesPerTask": instance.data.get("framesPerTask", 1), - - "Comment": context.data.get("comment", ""), - - "OutputFilename0": os.path.dirname(first_file), - }, - "PluginInfo": { - - # Renderer - "Renderer": "vray", - - # Mandatory for Deadline - "Version": cmds.about(version=True), - - # Input - "SceneFile": filepath, - - "SkipExistingFrames": True, - - "UsingRenderLayers": True, - - "UseLegacyRenderLayers": True - }, - - # Mandatory for Deadline, may be empty - "AuxFiles": [] - } - - environment = dict(AVALON_TOOLS="global;python36;maya2018") - environment.update(api.Session.copy()) - - jobinfo_environment = self.build_jobinfo_environment(environment) - - payload["JobInfo"].update(jobinfo_environment) - - self.log.info("Job Data:\n{}".format(json.dumps(payload))) - - response = self._requests_post(url=deadline_url, json=payload) - if not response.ok: - raise RuntimeError(response.text) - - # Secondary job - # Store job to create dependency chain - dependency = response.json() - - if instance.data["suspendRenderJob"]: - self.log.info("Skipping render job and publish job") - return - - self.log.info("Submitting render job ..") - - start_frame = int(instance.data["frameStart"]) - end_frame = int(instance.data["frameEnd"]) - ext = instance.data.get("ext") or "exr" - - # Create output directory for renders - render_ouput = self.format_output_filename(instance, - filename, - instance.data["outputDir"], - dir=True) - - self.log.info("Render output: %s" % render_ouput) - - # Update output dir - instance.data["outputDir"] = render_ouput - - # Format output file name - sequence_filename = ".".join([instance.name, ext]) - output_filename = os.path.join(render_ouput, sequence_filename) - - # Ensure folder exists: - if not os.path.exists(render_ouput): - os.makedirs(render_ouput) - - payload_b = { - "JobInfo": { - - "JobDependency0": dependency["_id"], - "BatchName": batch_name, - "Name": "Render {} [{}-{}]".format(task_name, - start_frame, - end_frame), - "UserName": deadline_user, - - "Frames": "{}-{}".format(start_frame, end_frame), - - "Plugin": "Vray", - "OverrideTaskExtraInfoNames": False, - - "OutputFilename0": render_ouput, - }, - "PluginInfo": { - - "InputFilename": first_file, - "OutputFilename": output_filename, - "SeparateFilesPerFrame": True, - "VRayEngine": "V-Ray", - - "Width": instance.data["resolution"][0], - "Height": instance.data["resolution"][1], - - }, - "AuxFiles": [], - } - - # Add vray renderslave to environment - tools = environment["AVALON_TOOLS"] + ";vrayrenderslave" - environment_b = deepcopy(environment) - environment_b["AVALON_TOOLS"] = tools - - jobinfo_environment_b = self.build_jobinfo_environment(environment_b) - payload_b["JobInfo"].update(jobinfo_environment_b) - - self.log.info(json.dumps(payload_b)) - - # Post job to deadline - response_b = self._requests_post(url=deadline_url, json=payload_b) - if not response_b.ok: - raise RuntimeError(response_b.text) - - # Add job for publish job - if not instance.data.get("suspendPublishJob", False): - instance.data["deadlineSubmissionJob"] = response_b.json() - - def build_command(self, instance): - """Create command for Render.exe to export vray scene - - Args: - instance - - Returns: - str - - """ - - cmd = ('-r vray -proj {project} -cam {cam} -noRender -s {startFrame} ' - '-e {endFrame} -rl {layer} -exportFramesSeparate') - - # Get the camera - cammera = instance.data["cameras"][0] - - return cmd.format(project=instance.context.data["workspaceDir"], - cam=cammera, - startFrame=instance.data["frameStart"], - endFrame=instance.data["frameEnd"], - layer=instance.name) - - def build_jobinfo_environment(self, env): - """Format environment keys and values to match Deadline rquirements - - Args: - env(dict): environment dictionary - - Returns: - dict - - """ - return {"EnvironmentKeyValue%d" % index: "%s=%s" % (k, env[k]) - for index, k in enumerate(env)} - - def format_output_filename(self, instance, filename, template, dir=False): - """Format the expected output file of the Export job. - - Example: - /_/ - "shot010_v006/shot010_v006_CHARS/CHARS" - - Args: - instance: - filename(str): - dir(bool): - - Returns: - str - - """ - - def smart_replace(string, key_values): - new_string = string - for key, value in key_values.items(): - new_string = new_string.replace(key, value) - return new_string - - # Ensure filename has no extension - file_name, _ = os.path.splitext(filename) - - # Reformat without tokens - output_path = smart_replace(template, - {"": file_name, - "": instance.name}) - - if dir: - return output_path.replace("\\", "/") - - start_frame = int(instance.data["frameStart"]) - filename_zero = "{}_{:04d}.vrscene".format(output_path, start_frame) - - result = filename_zero.replace("\\", "/") - - return result - - def _requests_post(self, *args, **kwargs): - """ Wrapper for requests, disabling SSL certificate validation if - DONT_VERIFY_SSL environment variable is found. This is useful when - Deadline or Muster server are running with self-signed certificates - and their certificate is not added to trusted certificates on - client machines. - - WARNING: disabling SSL certificate validation is defeating one line - of defense SSL is providing and it is not recommended. - """ - if 'verify' not in kwargs: - kwargs['verify'] = False if os.getenv("PYPE_DONT_VERIFY_SSL", True) else True # noqa - return requests.post(*args, **kwargs) From d8f1a0ae32a92e6421af36130f8c8c2c9fe4b397 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Mon, 8 Jun 2020 09:43:11 +0200 Subject: [PATCH 08/22] fixed merge marks --- pype/plugins/maya/publish/collect_render.py | 428 -------------------- 1 file changed, 428 deletions(-) diff --git a/pype/plugins/maya/publish/collect_render.py b/pype/plugins/maya/publish/collect_render.py index 0e2d16403c..445f646cb5 100644 --- a/pype/plugins/maya/publish/collect_render.py +++ b/pype/plugins/maya/publish/collect_render.py @@ -53,47 +53,6 @@ from pype.hosts.maya.expected_files import ExpectedFiles from pype.hosts.maya import lib -<<<<<<< HEAD -======= -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)|(?:)|(?:))", - 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 -ImagePrefixes = { - "mentalray": "defaultRenderGlobals.imageFilePrefix", - "vray": "vraySettings.fileNamePrefix", - "arnold": "defaultRenderGlobals.imageFilePrefix", - "renderman": "rmanGlobals.imageFileFormat", - "redshift": "defaultRenderGlobals.imageFilePrefix", -} - - ->>>>>>> origin/develop class CollectMayaRender(pyblish.api.ContextPlugin): """Gather all publishable render layers from renderSetup.""" @@ -410,218 +369,7 @@ class CollectMayaRender(pyblish.api.ContextPlugin): return rset.getOverrides() def get_render_attribute(self, attr, layer): -<<<<<<< HEAD """Get attribute from render options. -======= - return lib.get_attr_in_layer( - "defaultRenderGlobals.{}".format(attr), layer=layer - ) - - -class ExpectedFiles: - multipart = False - - def get(self, renderer, layer): - renderSetup.instance().switchToLayerUsingLegacyName(layer) - - if renderer.lower() == "arnold": - return self._get_files(ExpectedFilesArnold(layer)) - elif renderer.lower() == "vray": - return self._get_files(ExpectedFilesVray(layer)) - elif renderer.lower() == "redshift": - return self._get_files(ExpectedFilesRedshift(layer)) - elif renderer.lower() == "mentalray": - return self._get_files(ExpectedFilesMentalray(layer)) - elif renderer.lower() == "renderman": - return self._get_files(ExpectedFilesRenderman(layer)) - else: - raise UnsupportedRendererException( - "unsupported {}".format(renderer) - ) - - def _get_files(self, renderer): - files = renderer.get_files() - self.multipart = renderer.multipart - return files - - -@six.add_metaclass(ABCMeta) -class AExpectedFiles: - renderer = None - layer = None - multipart = False - - def __init__(self, layer): - self.layer = layer - - @abstractmethod - def get_aovs(self): - pass - - def get_renderer_prefix(self): - try: - file_prefix = cmds.getAttr(ImagePrefixes[self.renderer]) - except KeyError: - raise UnsupportedRendererException( - "Unsupported renderer {}".format(self.renderer) - ) - return file_prefix - - def _get_layer_data(self): - # ______________________________________________ - # ____________________/ ____________________________________________/ - # 1 - get scene name /__________________/ - # ____________________/ - scene_dir, scene_basename = os.path.split(cmds.file(q=True, loc=True)) - scene_name, _ = os.path.splitext(scene_basename) - - # ______________________________________________ - # ____________________/ ____________________________________________/ - # 2 - detect renderer /__________________/ - # ____________________/ - renderer = self.renderer - - # ________________________________________________ - # __________________/ ______________________________________________/ - # 3 - image prefix /__________________/ - # __________________/ - file_prefix = self.get_renderer_prefix() - - if not file_prefix: - raise RuntimeError("Image prefix not set") - - default_ext = cmds.getAttr("defaultRenderGlobals.imfPluginKey") - - # ________________________________________________ - # __________________/ ______________________________________________/ - # 4 - get renderable cameras_____________/ - # __________________/ - - # if we have token in prefix path we'll expect output for - # every renderable camera in layer. - - renderable_cameras = self.get_renderable_cameras() - # ________________________________________________ - # __________________/ ______________________________________________/ - # 5 - get AOVs /____________________/ - # __________________/ - - enabled_aovs = self.get_aovs() - - layer_name = self.layer - if self.layer.startswith("rs_"): - layer_name = self.layer[3:] - start_frame = int(self.get_render_attribute("startFrame")) - end_frame = int(self.get_render_attribute("endFrame")) - frame_step = int(self.get_render_attribute("byFrameStep")) - padding = int(self.get_render_attribute("extensionPadding")) - - scene_data = { - "frameStart": start_frame, - "frameEnd": end_frame, - "frameStep": frame_step, - "padding": padding, - "cameras": renderable_cameras, - "sceneName": scene_name, - "layerName": layer_name, - "renderer": renderer, - "defaultExt": default_ext, - "filePrefix": file_prefix, - "enabledAOVs": enabled_aovs, - } - return scene_data - - def _generate_single_file_sequence(self, layer_data, aov_name=None): - expected_files = [] - file_prefix = layer_data["filePrefix"] - for cam in layer_data["cameras"]: - mappings = [ - (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, cam), - (R_CLEAN_FRAME_TOKEN, ""), - (R_CLEAN_EXT_TOKEN, ""), - ] - # this is required to remove unfilled aov token, for example - # in Redshift - if aov_name: - mappings.append((R_SUBSTITUTE_AOV_TOKEN, aov_name)) - else: - mappings.append((R_REMOVE_AOV_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): - expected_files = [] - aov_file_list = {} - file_prefix = layer_data["filePrefix"] - for aov in layer_data["enabledAOVs"]: - for cam in layer_data["cameras"]: - - mappings = ( - (R_SUBSTITUTE_SCENE_TOKEN, layer_data["sceneName"]), - (R_SUBSTITUTE_LAYER_TOKEN, layer_data["layerName"]), - (R_SUBSTITUTE_CAMERA_TOKEN, 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], 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): - """ - This method will 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() ->>>>>>> origin/develop Args: attr (str): name of attribute to be looked up. @@ -633,179 +381,3 @@ class AExpectedFiles: return lib.get_attr_in_layer( "defaultRenderGlobals.{}".format(attr), layer=layer ) -<<<<<<< HEAD -======= - pass_type = vray_node_attr.rsplit("_", 1)[-1] - - # Support V-Ray extratex explicit name (if set by user) - if pass_type == "extratex": - explicit_attr = "{}.vray_explicit_name_extratex".format(node) - explicit_name = cmds.getAttr(explicit_attr) - if explicit_name: - return explicit_name - - # Node type is in the attribute name but we need to check if value - # of the attribute as it can be changed - return cmds.getAttr("{}.{}".format(node, vray_node_attr)) - - -class ExpectedFilesRedshift(AExpectedFiles): - - # mapping redshift extension dropdown values to strings - ext_mapping = ["iff", "exr", "tif", "png", "tga", "jpg"] - - # 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): - super(ExpectedFilesRedshift, self).__init__(layer) - self.renderer = "redshift" - - def get_renderer_prefix(self): - prefix = super(ExpectedFilesRedshift, self).get_renderer_prefix() - prefix = "{}.".format(prefix) - return prefix - - def get_files(self): - expected_files = super(ExpectedFilesRedshift, self).get_files() - - # we need to add one sequence for plain beauty if AOVs are enabled. - # as redshift output beauty without 'beauty' in filename. - - layer_data = self._get_layer_data() - if layer_data.get("enabledAOVs"): - expected_files[0][u"beauty"] = self._generate_single_file_sequence( - 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.get("enabledAOVs"): - if aov[0].lower() == "cryptomatte": - aov_name = aov[0] - expected_files.append( - {aov_name: self._generate_single_file_sequence( - layer_data, aov_name=aov_name)}) - - return expected_files - - def get_aovs(self): - enabled_aovs = [] - - try: - default_ext = self.ext_mapping[ - cmds.getAttr("redshiftOptions.imageFormat") - ] - except ValueError: - # this occurs when Render Setting windows was not opened yet. In - # such case there are no Redshift options created so query - # will fail. - raise ValueError("Render settings are not initialized") - - rs_aovs = [n for n in cmds.ls(type="RedshiftAOV")] - - # todo: find out how to detect multichannel exr for redshift - 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), self.layer - ): - 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): - def __init__(self, layer): - super(ExpectedFilesRenderman, self).__init__(layer) - self.renderer = "renderman" - - def get_aovs(self): - 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), self.layer - ): - enabled = self.maya_is_true(override) - - if enabled: - enabled_aovs.append((aov_name, default_ext)) - - return enabled_aovs - - def get_files(self): - """ - In renderman we hack it with prepending path. This path would - normally be translated from `rmanGlobals.imageOutputDir`. We skip - this and harcode 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): - def __init__(self, layer): - raise UnimplementedRendererException("Mentalray not implemented") - - def get_aovs(self): - return [] - - -class AOVError(Exception): - pass - - -class UnsupportedRendererException(Exception): - pass - - -class UnimplementedRendererException(Exception): - pass ->>>>>>> origin/develop From 8c9dfbf6b06b52f8c90d88d86dc9eddace1d7e18 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Jun 2020 11:18:09 +0200 Subject: [PATCH 09/22] removed deprecated collector where already deleted output_representation is collected --- .../publish/collect_output_repre_config.py | 22 ------------------- 1 file changed, 22 deletions(-) delete mode 100644 pype/plugins/global/publish/collect_output_repre_config.py diff --git a/pype/plugins/global/publish/collect_output_repre_config.py b/pype/plugins/global/publish/collect_output_repre_config.py deleted file mode 100644 index 063af9ba26..0000000000 --- a/pype/plugins/global/publish/collect_output_repre_config.py +++ /dev/null @@ -1,22 +0,0 @@ -""" -Requires: - config_data -> ftrack.output_representation - -Provides: - context -> output_repre_config (str) -""" - -import pyblish.api -from pype.api import config - - -class CollectOutputRepreConfig(pyblish.api.ContextPlugin): - """Inject the current working file into context""" - - order = pyblish.api.CollectorOrder - label = "Collect Config for representation" - hosts = ["shell", "standalonepublisher"] - - def process(self, context): - config_data = config.get_presets()["ftrack"]["output_representation"] - context.data['output_repre_config'] = config_data From f3ddc513c3fe2d0e53ed632ff9399361b1a4c7e6 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Jun 2020 11:23:35 +0200 Subject: [PATCH 10/22] changes to be able show logs by process --- pype/modules/logging/gui/app.py | 8 +- pype/modules/logging/gui/models.py | 86 +++++++---- pype/modules/logging/gui/widgets.py | 214 ++++++++++++++-------------- 3 files changed, 168 insertions(+), 140 deletions(-) diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 74849b23bc..19256af1a8 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -1,6 +1,6 @@ from Qt import QtWidgets, QtCore -from .widgets import LogsWidget, LogDetailWidget -from avalon import style +from .widgets import LogsWidget, OutputWidget +from pypeapp import style class LogsWindow(QtWidgets.QWidget): @@ -10,7 +10,7 @@ class LogsWindow(QtWidgets.QWidget): self.setStyleSheet(style.load_stylesheet()) self.resize(1200, 800) logs_widget = LogsWidget(parent=self) - log_detail = LogDetailWidget(parent=self) + log_detail = OutputWidget(parent=self) main_layout = QtWidgets.QHBoxLayout() @@ -33,7 +33,5 @@ class LogsWindow(QtWidgets.QWidget): def on_selection_changed(self): index = self.logs_widget.selected_log() - if not index or not index.isValid(): - return node = index.data(self.logs_widget.model.NodeRole) self.log_detail.set_detail(node) diff --git a/pype/modules/logging/gui/models.py b/pype/modules/logging/gui/models.py index 2ef79554fe..484fd6dc69 100644 --- a/pype/modules/logging/gui/models.py +++ b/pype/modules/logging/gui/models.py @@ -1,4 +1,5 @@ import os +import collections from Qt import QtCore from pype.api import Logger from pypeapp.lib.log import _bootstrap_mongo_log @@ -8,31 +9,32 @@ log = Logger().get_logger("LogModel", "LoggingModule") class LogModel(QtCore.QAbstractItemModel): COLUMNS = [ - "user", - "host", - "lineNumber", - "method", - "module", - "fileName", - "loggerName", - "message", - "level", - "timestamp", + "process_name", + "hostname", + "hostip", + "username", + "system_name", + "started" ] colums_mapping = { - "user": "User", - "host": "Host", - "lineNumber": "Line n.", - "method": "Method", - "module": "Module", - "fileName": "File name", - "loggerName": "Logger name", - "message": "Message", - "level": "Level", - "timestamp": "Timestamp", + "process_name": "Process Name", + "process_id": "Process Id", + "hostname": "Hostname", + "hostip": "Host IP", + "username": "Username", + "system_name": "System name", + "started": "Started at" } - + process_keys = [ + "process_id", "hostname", "hostip", + "username", "system_name", "process_name" + ] + log_keys = [ + "timestamp", "level", "thread", "threadName", "message", "loggerName", + "fileName", "module", "method", "lineNumber" + ] + default_value = "- Not set -" NodeRole = QtCore.Qt.UserRole + 1 def __init__(self, parent=None): @@ -50,14 +52,47 @@ class LogModel(QtCore.QAbstractItemModel): self._root_node.add_child(node) def refresh(self): + self.log_by_process = collections.defaultdict(list) + self.process_info = {} + self.clear() self.beginResetModel() if self.dbcon: result = self.dbcon.find({}) for item in result: - self.add_log(item) - self.endResetModel() + process_id = item.get("process_id") + # backwards (in)compatibility + if not process_id: + continue + if process_id not in self.process_info: + proc_dict = {} + for key in self.process_keys: + proc_dict[key] = ( + item.get(key) or self.default_value + ) + self.process_info[process_id] = proc_dict + + if "_logs" not in self.process_info[process_id]: + self.process_info[process_id]["_logs"] = [] + + log_item = {} + for key in self.log_keys: + log_item[key] = item.get(key) or self.default_value + + if "exception" in item: + log_item["exception"] = item["exception"] + + self.process_info[process_id]["_logs"].append(log_item) + + for item in self.process_info.values(): + item["_logs"] = sorted( + item["_logs"], key=lambda item: item["timestamp"] + ) + item["started"] = item["_logs"][0]["timestamp"] + self.add_log(item) + + self.endResetModel() def data(self, index, role): if not index.isValid(): @@ -68,7 +103,7 @@ class LogModel(QtCore.QAbstractItemModel): column = index.column() key = self.COLUMNS[column] - if key == "timestamp": + if key == "started": return str(node.get(key, None)) return node.get(key, None) @@ -86,8 +121,7 @@ class LogModel(QtCore.QAbstractItemModel): child_item = parent_node.child(row) if child_item: return self.createIndex(row, column, child_item) - else: - return QtCore.QModelIndex() + return QtCore.QModelIndex() def rowCount(self, parent): node = self._root_node diff --git a/pype/modules/logging/gui/widgets.py b/pype/modules/logging/gui/widgets.py index 1daaa28326..cf20066397 100644 --- a/pype/modules/logging/gui/widgets.py +++ b/pype/modules/logging/gui/widgets.py @@ -1,5 +1,5 @@ -import getpass from Qt import QtCore, QtWidgets, QtGui +from PyQt5.QtCore import QVariant from .models import LogModel @@ -97,7 +97,6 @@ class SelectableMenu(QtWidgets.QMenu): class CustomCombo(QtWidgets.QWidget): selection_changed = QtCore.Signal() - checked_changed = QtCore.Signal(bool) def __init__(self, title, parent=None): super(CustomCombo, self).__init__(parent) @@ -126,27 +125,12 @@ class CustomCombo(QtWidgets.QWidget): self.toolmenu.clear() self.addItems(items) - def select_items(self, items, ignore_input=False): - if not isinstance(items, list): - items = [items] - - for action in self.toolmenu.actions(): - check = True - if ( - action.text() in items and ignore_input or - action.text() not in items and not ignore_input - ): - check = False - - action.setChecked(check) - def addItems(self, items): for item in items: action = self.toolmenu.addAction(item) action.setCheckable(True) - self.toolmenu.addAction(action) action.setChecked(True) - action.triggered.connect(self.checked_changed) + self.toolmenu.addAction(action) def items(self): for action in self.toolmenu.actions(): @@ -200,42 +184,15 @@ class CheckableComboBox(QtWidgets.QComboBox): for text, checked in items: text_item = QtGui.QStandardItem(text) checked_item = QtGui.QStandardItem() - checked_item.setData( - QtCore.QVariant(checked), QtCore.Qt.CheckStateRole - ) + checked_item.setData(QVariant(checked), QtCore.Qt.CheckStateRole) self.model.appendRow([text_item, checked_item]) -class FilterLogModel(QtCore.QSortFilterProxyModel): - sub_dict = ["$gt", "$lt", "$not"] - def __init__(self, key_values, parent=None): - super(FilterLogModel, self).__init__(parent) - self.allowed_key_values = key_values - - def filterAcceptsRow(self, row, parent): - """ - Reimplemented from base class. - """ - model = self.sourceModel() - for key, values in self.allowed_key_values.items(): - col_indx = model.COLUMNS.index(key) - value = model.index(row, col_indx, parent).data( - QtCore.Qt.DisplayRole - ) - if value not in values: - return False - return True - - class LogsWidget(QtWidgets.QWidget): """A widget that lists the published subsets for an asset""" active_changed = QtCore.Signal() - _level_order = [ - "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL" - ] - def __init__(self, parent=None): super(LogsWidget, self).__init__(parent=parent) @@ -243,41 +200,47 @@ class LogsWidget(QtWidgets.QWidget): filter_layout = QtWidgets.QHBoxLayout() + # user_filter = SearchComboBox(self, "Users") user_filter = CustomCombo("Users", self) users = model.dbcon.distinct("user") user_filter.populate(users) - user_filter.checked_changed.connect(self.user_changed) - user_filter.select_items(getpass.getuser()) + user_filter.selection_changed.connect(self.user_changed) level_filter = CustomCombo("Levels", self) + # levels = [(level, True) for level in model.dbcon.distinct("level")] levels = model.dbcon.distinct("level") - _levels = [] - for level in self._level_order: - if level in levels: - _levels.append(level) - level_filter.populate(_levels) - level_filter.checked_changed.connect(self.level_changed) + level_filter.addItems(levels) - # date_from_label = QtWidgets.QLabel("From:") - # date_filter_from = QtWidgets.QDateTimeEdit() - # - # date_from_layout = QtWidgets.QVBoxLayout() - # date_from_layout.addWidget(date_from_label) - # date_from_layout.addWidget(date_filter_from) - # - # date_to_label = QtWidgets.QLabel("To:") - # date_filter_to = QtWidgets.QDateTimeEdit() - # - # date_to_layout = QtWidgets.QVBoxLayout() - # date_to_layout.addWidget(date_to_label) - # date_to_layout.addWidget(date_filter_to) + date_from_label = QtWidgets.QLabel("From:") + date_filter_from = QtWidgets.QDateTimeEdit() + + date_from_layout = QtWidgets.QVBoxLayout() + date_from_layout.addWidget(date_from_label) + date_from_layout.addWidget(date_filter_from) + + # now = datetime.datetime.now() + # QtCore.QDateTime( + # now.year, + # now.month, + # now.day, + # now.hour, + # now.minute, + # second=0, + # msec=0, + # timeSpec=0 + # ) + date_to_label = QtWidgets.QLabel("To:") + date_filter_to = QtWidgets.QDateTimeEdit() + + date_to_layout = QtWidgets.QVBoxLayout() + date_to_layout.addWidget(date_to_label) + date_to_layout.addWidget(date_filter_to) filter_layout.addWidget(user_filter) filter_layout.addWidget(level_filter) - filter_layout.setAlignment(QtCore.Qt.AlignLeft) - # filter_layout.addLayout(date_from_layout) - # filter_layout.addLayout(date_to_layout) + filter_layout.addLayout(date_from_layout) + filter_layout.addLayout(date_to_layout) view = QtWidgets.QTreeView(self) view.setAllColumnsShowFocus(True) @@ -290,58 +253,28 @@ class LogsWidget(QtWidgets.QWidget): view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) view.setSortingEnabled(True) view.sortByColumn( - model.COLUMNS.index("timestamp"), + model.COLUMNS.index("started"), QtCore.Qt.AscendingOrder ) - key_val = { - "user": users, - "level": levels - } - proxy_model = FilterLogModel(key_val, view) - proxy_model.setSourceModel(model) - view.setModel(proxy_model) - - view.customContextMenuRequested.connect(self.on_context_menu) - view.selectionModel().selectionChanged.connect(self.active_changed) - - # WARNING this is cool but slows down widget a lot - # header = view.header() - # # Enforce the columns to fit the data (purely cosmetic) - # if Qt.__binding__ in ("PySide2", "PyQt5"): - # header.setSectionResizeMode(QtWidgets.QHeaderView.ResizeToContents) - # else: - # header.setResizeMode(QtWidgets.QHeaderView.ResizeToContents) - + view.setModel(model) + view.pressed.connect(self._on_activated) # prepare model.refresh() # Store to memory self.model = model - self.proxy_model = proxy_model self.view = view self.user_filter = user_filter self.level_filter = level_filter + def _on_activated(self, *args, **kwargs): + self.active_changed.emit() + def user_changed(self): - valid_actions = [] for action in self.user_filter.items(): - if action.isChecked(): - valid_actions.append(action.text()) - - self.proxy_model.allowed_key_values["user"] = valid_actions - self.proxy_model.invalidate() - - def level_changed(self): - valid_actions = [] - for action in self.level_filter.items(): - if action.isChecked(): - valid_actions.append(action.text()) - - self.proxy_model.allowed_key_values["level"] = valid_actions - self.proxy_model.invalidate() - + print(action) def on_context_menu(self, point): # TODO will be any actions? it's ready @@ -360,10 +293,74 @@ class LogsWidget(QtWidgets.QWidget): rows = selection.selectedRows(column=0) if len(rows) == 1: return rows[0] - return None +class OutputWidget(QtWidgets.QWidget): + def __init__(self, parent=None): + super(OutputWidget, self).__init__(parent=parent) + layout = QtWidgets.QVBoxLayout(self) + output_text = QtWidgets.QTextEdit() + output_text.setReadOnly(True) + # output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth) + + layout.addWidget(output_text) + + self.setLayout(layout) + self.output_text = output_text + + def add_line(self, line): + self.output_text.append(line) + + def set_detail(self, node): + self.output_text.clear() + for log in node["_logs"]: + level = log["level"].lower() + + line_f = "{message}" + if level == "debug": + line_f = ( + " -" + " {{ {loggerName} }}: [" + " {message}" + " ]" + ) + elif level == "info": + line_f = ( + ">>> [" + " {message}" + " ]" + ) + elif level == "warning": + line_f = ( + "*** WRN:" + " >>> {{ {loggerName} }}: [" + " {message}" + " ]" + ) + elif level == "error": + line_f = ( + "!!! ERR:" + " {timestamp}" + " >>> {{ {loggerName} }}: [" + " {message}" + " ]" + ) + + exc = log.get("exception") + if exc: + log["message"] = exc["message"] + + line = line_f.format(**log) + + self.add_line(line) + + if not exc: + continue + for _line in exc["stackTrace"].split("\n"): + self.add_line(_line) + + class LogDetailWidget(QtWidgets.QWidget): """A Widget that display information about a specific version""" data_rows = [ @@ -418,5 +415,4 @@ class LogDetailWidget(QtWidgets.QWidget): value = detail_data.get(row) or "< Not set >" data[row] = value - self.detail_widget.setHtml(self.html_text.format(**data)) From 1c84f31520e9967250450b85225194eee5783cc0 Mon Sep 17 00:00:00 2001 From: iLLiCiTiT Date: Mon, 8 Jun 2020 18:10:07 +0200 Subject: [PATCH 11/22] fix bad imports --- pype/modules/logging/gui/app.py | 2 +- pype/modules/logging/tray/logging_module.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pype/modules/logging/gui/app.py b/pype/modules/logging/gui/app.py index 19256af1a8..99b0b230a9 100644 --- a/pype/modules/logging/gui/app.py +++ b/pype/modules/logging/gui/app.py @@ -1,6 +1,6 @@ from Qt import QtWidgets, QtCore from .widgets import LogsWidget, OutputWidget -from pypeapp import style +from avalon import style class LogsWindow(QtWidgets.QWidget): diff --git a/pype/modules/logging/tray/logging_module.py b/pype/modules/logging/tray/logging_module.py index 15bec1aad5..087a51f322 100644 --- a/pype/modules/logging/tray/logging_module.py +++ b/pype/modules/logging/tray/logging_module.py @@ -1,4 +1,3 @@ -import os from Qt import QtWidgets from pype.api import Logger From 7dbc94eda767b3eeb5f1d20c4a5379a997df2aee Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Jun 2020 10:06:39 +0100 Subject: [PATCH 12/22] Support of collections in Rig assets --- pype/plugins/blender/create/create_rig.py | 17 +++-------------- pype/plugins/blender/load/load_rig.py | 13 +++++++------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/pype/plugins/blender/create/create_rig.py b/pype/plugins/blender/create/create_rig.py index 855c2ab461..5c85bf969d 100644 --- a/pype/plugins/blender/create/create_rig.py +++ b/pype/plugins/blender/create/create_rig.py @@ -31,22 +31,11 @@ class CreateRig(Creator): # This links automatically the children meshes if they were not # selected, and doesn't link them twice if they, insted, # were manually selected by the user. - objects_to_link = set() if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - - objects_to_link.add(obj) - - if obj.type == 'ARMATURE': - - for subobj in obj.children: - - objects_to_link.add(subobj) - - for obj in objects_to_link: - - collection.objects.link(obj) + for child in obj.users_collection[0].children: + collection.children.link(child) + collection.objects.link(obj) return collection diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 634a627a4a..2b5d7fe0d7 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -40,6 +40,9 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): elif obj.type == 'MESH': bpy.data.meshes.remove(obj.data) + for child in bpy.data.collections[lib_container].children: + bpy.data.collections.remove(child) + bpy.data.collections.remove(bpy.data.collections[lib_container]) @staticmethod @@ -57,32 +60,30 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): rig_container = scene.collection.children[lib_container].make_local() - meshes = [obj for obj in rig_container.objects if obj.type == 'MESH'] + meshes = [] armatures = [ obj for obj in rig_container.objects if obj.type == 'ARMATURE'] objects_list = [] - assert(len(armatures) == 1) + for child in rig_container.children: + child.make_local() + meshes.extend( child.objects ) # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - obj = obj.make_local() - obj.data.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[blender.pipeline.AVALON_PROPERTY] avalon_info.update({"container_name": container_name}) if obj.type == 'ARMATURE' and action is not None: - obj.animation_data.action = action objects_list.append(obj) From 08ca6dda9bb63bea9e82a58e41aa47f54239c9a7 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Jun 2020 10:25:06 +0100 Subject: [PATCH 13/22] Support of collections in Layout assets --- pype/plugins/blender/create/create_layout.py | 14 +------------ pype/plugins/blender/load/load_layout.py | 22 +++++++++++++------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/pype/plugins/blender/create/create_layout.py b/pype/plugins/blender/create/create_layout.py index 2d2b0e72ca..010eec539b 100644 --- a/pype/plugins/blender/create/create_layout.py +++ b/pype/plugins/blender/create/create_layout.py @@ -34,19 +34,7 @@ class CreateLayout(Creator): objects_to_link = set() if (self.options or {}).get("useSelection"): - for obj in lib.get_selection(): - - objects_to_link.add(obj) - - if obj.type == 'ARMATURE': - - for subobj in obj.children: - - objects_to_link.add(subobj) - - for obj in objects_to_link: - - collection.objects.link(obj) + collection.children.link(obj.users_collection[0]) return collection diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index 9158f71c75..b4e71fb183 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -39,6 +39,11 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): elif obj.type == 'MESH': bpy.data.meshes.remove(obj.data) + for element_container in bpy.data.collections[lib_container].children: + for child in element_container.children: + bpy.data.collections.remove(child) + bpy.data.collections.remove(element_container) + bpy.data.collections.remove(bpy.data.collections[lib_container]) @staticmethod @@ -56,24 +61,26 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): layout_container = scene.collection.children[lib_container].make_local() - meshes = [ - obj for obj in layout_container.objects if obj.type == 'MESH'] - armatures = [ - obj for obj in layout_container.objects if obj.type == 'ARMATURE'] + meshes = [] + armatures = [] objects_list = [] + for element_container in layout_container.children: + element_container.make_local() + armatures.extend([obj for obj in element_container.objects if obj.type == 'ARMATURE']) + for child in element_container.children: + child.make_local() + meshes.extend(child.objects) + # Link meshes first, then armatures. # The armature is unparented for all the non-local meshes, # when it is made local. for obj in meshes + armatures: - obj = obj.make_local() - obj.data.make_local() if not obj.get(blender.pipeline.AVALON_PROPERTY): - obj[blender.pipeline.AVALON_PROPERTY] = dict() avalon_info = obj[blender.pipeline.AVALON_PROPERTY] @@ -82,7 +89,6 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): action = actions.get( obj.name, None ) if obj.type == 'ARMATURE' and action is not None: - obj.animation_data.action = action objects_list.append(obj) From 1531057a24c28493e41f200287c863f385d9fc59 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Jun 2020 10:26:28 +0100 Subject: [PATCH 14/22] Refactoring --- pype/plugins/blender/load/load_animation.py | 14 ++++---------- pype/plugins/blender/load/load_layout.py | 10 ++++------ pype/plugins/blender/load/load_model.py | 10 ++++------ pype/plugins/blender/load/load_rig.py | 10 ++++------ 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/pype/plugins/blender/load/load_animation.py b/pype/plugins/blender/load/load_animation.py index a26f402d47..1c0e6e0906 100644 --- a/pype/plugins/blender/load/load_animation.py +++ b/pype/plugins/blender/load/load_animation.py @@ -29,7 +29,6 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod def _remove(self, objects, lib_container): for obj in objects: @@ -41,7 +40,6 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): bpy.data.collections.remove(bpy.data.collections[lib_container]) - @staticmethod def _process(self, libpath, lib_container, container_name): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -131,7 +129,7 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["lib_container"] = lib_container objects_list = self._process( - self, libpath, lib_container, container_name) + libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -205,14 +203,10 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - # Get the armature of the rig - armatures = [obj for obj in objects if obj.type == 'ARMATURE'] - assert(len(armatures) == 1) - - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) objects_list = self._process( - self, str(libpath), lib_container, collection.name) + str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -249,7 +243,7 @@ class BlendAnimationLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index b4e71fb183..4247d25caa 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -29,7 +29,6 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod def _remove(self, objects, lib_container): for obj in objects: @@ -46,7 +45,6 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): bpy.data.collections.remove(bpy.data.collections[lib_container]) - @staticmethod def _process(self, libpath, lib_container, container_name, actions): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -136,7 +134,7 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["lib_container"] = lib_container objects_list = self._process( - self, libpath, lib_container, container_name, {}) + libpath, lib_container, container_name, {}) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -218,10 +216,10 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): actions[obj.name] = obj.animation_data.action - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) objects_list = self._process( - self, str(libpath), lib_container, collection.name, actions) + str(libpath), lib_container, collection.name, actions) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -258,7 +256,7 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/load/load_model.py b/pype/plugins/blender/load/load_model.py index 2abc4175c6..4a8f43cd48 100644 --- a/pype/plugins/blender/load/load_model.py +++ b/pype/plugins/blender/load/load_model.py @@ -30,7 +30,6 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod def _remove(self, objects, lib_container): for obj in objects: @@ -39,7 +38,6 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): bpy.data.collections.remove(bpy.data.collections[lib_container]) - @staticmethod def _process(self, libpath, lib_container, container_name): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -118,7 +116,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["lib_container"] = lib_container objects_list = self._process( - self, libpath, lib_container, container_name) + libpath, lib_container, container_name) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -189,10 +187,10 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): logger.info("Library already loaded, not updating...") return - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) objects_list = self._process( - self, str(libpath), lib_container, collection.name) + str(libpath), lib_container, collection.name) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -226,7 +224,7 @@ class BlendModelLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) bpy.data.collections.remove(collection) diff --git a/pype/plugins/blender/load/load_rig.py b/pype/plugins/blender/load/load_rig.py index 2b5d7fe0d7..3e53ff0363 100644 --- a/pype/plugins/blender/load/load_rig.py +++ b/pype/plugins/blender/load/load_rig.py @@ -30,7 +30,6 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): icon = "code-fork" color = "orange" - @staticmethod def _remove(self, objects, lib_container): for obj in objects: @@ -45,7 +44,6 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): bpy.data.collections.remove(bpy.data.collections[lib_container]) - @staticmethod def _process(self, libpath, lib_container, container_name, action): relative = bpy.context.preferences.filepaths.use_relative_paths @@ -131,7 +129,7 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): container_metadata["lib_container"] = lib_container objects_list = self._process( - self, libpath, lib_container, container_name, None) + libpath, lib_container, container_name, None) # Save the list of objects in the metadata container container_metadata["objects"] = objects_list @@ -210,10 +208,10 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): action = armatures[0].animation_data.action - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) objects_list = self._process( - self, str(libpath), lib_container, collection.name, action) + str(libpath), lib_container, collection.name, action) # Save the list of objects in the metadata container collection_metadata["objects"] = objects_list @@ -250,7 +248,7 @@ class BlendRigLoader(pype.hosts.blender.plugin.AssetLoader): objects = collection_metadata["objects"] lib_container = collection_metadata["lib_container"] - self._remove(self, objects, lib_container) + self._remove(objects, lib_container) bpy.data.collections.remove(collection) From bd69ae2b546d7468bb13f361a307d9a5fc8f0099 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 10 Jun 2020 11:09:04 +0100 Subject: [PATCH 15/22] Initial Photoshop integration. --- pype/hosts/photoshop/__init__.py | 24 +++++++ pype/plugins/global/publish/integrate_new.py | 2 +- pype/plugins/photoshop/create/create_image.py | 12 ++++ pype/plugins/photoshop/load/load_image.py | 43 ++++++++++++ .../photoshop/publish/collect_current_file.py | 17 +++++ .../photoshop/publish/collect_instances.py | 55 ++++++++++++++++ .../photoshop/publish/extract_image.py | 62 ++++++++++++++++++ res/app_icons/photoshop.png | Bin 0 -> 10669 bytes 8 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 pype/hosts/photoshop/__init__.py create mode 100644 pype/plugins/photoshop/create/create_image.py create mode 100644 pype/plugins/photoshop/load/load_image.py create mode 100644 pype/plugins/photoshop/publish/collect_current_file.py create mode 100644 pype/plugins/photoshop/publish/collect_instances.py create mode 100644 pype/plugins/photoshop/publish/extract_image.py create mode 100644 res/app_icons/photoshop.png diff --git a/pype/hosts/photoshop/__init__.py b/pype/hosts/photoshop/__init__.py new file mode 100644 index 0000000000..709fb24bf5 --- /dev/null +++ b/pype/hosts/photoshop/__init__.py @@ -0,0 +1,24 @@ +import os + +from avalon import api +import pyblish.api + + +def install(): + print("Installing Pype config...") + + plugins_directory = os.path.join( + os.path.dirname(os.path.dirname(os.path.dirname(__file__))), + "plugins", + "photoshop" + ) + + pyblish.api.register_plugin_path( + os.path.join(plugins_directory, "publish") + ) + api.register_plugin_path( + api.Loader, os.path.join(plugins_directory, "load") + ) + api.register_plugin_path( + api.Creator, os.path.join(plugins_directory, "create") + ) diff --git a/pype/plugins/global/publish/integrate_new.py b/pype/plugins/global/publish/integrate_new.py index adff8aa3fa..f8429e8b58 100644 --- a/pype/plugins/global/publish/integrate_new.py +++ b/pype/plugins/global/publish/integrate_new.py @@ -77,7 +77,7 @@ class IntegrateAssetNew(pyblish.api.InstancePlugin): "gizmo", "source", "matchmove", - "image" + "image", "source", "assembly", "fbx", diff --git a/pype/plugins/photoshop/create/create_image.py b/pype/plugins/photoshop/create/create_image.py new file mode 100644 index 0000000000..a840dd13a7 --- /dev/null +++ b/pype/plugins/photoshop/create/create_image.py @@ -0,0 +1,12 @@ +from avalon import photoshop + + +class CreateImage(photoshop.Creator): + """Image folder for publish.""" + + name = "imageDefault" + label = "Image" + family = "image" + + def __init__(self, *args, **kwargs): + super(CreateImage, self).__init__(*args, **kwargs) diff --git a/pype/plugins/photoshop/load/load_image.py b/pype/plugins/photoshop/load/load_image.py new file mode 100644 index 0000000000..18efe750d5 --- /dev/null +++ b/pype/plugins/photoshop/load/load_image.py @@ -0,0 +1,43 @@ +from avalon import api, photoshop + + +class ImageLoader(api.Loader): + """Load images + + Stores the imported asset in a container named after the asset. + """ + + families = ["image"] + representations = ["*"] + + def load(self, context, name=None, namespace=None, data=None): + with photoshop.maintained_selection(): + layer = photoshop.import_smart_object(self.fname) + + self[:] = [layer] + + return photoshop.containerise( + name, + namespace, + layer, + context, + self.__class__.__name__ + ) + + def update(self, container, representation): + layer = container.pop("layer") + + with photoshop.maintained_selection(): + photoshop.replace_smart_object( + layer, api.get_representation_path(representation) + ) + + photoshop.imprint( + layer, {"representation": str(representation["_id"])} + ) + + def remove(self, container): + container["layer"].Delete() + + def switch(self, container, representation): + self.update(container, representation) diff --git a/pype/plugins/photoshop/publish/collect_current_file.py b/pype/plugins/photoshop/publish/collect_current_file.py new file mode 100644 index 0000000000..4308588559 --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_current_file.py @@ -0,0 +1,17 @@ +import os + +import pyblish.api +from avalon import photoshop + + +class CollectCurrentFile(pyblish.api.ContextPlugin): + """Inject the current working file into context""" + + order = pyblish.api.CollectorOrder - 0.5 + label = "Current File" + hosts = ["photoshop"] + + def process(self, context): + context.data["currentFile"] = os.path.normpath( + photoshop.app().ActiveDocument.FullName + ).replace("\\", "/") diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py new file mode 100644 index 0000000000..b253365465 --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -0,0 +1,55 @@ +import pythoncom + +from avalon import photoshop + +import pyblish.api + + +class CollectInstances(pyblish.api.ContextPlugin): + """Gather instances by LayerSet and file metadata + + This collector takes into account assets that are associated with + an LayerSet and marked with a unique identifier; + + Identifier: + id (str): "pyblish.avalon.instance" + """ + + label = "Instances" + order = pyblish.api.CollectorOrder + hosts = ["photoshop"] + families_mapping = { + "image": [] + } + + def process(self, context): + # Necessary call when running in a different thread which pyblish-qml + # can be. + pythoncom.CoInitialize() + + for layer in photoshop.get_layers_in_document(): + layer_data = photoshop.read(layer) + + # Skip layers without metadata. + if layer_data is None: + continue + + # Skip containers. + if "container" in layer_data["id"]: + continue + + child_layers = [*layer.Layers] + if not child_layers: + self.log.info("%s skipped, it was empty." % layer.Name) + continue + + instance = context.create_instance(layer.Name) + instance.append(layer) + instance.data.update(layer_data) + instance.data["families"] = self.families_mapping[ + layer_data["family"] + ] + + # Produce diagnostic message for any graphical + # user interface interested in visualising it. + self.log.info("Found: \"%s\" " % instance.data["name"]) diff --git a/pype/plugins/photoshop/publish/extract_image.py b/pype/plugins/photoshop/publish/extract_image.py new file mode 100644 index 0000000000..da3197c7da --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_image.py @@ -0,0 +1,62 @@ +import os + +import pype.api +from avalon import photoshop + + +class ExtractImage(pype.api.Extractor): + """Produce a flattened image file from instance + + This plug-in takes into account only the layers in the group. + """ + + label = "Extract Image" + hosts = ["photoshop"] + families = ["image"] + + def process(self, instance): + + staging_dir = self.staging_dir(instance) + self.log.info("Outputting image to {}".format(staging_dir)) + + # Perform extraction + files = {} + with photoshop.maintained_selection(): + self.log.info("Extracting %s" % str(list(instance))) + with photoshop.maintained_visibility(): + # Hide all other layers. + extract_ids = [ + x.id for x in photoshop.get_layers_in_layers([instance[0]]) + ] + for layer in photoshop.get_layers_in_document(): + if layer.id not in extract_ids: + layer.Visible = False + + save_options = { + "png": photoshop.com_objects.PNGSaveOptions(), + "jpg": photoshop.com_objects.JPEGSaveOptions() + } + + for extension, save_option in save_options.items(): + photoshop.app().ActiveDocument.SaveAs( + staging_dir, save_option, True + ) + files[extension] = "{} copy.{}".format( + os.path.splitext( + photoshop.app().ActiveDocument.Name + )[0], + extension + ) + + representations = [] + for extension, filename in files.items(): + representations.append({ + "name": extension, + "ext": extension, + "files": filename, + "stagingDir": staging_dir + }) + instance.data["representations"] = representations + instance.data["stagingDir"] = staging_dir + + self.log.info(f"Extracted {instance} to {staging_dir}") diff --git a/res/app_icons/photoshop.png b/res/app_icons/photoshop.png new file mode 100644 index 0000000000000000000000000000000000000000..c7e9d1471124e9a6a3cef8144f7ca169e6ce9758 GIT binary patch literal 10669 zcmcI~cUV(hvoDH*6sdxtS7`zXy@uYKRB2K|5;_UJcR~?E6X~Edks`fF6A%*5MXF2B3d*|O($324fPk_Zi)cf%T@{h`4r>1HNCSTzL%6>a zTmr!=E5j`9D+wrYL3+ZNeO;VU9+JLL)_>`h1dgvygISsXCF1D>WtG3~kl93Amst_* zj${_)hw#B65J6@!34Q@lVIffnFS8&-5CVn>f(3;5AOeyQF-bu|=D%O8KyU5{TSq+uQkGK&|aQURsiW=Cb+o%qZZ}y_b>qi2KI%yf(7^?*Czd2P+R-I7j<#@ zN7};>4Elgha;7}ktk2Le|Ls}|8uRYm%H=7EJeV3Hjp*+SkV-I5B$V~qYkUw&fUqP)R8mCZ;kEl9e~D@Xae#n%!v1H%XoQ2U z-~U8ZTU$~c<>3iK!IA1pP*wmGzk>rpQWP$T5EQo+;*$`zwdJ#c2?Jjuf;M~zh@g!a zTmU9$ixB@8F5r44G~DZY_^z-2D{K*HIH2R75t2j*3Jbwt5;lAiA}|P_Fhbad&&C!| zDJUdh3xf%Z3Q52a|JL@%-2s?6Fz5fM^%@ld(1?^khzKI!Hhf|*5dl77q%e|CTu{J< z4=G`bfQyRQ2-(=!uredylD24f7Z?yH4lXb|B-j;Y$IAS_(I}#w(eB!41Tf}8|M9%K zqN1)l+Sb7tXztAjEfw%rG4v^vX z;Xf=4{P~B+ktjg5JHXOAwo$;sz-FVaByZ@Oy*29>Xn64cYG;CXmAwmtkt2H!>b1TT ztKd(-dXRsU&)!Y3&dm8k`4LaU$cW{;$L8z$E!?tO;g-WZ)bFgm>ZiokIp?d~;?XU@ zauZnJOp;Sm6U^|iZ3@ZGCQD?u6HG=3?)k|u`W@p8WbY~O4jcuvmGRM&jApfJ7ptBh zYtrS_(0n;}6~{r|xPu>1Z_j#TieC2zMqYmB&?s;zA-btVTCJ@|UPkSg(z$@58KZEg-zf8?uLTMCND0y+f_+d=9AwXQ)IQux zoDdO6@amV-eieD2=Iq-xG>BBE@29m7w9LHIME~m$-O-@tvpVjsKY6>fxGh(K2WmCi=N7r9KQ6YPE$44RH5p&lZCJIo%t5EI4WjI z%)k;Z-YY*Rpt)L$uXd_U4YUQln+cTMX!d`_AHBm5es|RzB}J6e4NOSMB>kjrGt_tika9!04`PA*s^m9cu&o3mLY1EusFnhBFzT zDnpEVnVNS`_#<~}7|z@3GKPmmHC;$L2yV$uTj`8`3mIzNfCfs56&U%(IT=>9jKqZ1MOCvjMBRV+_$(d;hkE>Rb=J@*d`FRQ z9_c0Pzic^^Yzz6-&e&mOIJ3M`#%SAh+rioY(mB|1*_ta)`YPuf0PT~7p+u%9H!DN4nA&Y z$K*dEJfXb3fdAc_0A zr0|j9{TI#eV;i5%8i~iJ>g9f4QWQvLf`(wA!I^GazK(r0y#0RLwaaM^m8T1m$81cqGo4>l};TAAaC-rXmgWyVomRoIKs*UH!3oDVEEf2NB zxhP_1Q&c7=&MZX(^~M@^<2Ez?P;~Ee_Y3%yT$kByp)_mDakXu9-roNgp=zH(){<> z4qmbEHg8+t?#*Q|M&P*)T^8>CAZgW+Ioz#~wC?;q8LJ+~OIFmF_T=j&@A__hMe^N=lEPrU2NcLGJ-Ej8H!t)SA!-6 zPGLRrpK4zTjGucs^@}?aIfbs}%ytyXji$zJw!74f#VTYgmHk;Xt;QD_gqL{NNyPj7 zocsX#d4yM${Z_HP#lTUSMMwTKgHFG}k!b~k&l&f=Xr6*5NQ?>gJSCnk>t@eoHAVXo zZpJ#tNAAfN_u}85H+$n6O08Tc1z1p5_Y-3KdgIO zOvJ4uND$aKtY7Jf6q@|{mKR&6xL0vh)iR@ZUnd3ud-PRd`l{r0kz)qaW;8B!{2qzi z(ivIrsmi*wEf|6F{%rjdS>_TqJx3>&8Uv0|BO|d|Q+% z{6Ll0;-TG5qT&9|HayJnq75Agcb=NzzmtU79-$QVir6_!N|MlVL3<@%J# z3@CSdWIt7L|0bV7TsYs=_@hi}a`Cl^IG40qlXL~xWu@nvh7&%{!lKa8uu>&g$yhZF z0tS_G8?dANjoVEUg-6?ubWr@igHa==N9gz6Ikvp1-aLdQ+}%pY*uJIJsK(O+#cDEk zi-{c(ksG?k0T|o%SIytezQ`QZ>_n8?X!8-2JI3#9+@R8rHN z^L#re!RMxsPPSadyn)Ka-#p59OEb&bY69Xqc{??q`X)6#hI|4yB7Cer_H6 zk+`5}11l3C{E4!AWOF0Kt|1M*SLo2f5VgFTB+Ht>JzC^?7JfosQnI>rcf;gjBIRw+ z8nT0+>5Y1_sQLp>e4RoOYahZ$W)O~r)MbhQbI!xcjL*-JgVhHetGxbej}UgqJR{$P z_Kn!Wq#+pACohrwi|zc5bz7|$(coLlo?qcN36Rf|Al(E)>CJcsMcNDKjrqmTMo!FR zN@e96TZ>N_nPFH(TNjC-Zcp#+sjqL*Po6zq*xIsrisFD_;mx+5=e%a;w;olPvGsqu z#R~HJJstbphcs^T>*}SIPM;awQ}d$PgGcavWhsAq8KcB?3%7Q}v<^W)WE(;xA}iKF zN(R;FoFvI&I6QB`I&o6ra%cZ(|1vw^{-)dZhi`JV>*DPv0vogg?XH6G*zWvd6$qT> zi!~1aE0N?DqTS&@?V7~`(qxEP?h5HU?}Zdk@ua@z{4CM$v7V zyK=wA`yfk8CgdhCGHxG#Am#VE>S)sBExh){DBmk|PHK+z5tA4`cDpc2BPUj}QjAjg z^Lnv5aU;pWJ?I6d`hJN6sxzFtp17B=f;yziG z4M2#-7i|m;-SWCy_pO&kv3G!}`%i1@w~h4#2DTsQmGsICToDo1x;A9ASLi+0H(?K{ z^v4Qwl-XfeO2^EsWpMi!wghO{EaU8prX)5ZZf-xO=Cv%dJrTtNSZ-41)BAr?*3xth zrC)Xm4YER^xS^M8YlY46Asis-HXq)O>QjH5!>#h%!(P~-@e6~XKg3(yc{#y8?MN~a-S|H-FkdG!nM0Msaqpn*oT-XS|BgCBGaK3ZQV^J)MUMD zh)1f9p5;CLNqb?~(v-FRi3WG%21v-7S5#_p%K)d*ZXrS3&_!!#e)qR+D&JW$`M{G$ zozePpG)u8r@0SDegVBAx^u8n9X2Zj@w8K2DeP?rl-?rv@08(&U77_XkO4O;{J`S$T z8+vN>#(h_WbGr_O`X&8)#%-p}S|&@Qp{f;uAL;jo^Zcv`0FEeJxM&n!p9XmGnLNx) z5(a3@5Q;9xU%y7uOP4x9+hGDG5URh+1xluQ85nHZi56n*AM?!@cM$g`j$S$rlSm?r zw3q0tc=le7uP#buOjs=Bdu!a@08{jetXFMqOQeEaXsXZfq-e`h+!%J}a zKS*Ahozi;;Y<{zd9Z&}?Jsl-ao~AD^-x^u1i`7`+HR(`HkfGRPAW&Lm$L_F`v2%(w zr<^wMUp{lZ8>f{Obe9x*dMDE=e3I_E8T4@$e5w=soh@46aI0En)C+*#ZoZp+tNOk= z+EIE(Bw5PY#E_GJdP~>JSXJq&E@ip(Nex?3z+OzRdOzq9EgeW=y|37*sQEkF*0!|| zMR{J51krC`oiHJ=0VhMcsbpUY5#CWHSBb{GyjXkFUFh`Ck>_O2;waxMh+XMpjIhBH zrf36kKS%R2rnT6y*qTh{V$7+qrq-@uEH)y#um>SGk9vcA>ClrPmbH+VD8SFNU5rNn zZ?(5y%$h%YZ1@h+b6ayR7+*xFV2^an(0$yiwxEKlA5Ip2WAgHYcSCDS2E)~i4$_FK zUNxb?GEKr7r%6KM9+nr;JCuiIJbR#$t=G}P3m@^Vi8km2f!9kj+?I_jEWg^9o;$Z! zD?Z_bjd?PNv?neSzza&L`i(%EuYyw}No5gzO#PsnCEQjZF{rMA;u>d*Y0Ihhom;up zH~u7Bc=8KXW`BMGOsjpSB{i?GiV!y{wR~VeZU7@>`flz64clHmVN@4^R0Fzegnl53 z19dWYUL1hhi2o*ZL^q+^>_+)>1)wtTa~5TZOBpF&GHZq(1p8 zq}#jxT@6x|WwD)$J+G|Si($O!Vy`aF4to%*t!;9S-Ihg0zNW{~cKH=lTi$S^PrGkk z*89>Lu>p>3p1J2K4jX@Etl#YBvRV(r07Tl`XMJLoS_=@Opxp?w=}qVE%Q!?!ux(Jo~d0mMnoC3;j5wY6}pm>AL_cr%_>Q zn6AN(j8E-CQTKQySDfNcu?YYXvY)pJL)AMcwS_Iv1lF&I+28w9SEA;+pFO-BX-?4M zO|M^E#AmD$9hzTsg&P)H4 z5MD|65iW6RA^s$KdO{D&SjX=9o*O5~Jx)Cs5(6(2;wt^P~Gt>|(EsnXvv{2#W~ViC^p4VT)Fl8-ZD*STh}m0Fd6i zSPx&Wd@G#!CZa|B;R7d}ZD$iS^0m>M@EZkG)rct%=_bd!W-d8y_d;U;^Aps)KFrTQ z84RB?Ru;Ldt?jYbNd#6B(Q7RS3;6 zD(beCvDbcJKA&*;@n?f)fAIq*|9$dnYwAi2u)1G-5pRG53}eb>DODowO<7y(?zaWL zl3n5K$*4!nRY|Av2E-eSp@m2)l61Y*Izg+O-nxb+=f{ha<1u^yL4tWtZ(m({hG;pd zCewUzTaAZrwPg9)9!L@(@!7F%T6%GxTCS55D_#?_z1JEzh~?eh&a;+{Cq_Sx#W!9! z7?`|wqXg1P+%RS{W#kdGj`TUCo26yeKDENC<>k(l#9NCqpRx0W*nI%jD&r{h;U^zc;AqayJh)-vksfTmUU#`UQ^^ZJ(TUd$d z?-4aEZ&pz zS-(}7&qMo14vE!PcQxBIxbDqrkd*Zuse1(OW{huOi<~GXo&#ytkZ5QgS!?|%Uq#EJ zV^KWqH`>mZmRe;zg!nv%Ze3o+?^3Hd)AOwH!(2a=- zyAQ1=Soi71tuLl80;}qc6(&nQF4MQIn9NyR zVYB{lHc3T}VPESj>As`wOT7d}nu*N_VCnJm+H05`kc@>>bn7%NwQ+3lm*&d_vpi!8 z!rEB|CdoqI)u}ptwq?rpxfFl-q&{l8qE&nWmzzO6-(dZRynP$TwbpRIy%H(2N2O(U zlD?Tif9k)!5!&V?HJ`pL1X6fHO+#h*elP+cb0Hb22q2CK6j=P(Ut}`1g&ZaH@`I9w zC~?`btgMcibii^rf4+G`<*sbbrvxy^J;5lK=7%pZr1MIqKRw^W%;~eq9X!|!W-oWe z-Wdf_Wyj`Er!OVfa!~KQo0yALXO6hRMQco~*3YG+1O!aOLvXw)bAiN9k(cTq2dpz*~+(CCzn=|}vRbwH^+>onFvY6qiTW$)ST>hyKQ@#Y91V8^QSf*{ntGv6qePrpwvH69dHn7UiGUQY;qX6c^+tUMNT4PCZk@v@JU zC{)}!$x+X!WnscrA*Yl}w-!^6zSwu~1BlEb(*me3!hroO*3sZ$3xhXmp71yS%Srkk zgd)#ysS6j{3i4|De&1@Bi>`l@%bof$OJIk(3vA?Oc4YS? z3@rxR-cxPz%|{$9>6ey~?p87|>FasiDn}NxNIiVE;OB>>H|=dCSk)7*NEcFB$=gt3 zP-9jGm}e5|xWn1^aRI{9kz7BkJu96hKuxdX8}#Yb zhaBoGx^5bRf~vkvPXVX?!#UO#cz5wEO8N>|%u^G!4HHS|hgHS1|9sy~u}2~a#M4GaQ64;qS3kX%K zj)!}m^8hWUoF^gU<1>PAN2jUhdJ0i` zM)$rP{(b<0TvoU2F1vPo_%hg$^L;ub2L%rv1-I_;ciY)jSp(a&N{bK$2*bc6W)vkW z5DM(iO%GO60$sj%0z8@jGorlsB$5CcKbe8a$tqowv`dhPK7Si#(H8yA`BCT9i^o*A z@kqlzM3}U>Q+<7PYSr=i*Ruh;?$>E<-z4CFfJDCPTzb>{dk*~^PL)weG?1 zR-B1o?$1fo%*{`!f(bwHsO6*uNZK}PXFR{|Ef$!J@}zRiIB^Ww%$qv5-%#G;EG*VP zGfn&hyH{PnLs)mQAli8J<#`N64X$cTn_4HAQg$MQkerhptMHM4pn&X}9{!)4(uCE$ z;h0E`RQUM3Af)K^25;nW>UwxCwNV&GO@+k@3fwoxNVYfZL87pT=)eO3u1vCgXsV;i zY9egbf`o-a-voE{2fgORcb9k_6!ondDCTtvkJ3kanl*8JB%Q>1X#b7Fn?wPzWjeY< z1M%8x7&Qvm?4$K`^-11rh~AG|XUv?x7IZ2+724el?wrut=&P|jmlw}nl3e|o(?+}w z1O2=eARimq64fEH64==f+#h{QC?@vfbmVrbzdn(E$4jSW-XXHWmqJO_Is3(gFr!}M z1i`n7RdXlxO(oV$TO2LYYRanjqat!ij&iB9wM~wCii;K4E2@6Kna9v@NlfKfnll#K z)MYuJebgc`Yi?l>tUQxpv4Ynv-(y{fq;*tpKQ>RzEXJV}lCa1BBQ9A%bK{O&`;HY* zD&WWzBuu&&?sd=@%V|ys&!Ys_?e9HF>@FJcH|&zvg))wC48ABTC1U^#1G#Sb=($TW zS⪙hP4UmFgy9gqGkbvh~TBOUnv0vycJ|mAL?2tuWT~Ob$ma#zRRABSJPChLmyg2 za=~GVaXLqoFhq(+0>G~xOceM_=X{H`_MFqA?X|ThfWu^kYiGZgkP){niRnubx)A~; zs~S{LuY0*T&uui&YA+=A<2QY9N%YjvRCJeQV{GhN$matcMtm)e>@7=QshjKd-l+Vp zFHbS19v(s_-`Av#urG}saAs}dTPao6d~2p+O>&^JtYNZ*mEcohEytQ@cwNo{I5BRP zB+tq{f8t;+5`)*AM~W=Ll)cLOMBkWT9rg3Cuj(=|d~HoB_$BFSU5gFvJ_;aCTlYWu zVrAuH@i-V}rv*VAzmR+C+;7j^i0L~WWY5c&nb%>BWBu#NcI`nhx1OLb-y!!c&@H3L zR!%WWarXCRtWEaKI=vIUK;i3kt@5c0pu7<%=)OEZv%0ecxW1uP3RUI4RE6s6`yAl^ z&@8f5HkMuAbfC(vA(y!R@Z}v9()AhQ)R&Gr#{}<7IM2gAy&V6xVUSybU^_!+@j~CS z<~cOSZ|O?-JwksHDViLi;2!-9Eq1AI!8-{XJuVslPL3tX38nKRFBo*qI+46L{ZheNlf^uQ@qP*jOCHvOAOchoLj8 z7q(@;YJsZjJ`Y%;-SWc_{!964SW|8H&%3%ivC$iS62)6YwNvhP)a~&$8z%|w%Cf)6AH1`1`R)5qZ4FQUjf;lX8$7@J7AGcbYhTwoO>b#Kr|vTC z)V`x@)U6hf7`Zt`A3FNWrQEcJZ2sF_pe|O;{kWlsGJh2lc=uLmnOc@~a+ug`bXhUz zX3|8IwddsZR9hWHwT~9@eTCg>bjEK70}lll-z2C*t*lSi$FYS3#zftH2-L1V{MOj` zvW1Cx3eCJ1gr|s?7 z$A{!V;mWzhVKO^*EHi%^C?+n1>NGZmSzAC$RD)qG-KMN;I?iHTcW~8A!r}o_|rpmf%~cfmFZ&=K>c_}JL0%NOir17bQSZfY`pX;zD9`JV)*m&qO&{J5)o#~SY*dtv@r{g%CO8UeNBPP|s0Gw=+FJB&JuP`~^) z5hERxvD<*V1bFh14WB0~ua7huSC@ME1v`t*rsL5@ XSIt2=`ivCU4^*lv>nK$yScm^F^d!Rc literal 0 HcmV?d00001 From cbc9fe41d8f0c94c821a5ab585abcb67ac6cc575 Mon Sep 17 00:00:00 2001 From: Simone Barbieri Date: Wed, 10 Jun 2020 11:10:48 +0100 Subject: [PATCH 16/22] Fix handling of models when loading layout --- pype/plugins/blender/load/load_layout.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pype/plugins/blender/load/load_layout.py b/pype/plugins/blender/load/load_layout.py index 4247d25caa..0c1032c4fb 100644 --- a/pype/plugins/blender/load/load_layout.py +++ b/pype/plugins/blender/load/load_layout.py @@ -66,6 +66,7 @@ class BlendLayoutLoader(pype.hosts.blender.plugin.AssetLoader): for element_container in layout_container.children: element_container.make_local() + meshes.extend([obj for obj in element_container.objects if obj.type == 'MESH']) armatures.extend([obj for obj in element_container.objects if obj.type == 'ARMATURE']) for child in element_container.children: child.make_local() From ee247ea58b8a641327f6911def5590bd13f47ae7 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 10 Jun 2020 15:13:21 +0100 Subject: [PATCH 17/22] Toggle instances with visibility. --- pype/hosts/photoshop/__init__.py | 9 +++++++++ pype/plugins/photoshop/publish/collect_instances.py | 1 + 2 files changed, 10 insertions(+) diff --git a/pype/hosts/photoshop/__init__.py b/pype/hosts/photoshop/__init__.py index 709fb24bf5..01ed757a8d 100644 --- a/pype/hosts/photoshop/__init__.py +++ b/pype/hosts/photoshop/__init__.py @@ -22,3 +22,12 @@ def install(): api.register_plugin_path( api.Creator, os.path.join(plugins_directory, "create") ) + + pyblish.api.register_callback( + "instanceToggled", on_pyblish_instance_toggled + ) + + +def on_pyblish_instance_toggled(instance, old_value, new_value): + """Toggle layer visibility on instance toggles.""" + instance[0].Visible = new_value diff --git a/pype/plugins/photoshop/publish/collect_instances.py b/pype/plugins/photoshop/publish/collect_instances.py index b253365465..4937f2a1e4 100644 --- a/pype/plugins/photoshop/publish/collect_instances.py +++ b/pype/plugins/photoshop/publish/collect_instances.py @@ -49,6 +49,7 @@ class CollectInstances(pyblish.api.ContextPlugin): instance.data["families"] = self.families_mapping[ layer_data["family"] ] + instance.data["publish"] = layer.Visible # Produce diagnostic message for any graphical # user interface interested in visualising it. From 0a3977d4bf27e07cbf65910e89ccbf33d1e67600 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 10 Jun 2020 15:13:36 +0100 Subject: [PATCH 18/22] Publish workfile. --- .../photoshop/publish/collect_workfile.py | 39 +++++++++++++++++++ .../photoshop/publish/extract_save_scene.py | 14 +++++++ 2 files changed, 53 insertions(+) create mode 100644 pype/plugins/photoshop/publish/collect_workfile.py create mode 100644 pype/plugins/photoshop/publish/extract_save_scene.py diff --git a/pype/plugins/photoshop/publish/collect_workfile.py b/pype/plugins/photoshop/publish/collect_workfile.py new file mode 100644 index 0000000000..766be02354 --- /dev/null +++ b/pype/plugins/photoshop/publish/collect_workfile.py @@ -0,0 +1,39 @@ +import pyblish.api +import os + + +class CollectWorkfile(pyblish.api.ContextPlugin): + """Collect current script for publish.""" + + order = pyblish.api.CollectorOrder + 0.1 + label = "Collect Workfile" + hosts = ["photoshop"] + + def process(self, context): + family = "workfile" + task = os.getenv("AVALON_TASK", None) + subset = family + task.capitalize() + + file_path = context.data["currentFile"] + staging_dir = os.path.dirname(file_path) + base_name = os.path.basename(file_path) + + # Create instance + instance = context.create_instance(subset) + instance.data.update({ + "subset": subset, + "label": base_name, + "name": base_name, + "family": family, + "families": [], + "representations": [], + "asset": os.environ["AVALON_ASSET"] + }) + + # creating representation + instance.data["representations"].append({ + "name": "psd", + "ext": "psd", + "files": base_name, + "stagingDir": staging_dir, + }) diff --git a/pype/plugins/photoshop/publish/extract_save_scene.py b/pype/plugins/photoshop/publish/extract_save_scene.py new file mode 100644 index 0000000000..b3d4f0e447 --- /dev/null +++ b/pype/plugins/photoshop/publish/extract_save_scene.py @@ -0,0 +1,14 @@ +import pype.api +from avalon import photoshop + + +class ExtractSaveScene(pype.api.Extractor): + """Save scene before extraction.""" + + order = pype.api.Extractor.order - 0.49 + label = "Extract Save Scene" + hosts = ["photoshop"] + families = ["workfile"] + + def process(self, instance): + photoshop.app().ActiveDocument.Save() From 0f5dd4681b0fb6e0d738e0624e38fbb6c3aaf1d0 Mon Sep 17 00:00:00 2001 From: Toke Stuart Jepsen Date: Wed, 10 Jun 2020 15:13:48 +0100 Subject: [PATCH 19/22] Validate instance asset. --- .../publish/validate_instance_asset.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 pype/plugins/photoshop/publish/validate_instance_asset.py diff --git a/pype/plugins/photoshop/publish/validate_instance_asset.py b/pype/plugins/photoshop/publish/validate_instance_asset.py new file mode 100644 index 0000000000..ab1d02269f --- /dev/null +++ b/pype/plugins/photoshop/publish/validate_instance_asset.py @@ -0,0 +1,48 @@ +import os + +import pyblish.api +import pype.api +from avalon import photoshop + + +class ValidateInstanceAssetRepair(pyblish.api.Action): + """Repair the instance asset.""" + + label = "Repair" + icon = "wrench" + on = "failed" + + def process(self, context, plugin): + + # Get the errored instances + failed = [] + for result in context.data["results"]: + if (result["error"] is not None and result["instance"] is not None + and result["instance"] not in failed): + failed.append(result["instance"]) + + # Apply pyblish.logic to get the instances for the plug-in + instances = pyblish.api.instances_by_plugin(failed, plugin) + + for instance in instances: + data = photoshop.read(instance[0]) + data["asset"] = os.environ["AVALON_ASSET"] + photoshop.imprint(instance[0], data) + + +class ValidateInstanceAsset(pyblish.api.InstancePlugin): + """Validate the instance asset is the current asset.""" + + label = "Validate Instance Asset" + hosts = ["photoshop"] + actions = [ValidateInstanceAssetRepair] + order = pype.api.ValidateContentsOrder + + def process(self, instance): + instance_asset = instance.data["asset"] + current_asset = os.environ["AVALON_ASSET"] + msg = ( + "Instance asset is not the same as current asset:" + f"\nInstance: {instance_asset}\nCurrent: {current_asset}" + ) + assert instance_asset == current_asset, msg From a160751e829548d16222de72b0f34fcd82d77261 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 10 Jun 2020 17:54:45 +0200 Subject: [PATCH 20/22] fix import --- pype/hosts/maya/expected_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index a737268354..3292b867ce 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -41,7 +41,7 @@ from abc import ABCMeta, abstractmethod import six -import pype.maya.lib as lib +import pype.hosts.maya.lib as lib from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup From 6616a3fdc1b5627b17cace4ab04d4dc357fc9782 Mon Sep 17 00:00:00 2001 From: Milan Kolar Date: Wed, 10 Jun 2020 18:17:19 +0200 Subject: [PATCH 21/22] fix import --- pype/hosts/maya/expected_files.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pype/hosts/maya/expected_files.py b/pype/hosts/maya/expected_files.py index a737268354..3292b867ce 100644 --- a/pype/hosts/maya/expected_files.py +++ b/pype/hosts/maya/expected_files.py @@ -41,7 +41,7 @@ from abc import ABCMeta, abstractmethod import six -import pype.maya.lib as lib +import pype.hosts.maya.lib as lib from maya import cmds import maya.app.renderSetup.model.renderSetup as renderSetup From 80bdaf49b485da6f59e98bc34f5c14fec8e6ab23 Mon Sep 17 00:00:00 2001 From: Ondrej Samohel Date: Wed, 10 Jun 2020 18:29:39 +0200 Subject: [PATCH 22/22] disable arnold scene export option --- pype/plugins/maya/create/create_render.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pype/plugins/maya/create/create_render.py b/pype/plugins/maya/create/create_render.py index 2e603d9a5a..dbcade9b77 100644 --- a/pype/plugins/maya/create/create_render.py +++ b/pype/plugins/maya/create/create_render.py @@ -181,7 +181,8 @@ class CreateRender(avalon.maya.Creator): self.data["machineList"] = "" self.data["useMayaBatch"] = True self.data["vrayScene"] = False - self.data["assScene"] = False + # Disable for now as this feature is not working yet + # self.data["assScene"] = False self.options = {"useSelection": False} # Force no content