From 5f9dbb6b52527bca1165ce1d0dc9bbd772962617 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 16:21:50 +0800 Subject: [PATCH 01/18] clean up the validation plugins --- client/ayon_max/api/validate_plugins.py | 9 --------- .../ayon_max/plugins/publish/validate_rendersettings.py | 9 --------- 2 files changed, 18 deletions(-) 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/publish/validate_rendersettings.py b/client/ayon_max/plugins/publish/validate_rendersettings.py index cbe9a2d3ee..4f326023c1 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 From 28d56d399d9d91c720f835ecae86cd5dc6fcfd8f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 16:22:53 +0800 Subject: [PATCH 02/18] make sure we got the 3dsmax executable directory for 2026/2027 as sys.executable is referring to python executable in 2026/2027 --- .../ayon_max/plugins/publish/save_scenes_for_cameras.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) 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..d59db85925 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 @@ -88,8 +89,10 @@ def process(self, instance): ext=fmt, 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 +107,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, From b5217361b63f4e5989b496faa7be06ef6fe4c43e Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 19:01:55 +0800 Subject: [PATCH 03/18] support vray multicamera render submission --- .../publish/save_scenes_for_cameras.py | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) 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 d59db85925..46ef59b17c 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -39,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) @@ -61,6 +62,7 @@ def process(self, instance): new_output = "{new_output}" camera = "{camera}" farm = {farm} +renderer = "{renderer}" camera_name = camera.replace(":", "_") target_camera_node = rt.getNodeByName(camera) rt.viewport.setCamera(target_camera_node) @@ -69,24 +71,44 @@ def process(self, instance): 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) + +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}" + +else: + render_elem = rt.maxOps.GetCurRenderElementMgr() + render_elem_num = render_elem.NumRenderElements() + if render_elem_num > 0: + else: + 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) + rt.render(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) max_directory = os.path.dirname(sys.executable) From 371bf1beabc6da244398e46bc11ec7e414065fac Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 19:28:30 +0800 Subject: [PATCH 04/18] bugfix: make sure the multiple render product does not get camera name embedded in expected files. --- client/ayon_max/api/lib_renderproducts.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index 4fd4dba01a..9e460e9eff 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 @@ -289,6 +301,8 @@ def get_expected_files( name, ext = os.path.splitext(filename) name = name.lstrip(".") 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) From 7a0abf15ec87a40a2eabd5113f018fb21eafc299 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 30 Jul 2026 19:31:00 +0800 Subject: [PATCH 05/18] bugfix: make sure the multiple render product does not get camera name embedded in expected files. --- client/ayon_max/api/lib_renderproducts.py | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index 4fd4dba01a..9e460e9eff 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 @@ -289,6 +301,8 @@ def get_expected_files( name, ext = os.path.splitext(filename) name = name.lstrip(".") 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) From 21f2a51f93b366d4f593d33569a637d94af6bd5a Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Aug 2026 17:27:49 +0800 Subject: [PATCH 06/18] fix the syntax error --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 1 - 1 file changed, 1 deletion(-) 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 46ef59b17c..50b98bb9df 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -90,7 +90,6 @@ def process(self, instance): render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() if render_elem_num > 0: - else: ext = "{ext}" for i in range(render_elem_num): renderlayer_name = render_elem.GetRenderElement(i) From adc72bc08a0387498e8ab9bbba068043a2625681 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Aug 2026 19:55:22 +0800 Subject: [PATCH 07/18] fix the os error during suprocess --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 50b98bb9df..b5915ebba2 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -69,8 +69,7 @@ def process(self, instance): 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) +os.makedirs(directory, exist_ok=True) if renderer.startswith("V_Ray_"): if "GPU" in renderer: From e1d16a1901165204ec2153c98c19e3673494d210 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Mon, 3 Aug 2026 19:55:48 +0800 Subject: [PATCH 08/18] refactor get the batch render output name --- client/ayon_max/api/lib_rendersettings.py | 59 ++++++++++--------- .../ayon_max/plugins/create/create_render.py | 5 +- .../plugins/publish/collect_render.py | 44 +------------- 3 files changed, 33 insertions(+), 75 deletions(-) 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/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..a06da00d91 100644 --- a/client/ayon_max/plugins/publish/collect_render.py +++ b/client/ayon_max/plugins/publish/collect_render.py @@ -90,18 +90,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 +213,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. From 5a3d45e368983d636354763131bc9f705a652cd1 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Tue, 4 Aug 2026 18:46:21 +0800 Subject: [PATCH 09/18] Make sure view is active for changing camera in the render setting. --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 1 + 1 file changed, 1 insertion(+) 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 b5915ebba2..8c97aadb37 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -64,6 +64,7 @@ def process(self, instance): 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 From 8d7de90f5d3ca3db170daef89d8cac569e6cdd14 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Wed, 5 Aug 2026 13:07:48 +0800 Subject: [PATCH 10/18] ruff fix --- client/ayon_max/plugins/publish/collect_render.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/ayon_max/plugins/publish/collect_render.py b/client/ayon_max/plugins/publish/collect_render.py index a06da00d91..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 From 724a980fa4ebac402e64d5d1409cfb3dfe90832f Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Aug 2026 15:23:03 +0800 Subject: [PATCH 11/18] strip(".") should be correct --- client/ayon_max/api/lib_renderproducts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_max/api/lib_renderproducts.py b/client/ayon_max/api/lib_renderproducts.py index 9e460e9eff..6977f2c84a 100644 --- a/client/ayon_max/api/lib_renderproducts.py +++ b/client/ayon_max/api/lib_renderproducts.py @@ -299,7 +299,7 @@ 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}" From 18f2d50f4a4990b3c8f6e1c5b4696ca262662c5d Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Thu, 6 Aug 2026 15:23:27 +0800 Subject: [PATCH 12/18] Add support for arnold renders in terms of multi-camera workflow(only for beauty as usual) --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 6 ++++++ 1 file changed, 6 insertions(+) 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 8c97aadb37..8bf7068073 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -86,6 +86,12 @@ def process(self, instance): else: rt.rendOutputFilename = f"{{directory}}_{{camera_name}}_tmp..{ext}" +elif renderer.startswith("Arnold"): + aov_manager = rt.renderers.current.AOVManager + instance_name = aov_manager.drivers[1].filenameSuffix + instance_name = instance_name.strip(".") + aov_manager.drivers[1].filenameSuffix = f"{{instance_name}}_{{camera_name}}." + else: render_elem = rt.maxOps.GetCurRenderElementMgr() render_elem_num = render_elem.NumRenderElements() From d2769dbb6e73a8c3f4b24b4641a9c8f9bf9bce99 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Aug 2026 17:55:35 +0800 Subject: [PATCH 13/18] make sure returning correct iterations for Arnold aov drivers. --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 8bf7068073..8ff86162e6 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -88,9 +88,10 @@ def process(self, instance): elif renderer.startswith("Arnold"): aov_manager = rt.renderers.current.AOVManager - instance_name = aov_manager.drivers[1].filenameSuffix + aov_driver = aov_manager.drivers[0] + instance_name = aov_driver.filenameSuffix instance_name = instance_name.strip(".") - aov_manager.drivers[1].filenameSuffix = f"{{instance_name}}_{{camera_name}}." + aov_driver.filenameSuffix = f"{{instance_name}}_{{camera_name}}." else: render_elem = rt.maxOps.GetCurRenderElementMgr() From 31614d258cd2610872437e3d09e96484ec045012 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Aug 2026 18:50:37 +0800 Subject: [PATCH 14/18] arnold output path should be camera-based --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 8ff86162e6..7b04234c98 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -88,10 +88,8 @@ def process(self, instance): elif renderer.startswith("Arnold"): aov_manager = rt.renderers.current.AOVManager + aov_manager.outputPath = f"{{directory}}_{{camera_name}}..{ext}" 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() From b467e934c30c9c82ba9305733376157a3f0b7096 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Fri, 7 Aug 2026 19:01:17 +0800 Subject: [PATCH 15/18] arnold: fix the validate settings and filenameSuffix. --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 4 +++- client/ayon_max/plugins/publish/validate_rendersettings.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) 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 7b04234c98..8ff86162e6 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -88,8 +88,10 @@ def process(self, instance): elif renderer.startswith("Arnold"): aov_manager = rt.renderers.current.AOVManager - aov_manager.outputPath = f"{{directory}}_{{camera_name}}..{ext}" 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() diff --git a/client/ayon_max/plugins/publish/validate_rendersettings.py b/client/ayon_max/plugins/publish/validate_rendersettings.py index 4f326023c1..08db3a1326 100644 --- a/client/ayon_max/plugins/publish/validate_rendersettings.py +++ b/client/ayon_max/plugins/publish/validate_rendersettings.py @@ -487,7 +487,7 @@ def repair_arnold_settings( if not path: path = reset_rendersetting(instance, project_settings) render_dir = os.path.dirname(path) - aov_manager.outputPath = path + aov_manager.outputPath = render_dir filename = os.path.basename(path) rt.rendOutputFilename = build_general_output_filename( render_dir, From 314d619e0094fe094aed00e0c65eae3b45035f97 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 8 Aug 2026 22:02:36 +0800 Subject: [PATCH 16/18] update the save camera extract script to support arnold --- .../ayon_max/plugins/publish/save_scenes_for_cameras.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 8ff86162e6..72bd6b15dd 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -107,8 +107,12 @@ def process(self, instance): rt.renderSceneDialog.update() rt.saveMaxFile(new_filepath) if not farm: - for frame in range(int(rt.rendStart), int(rt.rendEnd) + 1): - rt.render(frame=frame, camera=target_camera_node, 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): + rt.render(outputfile=rt.rendOutputFilename, frame=frame, camera=target_camera_node, vfb=False) """).format(filename=instance.name, new_filepath=new_filepath, new_output=new_output, From 7c6bb3813779bf7cc2006c28cfb29d707058c261 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sat, 8 Aug 2026 22:25:45 +0800 Subject: [PATCH 17/18] use outputfile and specific outputfile name for arnold render --- client/ayon_max/plugins/publish/save_scenes_for_cameras.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 72bd6b15dd..105aff2c02 100644 --- a/client/ayon_max/plugins/publish/save_scenes_for_cameras.py +++ b/client/ayon_max/plugins/publish/save_scenes_for_cameras.py @@ -112,7 +112,8 @@ def process(self, instance): rt.render(frame=frame, camera=target_camera_node, vfb=False) else: for frame in range(int(rt.rendStart), int(rt.rendEnd) + 1): - rt.render(outputfile=rt.rendOutputFilename, frame=frame, camera=target_camera_node, vfb=False) + 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, From 11d0656be05faace6a5cb675a99fc18ceadf1403 Mon Sep 17 00:00:00 2001 From: Kayla Man Date: Sun, 9 Aug 2026 15:43:19 +0800 Subject: [PATCH 18/18] fix the validator repair action --- client/ayon_max/plugins/publish/validate_rendersettings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/ayon_max/plugins/publish/validate_rendersettings.py b/client/ayon_max/plugins/publish/validate_rendersettings.py index 08db3a1326..f76a57bb4a 100644 --- a/client/ayon_max/plugins/publish/validate_rendersettings.py +++ b/client/ayon_max/plugins/publish/validate_rendersettings.py @@ -487,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 = render_dir 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,