diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index 4fd4dba01a..6977f2c84a 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -109,7 +109,12 @@ def get_multiple_render_products( end_frame = int(rt.rendEnd) # Always add beauty pass - beauty_files = self.get_expected_beauty(start_frame, end_frame, ext) + beauty_files = self.get_expected_beauty( + start_frame, + end_frame, + ext, + camera=camera, + ) render_output_frames[f"{camera}_beauty"] = beauty_files # Add AOVs @@ -121,14 +126,19 @@ def get_multiple_render_products( start_frame, end_frame, aov_name, - renderer_name + renderer_name, + camera=camera, ) render_output_frames[f"{camera}_{aov_name}"] = aov_expected_files return render_output_frames def get_expected_beauty( - self, start_frame: int, end_frame: int, extension: str + self, + start_frame: int, + end_frame: int, + extension: str, + camera: str | None = None ) -> list[str]: """Get expected beauty render output file paths for each frame. @@ -154,7 +164,8 @@ def get_expected_beauty( start_frame, end_frame, "", - renderer_name + renderer_name, + camera=camera, ) def get_render_element_outputfilename( @@ -268,6 +279,7 @@ def get_expected_files( end_frame: int, aov_name: str, renderer_name: str, + camera: str | None = None, ) -> list[str]: """Get expected files @@ -287,8 +299,10 @@ def get_expected_files( directory = os.path.dirname(filepath) filename = os.path.basename(filepath) name, ext = os.path.splitext(filename) - name = name.lstrip(".") + name = name.strip(".") aov_name = aov_name.strip() + if camera is not None: + name = f"{name}_{camera}" for frame in range(start_frame, end_frame + 1): aov_filename = f"{name}.{frame:04d}{ext}" expected_aov = os.path.join(directory, aov_filename) diff --git a/client/ayon_max/api/lib_rendersettings.py b/client/ayon_max/api/lib_rendersettings.py index 440259e7c6..f21f728346 100644 --- a/client/ayon_max/api/lib_rendersettings.py +++ b/client/ayon_max/api/lib_rendersettings.py @@ -66,12 +66,20 @@ def __init__(self, project_settings=None, data: dict=None): """ self._project_settings = project_settings + self._data = data if data else {} + self.img_fmt = self.image_format + + @property + def project_settings(self): if not self._project_settings: self._project_settings = get_project_settings( get_current_project_name() ) - self._data = data if data else {} + return self._project_settings + @property + def image_format(self): + return self.project_settings["max"]["RenderSettings"]["image_format"] # noqa def set_render_camera(self, selection): for sel in selection: # to avoid Attribute Error from pymxs wrapper @@ -84,7 +92,7 @@ def render_output(self): # Set output paths for current workfile using project settings templates. file = rt.maxFileName # Resolve project settings used for output path formatting. - setting = self._project_settings + setting = self.project_settings container = self._data["instance_node"] render_folder = get_default_render_folder(self._data, setting) filename, _ = os.path.splitext(file) @@ -106,17 +114,16 @@ def render_output(self): renderer = get_current_renderer() renderer_name = str(renderer).split(":")[0] - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa output = os.path.join(output_dir, container) try: aov_separator = self._aov_chars[( - self._project_settings["max"] - ["RenderSettings"] - ["aov_separator"] + self.project_settings["max"] + ["RenderSettings"] + ["aov_separator"] )] except KeyError: aov_separator = "." - output_filename = f"{output}..{img_fmt}" + output_filename = f"{output}..{self.image_format}" output_filename = output_filename.replace("{aov_separator}", aov_separator) multipass_enabled = get_multipass_setting(renderer_name, setting) @@ -131,27 +138,27 @@ def render_output(self): elif is_supported_renderer(renderer_name): rt.rendOutputFilename = output_filename - self.render_element_layer(output, width, height, img_fmt) + self.render_element_layer(output, width, height, self.image_format) elif renderer_name.startswith("V_Ray_"): vr_settings = get_vray_settings(renderer_name, renderer) vr_settings.output_force32bit_3dsmax_vfb = True vr_settings.output_splitgbuffer = multipass_enabled - if img_fmt == "exr": + if self.image_format == "exr": vr_settings.output_saverawfile = True - vr_settings.output_rawfilename = f"{output}.{img_fmt}" + vr_settings.output_rawfilename = f"{output}.{self.image_format}" if multipass_enabled: rt.rendOutputFilename = output_filename - vr_settings.output_splitfilename = f"{output}.{img_fmt}" + vr_settings.output_splitfilename = f"{output}.{self.image_format}" else: - rt.rendOutputFilename = f"{output}_tmp..{img_fmt}" - self.render_element_layer(output, width, height, img_fmt) + rt.rendOutputFilename = f"{output}_tmp..{self.image_format}" + self.render_element_layer(output, width, height, self.image_format) # TODO: supports multipass for different renderers elif renderer_name == "Redshift_Renderer": rt.rendOutputFilename = output_filename rt.renderers.production.separateAovFiles = multipass_enabled - if img_fmt == "exr" and multipass_enabled: + if self.image_format == "exr" and multipass_enabled: rt.renderers.production.OutputExrMultipart = multipass_enabled # prevent rendering extra files when using V-Ray @@ -167,9 +174,6 @@ def arnold_setup(self, output_dir, container, multipass_enabled): render_camera = rt.viewport.GetCamera() if render_camera: arv.setOption("Camera", str(render_camera)) - - # TODO: add AOVs and extension - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa # TODO: enhance this maxscript to make sure it supports separate AOVs # with Arnold drivers. setup_cmd = ( @@ -179,7 +183,7 @@ def arnold_setup(self, output_dir, container, multipass_enabled): aovmgr = renderers.current.AOVManager aovmgr.drivers = #() aovmgr.outputPath = "{output_dir}" - img_fmt = "{img_fmt}" + img_fmt = "{self.image_format}" if img_fmt == "png" then driver = ArnoldPNGDriver() if img_fmt == "jpg" then driver = ArnoldJPEGDriver() if img_fmt == "exr" then driver = ArnoldEXRDriver() @@ -214,8 +218,7 @@ def render_element_layer(self, dir, width, height, ext): def get_render_output(self, container, output_dir): output = os.path.join(output_dir, container) - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa - output_filename = f"{output}..{img_fmt}" + output_filename = f"{output}..{self.image_format}" return output_filename def get_render_element(self): @@ -239,13 +242,12 @@ def get_batch_render_elements(self, container, render_elem_num = render_elem.NumRenderElements() if render_elem_num < 0: return - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") camera = camera.replace(":", "_") - aov_name = f"{output}_{camera}_{renderpass}..{img_fmt}" + aov_name = f"{output}_{camera}_{renderpass}..{self.image_format}" render_element_list.append(aov_name) return render_element_list @@ -263,20 +265,18 @@ def batch_render_elements(self, camera): render_elem_num = render_elem.NumRenderElements() if render_elem_num < 0: return - ext = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) target, renderpass = str(renderlayer_name).split(":") - aov_name = f"{directory}_{camera}_{renderpass}..{ext}" + aov_name = f"{directory}_{camera}_{renderpass}..{self.image_format}" render_elem.SetRenderElementFileName(i, aov_name) - def batch_render_layers_by_multi_camera(self, output_dir, cameras): + def batch_render_layers_by_multi_camera(self, cameras): """Get the list of renderlayers for the multi-camera from batch render manager. Args: - output_dir (str): output render directory cameras (list): Cameras to create render layers for. Returns: @@ -284,8 +284,11 @@ def batch_render_layers_by_multi_camera(self, output_dir, cameras): """ outputs = list() container = self._data["instance_node"] + render_folder = get_default_render_folder(self._data, self.project_settings) + sync_name = self._data.get("sync_current_workfile_name", True) + filename, _ = os.path.splitext(rt.MaxFileName) + output_dir = os.path.join(render_folder, filename.strip(".")) if sync_name else render_folder output = os.path.join(output_dir, container) - img_fmt = self._project_settings["max"]["RenderSettings"]["image_format"] # noqa for cam in cameras: camera = rt.getNodeByName(cam) layer_no = rt.batchRenderMgr.FindView(cam) @@ -297,6 +300,6 @@ def batch_render_layers_by_multi_camera(self, output_dir, cameras): # use camera name as renderlayer name renderlayer.name = cam cam = cam.replace(":", "_") - renderlayer.outputFilename = f"{output}_{cam}..{img_fmt}" + renderlayer.outputFilename = f"{output}_{cam}..{self.image_format}" outputs.append(renderlayer.outputFilename) return outputs diff --git a/client/ayon_max/api/validate_plugins.py b/client/ayon_max/api/validate_plugins.py index 0e7ea9157c..08007f09f6 100644 --- a/client/ayon_max/api/validate_plugins.py +++ b/client/ayon_max/api/validate_plugins.py @@ -160,16 +160,7 @@ def get_invalid_renderoutput( f"Filename should contain the workfile name pattern: {workfile_pattern}." ) invalid.append((msg, beauty_dir)) - beauty_fname = os.path.basename(rt.rendOutputFilename) - if multicam and cameras: - for camera in cameras: - if camera not in beauty_fname: - invalid.append(( - "Invalid render output filename", - "Render output filename should contain camera name " - f"{camera} when multiCamera is enabled. Found: {beauty_fname}", - )) if not is_general_default_output_regex_matched(beauty_fname): invalid.append(( diff --git a/client/ayon_max/plugins/create/create_render.py b/client/ayon_max/plugins/create/create_render.py index 0959bd921e..5c01f1a994 100644 --- a/client/ayon_max/plugins/create/create_render.py +++ b/client/ayon_max/plugins/create/create_render.py @@ -56,10 +56,7 @@ def create(self, product_name, instance_data, pre_create_data): for sel in self.selected_nodes: name = sel.name selected_nodes_name.append(name) - output_dir = os.path.dirname(rt.rendOutputFilename) - render_settings.batch_render_layers_by_multi_camera( - output_dir, selected_nodes_name - ) + render_settings.batch_render_layers_by_multi_camera(selected_nodes_name) def get_instance_attr_defs(self): render_target_items: dict[str, str] = { diff --git a/client/ayon_max/plugins/publish/collect_render.py b/client/ayon_max/plugins/publish/collect_render.py index 5babed4ba3..b651b65cb2 100644 --- a/client/ayon_max/plugins/publish/collect_render.py +++ b/client/ayon_max/plugins/publish/collect_render.py @@ -4,7 +4,7 @@ import os import pyblish.api import ayon_api -from typing import Dict, Any +from typing import Dict import pymxs from pymxs import runtime as rt @@ -13,8 +13,6 @@ from ayon_max.api.lib import ( get_max_version, get_current_renderer, - get_vray_settings, - get_multipass_setting, ) from ayon_max.api.lib_rendersettings import RenderSettings from ayon_max.api.lib_renderproducts import RenderProducts @@ -90,18 +88,8 @@ def process(self, instance): " one renderable camera in container") sel_cam = [camera.name for camera in get_cameras_from_node(cameras)] - - render_output = self.get_render_output( - renderer, - renderer_name, - img_format, - context.data["project_settings"] - ) - render_dir = os.path.dirname(render_output) render_settings = RenderSettings(data=instance.data) - outputs = render_settings.batch_render_layers_by_multi_camera( - render_dir, sel_cam - ) + outputs = render_settings.batch_render_layers_by_multi_camera(sel_cam) instance.data["cameras"] = sel_cam @@ -223,38 +211,6 @@ def get_colorspace_data(self) -> Dict[str, str]: "colorspace": colorspace_mgr.RenderingColorSpace } - def get_render_output( - self, - renderer: Any, - renderer_name: str, - img_format: str, - project_settings: Dict - ) -> str: - """Get render output path for the given renderer and instance. - - Args: - renderer (Any, rt.Renderers.current): The renderer to get the - output path from. - renderer_name (str): The name of the renderer. - img_format (str): The image format. - project_settings (Dict): The project settings. - - Returns: - str: The render output path. - """ - if renderer_name == "Redshift_Renderer": - return rt.rendOutputFilename - elif renderer_name == "Arnold_Renderer": - return renderer.AOVManager.outputPath - elif renderer_name.startswith("V-Ray"): - vr_settings = get_vray_settings(renderer_name, renderer) - multipass = get_multipass_setting(renderer, project_settings) - if multipass and img_format == "exr": - return vr_settings.output_rawfilename - else: - return vr_settings.output_splitfilename - return rt.rendOutputFilename - def _precollect_required_data(self, instance: pyblish.api.Instance) -> None: """Ensure required data is present. diff --git a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py index ecfac54bdb..105aff2c02 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -7,6 +7,7 @@ from pymxs import runtime as rt from ayon_core.lib import run_subprocess +from ayon_max.api.lib import get_max_version from ayon_max.api.lib_rendersettings import RenderSettings from ayon_max.api.lib_renderproducts import RenderProducts @@ -38,6 +39,7 @@ def process(self, instance): cameras = instance.data.get("cameras") if not cameras: return + renderer = instance.data["renderer"] new_folder = f"{current_folder}_{filename}" os.makedirs(new_folder, exist_ok=True) render_settings = RenderSettings(data=instance.data) @@ -60,36 +62,70 @@ def process(self, instance): new_output = "{new_output}" camera = "{camera}" farm = {farm} +renderer = "{renderer}" camera_name = camera.replace(":", "_") +rt.rendUseActiveView = True target_camera_node = rt.getNodeByName(camera) rt.viewport.setCamera(target_camera_node) rt.rendOutputFilename = new_output directory = os.path.dirname(rt.rendOutputFilename) directory = os.path.join(directory, filename) -if not os.path.exists(directory): - os.mkdir(directory) -render_elem = rt.maxOps.GetCurRenderElementMgr() -render_elem_num = render_elem.NumRenderElements() -if render_elem_num > 0: - ext = "{ext}" - for i in range(render_elem_num): - renderlayer_name = render_elem.GetRenderElement(i) - target, renderpass = str(renderlayer_name).split(":") - aov_name = f"{{directory}}_{{camera_name}}_{{renderpass}}..{ext}" - render_elem.SetRenderElementFileName(i, aov_name) +os.makedirs(directory, exist_ok=True) + +if renderer.startswith("V_Ray_"): + if "GPU" in renderer: + vray_settings = rt.renderers.current.V_Ray_settings + else: + vray_settings = rt.renderers.current + + if vray_settings.output_saverawfile: + vray_settings.output_rawfilename = f"{{directory}}_{{camera_name}}.{ext}" + + if vray_settings.output_splitgbuffer: + vray_settings.output_splitfilename = f"{{directory}}_{{camera_name}}.{ext}" + else: + rt.rendOutputFilename = f"{{directory}}_{{camera_name}}_tmp..{ext}" + +elif renderer.startswith("Arnold"): + aov_manager = rt.renderers.current.AOVManager + aov_driver = aov_manager.drivers[0] + instance_name = aov_driver.filenameSuffix + instance_name = instance_name.strip(".") + aov_driver.filenameSuffix = f"{{instance_name}}_{{camera_name}}." + +else: + render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem_num = render_elem.NumRenderElements() + if render_elem_num > 0: + ext = "{ext}" + for i in range(render_elem_num): + renderlayer_name = render_elem.GetRenderElement(i) + target, renderpass = str(renderlayer_name).split(":") + aov_name = f"{{directory}}_{{camera_name}}_{{renderpass}}..{ext}" + render_elem.SetRenderElementFileName(i, aov_name) + +rt.renderSceneDialog.update() rt.saveMaxFile(new_filepath) if not farm: - for frame in range(int(rt.rendStart), int(rt.rendEnd) + 1): - rt.render(outputFile=rt.rendOutputFilename, frame=frame, vfb=False) + if not renderer.startswith("Arnold"): + for frame in range(int(rt.rendStart), int(rt.rendEnd) + 1): + rt.render(frame=frame, camera=target_camera_node, vfb=False) + else: + for frame in range(int(rt.rendStart), int(rt.rendEnd) + 1): + outputfile = f"{{directory}}_{{camera_name}}.{{frame}}.{ext}" + rt.render(outputfile=outputfile, frame=frame, camera=target_camera_node, vfb=False) """).format(filename=instance.name, new_filepath=new_filepath, new_output=new_output, camera=camera, ext=fmt, + renderer=renderer, farm=instance.data.get("farm")) scripts.append(script) - maxbatch_exe = os.path.join( - os.path.dirname(sys.executable), "3dsmaxbatch") + max_directory = os.path.dirname(sys.executable) + if get_max_version() >= 2026: + max_directory = os.path.dirname(max_directory) + maxbatch_exe = os.path.join(max_directory, "3dsmaxbatch") maxbatch_exe = maxbatch_exe.replace("\\", "/") if platform.system().lower() == "windows": maxbatch_exe += ".exe" @@ -104,7 +140,7 @@ def process(self, instance): tmp.write(script + "\n") full_script = "\n".join(scripts) - self.log.debug(f"Failed running script {tmp_script_path}:\n{full_script}") + self.log.debug(f"Prepared script {tmp_script_path}:\n{full_script}") current_filepath = current_filepath.replace("\\", "/") tmp_script_path = tmp_script_path.replace("\\", "/") run_subprocess([maxbatch_exe, tmp_script_path, diff --git a/client/ayon_max/plugins/publish/validate_rendersettings.py b/client/ayon_max/plugins/publish/validate_rendersettings.py index cbe9a2d3ee..f76a57bb4a 100644 --- a/client/ayon_max/plugins/publish/validate_rendersettings.py +++ b/client/ayon_max/plugins/publish/validate_rendersettings.py @@ -137,15 +137,6 @@ def get_invalid_render_element_directory( cls.log.error(msg) invalid.append((msg, directory)) - if multi_camera and cameras: - for camera in cameras: - if camera not in directory: - invalid.append(( - "Invalid render element output directory", - "Render element output directory should contain camera name " - f"{camera} when multiCamera is enabled. Found: {directory}", - )) - return invalid @classmethod @@ -496,12 +487,12 @@ def repair_arnold_settings( if not path: path = reset_rendersetting(instance, project_settings) render_dir = os.path.dirname(path) - aov_manager.outputPath = path filename = os.path.basename(path) rt.rendOutputFilename = build_general_output_filename( render_dir, filename, ) + aov_manager.outputPath = os.path.dirname(rt.rendOutputFilename) driver = aov_manager.drivers[0] driver.multipart = get_multipass_setting( renderer_name,