@nodetool-ai/blender-nodes 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/blender_ops/depth.py +133 -0
- package/blender_ops/errors.py +46 -0
- package/blender_ops/exr.py +205 -0
- package/blender_ops/framing.py +75 -0
- package/blender_ops/ops/__init__.py +24 -0
- package/blender_ops/ops/common.py +310 -0
- package/blender_ops/ops/export_model.py +95 -0
- package/blender_ops/ops/prepare_for_engine.py +352 -0
- package/blender_ops/ops/render_animation.py +382 -0
- package/blender_ops/ops/render_image.py +41 -0
- package/blender_ops/ops/render_passes.py +363 -0
- package/blender_ops/run_job.py +110 -0
- package/blender_ops/tests/test_camera_lens.py +107 -0
- package/blender_ops/tests/test_depth.py +158 -0
- package/blender_ops/tests/test_exr.py +218 -0
- package/blender_ops/tests/test_framing.py +50 -0
- package/dist/.nodetool-build-stamp +1 -0
- package/dist/bake.d.ts +107 -0
- package/dist/bake.d.ts.map +1 -0
- package/dist/bake.js +209 -0
- package/dist/bake.js.map +1 -0
- package/dist/blender-binary.d.ts +38 -0
- package/dist/blender-binary.d.ts.map +1 -0
- package/dist/blender-binary.js +169 -0
- package/dist/blender-binary.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -0
- package/dist/job.d.ts +163 -0
- package/dist/job.d.ts.map +1 -0
- package/dist/job.js +67 -0
- package/dist/job.js.map +1 -0
- package/dist/nodes/bake-timeline-clip.d.ts +48 -0
- package/dist/nodes/bake-timeline-clip.d.ts.map +1 -0
- package/dist/nodes/bake-timeline-clip.js +152 -0
- package/dist/nodes/bake-timeline-clip.js.map +1 -0
- package/dist/nodes/blender-error.d.ts +21 -0
- package/dist/nodes/blender-error.d.ts.map +1 -0
- package/dist/nodes/blender-error.js +38 -0
- package/dist/nodes/blender-error.js.map +1 -0
- package/dist/nodes/defaults.d.ts +18 -0
- package/dist/nodes/defaults.d.ts.map +1 -0
- package/dist/nodes/defaults.js +18 -0
- package/dist/nodes/defaults.js.map +1 -0
- package/dist/nodes/export-model.d.ts +49 -0
- package/dist/nodes/export-model.d.ts.map +1 -0
- package/dist/nodes/export-model.js +127 -0
- package/dist/nodes/export-model.js.map +1 -0
- package/dist/nodes/prepare-for-engine.d.ts +52 -0
- package/dist/nodes/prepare-for-engine.d.ts.map +1 -0
- package/dist/nodes/prepare-for-engine.js +127 -0
- package/dist/nodes/prepare-for-engine.js.map +1 -0
- package/dist/nodes/progress.d.ts +11 -0
- package/dist/nodes/progress.d.ts.map +1 -0
- package/dist/nodes/progress.js +21 -0
- package/dist/nodes/progress.js.map +1 -0
- package/dist/nodes/render-animation.d.ts +52 -0
- package/dist/nodes/render-animation.d.ts.map +1 -0
- package/dist/nodes/render-animation.js +136 -0
- package/dist/nodes/render-animation.js.map +1 -0
- package/dist/nodes/render-base.d.ts +35 -0
- package/dist/nodes/render-base.d.ts.map +1 -0
- package/dist/nodes/render-base.js +95 -0
- package/dist/nodes/render-base.js.map +1 -0
- package/dist/nodes/render-image.d.ts +41 -0
- package/dist/nodes/render-image.d.ts.map +1 -0
- package/dist/nodes/render-image.js +98 -0
- package/dist/nodes/render-image.js.map +1 -0
- package/dist/nodes/render-passes.d.ts +54 -0
- package/dist/nodes/render-passes.d.ts.map +1 -0
- package/dist/nodes/render-passes.js +167 -0
- package/dist/nodes/render-passes.js.map +1 -0
- package/dist/run-job.d.ts +34 -0
- package/dist/run-job.d.ts.map +1 -0
- package/dist/run-job.js +137 -0
- package/dist/run-job.js.map +1 -0
- package/dist/runner.d.ts +124 -0
- package/dist/runner.d.ts.map +1 -0
- package/dist/runner.js +521 -0
- package/dist/runner.js.map +1 -0
- package/package.json +73 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"""Unit tests for `depth.py`: normalization contracts and the PNG writer.
|
|
2
|
+
|
|
3
|
+
Plain asserts, no pytest, no `bpy`: runs under system Python (`python3
|
|
4
|
+
tests/test_depth.py`) and under Blender's interpreter alike. Exits nonzero
|
|
5
|
+
on the first mismatch.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
import struct
|
|
10
|
+
import sys
|
|
11
|
+
import zlib
|
|
12
|
+
|
|
13
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
|
14
|
+
|
|
15
|
+
from depth import (
|
|
16
|
+
BACKGROUND16,
|
|
17
|
+
depth_range,
|
|
18
|
+
normalize_to_u16,
|
|
19
|
+
write_gray8_png,
|
|
20
|
+
write_gray16_png,
|
|
21
|
+
write_rgb8_png,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def check(label, actual, expected):
|
|
26
|
+
if actual != expected:
|
|
27
|
+
print("MISMATCH %s: got %r want %r" % (label, actual, expected))
|
|
28
|
+
sys.exit(1)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def check_close(label, actual, expected, tol=1e-9):
|
|
32
|
+
if abs(actual - expected) > tol:
|
|
33
|
+
print("MISMATCH %s: got %r want %r" % (label, actual, expected))
|
|
34
|
+
sys.exit(1)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# min/max over the masked finite pixels; inf and unmasked pixels never win.
|
|
38
|
+
near, far = depth_range(
|
|
39
|
+
[1.0, float("inf"), 5.0, 2.0],
|
|
40
|
+
[True, True, False, True],
|
|
41
|
+
)
|
|
42
|
+
check_close("near", near, 1.0)
|
|
43
|
+
check_close("far", far, 2.0)
|
|
44
|
+
|
|
45
|
+
# no finite foreground depth is an error, not a silent (0, 0).
|
|
46
|
+
try:
|
|
47
|
+
depth_range([float("inf"), 3.0], [True, False])
|
|
48
|
+
print("MISMATCH empty-range: expected ValueError")
|
|
49
|
+
sys.exit(1)
|
|
50
|
+
except ValueError:
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
# endpoints land exactly; background and +inf land on 65535.
|
|
54
|
+
values = normalize_to_u16(
|
|
55
|
+
[1.0, 2.0, 3.0, float("inf"), 99.0],
|
|
56
|
+
[True, True, True, True, False],
|
|
57
|
+
1.0,
|
|
58
|
+
3.0,
|
|
59
|
+
)
|
|
60
|
+
check("normalized", values, [0, 32768, 65535, BACKGROUND16, BACKGROUND16])
|
|
61
|
+
|
|
62
|
+
# degenerate range maps finite foreground to 0, never divides by zero.
|
|
63
|
+
check(
|
|
64
|
+
"degenerate",
|
|
65
|
+
normalize_to_u16([2.0, float("inf")], [True, True], 2.0, 2.0),
|
|
66
|
+
[0, BACKGROUND16],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# past the endpoints clamps to the endpoint values.
|
|
70
|
+
check(
|
|
71
|
+
"clamped",
|
|
72
|
+
normalize_to_u16([0.0, 9.0], [True, True], 1.0, 3.0),
|
|
73
|
+
[0, 65535],
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# the writer round-trips: parse the chunks, inflate the IDAT, compare bytes.
|
|
77
|
+
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_depth_test.png")
|
|
78
|
+
try:
|
|
79
|
+
write_gray16_png(out, 2, 2, [0, 32768, 65535, 12345])
|
|
80
|
+
with open(out, "rb") as handle:
|
|
81
|
+
blob = handle.read()
|
|
82
|
+
check("png signature", blob[:8], b"\x89PNG\r\n\x1a\n")
|
|
83
|
+
pos = 8
|
|
84
|
+
seen = {}
|
|
85
|
+
while pos < len(blob):
|
|
86
|
+
(length,) = struct.unpack(">I", blob[pos : pos + 4])
|
|
87
|
+
kind = blob[pos + 4 : pos + 8]
|
|
88
|
+
payload = blob[pos + 8 : pos + 8 + length]
|
|
89
|
+
(crc,) = struct.unpack(">I", blob[pos + 8 + length : pos + 12 + length])
|
|
90
|
+
check("crc %s" % (kind,), crc, zlib.crc32(kind + payload) & 0xFFFFFFFF)
|
|
91
|
+
seen[kind] = payload
|
|
92
|
+
pos += 12 + length
|
|
93
|
+
ihdr = seen[b"IHDR"]
|
|
94
|
+
check("ihdr size", struct.unpack(">II", ihdr[:8]), (2, 2))
|
|
95
|
+
check("ihdr depth/type", (ihdr[8], ihdr[9]), (16, 0))
|
|
96
|
+
raw = zlib.decompress(seen[b"IDAT"])
|
|
97
|
+
expect = b"".join(
|
|
98
|
+
[
|
|
99
|
+
b"\x00" + struct.pack(">HH", 0, 32768),
|
|
100
|
+
b"\x00" + struct.pack(">HH", 65535, 12345),
|
|
101
|
+
]
|
|
102
|
+
)
|
|
103
|
+
check("idat samples", raw, expect)
|
|
104
|
+
finally:
|
|
105
|
+
if os.path.exists(out):
|
|
106
|
+
os.remove(out)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def read_rows(path):
|
|
110
|
+
with open(path, "rb") as handle:
|
|
111
|
+
blob = handle.read()
|
|
112
|
+
pos = 8
|
|
113
|
+
ihdr = None
|
|
114
|
+
idat = b""
|
|
115
|
+
while pos < len(blob):
|
|
116
|
+
(length,) = struct.unpack(">I", blob[pos : pos + 4])
|
|
117
|
+
kind = blob[pos + 4 : pos + 8]
|
|
118
|
+
payload = blob[pos + 8 : pos + 8 + length]
|
|
119
|
+
if kind == b"IHDR":
|
|
120
|
+
ihdr = payload
|
|
121
|
+
elif kind == b"IDAT":
|
|
122
|
+
idat += payload
|
|
123
|
+
pos += 12 + length
|
|
124
|
+
width, height = struct.unpack(">II", ihdr[:8])
|
|
125
|
+
return width, height, ihdr[8], ihdr[9], zlib.decompress(idat)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
out8 = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_mask_test.png")
|
|
129
|
+
try:
|
|
130
|
+
write_gray8_png(out8, 3, 1, [0, 255, 128])
|
|
131
|
+
width, height, depth, ctype, raw = read_rows(out8)
|
|
132
|
+
check("gray8 geometry", (width, height, depth, ctype), (3, 1, 8, 0))
|
|
133
|
+
check("gray8 samples", raw, b"\x00\x00\xff\x80")
|
|
134
|
+
finally:
|
|
135
|
+
if os.path.exists(out8):
|
|
136
|
+
os.remove(out8)
|
|
137
|
+
|
|
138
|
+
outn = os.path.join(os.path.dirname(os.path.abspath(__file__)), "_normal_test.png")
|
|
139
|
+
try:
|
|
140
|
+
write_rgb8_png(outn, 2, 1, [128, 128, 255, 255, 0, 0])
|
|
141
|
+
width, height, depth, ctype, raw = read_rows(outn)
|
|
142
|
+
check("rgb8 geometry", (width, height, depth, ctype), (2, 1, 8, 2))
|
|
143
|
+
check("rgb8 samples", raw, b"\x00\x80\x80\xff\xff\x00\x00")
|
|
144
|
+
finally:
|
|
145
|
+
if os.path.exists(outn):
|
|
146
|
+
os.remove(outn)
|
|
147
|
+
|
|
148
|
+
# a short row is an error, never a truncated file.
|
|
149
|
+
try:
|
|
150
|
+
write_gray8_png(out8, 2, 2, [0, 0, 0])
|
|
151
|
+
print("MISMATCH short-row: expected ValueError")
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
except ValueError:
|
|
154
|
+
pass
|
|
155
|
+
if os.path.exists(out8):
|
|
156
|
+
os.remove(out8)
|
|
157
|
+
|
|
158
|
+
print("depth: all checks passed")
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Unit tests for `exr.py`: the minimal scanline reader.
|
|
2
|
+
|
|
3
|
+
Plain asserts, no pytest, no `bpy`: runs under system Python (`python3
|
|
4
|
+
tests/test_exr.py`) and under Blender's interpreter alike. Exits nonzero
|
|
5
|
+
on the first mismatch.
|
|
6
|
+
|
|
7
|
+
The synthetic file mirrors exactly what Blender's File Output writes for a
|
|
8
|
+
staged pass: single-part scanline, `NO_COMPRESSION`, `FLOAT` channels named
|
|
9
|
+
`Image.R/G/B/A`, increasing line order.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import struct
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
|
17
|
+
|
|
18
|
+
from exr import ExrError, read_exr_rgba, rewrite_background_to_inf
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def check(label, actual, expected):
|
|
22
|
+
if actual != expected:
|
|
23
|
+
print("MISMATCH %s: got %r want %r" % (label, actual, expected))
|
|
24
|
+
sys.exit(1)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def read_channel(path, wanted):
|
|
28
|
+
"""Float samples of one named channel, in row-major order."""
|
|
29
|
+
from exr import _parse_header, _parse_header_channels
|
|
30
|
+
|
|
31
|
+
with open(path, "rb") as handle:
|
|
32
|
+
blob = handle.read()
|
|
33
|
+
attrs, pos = _parse_header(blob)
|
|
34
|
+
(x0, y0, x1, y1) = struct.unpack("<iiii", attrs["dataWindow"])
|
|
35
|
+
width, height = x1 - x0 + 1, y1 - y0 + 1
|
|
36
|
+
channels = _parse_header_channels(attrs)
|
|
37
|
+
offsets = struct.unpack("<%dq" % (height,), blob[pos : pos + 8 * height])
|
|
38
|
+
values = []
|
|
39
|
+
for row, offset in enumerate(offsets):
|
|
40
|
+
cursor = offset + 8
|
|
41
|
+
for name, pixel_type, _ in channels:
|
|
42
|
+
count = width * 4
|
|
43
|
+
if name == wanted:
|
|
44
|
+
values.extend(
|
|
45
|
+
struct.unpack(
|
|
46
|
+
"<%df" % (width,), blob[cursor : cursor + count]
|
|
47
|
+
)
|
|
48
|
+
)
|
|
49
|
+
cursor += count
|
|
50
|
+
return values
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def write_exr(path, width, height, channels, compression=0):
|
|
54
|
+
"""Minimal uncompressed scanline EXR: `channels` is [(name, [floats])]."""
|
|
55
|
+
blob = bytearray(b"v/1\x01" + struct.pack("<I", 2))
|
|
56
|
+
|
|
57
|
+
def attr(name, typ, payload):
|
|
58
|
+
blob.extend(name + b"\x00" + typ + b"\x00")
|
|
59
|
+
blob.extend(struct.pack("<i", len(payload)) + payload)
|
|
60
|
+
|
|
61
|
+
attr(b"compression", b"compression", struct.pack("B", compression))
|
|
62
|
+
attr(
|
|
63
|
+
b"dataWindow",
|
|
64
|
+
b"box2i",
|
|
65
|
+
struct.pack("<iiii", 0, 0, width - 1, height - 1),
|
|
66
|
+
)
|
|
67
|
+
attr(
|
|
68
|
+
b"displayWindow",
|
|
69
|
+
b"box2i",
|
|
70
|
+
struct.pack("<iiii", 0, 0, width - 1, height - 1),
|
|
71
|
+
)
|
|
72
|
+
attr(b"lineOrder", b"lineOrder", b"\x00")
|
|
73
|
+
chlist = b""
|
|
74
|
+
for name, _values in channels:
|
|
75
|
+
chlist += name + b"\x00" + struct.pack("<i", 2) + b"\x00\x00\x00\x00"
|
|
76
|
+
chlist += struct.pack("<ii", 1, 1)
|
|
77
|
+
chlist += b"\x00"
|
|
78
|
+
attr(b"channels", b"chlist", chlist)
|
|
79
|
+
blob += b"\x00"
|
|
80
|
+
table_at = len(blob)
|
|
81
|
+
blob += b"\x00" * (8 * height)
|
|
82
|
+
offsets = []
|
|
83
|
+
for row in range(height):
|
|
84
|
+
offsets.append(len(blob))
|
|
85
|
+
blob += struct.pack("<i", row)
|
|
86
|
+
body = b"".join(
|
|
87
|
+
struct.pack("<%df" % (width,), *values[row * width : (row + 1) * width])
|
|
88
|
+
for _name, values in channels
|
|
89
|
+
)
|
|
90
|
+
blob += struct.pack("<i", len(body)) + body
|
|
91
|
+
struct.pack_into("<%dq" % (height,), blob, table_at, *offsets)
|
|
92
|
+
with open(path, "wb") as handle:
|
|
93
|
+
handle.write(bytes(blob))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
97
|
+
GOOD = os.path.join(HERE, "_exr_test.exr")
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
depth = [float(i) for i in range(6)]
|
|
101
|
+
write_exr(
|
|
102
|
+
GOOD,
|
|
103
|
+
3,
|
|
104
|
+
2,
|
|
105
|
+
[
|
|
106
|
+
(b"Image.A", [1.0] * 6),
|
|
107
|
+
(b"Image.B", depth),
|
|
108
|
+
(b"Image.G", depth),
|
|
109
|
+
(b"Image.R", depth),
|
|
110
|
+
],
|
|
111
|
+
)
|
|
112
|
+
width, height, r, g, b = read_exr_rgba(GOOD)
|
|
113
|
+
check("geometry", (width, height), (3, 2))
|
|
114
|
+
check("r", r, depth)
|
|
115
|
+
check("g", g, depth)
|
|
116
|
+
check("b", b, depth)
|
|
117
|
+
finally:
|
|
118
|
+
if os.path.exists(GOOD):
|
|
119
|
+
os.remove(GOOD)
|
|
120
|
+
|
|
121
|
+
# inf and nan survive the round trip bit-exact (the sentinel gate needs them).
|
|
122
|
+
try:
|
|
123
|
+
write_exr(
|
|
124
|
+
GOOD,
|
|
125
|
+
2,
|
|
126
|
+
1,
|
|
127
|
+
[
|
|
128
|
+
(b"L.R", [float("inf"), float("nan")]),
|
|
129
|
+
(b"L.G", [0.0, 0.0]),
|
|
130
|
+
(b"L.B", [0.0, 0.0]),
|
|
131
|
+
],
|
|
132
|
+
)
|
|
133
|
+
_w, _h, r, _g, _b = read_exr_rgba(GOOD)
|
|
134
|
+
check("inf", r[0] == float("inf"), True)
|
|
135
|
+
check("nan", r[1] != r[1], True)
|
|
136
|
+
finally:
|
|
137
|
+
if os.path.exists(GOOD):
|
|
138
|
+
os.remove(GOOD)
|
|
139
|
+
|
|
140
|
+
# a compressed file is refused, never misread.
|
|
141
|
+
try:
|
|
142
|
+
write_exr(
|
|
143
|
+
GOOD,
|
|
144
|
+
1,
|
|
145
|
+
1,
|
|
146
|
+
[(b"L.R", [1.0]), (b"L.G", [1.0]), (b"L.B", [1.0])],
|
|
147
|
+
compression=3,
|
|
148
|
+
)
|
|
149
|
+
read_exr_rgba(GOOD)
|
|
150
|
+
print("MISMATCH compressed: expected ExrError")
|
|
151
|
+
sys.exit(1)
|
|
152
|
+
except ExrError:
|
|
153
|
+
pass
|
|
154
|
+
finally:
|
|
155
|
+
if os.path.exists(GOOD):
|
|
156
|
+
os.remove(GOOD)
|
|
157
|
+
|
|
158
|
+
# not an EXR file at all.
|
|
159
|
+
try:
|
|
160
|
+
with open(GOOD, "wb") as handle:
|
|
161
|
+
handle.write(b"definitely not exr")
|
|
162
|
+
read_exr_rgba(GOOD)
|
|
163
|
+
print("MISMATCH magic: expected ExrError")
|
|
164
|
+
sys.exit(1)
|
|
165
|
+
except ExrError:
|
|
166
|
+
pass
|
|
167
|
+
finally:
|
|
168
|
+
if os.path.exists(GOOD):
|
|
169
|
+
os.remove(GOOD)
|
|
170
|
+
|
|
171
|
+
# background samples become +inf, foreground is bit-exact, alpha is kept.
|
|
172
|
+
try:
|
|
173
|
+
write_exr(
|
|
174
|
+
GOOD,
|
|
175
|
+
3,
|
|
176
|
+
2,
|
|
177
|
+
[
|
|
178
|
+
(b"Image.A", [1.0] * 6),
|
|
179
|
+
(b"Image.B", [1e10, 2.0, 1e10, 4.0, 5.0, 1e10]),
|
|
180
|
+
(b"Image.G", [1e10, 2.0, 1e10, 4.0, 5.0, 1e10]),
|
|
181
|
+
(b"Image.R", [1e10, 2.0, 1e10, 4.0, 5.0, 1e10]),
|
|
182
|
+
],
|
|
183
|
+
)
|
|
184
|
+
rewrite_background_to_inf(
|
|
185
|
+
GOOD, [False, True, False, True, True, False]
|
|
186
|
+
)
|
|
187
|
+
_w, _h, r, g, b = read_exr_rgba(GOOD)
|
|
188
|
+
check("bg", [r[0], r[2], r[5]], [float("inf")] * 3)
|
|
189
|
+
check("fg", [r[1], r[3], r[4]], [2.0, 4.0, 5.0])
|
|
190
|
+
check("g-matches", g, r)
|
|
191
|
+
check("b-matches", b, r)
|
|
192
|
+
check("alpha-kept", read_channel(GOOD, "Image.A"), [1.0] * 6)
|
|
193
|
+
finally:
|
|
194
|
+
if os.path.exists(GOOD):
|
|
195
|
+
os.remove(GOOD)
|
|
196
|
+
|
|
197
|
+
# a mask that does not fill the frame is refused, never half-applied.
|
|
198
|
+
try:
|
|
199
|
+
write_exr(
|
|
200
|
+
GOOD,
|
|
201
|
+
2,
|
|
202
|
+
1,
|
|
203
|
+
[
|
|
204
|
+
(b"L.R", [1.0, 2.0]),
|
|
205
|
+
(b"L.G", [1.0, 2.0]),
|
|
206
|
+
(b"L.B", [1.0, 2.0]),
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
rewrite_background_to_inf(GOOD, [True])
|
|
210
|
+
print("MISMATCH mask-size: expected ValueError")
|
|
211
|
+
sys.exit(1)
|
|
212
|
+
except ValueError:
|
|
213
|
+
pass
|
|
214
|
+
finally:
|
|
215
|
+
if os.path.exists(GOOD):
|
|
216
|
+
os.remove(GOOD)
|
|
217
|
+
|
|
218
|
+
print("exr: all checks passed")
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""T3, Python side: pin the `framing.py` port to the shared goldens.
|
|
2
|
+
|
|
3
|
+
Runs under Blender's own interpreter (`blender -b --python ...`), which the
|
|
4
|
+
TypeScript suite (`tests/framing.test.ts`) drives when Blender is present.
|
|
5
|
+
Plain asserts only: Blender's Python has no pytest. Exits nonzero on the
|
|
6
|
+
first mismatch, printing both values.
|
|
7
|
+
|
|
8
|
+
Goldens: radius 2, fov 40, aspect 4/3, zoom 1.2; azimuth 30, elevation 20;
|
|
9
|
+
bounds center (0.5, 0.25, -1). Same numbers as `framing.test.ts`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import os
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), ".."))
|
|
16
|
+
|
|
17
|
+
from framing import compute_framing, orbit_offset
|
|
18
|
+
|
|
19
|
+
GOLDEN = {
|
|
20
|
+
"distance": 4.8730,
|
|
21
|
+
"near": 0.0487,
|
|
22
|
+
"far": 24.8730,
|
|
23
|
+
"offset": (2.2896, 1.6667, 3.9656),
|
|
24
|
+
"position": (2.7896, 1.9167, 2.9656),
|
|
25
|
+
"center": (0.5, 0.25, -1.0),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check(label, actual, expected):
|
|
30
|
+
if abs(actual - expected) > 5e-5:
|
|
31
|
+
print("MISMATCH %s: got %r want %r" % (label, actual, expected))
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
framing = compute_framing(2, 40, 4.0 / 3.0, 1.2)
|
|
36
|
+
check("distance", framing["distance"], GOLDEN["distance"])
|
|
37
|
+
check("near", framing["near"], GOLDEN["near"])
|
|
38
|
+
check("far", framing["far"], GOLDEN["far"])
|
|
39
|
+
|
|
40
|
+
offset = orbit_offset(30, 20, framing["distance"])
|
|
41
|
+
check("offset.x", offset["x"], GOLDEN["offset"][0])
|
|
42
|
+
check("offset.y", offset["y"], GOLDEN["offset"][1])
|
|
43
|
+
check("offset.z", offset["z"], GOLDEN["offset"][2])
|
|
44
|
+
|
|
45
|
+
cx, cy, cz = GOLDEN["center"]
|
|
46
|
+
check("position.x", cx + offset["x"], GOLDEN["position"][0])
|
|
47
|
+
check("position.y", cy + offset["y"], GOLDEN["position"][1])
|
|
48
|
+
check("position.z", cz + offset["z"], GOLDEN["position"][2])
|
|
49
|
+
|
|
50
|
+
print("framing goldens ok")
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
2026-09-28T09:19:11.534Z
|
package/dist/bake.d.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The timeline's 3D bake: a sampled Blender render muxed into one video
|
|
3
|
+
* (design §D6).
|
|
4
|
+
*
|
|
5
|
+
* The caller hands over one model time and one camera per output frame — the
|
|
6
|
+
* evaluated clip, with its in point, speed, time remap, animation speed and
|
|
7
|
+
* camera curves already applied — and gets back an MP4 the clip can play as
|
|
8
|
+
* an ordinary video layer. Blender renders a still per entry rather than a
|
|
9
|
+
* frame range, because a trimmed, sped-up, reversed or looped clip is not a
|
|
10
|
+
* run of consecutive scene frames.
|
|
11
|
+
*
|
|
12
|
+
* The mux is ffmpeg and not Blender's own writer, which is what
|
|
13
|
+
* `render_animation`'s video mode uses. Two reasons: one sequence feeds both
|
|
14
|
+
* bake formats, and Blender cannot write VP9 with an alpha channel. ffmpeg is
|
|
15
|
+
* therefore a host binary this path needs and the video mode does not; a
|
|
16
|
+
* machine without it fails here with the binary named.
|
|
17
|
+
*/
|
|
18
|
+
import type { ProcessingContext } from "@nodetool-ai/runtime";
|
|
19
|
+
import type { BakeCameraParams, BlenderJob, BlenderResultStats, RenderAnimationParams } from "./job.js";
|
|
20
|
+
/**
|
|
21
|
+
* Ceiling on the frames one bake asks for. Every frame is a declared output
|
|
22
|
+
* the runner reads into memory, so this is the bound on that, not a limit on
|
|
23
|
+
* clip length in the abstract: two minutes at 30 fps.
|
|
24
|
+
*/
|
|
25
|
+
export declare const MAX_BAKE_FRAMES = 3600;
|
|
26
|
+
/** What one bake renders. Cameras and times are parallel, one pair per frame. */
|
|
27
|
+
export interface Model3DBakeRequest {
|
|
28
|
+
/** Model time in seconds, one entry per output frame. */
|
|
29
|
+
frameTimes: readonly number[];
|
|
30
|
+
/** The camera each of those frames renders through. */
|
|
31
|
+
cameras: readonly BakeCameraParams[];
|
|
32
|
+
/** glTF animation to play; every one plays when absent (the D2 default). */
|
|
33
|
+
animationName?: string;
|
|
34
|
+
width: number;
|
|
35
|
+
height: number;
|
|
36
|
+
fps: number;
|
|
37
|
+
}
|
|
38
|
+
export interface Model3DBakeOptions {
|
|
39
|
+
timeoutMs: number;
|
|
40
|
+
signal?: AbortSignal;
|
|
41
|
+
/** Reported per rendered frame, before the mux. */
|
|
42
|
+
onProgress?: (frame: number, total: number) => void;
|
|
43
|
+
}
|
|
44
|
+
/** The container, encoder and pixel format one bake is written as. */
|
|
45
|
+
export interface BakeVideoFormat {
|
|
46
|
+
/** File extension of the muxed bytes, without the dot. */
|
|
47
|
+
extension: "mp4" | "webm";
|
|
48
|
+
mimeType: string;
|
|
49
|
+
/** ffmpeg output arguments: the encoder and its pixel format. */
|
|
50
|
+
encoderArgs: readonly string[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* How a bake is encoded: opaque to MP4/H.264 `yuv420p`, transparent to WebM
|
|
54
|
+
* VP9 `yuva420p` (design §D6).
|
|
55
|
+
*
|
|
56
|
+
* The alpha pair is the one `resolveTimelineOutput({ format: "webm", alpha:
|
|
57
|
+
* true })` already declares for a timeline render, copied rather than imported
|
|
58
|
+
* because `@nodetool-ai/blender-nodes` does not depend on
|
|
59
|
+
* `@nodetool-ai/video-nodes` and a bake needs four strings from it, not a
|
|
60
|
+
* package. `packages/agents/tests/timeline-bake-format.test.ts` depends on both
|
|
61
|
+
* and fails when the two drift.
|
|
62
|
+
*/
|
|
63
|
+
export declare function bakeVideoFormat(alpha: boolean): BakeVideoFormat;
|
|
64
|
+
export interface Model3DBakeResult {
|
|
65
|
+
/** The muxed bytes, in {@link Model3DBakeResult.format}. */
|
|
66
|
+
video: Uint8Array;
|
|
67
|
+
/** What those bytes are, so the caller names and stores them correctly. */
|
|
68
|
+
format: BakeVideoFormat;
|
|
69
|
+
stats: BlenderResultStats;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The `frame_*` outputs a sampled job declares: one per entry, in play order.
|
|
73
|
+
* The op writes the file each name carries and reports the names back.
|
|
74
|
+
*/
|
|
75
|
+
export declare function bakeFrameOutputs(count: number): BlenderJob["outputs"];
|
|
76
|
+
/** The sampled `render_animation` params for one bake. */
|
|
77
|
+
export declare function bakeRenderAnimationParams(request: Model3DBakeRequest): RenderAnimationParams;
|
|
78
|
+
/** Render the sequence. Frames come back in play order. */
|
|
79
|
+
export declare function renderModel3DBakeFrames(context: ProcessingContext, modelBytes: Uint8Array, request: Model3DBakeRequest, options: Model3DBakeOptions): Promise<{
|
|
80
|
+
frames: Uint8Array[];
|
|
81
|
+
stats: BlenderResultStats;
|
|
82
|
+
}>;
|
|
83
|
+
/**
|
|
84
|
+
* The ffmpeg call that turns the numbered PNGs into one video.
|
|
85
|
+
*
|
|
86
|
+
* Pure, because the arguments are the whole difference between the two bakes
|
|
87
|
+
* and a test should not need ffmpeg to read them.
|
|
88
|
+
*/
|
|
89
|
+
export declare function bakeMuxArgs(format: BakeVideoFormat, fps: number, output: string): string[];
|
|
90
|
+
/**
|
|
91
|
+
* Mux a PNG sequence into one video through ffmpeg's image2 demuxer.
|
|
92
|
+
*
|
|
93
|
+
* The frames are written under the workspace's scratch directory rather than
|
|
94
|
+
* piped: image2 reads a numbered pattern, and the same files reach either
|
|
95
|
+
* encoder unchanged — an alpha bake differs only in the arguments
|
|
96
|
+
* {@link bakeVideoFormat} names, because the PNGs already carry the channel.
|
|
97
|
+
*/
|
|
98
|
+
export declare function muxPngSequenceToVideo(context: ProcessingContext, frames: readonly Uint8Array[], fps: number, format: BakeVideoFormat, options: Model3DBakeOptions): Promise<Uint8Array>;
|
|
99
|
+
/**
|
|
100
|
+
* Render the sampled sequence and mux it: the whole bake (§D6).
|
|
101
|
+
*
|
|
102
|
+
* Whether it carries alpha is read off the frames' own camera rather than
|
|
103
|
+
* taken as a second field: `transparent` is what Blender rendered the film
|
|
104
|
+
* under, so a separate flag could only ever disagree with the pixels.
|
|
105
|
+
*/
|
|
106
|
+
export declare function bakeModel3DClipToVideo(context: ProcessingContext, modelBytes: Uint8Array, request: Model3DBakeRequest, options: Model3DBakeOptions): Promise<Model3DBakeResult>;
|
|
107
|
+
//# sourceMappingURL=bake.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bake.d.ts","sourceRoot":"","sources":["../src/bake.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAMH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAE9D,OAAO,KAAK,EACV,gBAAgB,EAChB,UAAU,EACV,kBAAkB,EAClB,qBAAqB,EACtB,MAAM,UAAU,CAAC;AAIlB;;;;GAIG;AACH,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,yDAAyD;IACzD,UAAU,EAAE,SAAS,MAAM,EAAE,CAAC;IAC9B,uDAAuD;IACvD,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAC;IACrC,4EAA4E;IAC5E,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,EAAE,MAAM,CAAC;CACb;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mDAAmD;IACnD,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CACrD;AAED,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,0DAA0D;IAC1D,SAAS,EAAE,KAAK,GAAG,MAAM,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,OAAO,GAAG,eAAe,CAe/D;AAED,MAAM,WAAW,iBAAiB;IAChC,4DAA4D;IAC5D,KAAK,EAAE,UAAU,CAAC;IAClB,2EAA2E;IAC3E,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,kBAAkB,CAAC;CAC3B;AAOD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,CAAC,SAAS,CAAC,CAMrE;AAyBD,0DAA0D;AAC1D,wBAAgB,yBAAyB,CACvC,OAAO,EAAE,kBAAkB,GAC1B,qBAAqB,CAuBvB;AAED,2DAA2D;AAC3D,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC;IAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAAC,KAAK,EAAE,kBAAkB,CAAA;CAAE,CAAC,CA+B9D;AAED;;;;;GAKG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,eAAe,EACvB,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GACb,MAAM,EAAE,CAqBV;AAED;;;;;;;GAOG;AACH,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,SAAS,UAAU,EAAE,EAC7B,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,UAAU,CAAC,CAiCrB;AAED;;;;;;GAMG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,iBAAiB,EAC1B,UAAU,EAAE,UAAU,EACtB,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC,iBAAiB,CAAC,CAgB5B"}
|