Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
5f9dbb6
clean up the validation plugins
moonyuet Jul 30, 2026
28d56d3
make sure we got the 3dsmax executable directory for 2026/2027 as sys…
moonyuet Jul 30, 2026
b521736
support vray multicamera render submission
moonyuet Jul 30, 2026
371bf1b
bugfix: make sure the multiple render product does not get camera nam…
moonyuet Jul 30, 2026
7a0abf1
bugfix: make sure the multiple render product does not get camera nam…
moonyuet Jul 30, 2026
3bdaf8d
Merge remote-tracking branch 'origin/bugfix/3dsmax_batch_directory_sh…
moonyuet Jul 30, 2026
21f2a51
fix the syntax error
moonyuet Aug 3, 2026
adc72bc
fix the os error during suprocess
moonyuet Aug 3, 2026
e1d16a1
refactor get the batch render output name
moonyuet Aug 3, 2026
5a3d45e
Make sure view is active for changing camera in the render setting.
moonyuet Aug 4, 2026
db92feb
Merge pull request #160 from ynput/enhancement/add_vray_support_for_m…
moonyuet Aug 4, 2026
8d7de90
ruff fix
moonyuet Aug 5, 2026
724a980
strip(".") should be correct
moonyuet Aug 6, 2026
18f2d50
Add support for arnold renders in terms of multi-camera workflow(only…
moonyuet Aug 6, 2026
d2769db
make sure returning correct iterations for Arnold aov drivers.
moonyuet Aug 7, 2026
31614d2
arnold output path should be camera-based
moonyuet Aug 7, 2026
b467e93
arnold: fix the validate settings and filenameSuffix.
moonyuet Aug 7, 2026
52631cd
Merge branch 'develop' into bugfix/3dsmax_batch_directory_should_be_c…
moonyuet Aug 8, 2026
314d619
update the save camera extract script to support arnold
moonyuet Aug 8, 2026
7c6bb38
use outputfile and specific outputfile name for arnold render
moonyuet Aug 8, 2026
11d0656
fix the validator repair action
moonyuet Aug 9, 2026
1e9d7cb
Merge branch 'develop' into bugfix/3dsmax_batch_directory_should_be_c…
moonyuet Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions client/ayon_max/api/lib_renderproducts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.

Expand All @@ -154,7 +164,8 @@ def get_expected_beauty(
start_frame,
end_frame,
"",
renderer_name
renderer_name,
camera=camera,
)

def get_render_element_outputfilename(
Expand Down Expand Up @@ -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

Expand All @@ -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)
Expand Down
59 changes: 31 additions & 28 deletions client/ayon_max/api/lib_rendersettings.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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 = (
Expand All @@ -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()
Expand Down Expand Up @@ -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):
Expand All @@ -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

Expand All @@ -263,29 +265,30 @@ 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:
list: List of output filenames for the render layers
"""
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)
Expand All @@ -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
9 changes: 0 additions & 9 deletions client/ayon_max/api/validate_plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -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((
Expand Down
5 changes: 1 addition & 4 deletions client/ayon_max/plugins/create/create_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {
Expand Down
48 changes: 2 additions & 46 deletions client/ayon_max/plugins/publish/collect_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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.

Expand Down
Loading
Loading