Skip to content

Commit 2d2a5b3

Browse files
author
Dallin Clark
committed
Clean commit: only Nuke-related changes
1 parent f0bef32 commit 2d2a5b3

File tree

3 files changed

+219
-60
lines changed

3 files changed

+219
-60
lines changed

pipeline/software/nuke/tools/BobukeTools/menu.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ def make_bobo_fx_read_node():
2121

2222
# run the FX read now
2323
bobo_read_node.auto_read_latest_fx_exr()
24+
def make_bobo_cfx_read_node():
25+
import bobo_read_node # type: ignore[import-not-found]
26+
27+
# run the FX read now
28+
bobo_read_node.auto_read_latest_cfx_exr()
2429

2530

2631
def make_bobo_write_node():
@@ -111,7 +116,7 @@ def set_frameRange_and_aspectRatio():
111116
m.addCommand("Bobo Read Node", "make_bobo_read_node()", icon="BobukeIcon.png")
112117

113118
m.addCommand("Bobo FX Read", "make_bobo_fx_read_node()", icon="BobukeIcon.png")
114-
119+
m.addCommand("Bobo CFX Read", "make_bobo_cfx_read_node()", icon="BobukeIcon.png")
115120

116121
################################### Nungeon Shelf Tool Buttons ###################################
117122
menu = nuke.menu("Nuke")

pipeline/software/nuke/tools/BobukeTools/scripts/bobo_read_node.py

Lines changed: 212 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,83 @@
22
import os
33
import re
44
import glob
5+
import math
6+
from functools import reduce
57
from datetime import datetime
68

79

8-
def get_latest_exr_sequence(render_root):
10+
def _find_images_dir(base):
11+
"""Return images dir under base, preferring images_dn over images."""
12+
for sub in ("images_dn", "images"):
13+
p = os.path.join(base, sub)
14+
if os.path.isdir(p):
15+
return p
16+
return None
17+
18+
19+
def _gcd_list(values):
20+
"""Greatest common divisor for a list of positive integers."""
21+
if not values:
22+
return 1
23+
return reduce(math.gcd, values)
24+
25+
26+
def _scan_exr_sequence(images_dir):
927
"""
10-
Scan render_root for MM-DD-YYYY_V_### folders and, inside the newest one,
11-
discover all .exr files, extract their frame numbers & padding, and return:
12-
(pattern, first_frame, last_frame)
13-
where pattern is something like '/…/images/%04d.exr'
28+
Return (pattern, first_frame, last_frame, pad, cadence_step)
29+
cadence_step is the GCD of frame gaps (1 = full, 2 = on 2s, 4 = on 4s, etc.)
30+
"""
31+
files = glob.glob(os.path.join(images_dir, "*.exr"))
32+
if not files:
33+
return None
34+
35+
nums, pad = [], 0
36+
for f in files:
37+
fname = os.path.basename(f)
38+
m = re.match(r"^(.*?)(\d+)\.exr$", fname)
39+
if not m:
40+
continue
41+
n = int(m.group(2))
42+
nums.append(n)
43+
pad = max(pad, len(m.group(2)))
44+
45+
if not nums:
46+
return None
47+
48+
nums = sorted(set(nums))
49+
first_frame = nums[0]
50+
last_frame = nums[-1]
51+
diffs = [b - a for a, b in zip(nums, nums[1:]) if b > a]
52+
step = _gcd_list(diffs) if diffs else 1
53+
if step <= 0:
54+
step = 1
55+
56+
pattern = os.path.join(images_dir, "%%0%dd.exr" % pad)
57+
return pattern, first_frame, last_frame, pad, step
58+
59+
60+
def get_latest_exr_sequences(render_root):
61+
"""
62+
Discover EXR sequences in the newest MM-DD-YY_V_### directory.
63+
64+
- If subfolders exist under the newest date/version dir:
65+
scan each subfolder; in each, prefer images_dn/ over images/
66+
- If no subfolders:
67+
look directly under the newest date/version dir for images_dn/ or images/
68+
69+
Returns a list of dicts:
70+
[{
71+
'label': <subfolder or 'root'>,
72+
'pattern': '/…/%04d.exr',
73+
'first': int,
74+
'last': int,
75+
'step': int # 1 (full), 2 (on 2s), 4 (on 4s), etc.
76+
}, ...]
1477
"""
1578
if not os.path.isdir(render_root):
1679
nuke.message(f"[Auto Read] Render folder does not exist:\n {render_root}")
17-
return None, None, None
80+
return []
1881

19-
# find latest date/version subfolder
2082
folder_pattern = re.compile(r"^(\d{2}-\d{2}-\d{2})_V_(\d{3})$")
2183
candidates = []
2284
for name in os.listdir(render_root):
@@ -28,77 +90,169 @@ def get_latest_exr_sequence(render_root):
2890
candidates.append((date_obj, version, path))
2991
if not candidates:
3092
nuke.message("[Auto Read] No valid render subfolders found.")
31-
return None, None, None
93+
return []
3294

33-
# pick newest, then check for images_dn/images
3495
_, _, newest = sorted(candidates, key=lambda x: (x[0], x[1]))[-1]
35-
for sub in ("images_dn", "images"):
36-
subdir = os.path.join(newest, sub)
37-
if os.path.isdir(subdir):
38-
newest = subdir
39-
break
4096

41-
# glob all .exr files
42-
files = glob.glob(os.path.join(newest, "*.exr"))
43-
if not files:
44-
nuke.message(f"[Auto Read] No .exr files found in:\n {newest}")
45-
return None, None, None
97+
subfolders = [d for d in os.listdir(newest)
98+
if os.path.isdir(os.path.join(newest, d))]
4699

47-
# extract frame numbers and detect padding
48-
nums, pad = [], 0
49-
for f in files:
50-
fname = os.path.basename(f)
51-
m = re.match(r"^(.*?)(\d+)\.exr$", fname)
52-
if not m:
53-
continue
54-
nums.append(int(m.group(2)))
55-
pad = max(pad, len(m.group(2)))
100+
sequences = []
56101

57-
if not nums:
58-
nuke.message(f"[Auto Read] Couldn't parse frame numbers in:\n {newest}")
59-
return None, None, None
102+
if subfolders:
103+
for sub in sorted(subfolders):
104+
base = os.path.join(newest, sub)
105+
images_dir = _find_images_dir(base)
106+
if not images_dir:
107+
continue
108+
seq = _scan_exr_sequence(images_dir)
109+
if not seq:
110+
continue
111+
pattern, first, last, _pad, step = seq
112+
sequences.append({
113+
"label": sub,
114+
"pattern": pattern,
115+
"first": first,
116+
"last": last,
117+
"step": step
118+
})
119+
if not sequences:
120+
nuke.message(f"[Auto Read] No .exr sequences found under any subfolder in:\n {newest}")
121+
return []
122+
else:
123+
images_dir = _find_images_dir(newest)
124+
if not images_dir:
125+
nuke.message(f"[Auto Read] Neither images_dn nor images found in:\n {newest}")
126+
return []
127+
seq = _scan_exr_sequence(images_dir)
128+
if not seq:
129+
nuke.message(f"[Auto Read] No .exr files found in:\n {images_dir}")
130+
return []
131+
pattern, first, last, _pad, step = seq
132+
sequences.append({
133+
"label": "root",
134+
"pattern": pattern,
135+
"first": first,
136+
"last": last,
137+
"step": step
138+
})
139+
140+
return sequences
60141

61-
first_frame = min(nums)
62-
last_frame = max(nums)
63-
pattern = os.path.join(newest, "%%0%dd.exr" % pad)
64142

65-
return pattern, first_frame, last_frame
143+
def _sanitize_for_nuke(name):
144+
"""Keep node names safe for Nuke."""
145+
safe = re.sub(r"[^0-9a-zA-Z_]", "_", name)
146+
if safe and safe[0].isdigit():
147+
safe = "_" + safe
148+
return safe or "seq"
66149

67150

68-
def make_read_node(render_subdir="render", node_name="EXR_read"):
151+
def _nearest_hold_expr(first, last, step):
69152
"""
70-
Create a Read node pointing at the full sequence (from first_frame to last_frame).
153+
Expression that maps timeline frame -> nearest existing frame in a stepped sequence.
154+
Uses floor(x + 0.5) to emulate round() for stability.
155+
"""
156+
if step <= 1:
157+
# identity mapping (no missing frames cadence)
158+
return f"clamp(frame, {first}, {last})"
159+
# nearest multiple of 'step' from 'first', then clamp to [first,last]
160+
return f"clamp({first}+{step}*floor((frame-{first})/{step}+0.5), {first}, {last})"
161+
162+
163+
def make_read_nodes(render_subdir="render", node_name_prefix="EXR_read"):
164+
"""
165+
Create one Read node per discovered sequence inside the newest date/version folder.
166+
167+
- Nodes are named: <prefix>_<label> (e.g., EXR_read_beauty)
168+
- Project frame range is set to the union [min(first), max(last)] across all sequences.
169+
- For sequences detected as rendered on 2s/4s (or any N-s cadence), the Read node's
170+
'frame' knob is set to hold the nearest available frame so playback never errors.
171+
- If ALL sequences share the same cadence of 2 or 4, project FPS is divided by that cadence.
71172
"""
72173
script_path = nuke.root()["name"].value()
73174
if not script_path or script_path == "Root":
74175
nuke.message("Open your shot before using Auto Read.")
75-
return None
176+
return []
76177

77178
shot_dir = os.path.dirname(os.path.dirname(script_path))
78179
render_dir = os.path.join(shot_dir, render_subdir)
79180

80-
seq_pattern, first, last = get_latest_exr_sequence(render_dir)
81-
if not seq_pattern:
82-
return None
181+
sequences = get_latest_exr_sequences(render_dir)
182+
if not sequences:
183+
return []
184+
185+
# remove any existing nodes created by this tool (by prefix)
83186
for n in nuke.allNodes("Read"):
84-
if n.name() == node_name:
85-
nuke.delete(n)
86-
read = nuke.nodes.Read(name=node_name, file=seq_pattern, on_error="black")
87-
# set both the sequence's native range and the playback range
88-
read["origfirst"].setValue(first)
89-
read["origlast"].setValue(last)
90-
read["first"].setValue(first)
91-
read["last"].setValue(last)
187+
try:
188+
if n.name().startswith(node_name_prefix):
189+
nuke.delete(n)
190+
except Exception:
191+
pass
192+
193+
created = []
194+
global_first = min(s["first"] for s in sequences)
195+
global_last = max(s["last"] for s in sequences)
196+
197+
for s in sequences:
198+
label = _sanitize_for_nuke(s["label"])
199+
node_name = f"{node_name_prefix}_{label}" if label != "root" else node_name_prefix
200+
read = nuke.nodes.Read(name=node_name, file=s["pattern"], on_error="black")
201+
# native sequence range
202+
read["origfirst"].setValue(s["first"])
203+
read["origlast"].setValue(s["last"])
204+
read["first"].setValue(s["first"])
205+
read["last"].setValue(s["last"])
206+
207+
# Time-map to handle sparse cadence (2s/4s/etc.) by holding nearest available frame
208+
expr = _nearest_hold_expr(s["first"], s["last"], s["step"])
209+
try:
210+
read["frame"].setExpression(expr)
211+
except Exception:
212+
# Fallback: if 'frame' knob is unavailable for some reason, do nothing
213+
pass
214+
215+
# Optional label so it's obvious what's happening
216+
try:
217+
read["label"].setValue(f"{s['label']} step:{s['step']}")
218+
except Exception:
219+
pass
220+
221+
created.append(read)
222+
223+
# Set the project frame range to cover all created sequences
224+
nuke.root()["first_frame"].setValue(global_first)
225+
nuke.root()["last_frame"].setValue(global_last)
92226

93-
nuke.root()["first_frame"].setValue(first)
94-
nuke.root()["last_frame"].setValue(last)
227+
# If ALL sequences share cadence 2 or 4, adjust project FPS accordingly
228+
steps = set(s["step"] for s in sequences)
229+
if len(steps) == 1:
230+
only_step = steps.pop()
231+
if only_step in (2, 4):
232+
try:
233+
current_fps = float(nuke.root()["fps"].value())
234+
new_fps = current_fps / float(only_step)
235+
nuke.root()["fps"].setValue(new_fps)
236+
nuke.tprint(f"[Auto Read] Detected cadence {only_step}s; FPS set to {new_fps:.3f}")
237+
except Exception as e:
238+
nuke.tprint(f"[Auto Read] Could not adjust FPS: {e}")
95239

96-
return read
240+
return created
97241

98242

99243
def auto_read_latest_fx_exr():
100-
read_node = make_read_node("FX/render", node_name="Bobo_FX_read")
101-
if not read_node:
244+
nodes = make_read_nodes("fx/render", node_name_prefix="Bobo_FX_read")
245+
if not nodes:
246+
return
247+
try:
248+
viewer = nuke.activeViewer().node()
249+
nuke.zoom(1, [viewer["xpos"].value(), viewer["ypos"].value()])
250+
except Exception as e:
251+
nuke.tprint(f"[Auto Read] Viewer zoom error: {e}")
252+
253+
def auto_read_latest_cfx_exr():
254+
nodes = make_read_nodes("cfx/render", node_name_prefix="Bobo_CFX_read")
255+
if not nodes:
102256
return
103257
try:
104258
viewer = nuke.activeViewer().node()
@@ -109,10 +263,10 @@ def auto_read_latest_fx_exr():
109263

110264
def auto_read_latest_exr():
111265
"""
112-
Callback: build the Read node and zoom the Viewer.
266+
Callback: build the Read nodes and zoom the Viewer.
113267
"""
114-
read_node = make_read_node()
115-
if not read_node:
268+
nodes = make_read_nodes()
269+
if not nodes:
116270
return
117271
try:
118272
viewer = nuke.activeViewer().node()
@@ -137,4 +291,4 @@ def main():
137291
main()
138292
nuke.tprint("[Auto Read] Module loaded and command registered.")
139293
except Exception as e:
140-
nuke.tprint(f"[Auto Read] Failed to initialize: {e}")
294+
nuke.tprint(f"[Auto Read] Failed to initialize: {e}")

0 commit comments

Comments
 (0)