Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
d91b922
clean up and add support for custom frame range in local rendering
moonyuet May 8, 2026
476d32f
make sure we can get the correct frame range in max
moonyuet May 8, 2026
c1ab810
Potential fix for pull request finding
moonyuet May 8, 2026
9fbe351
Potential fix for pull request finding
moonyuet May 8, 2026
8813e5d
Potential fix for pull request finding
moonyuet May 8, 2026
dd97e28
upload the code suggestion by copilot
moonyuet May 8, 2026
ff51562
Merge remote-tracking branch 'origin/develop' into enhancement/YN-072…
moonyuet May 8, 2026
2e113bb
wip: resolve conlfict
moonyuet May 26, 2026
ebe08da
wip: rename custom_frames to customFrames
moonyuet May 26, 2026
4127c20
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
moonyuet Jun 26, 2026
2249e81
remove unused logic.
moonyuet Jul 2, 2026
e6e48d6
Clean up and implement the existing logic from core
moonyuet Jul 2, 2026
492004b
add hasExplicitFrames and reuseLastVersion data
moonyuet Jul 2, 2026
0f38a37
revert vfb changes
moonyuet Jul 2, 2026
3f58cdb
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
moonyuet Jul 3, 2026
6267bba
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
moonyuet Jul 7, 2026
48a13e0
Potential fix for pull request finding
moonyuet Jul 7, 2026
b00226a
revert the change by cilpot suggestion & implement the changes to mak…
moonyuet Jul 7, 2026
944d506
add docstrings.
moonyuet Jul 7, 2026
ba0d366
add the colorspace data into the OIIO transcode.
moonyuet Jul 7, 2026
a5bb473
Merge remote-tracking branch 'origin/develop' into enhancement/YN-072…
moonyuet Jul 8, 2026
6a32413
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
moonyuet Jul 8, 2026
4aa7040
Merge remote-tracking branch 'origin/develop' into enhancement/YN-072…
moonyuet Jul 8, 2026
80cfbe7
Merge branch 'enhancement/YN-0726--Custom-Frame-Range-Support-for-Loc…
moonyuet Jul 8, 2026
4024560
Update client/ayon_max/api/lib.py
moonyuet Jul 15, 2026
ea91cfd
add docstring
moonyuet Jul 16, 2026
a0e7ad0
add comment for `hasExplicitFrames` and remove unneccessary instance …
moonyuet Jul 16, 2026
48812e1
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
moonyuet Jul 16, 2026
4e504aa
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
antirotor Jul 16, 2026
8a70eba
Update client/ayon_max/plugins/publish/collect_render_local.py
moonyuet Jul 16, 2026
c31a3d1
rename frame_range to frames
moonyuet Jul 30, 2026
f5de5af
add comment to explain why ordering move later
moonyuet Jul 30, 2026
87e8ebb
clean up
moonyuet Jul 30, 2026
6ddfc05
clarify the comment on HasExplicitFrames
moonyuet Jul 30, 2026
8d8e0e9
ruff fix
moonyuet Jul 30, 2026
f1035a8
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
moonyuet Aug 7, 2026
5868d9b
update the repair action for repairing arnold render settings.
moonyuet Aug 10, 2026
34a3fa2
fix the local rendering failed in Arnold.
moonyuet Aug 10, 2026
38b3a68
normalize the path for stagingDir when using local rendering
moonyuet Aug 14, 2026
83e6692
Merge remote-tracking branch 'origin/develop' into enhancement/YN-072…
moonyuet Aug 14, 2026
e5a4bab
use Pathlib only for expectedFiles.
moonyuet Aug 14, 2026
ecc9991
ruff fix
moonyuet Aug 14, 2026
67e2d2b
normalized the forward slashes to fix the path issue faced in Arnold
moonyuet Aug 14, 2026
d2135c7
Merge branch 'develop' into enhancement/YN-0726--Custom-Frame-Range-S…
moonyuet Sep 4, 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
55 changes: 55 additions & 0 deletions client/ayon_max/api/lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -1060,3 +1060,58 @@ def build_general_output_filename(
ext = match.group("ext")
filename = f"{name}_{element}..{ext}"
return os.path.join(output_dir, filename)


def get_expected_frames(instance: pyblish.api.Instance) -> list[int]:
"""Get expected frames from the instance.

Args:
instance (pyblish.api.Instance): The Pyblish instance.

Returns:
list[int]: A list containing the expected frames.
"""
def parse_frame_range(frame_str: str) -> list[int]:
"""Parse a frame range string into a list of frames.

Args:
frame_str (str): The frame range string.

Returns:
list[int]: A list of frames.
"""
frames = []
for part in frame_str.split(","):
part = part.strip()
if not part:
continue

if "-" in part:
start_end = part.split("-", 1)
if len(start_end) != 2 or not start_end[0] or not start_end[1]:
raise ValueError(
f"Invalid frame range segment: '{part}'"
)

start, end = map(int, start_end)
if start > end:
raise ValueError(
f"Frame range start must be less than or equal "
f"to end: '{part}'"
)
frames.extend(range(start, end + 1))
else:
frames.append(int(part))

if not frames:
raise ValueError(
f"No valid frames could be parsed from '{frame_str}'"
)
return frames

frames = instance.data.get("customFrames", "")
if frames:
return parse_frame_range(frames)
frame_start = int(rt.rendStart)
frame_end = int(rt.rendEnd)
return list(range(frame_start, frame_end + 1))
36 changes: 14 additions & 22 deletions client/ayon_max/api/lib_renderproducts.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ def __init__(self, project_settings: Dict[str, Any] = None):
get_current_project_name()
)

def get_render_products(self) -> Dict[str, list[str]]:
def get_render_products(self, frame_range: list[int]) -> Dict[str, list[str]]:
Comment thread
moonyuet marked this conversation as resolved.
Outdated
"""Get render output file paths for the current scene.

Handles both beauty and AOV extraction with shared setup logic.
Expand All @@ -50,8 +50,7 @@ def get_render_products(self) -> Dict[str, list[str]]:
(e.g., "Cryptomatte", "Alpha").
"""
extension = self.image_format()
start_frame = int(rt.rendStart)
end_frame = int(rt.rendEnd)

# todo: Support Custom Frames sequences 0,5-10,100-120
# we can add filtering frames list to get expected frames list
# instead of using start and end frame
Expand All @@ -60,7 +59,7 @@ def get_render_products(self) -> Dict[str, list[str]]:
render_dict: Dict[str, list[str]] = {}

# Always add beauty pass
render_dict["beauty"] = self.get_expected_beauty(start_frame, end_frame, extension)
render_dict["beauty"] = self.get_expected_beauty(frame_range, extension)

# Optionally add AOVs
renderer = get_current_renderer()
Expand All @@ -70,8 +69,7 @@ def get_render_products(self) -> Dict[str, list[str]]:
for aov_name, aov_filepath in render_elements:
aov_expected_files = self.get_expected_files(
aov_filepath,
start_frame,
end_frame,
frame_range,
aov_name,
renderer_name
)
Expand All @@ -80,7 +78,7 @@ def get_render_products(self) -> Dict[str, list[str]]:
return render_dict

def get_multiple_render_products(
self, outputs: list[str], cameras: list[str]
self, outputs: list[str], cameras: list[str], frame_range: list[int]
) -> Dict[str, list[str]]:
Comment thread
moonyuet marked this conversation as resolved.
"""Get render output file paths for multiple cameras.

Expand All @@ -105,11 +103,9 @@ def get_multiple_render_products(
filename, ext = os.path.splitext(output)
filename = filename.replace(".", "")
ext = ext.replace(".", "")
start_frame = int(rt.rendStart)
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(frame_range, ext)
render_output_frames[f"{camera}_beauty"] = beauty_files

# Add AOVs
Expand All @@ -118,8 +114,7 @@ def get_multiple_render_products(
for aov_name, aov_filepath in render_elements:
aov_expected_files = self.get_expected_files(
aov_filepath,
start_frame,
end_frame,
frame_range,
aov_name,
renderer_name
)
Expand All @@ -128,13 +123,13 @@ def get_multiple_render_products(
return render_output_frames

def get_expected_beauty(
self, start_frame: int, end_frame: int, extension: str
self, frame_range: list[int], extension: str
) -> list[str]:
"""Get expected beauty render output file paths for each frame.

Args:
start_frame (int): The starting frame number.
end_frame (int): The ending frame number.
frame_range (list[int]): The frame range to generate expected
output file paths for.
extension (str): The file extension for the output files.

Returns:
Expand All @@ -151,8 +146,7 @@ def get_expected_beauty(

return self.get_expected_files(
output_path,
start_frame,
end_frame,
frame_range,
"",
renderer_name
)
Expand Down Expand Up @@ -258,17 +252,15 @@ def get_arnold_render_output(self, arnold_renderer: Any, extension: str) -> str:
def get_expected_files(
self,
filepath: str,
start_frame: int,
end_frame: int,
frame_range: list[int],
aov_name: str,
renderer_name: str,
) -> list[str]:
"""Get expected files

Args:
filepath (str): filepath of the render output.
start_frame (int): start frame of the render sequence.
end_frame (int): end frame of the render sequence.
frame_range (list[int]): range of frames of the render sequence.
aov_name (str): name of the AOV.
renderer_name (str): name of the renderer.

Expand All @@ -281,7 +273,7 @@ def get_expected_files(
name, ext = os.path.splitext(filename)
name = name.lstrip(".")
aov_name = aov_name.strip()
for frame in range(start_frame, end_frame + 1):
for frame in frame_range:
aov_filename = f"{name}.{frame:04d}{ext}"
expected_aov = os.path.join(directory, aov_filename)
if aov_name and renderer_name.startswith("V_Ray_"):
Expand Down
9 changes: 6 additions & 3 deletions client/ayon_max/plugins/publish/collect_frame_range.py

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not really related to this PR but I am wondering why are we filling handles here explicitly.

@moonyuet moonyuet Jul 16, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is filled to ensure for the validation in frame range as the frame range data in 3dsmax workflow (I mean other than render product type) are based on frameStartHandle and frameEndHandle

We can move discussion as another github issue as I also noticed that frameStartHandle and frameEndHandle are handle differently on render product. And we should use frameStart and frameEnd in the render product in terms of collect frame range as well as the validation instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, we should render the full range. The handles metadata on this instance means that the handle frames are included in this output. If you don't want to render the handles, then the instance should not have handles set. But what we render and produce, should include handles.

Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
import pyblish.api
from pymxs import runtime as rt
from ayon_max.api.lib import get_expected_frames


class CollectFrameRange(pyblish.api.InstancePlugin):
"""Collect Frame Range."""

order = pyblish.api.CollectorOrder + 0.011
order = pyblish.api.CollectorOrder + 0.019
Comment thread
moonyuet marked this conversation as resolved.
label = "Collect Frame Range"
hosts = ['max']
families = ["camera", "maxrender",
Expand All @@ -16,8 +17,10 @@ class CollectFrameRange(pyblish.api.InstancePlugin):

def process(self, instance):
if instance.data["productBaseType"] == "maxrender":
instance.data["frameStartHandle"] = int(rt.rendStart)
instance.data["frameEndHandle"] = int(rt.rendEnd)
frame_range = get_expected_frames(instance)
instance.data["frameStartHandle"] = min(frame_range)
instance.data["frameEndHandle"] = max(frame_range)
instance.data["expectedFrameRange"] = frame_range
Comment thread
moonyuet marked this conversation as resolved.

elif instance.data["productBaseType"] in {"tycache", "tyspline"}:
operator = instance.data["operator"]
Expand Down
5 changes: 3 additions & 2 deletions client/ayon_max/plugins/publish/collect_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ def process(self, instance):
renderer_name = str(renderer).split(":")[0]
renderproducts = RenderProducts(context.data["project_settings"])
img_format = renderproducts.image_format()
files_by_aov: Dict[str, list[str]] = renderproducts.get_render_products()
expected_frames = instance.data.get("expectedFrameRange", [])
files_by_aov: Dict[str, list[str]] = renderproducts.get_render_products(expected_frames)
Comment thread
Copilot marked this conversation as resolved.
Outdated


camera = rt.viewport.GetCamera()
Expand Down Expand Up @@ -94,7 +95,7 @@ def process(self, instance):
instance.data["cameras"] = sel_cam

files_by_aov = renderproducts.get_multiple_render_products(
outputs, sel_cam
outputs, sel_cam, expected_frames
)

if "expectedFiles" not in instance.data:
Expand Down
7 changes: 4 additions & 3 deletions client/ayon_max/plugins/publish/extract_render.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def process(self, instance):
if camera else rt.viewport.GetCamera()
)

for frame in range(int(rt.rendStart), int(rt.rendEnd) + 1):
was_cancelled = rt.Name('wasCancelled')
was_cancelled.value = False

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems unused?

for frame in instance.data["expectedFrameRange"]:
_, cancelled = rt.render(
frame=frame,
vfb=False,
Expand All @@ -46,8 +48,7 @@ def process(self, instance):
)
if cancelled:
raise KnownPublishError(f"Render cancelled at frame {frame}.")

self.log.debug("Local render extraction completed.")
self.log.debug("Local render extraction completed.")
else:
self.log.debug(
"Local render extraction for multi-camera is already "
Expand Down
Loading