From f74bcd843c631f82daefe543fca7741fb5bb71f4 Mon Sep 17 00:00:00 2001 From: skal Date: Fri, 20 Mar 2026 08:42:07 +0100 Subject: feat(cnn_v3): G-buffer phase 1 + training infrastructure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit G-buffer (Phase 1): - Add NodeTypes GBUF_ALBEDO/DEPTH32/R8/RGBA32UINT to NodeRegistry - GBufferEffect: MRT raster pass (albedo+normal_mat+depth) + pack compute - Shaders: gbuf_raster.wgsl (MRT), gbuf_pack.wgsl (feature packing, 32B/px) - Shadow/SDF passes stubbed (placeholder textures), CMake integration deferred Training infrastructure (Phase 2): - blender_export.py: headless EXR export with all G-buffer render passes - pack_blender_sample.py: EXR → per-channel PNGs (oct-normals, 1/z depth) - pack_photo_sample.py: photo → zero-filled G-buffer sample layout handoff(Gemini): G-buffer phases 3-5 remain (U-Net shaders, CNNv3Effect, parity) --- cnn_v3/training/blender_export.py | 160 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 cnn_v3/training/blender_export.py (limited to 'cnn_v3/training/blender_export.py') diff --git a/cnn_v3/training/blender_export.py b/cnn_v3/training/blender_export.py new file mode 100644 index 0000000..63dd0e3 --- /dev/null +++ b/cnn_v3/training/blender_export.py @@ -0,0 +1,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() -- cgit v1.2.3