diff --git a/openpype/hosts/max/api/colorspace.py b/openpype/hosts/max/api/colorspace.py new file mode 100644 index 0000000000..fafee4ee04 --- /dev/null +++ b/openpype/hosts/max/api/colorspace.py @@ -0,0 +1,50 @@ +import attr +from pymxs import runtime as rt + + +@attr.s +class LayerMetadata(object): + """Data class for Render Layer metadata.""" + frameStart = attr.ib() + frameEnd = attr.ib() + + +@attr.s +class RenderProduct(object): + """Getting Colorspace as + Specific Render Product Parameter for submitting + publish job. + """ + colorspace = attr.ib() # colorspace + view = attr.ib() + productName = attr.ib(default=None) + + +class ARenderProduct(object): + + def __init__(self): + """Constructor.""" + # Initialize + self.layer_data = self._get_layer_data() + self.layer_data.products = self.get_colorspace_data() + + def _get_layer_data(self): + return LayerMetadata( + frameStart=int(rt.rendStart), + frameEnd=int(rt.rendEnd), + ) + + def get_colorspace_data(self): + """To be implemented by renderer class. + This should return a list of RenderProducts. + Returns: + list: List of RenderProduct + """ + colorspace_data = [ + RenderProduct( + colorspace="sRGB", + view="ACES 1.0", + productName="" + ) + ] + return colorspace_data diff --git a/openpype/hosts/max/api/lib_renderproducts.py b/openpype/hosts/max/api/lib_renderproducts.py index 8224d589ad..94b0aeb913 100644 --- a/openpype/hosts/max/api/lib_renderproducts.py +++ b/openpype/hosts/max/api/lib_renderproducts.py @@ -3,94 +3,126 @@ # arnold # https://help.autodesk.com/view/ARNOL/ENU/?guid=arnold_for_3ds_max_ax_maxscript_commands_ax_renderview_commands_html import os + from pymxs import runtime as rt -from openpype.hosts.max.api.lib import ( - get_current_renderer, - get_default_render_folder -) -from openpype.pipeline.context_tools import get_current_project_asset -from openpype.settings import get_project_settings + +from openpype.hosts.max.api.lib import get_current_renderer from openpype.pipeline import legacy_io +from openpype.settings import get_project_settings class RenderProducts(object): def __init__(self, project_settings=None): - self._project_settings = project_settings - if not self._project_settings: - self._project_settings = get_project_settings( - legacy_io.Session["AVALON_PROJECT"] - ) + self._project_settings = project_settings or get_project_settings( + legacy_io.Session["AVALON_PROJECT"]) + + def get_beauty(self, container): + render_dir = os.path.dirname(rt.rendOutputFilename) + + output_file = os.path.join(render_dir, container) - def render_product(self, container): - folder = rt.maxFilePath - file = rt.maxFileName - folder = folder.replace("\\", "/") setting = self._project_settings - render_folder = get_default_render_folder(setting) - filename, ext = os.path.splitext(file) + img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa - output_file = os.path.join(folder, - render_folder, - filename, + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 + + return { + "beauty": self.get_expected_beauty( + output_file, start_frame, end_frame, img_fmt + ) + } + + def get_aovs(self, container): + render_dir = os.path.dirname(rt.rendOutputFilename) + + output_file = os.path.join(render_dir, container) - context = get_current_project_asset() - # TODO: change the frame range follows the current render setting - startFrame = int(rt.rendStart) - endFrame = int(rt.rendEnd) + 1 - - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - full_render_list = self.beauty_render_product(output_file, - startFrame, - endFrame, - img_fmt) + setting = self._project_settings + img_fmt = setting["max"]["RenderSettings"]["image_format"] # noqa + start_frame = int(rt.rendStart) + end_frame = int(rt.rendEnd) + 1 renderer_class = get_current_renderer() renderer = str(renderer_class).split(":")[0] - - - if renderer == "VUE_File_Renderer": - return full_render_list + render_dict = {} if renderer in [ "ART_Renderer", - "Redshift_Renderer", "V_Ray_6_Hotfix_3", "V_Ray_GPU_6_Hotfix_3", "Default_Scanline_Renderer", "Quicksilver_Hardware_Renderer", ]: - render_elem_list = self.render_elements_product(output_file, - startFrame, - endFrame, - img_fmt) - if render_elem_list: - full_render_list.extend(iter(render_elem_list)) - return full_render_list + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, start_frame, + end_frame, img_fmt) + }) + elif renderer == "Redshift_Renderer": + render_name = self.get_render_elements_name() + if render_name: + rs_aov_files = rt.Execute("renderers.current.separateAovFiles") + # this doesn't work, always returns False + # rs_AovFiles = rt.RedShift_Renderer().separateAovFiles + if img_fmt == "exr" and not rs_aov_files: + for name in render_name: + if name == "RsCryptomatte": + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, start_frame, + end_frame, img_fmt) + }) + else: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, start_frame, + end_frame, img_fmt) + }) - if renderer == "Arnold": - aov_list = self.arnold_render_product(output_file, - startFrame, - endFrame, - img_fmt) - if aov_list: - full_render_list.extend(iter(aov_list)) - return full_render_list + elif renderer == "Arnold": + render_name = self.get_arnold_product_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_arnold_product( + output_file, name, start_frame, end_frame, img_fmt) + }) + elif renderer in [ + "V_Ray_6_Hotfix_3", + "V_Ray_GPU_6_Hotfix_3" + ]: + if img_fmt != "exr": + render_name = self.get_render_elements_name() + if render_name: + for name in render_name: + render_dict.update({ + name: self.get_expected_render_elements( + output_file, name, start_frame, + end_frame, img_fmt) # noqa + }) - def beauty_render_product(self, folder, startFrame, endFrame, fmt): + return render_dict + + def get_expected_beauty(self, folder, start_frame, end_frame, fmt): beauty_frame_range = [] - for f in range(startFrame, endFrame): - beauty_output = f"{folder}.{f}.{fmt}" + for f in range(start_frame, end_frame): + frame = "%04d" % f + beauty_output = f"{folder}.{frame}.{fmt}" beauty_output = beauty_output.replace("\\", "/") beauty_frame_range.append(beauty_output) return beauty_frame_range - # TODO: Get the arnold render product - def arnold_render_product(self, folder, startFrame, endFrame, fmt): - """Get all the Arnold AOVs""" - aovs = [] + def get_arnold_product_name(self): + """Get all the Arnold AOVs name""" + aov_name = [] amw = rt.MaxtoAOps.AOVsManagerWindow() aov_mgr = rt.renderers.current.AOVManager @@ -100,34 +132,51 @@ class RenderProducts(object): return for i in range(aov_group_num): # get the specific AOV group - for aov in aov_mgr.drivers[i].aov_list: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{aov.name}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - aovs.append(render_element) - + aov_name.extend(aov.name for aov in aov_mgr.drivers[i].aov_list) # close the AOVs manager window amw.close() - return aovs + return aov_name - def render_elements_product(self, folder, startFrame, endFrame, fmt): - """Get all the render element output files. """ - render_dirname = [] + def get_expected_arnold_product(self, folder, name, + start_frame, end_frame, fmt): + """Get all the expected Arnold AOVs""" + aov_list = [] + for f in range(start_frame, end_frame): + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" + render_element = render_element.replace("\\", "/") + aov_list.append(render_element) + return aov_list + + def get_render_elements_name(self): + """Get all the render element names for general """ + render_name = [] render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() + if render_elem_num < 1: + return # get render elements from the renders for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") if renderlayer_name.enabled: - for f in range(startFrame, endFrame): - render_element = f"{folder}_{renderpass}.{f}.{fmt}" - render_element = render_element.replace("\\", "/") - render_dirname.append(render_element) + target, renderpass = str(renderlayer_name).split(":") + render_name.append(renderpass) - return render_dirname + return render_name + + def get_expected_render_elements(self, folder, name, + start_frame, end_frame, fmt): + """Get all the expected render element output files. """ + render_elements = [] + for f in range(start_frame, end_frame): + frame = "%04d" % f + render_element = f"{folder}_{name}.{frame}.{fmt}" + render_element = render_element.replace("\\", "/") + render_elements.append(render_element) + + return render_elements def image_format(self): return self._project_settings["max"]["RenderSettings"]["image_format"] # noqa diff --git a/openpype/hosts/max/plugins/create/create_render.py b/openpype/hosts/max/plugins/create/create_render.py index 68ae5eac72..5ad895b86e 100644 --- a/openpype/hosts/max/plugins/create/create_render.py +++ b/openpype/hosts/max/plugins/create/create_render.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- """Creator plugin for creating camera.""" +import os from openpype.hosts.max.api import plugin from openpype.pipeline import CreatedInstance from openpype.hosts.max.api.lib_rendersettings import RenderSettings @@ -14,6 +15,10 @@ class CreateRender(plugin.MaxCreator): def create(self, subset_name, instance_data, pre_create_data): from pymxs import runtime as rt sel_obj = list(rt.selection) + file = rt.maxFileName + filename, _ = os.path.splitext(file) + instance_data["AssetName"] = filename + instance = super(CreateRender, self).create( subset_name, instance_data, diff --git a/openpype/hosts/max/plugins/publish/collect_render.py b/openpype/hosts/max/plugins/publish/collect_render.py index 00e00a8eb5..db5c84fad9 100644 --- a/openpype/hosts/max/plugins/publish/collect_render.py +++ b/openpype/hosts/max/plugins/publish/collect_render.py @@ -5,7 +5,8 @@ import pyblish.api from pymxs import runtime as rt from openpype.pipeline import get_current_asset_name -from openpype.hosts.max.api.lib import get_max_version +from openpype.hosts.max.api import colorspace +from openpype.hosts.max.api.lib import get_max_version, get_current_renderer from openpype.hosts.max.api.lib_renderproducts import RenderProducts from openpype.client import get_last_version_by_subset_name @@ -28,8 +29,16 @@ class CollectRender(pyblish.api.InstancePlugin): context.data['currentFile'] = current_file asset = get_current_asset_name() - render_layer_files = RenderProducts().render_product(instance.name) + files_by_aov = RenderProducts().get_beauty(instance.name) folder = folder.replace("\\", "/") + aovs = RenderProducts().get_aovs(instance.name) + files_by_aov.update(aovs) + + if "expectedFiles" not in instance.data: + instance.data["expectedFiles"] = list() + instance.data["files"] = list() + instance.data["expectedFiles"].append(files_by_aov) + instance.data["files"].append(files_by_aov) img_format = RenderProducts().image_format() project_name = context.data["projectName"] @@ -38,7 +47,6 @@ class CollectRender(pyblish.api.InstancePlugin): version_doc = get_last_version_by_subset_name(project_name, instance.name, asset_id) - self.log.debug("version_doc: {0}".format(version_doc)) version_int = 1 if version_doc: @@ -46,22 +54,42 @@ class CollectRender(pyblish.api.InstancePlugin): self.log.debug(f"Setting {version_int} to context.") context.data["version"] = version_int - # setup the plugin as 3dsmax for the internal renderer + # OCIO config not support in + # most of the 3dsmax renderers + # so this is currently hard coded + # TODO: add options for redshift/vray ocio config + instance.data["colorspaceConfig"] = "" + instance.data["colorspaceDisplay"] = "sRGB" + instance.data["colorspaceView"] = "ACES 1.0 SDR-video" + instance.data["renderProducts"] = colorspace.ARenderProduct() + instance.data["publishJobState"] = "Suspended" + instance.data["attachTo"] = [] + renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] + # also need to get the render dir for conversion data = { - "subset": instance.name, "asset": asset, + "subset": str(instance.name), "publish": True, "maxversion": str(get_max_version()), "imageFormat": img_format, "family": 'maxrender', "families": ['maxrender'], + "renderer": renderer, "source": filepath, - "expectedFiles": render_layer_files, "plugin": "3dsmax", "frameStart": int(rt.rendStart), "frameEnd": int(rt.rendEnd), "version": version_int, "farm": True } - self.log.info("data: {0}".format(data)) instance.data.update(data) + + # TODO: this should be unified with maya and its "multipart" flag + # on instance. + if renderer == "Redshift_Renderer": + instance.data.update( + {"separateAovFiles": rt.Execute( + "renderers.current.separateAovFiles")}) + + self.log.info("data: {0}".format(data)) diff --git a/openpype/hosts/max/plugins/publish/save_scene.py b/openpype/hosts/max/plugins/publish/save_scene.py new file mode 100644 index 0000000000..a40788ab41 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/save_scene.py @@ -0,0 +1,21 @@ +import pyblish.api +import os + + +class SaveCurrentScene(pyblish.api.ContextPlugin): + """Save current scene + + """ + + label = "Save current file" + order = pyblish.api.ExtractorOrder - 0.49 + hosts = ["max"] + families = ["maxrender", "workfile"] + + def process(self, context): + from pymxs import runtime as rt + folder = rt.maxFilePath + file = rt.maxFileName + current = os.path.join(folder, file) + assert context.data["currentFile"] == current + rt.saveMaxFile(current) diff --git a/openpype/hosts/max/plugins/publish/validate_deadline_publish.py b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py new file mode 100644 index 0000000000..b2f0e863f4 --- /dev/null +++ b/openpype/hosts/max/plugins/publish/validate_deadline_publish.py @@ -0,0 +1,43 @@ +import os +import pyblish.api +from pymxs import runtime as rt +from openpype.pipeline.publish import ( + RepairAction, + ValidateContentsOrder, + PublishValidationError, + OptionalPyblishPluginMixin +) +from openpype.hosts.max.api.lib_rendersettings import RenderSettings + + +class ValidateDeadlinePublish(pyblish.api.InstancePlugin, + OptionalPyblishPluginMixin): + """Validates Render File Directory is + not the same in every submission + """ + + order = ValidateContentsOrder + families = ["maxrender"] + hosts = ["max"] + label = "Render Output for Deadline" + optional = True + actions = [RepairAction] + + def process(self, instance): + if not self.is_active(instance.data): + return + file = rt.maxFileName + filename, ext = os.path.splitext(file) + if filename not in rt.rendOutputFilename: + raise PublishValidationError( + "Render output folder " + "doesn't match the max scene name! " + "Use Repair action to " + "fix the folder file path.." + ) + + @classmethod + def repair(cls, instance): + container = instance.data.get("instance_node") + RenderSettings().render_output(container) + cls.log.debug("Reset the render output folder...") diff --git a/openpype/modules/deadline/abstract_submit_deadline.py b/openpype/modules/deadline/abstract_submit_deadline.py index 558a637e4b..7938c27233 100644 --- a/openpype/modules/deadline/abstract_submit_deadline.py +++ b/openpype/modules/deadline/abstract_submit_deadline.py @@ -582,7 +582,6 @@ class AbstractSubmitDeadline(pyblish.api.InstancePlugin): metadata_folder = metadata_folder.replace(orig_scene, new_scene) instance.data["publishRenderMetadataFolder"] = metadata_folder - self.log.info("Scene name was switched {} -> {}".format( orig_scene, new_scene )) diff --git a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py index c728b6b9c7..b6a30e36b7 100644 --- a/openpype/modules/deadline/plugins/publish/submit_max_deadline.py +++ b/openpype/modules/deadline/plugins/publish/submit_max_deadline.py @@ -78,7 +78,7 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, job_info.BatchName = src_filename job_info.Plugin = instance.data["plugin"] job_info.UserName = context.data.get("deadlineUser", getpass.getuser()) - + job_info.EnableAutoTimeout = True # Deadline requires integers in frame range frames = "{start}-{end}".format( start=int(instance.data["frameStart"]), @@ -133,7 +133,8 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, # Add list of expected files to job # --------------------------------- exp = instance.data.get("expectedFiles") - for filepath in exp: + + for filepath in self._iter_expected_files(exp): job_info.OutputDirectory += os.path.dirname(filepath) job_info.OutputFilename += os.path.basename(filepath) @@ -162,10 +163,11 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, instance = self._instance filepath = self.scene_path - expected_files = instance.data["expectedFiles"] - if not expected_files: + files = instance.data["expectedFiles"] + if not files: raise RuntimeError("No Render Elements found!") - output_dir = os.path.dirname(expected_files[0]) + first_file = next(self._iter_expected_files(files)) + output_dir = os.path.dirname(first_file) instance.data["outputDir"] = output_dir instance.data["toBeRenderedOn"] = "deadline" @@ -196,25 +198,22 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, else: plugin_data["DisableMultipass"] = 1 - expected_files = instance.data.get("expectedFiles") - if not expected_files: + files = instance.data.get("expectedFiles") + if not files: raise RuntimeError("No render elements found") - old_output_dir = os.path.dirname(expected_files[0]) + first_file = next(self._iter_expected_files(files)) + old_output_dir = os.path.dirname(first_file) output_beauty = RenderSettings().get_render_output(instance.name, old_output_dir) - filepath = self.from_published_scene() - - def _clean_name(path): - return os.path.splitext(os.path.basename(path))[0] - - new_scene = _clean_name(filepath) - orig_scene = _clean_name(instance.context.data["currentFile"]) - - output_beauty = output_beauty.replace(orig_scene, new_scene) - output_beauty = output_beauty.replace("\\", "/") - plugin_data["RenderOutput"] = output_beauty - + rgb_bname = os.path.basename(output_beauty) + dir = os.path.dirname(first_file) + beauty_name = f"{dir}/{rgb_bname}" + beauty_name = beauty_name.replace("\\", "/") + plugin_data["RenderOutput"] = beauty_name + # as 3dsmax has version with different languages + plugin_data["Language"] = "ENU" renderer_class = get_current_renderer() + renderer = str(renderer_class).split(":")[0] if renderer in [ "ART_Renderer", @@ -226,14 +225,37 @@ class MaxSubmitDeadline(abstract_submit_deadline.AbstractSubmitDeadline, ]: render_elem_list = RenderSettings().get_render_element() for i, element in enumerate(render_elem_list): - element = element.replace(orig_scene, new_scene) - plugin_data["RenderElementOutputFilename%d" % i] = element # noqa + elem_bname = os.path.basename(element) + new_elem = f"{dir}/{elem_bname}" + new_elem = new_elem.replace("/", "\\") + plugin_data["RenderElementOutputFilename%d" % i] = new_elem # noqa + + if renderer == "Redshift_Renderer": + plugin_data["redshift_SeparateAovFiles"] = instance.data.get( + "separateAovFiles") self.log.debug("plugin data:{}".format(plugin_data)) plugin_info.update(plugin_data) return job_info, plugin_info + def from_published_scene(self, replace_in_path=True): + instance = self._instance + if instance.data["renderer"] == "Redshift_Renderer": + self.log.debug("Using Redshift...published scene wont be used..") + replace_in_path = False + return replace_in_path + + @staticmethod + def _iter_expected_files(exp): + if isinstance(exp[0], dict): + for _aov, files in exp[0].items(): + for file in files: + yield file + else: + for file in exp: + yield file + @classmethod def get_attribute_defs(cls): defs = super(MaxSubmitDeadline, cls).get_attribute_defs()