1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
"""
Blender export script for CNN v3 G-buffer training data.
Configures render passes and a compositor File Output node,
then renders the current scene to a multi-layer EXR.
Usage (headless):
blender -b scene.blend -P blender_export.py -- --output renders/frame_###
Each '#' in the output path is replaced by Blender with the frame number (zero-padded).
The script writes one multi-layer EXR per frame containing all required passes.
G-buffer pass mapping:
Combined → training target RGBA (beauty)
DiffCol → albedo.rgb (pre-lighting material color)
Normal → normal.xy (world-space, oct-encode in pack_blender_sample.py)
Z → depth (view-space distance, normalize in pack step)
IndexOB → mat_id (object index, u8 / 255)
Shadow → shadow (invert: shadow=1 means fully lit)
Alpha → transp. (0=opaque, 1=clear/transparent)
"""
import sys
import argparse
import bpy
def parse_args():
# Blender passes its own argv; our args follow '--'.
argv = sys.argv
if "--" in argv:
argv = argv[argv.index("--") + 1:]
else:
argv = []
parser = argparse.ArgumentParser(
description="Configure Blender render passes and export multi-layer EXR."
)
parser.add_argument(
"--output",
default="//renders/frame_###",
help="Output path prefix (use ### for frame number padding). "
"Default: //renders/frame_###",
)
parser.add_argument(
"--width", type=int, default=640,
help="Render width in pixels (default: 640)"
)
parser.add_argument(
"--height", type=int, default=360,
help="Render height in pixels (default: 360)"
)
parser.add_argument(
"--start-frame", type=int, default=None,
help="First frame to render (default: scene start frame)"
)
parser.add_argument(
"--end-frame", type=int, default=None,
help="Last frame to render (default: scene end frame)"
)
return parser.parse_args(argv)
def configure_scene(args):
scene = bpy.context.scene
# Render dimensions
scene.render.resolution_x = args.width
scene.render.resolution_y = args.height
scene.render.resolution_percentage = 100
# Frame range (optional override)
if args.start_frame is not None:
scene.frame_start = args.start_frame
if args.end_frame is not None:
scene.frame_end = args.end_frame
# Use Cycles for best multi-pass support
scene.render.engine = "CYCLES"
# Enable required render passes on the active view layer
vl = scene.view_layers["ViewLayer"]
vl.use_pass_combined = True # beauty target
vl.use_pass_diffuse_color = True # albedo
vl.use_pass_normal = True # world normals
vl.use_pass_z = True # depth (Z)
vl.use_pass_object_index = True # mat_id
vl.use_pass_shadow = True # shadow catcher
# Alpha is available via the combined pass alpha channel;
# the compositor node below also taps it separately.
print(f"[blender_export] Render passes configured on ViewLayer '{vl.name}'.")
print(f" Resolution: {args.width}x{args.height}")
print(f" Frames: {scene.frame_start} – {scene.frame_end}")
def configure_compositor(args):
scene = bpy.context.scene
scene.use_nodes = True
tree = scene.node_tree
# Clear all existing compositor nodes
tree.nodes.clear()
# Render Layers node (source of all passes)
rl_node = tree.nodes.new("CompositorNodeRLayers")
rl_node.location = (0, 0)
# File Output node — multi-layer EXR (all passes in one file)
out_node = tree.nodes.new("CompositorNodeOutputFile")
out_node.location = (600, 0)
out_node.format.file_format = "OPEN_EXR_MULTILAYER"
out_node.format.exr_codec = "ZIP"
out_node.base_path = args.output
# Map each render pass socket to a named layer in the EXR.
# Slot order matters: the first slot is created by default; we rename it
# and add the rest.
pass_sockets = [
("Image", "Combined"), # beauty / target
("Diffuse Color", "DiffCol"), # albedo
("Normal", "Normal"), # world normals
("Depth", "Z"), # view-space depth
("Object Index", "IndexOB"), # object index
("Shadow", "Shadow"), # shadow
("Alpha", "Alpha"), # transparency / alpha
]
# The node starts with one default slot; configure it first.
for i, (socket_name, layer_name) in enumerate(pass_sockets):
if i == 0:
# Rename the default slot
out_node.file_slots[0].path = layer_name
else:
out_node.file_slots.new(layer_name)
# Link render layer socket to file output slot
src_socket = rl_node.outputs.get(socket_name)
dst_socket = out_node.inputs[i]
if src_socket:
tree.links.new(src_socket, dst_socket)
else:
print(f"[blender_export] WARNING: pass socket '{socket_name}' "
f"not found on Render Layers node. Skipping.")
print(f"[blender_export] Compositor configured. Output → {args.output}")
print(" Layers: " + ", ".join(ln for _, ln in pass_sockets))
def main():
args = parse_args()
configure_scene(args)
configure_compositor(args)
# Trigger the render (only when running headless with -b)
bpy.ops.render.render(animation=True)
print("[blender_export] Render complete.")
if __name__ == "__main__":
main()
|