From 7fd7f9ab279a7ded962fd1c4537a08fd5c81f047 Mon Sep 17 00:00:00 2001 From: pullusb Date: Wed, 2 Apr 2025 11:53:11 +0200 Subject: [PATCH] Split tech passes in separate 32 bits hardcoded tech passes names : ['uv', 'normal', 'depth', 'position', 'vector', 'ao'] --- __init__.py | 4 +- constant.py | 2 + fn.py | 285 +++++++++++++++++++++++++---------------------- setup_outputs.py | 22 +++- ui.py | 2 +- 5 files changed, 180 insertions(+), 135 deletions(-) create mode 100644 constant.py diff --git a/__init__.py b/__init__.py index 7602772..2ebffbe 100755 --- a/__init__.py +++ b/__init__.py @@ -2,13 +2,13 @@ bl_info = { "name": "Render Toolbox", "description": "Perform checks and setup outputs", "author": "Samuel Bernou", - "version": (0, 2, 0), + "version": (0, 3, 0), "blender": (3, 0, 0), "location": "View3D", "warning": "", "doc_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox", "tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox/issues", - "category": "Object" + "category": "Render" } from . import setup_outputs diff --git a/constant.py b/constant.py new file mode 100644 index 0000000..dd2c882 --- /dev/null +++ b/constant.py @@ -0,0 +1,2 @@ +# Technical pass names (names that should be in 32bit) +TECH_PASS_KEYWORDS = ['uv', 'normal', 'depth', 'position', 'vector', 'ao'] \ No newline at end of file diff --git a/fn.py b/fn.py index 270ab29..9b6a7dc 100755 --- a/fn.py +++ b/fn.py @@ -3,6 +3,8 @@ import os import re import json +from .constant import TECH_PASS_KEYWORDS + ### --- Manage nodes --- ### def real_loc(n): @@ -60,7 +62,92 @@ def recursive_node_connect_check(l, target_node): return False -def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None, file_format=None): +def create_and_connect_file_output(node, outputs, file_out, out_name, base_path, out_base, + scene, remap_names, file_format, suffix='', color_depth=None, + location_offset=(500, 50)): + """Helper function to create file output node and connect it to outputs + + Args: + node: Source node + outputs: List of outputs to connect + file_out: Existing file output node or None + out_name: Name for the file output node + base_path: Base path for output files + out_base: Base name for output files + scene: Blender scene + remap_names: Dictionary for remapping output names + file_format: Format settings + suffix: Optional suffix for paths + color_depth: Optional override for color depth + location_offset: Offset for the node position + + Returns: + bpy.types.CompositorNode: Created or used file output node + """ + if not outputs: + return None + + nodes = scene.node_tree.nodes + links = scene.node_tree.links + + fo = file_out + if not fo: + fo = nodes.get(out_name) + if not fo: + fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, + location=(real_loc(node)[0] + location_offset[0], + real_loc(node)[1] + location_offset[1]), width=600) + fo.inputs.remove(fo.inputs[0]) # Remove default image input + + if file_format: + for k, v in file_format.items(): + setattr(fo.format, k, v) + else: + set_file_output_format(fo) + + if color_depth: + fo.format.color_depth = color_depth + + fo.name = out_name + if node.parent: + fo.parent = node.parent + + if base_path: + fo.base_path = base_path + else: + if fo.format.file_format == 'OPEN_EXR_MULTILAYER': + fo.base_path = f'//render/{out_base}/{suffix}{out_base}_' + else: + fo.base_path = f'//render/{out_base}/{suffix}' + + for o in outputs: + if next((l for l in o.links if recursive_node_connect_check(l, fo)), None): + continue + + if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)): + slot_name = bpy.path.clean_name(custom_name) + else: + slot_name = bpy.path.clean_name(o.name) + + fs = fo.file_slots.new('tmp') + ls = fo.layer_slots.new('tmp') + + ls = fo.layer_slots[-1] + ls.name = slot_name + + fs = fo.file_slots[-1] + fs.path = f'{slot_name}/{slot_name}_' + + out_input = fo.inputs[-1] + links.new(o, out_input) + + clear_disconnected(fo) + fo.update() + return fo + + +def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None, + file_format=None, split_tech_passes=False): """Connect selected nodes output to file output(s) if a file output is selected, add intputs on it @@ -81,159 +168,95 @@ def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None Defaults toNone. remap_names (dict, optionnal): List of output names to remap {node_name: {output_name: new_name}}. - - frame (bpy.types.CompositorNode, optional): If given, create nodes into a frame. - Defaults to None. + + split_tech_passes (bool, optional): When True, create a separate file output for technical passes + Defaults to False. Returns: list[bpy.types.CompositorNode]: All nodes created. """ scene = bpy.context.scene - nodes = scene.node_tree.nodes - links = scene.node_tree.links if not isinstance(node_list, list): node_list = [node_list] node_list = [n for n in node_list if n.type != 'OUTPUT_FILE'] if not node_list: - return + return [] + created_nodes = [] excludes = excludes or {} + remap_names = remap_names or {} for node in node_list: exclusions = excludes.get(node.name) or [] - ## create one output facing node and connect all - outs = [o for o in node.outputs if not o.is_unavailable and not 'crypto' in o.name.lower() and o.name not in exclusions] - cryptout = [o for o in node.outputs if not o.is_unavailable and 'crypto' in o.name.lower() and o.name not in exclusions] - - - ## Using render layer can force connexion to an existing FO node when user may want a new one - # if node.type == 'R_LAYERS': - # out_base = node.layer + + # Get all available outputs excluding those in exclusions + all_outputs = [o for o in node.outputs if not o.is_unavailable and o.name not in exclusions] + + # Base name for output nodes if node.label: out_base = node.label else: out_base = node.name out_base = bpy.path.clean_name(out_base) - out_name = f'OUT_{out_base}' + + # Categorize outputs + crypto_outputs = [o for o in all_outputs if 'crypto' in o.name.lower()] + + if split_tech_passes: + # Filter tech passes + tech_outputs = [o for o in all_outputs if o.name.lower() in TECH_PASS_KEYWORDS] # any(keyword in o.name.lower() for keyword in TECH_PASS_KEYWORDS)] + # Regular outputs (excluding crypto and tech passes) + regular_outputs = [o for o in all_outputs if o not in crypto_outputs and o not in tech_outputs] + else: + # If not splitting tech passes, include them with regular outputs + regular_outputs = [o for o in all_outputs if o not in crypto_outputs] + tech_outputs = [] - if outs: - fo = file_out - if not fo: - fo = nodes.get(out_name) - if not fo: - # color = (0.2,0.3,0.5) - fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+500, real_loc(node)[1]+50), width=600) - fo.inputs.remove(fo.inputs[0]) # Remove default image input - if file_format: - for k, v in file_format.items(): - setattr(fo.format, k, v) - else: - set_file_output_format(fo) + y_offset = 50 + node_margin = 100 + # Create and connect regular outputs + if regular_outputs: + out_name = f'OUT_{out_base}' + fo_regular = create_and_connect_file_output( + node, regular_outputs, file_out, out_name, base_path, + out_base, scene, remap_names, file_format, + location_offset=(500, y_offset) + ) + if fo_regular and fo_regular not in created_nodes: + created_nodes.append(fo_regular) - fo.name = out_name - if node.parent: - fo.parent = node.parent + y_offset += -22 * len(regular_outputs) - node_margin + # Create and connect tech outputs with 32-bit depth if split_tech_passes is True + if tech_outputs: + out_name = f'OUT_{out_base}_tech' - if base_path: - fo.base_path = base_path - else: - if fo.format.file_format == 'OPEN_EXR_MULTILAYER': - fo.base_path = f'//render/{out_base}/{out_base}_' - else: - fo.base_path = f'//render/{out_base}' + fo_tech = create_and_connect_file_output( + node, tech_outputs, None, out_name, base_path, + out_base, scene, remap_names, file_format, + suffix='tech/', color_depth='32', + location_offset=(500, y_offset) + ) - for o in outs: - if next((l for l in o.links if recursive_node_connect_check(l, fo)), None): - continue - - if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)): - slot_name = bpy.path.clean_name(custom_name) # clean name ? - else: - slot_name = bpy.path.clean_name(o.name) + if fo_tech and fo_tech not in created_nodes: + created_nodes.append(fo_tech) + + y_offset -= node_margin - # if fo.format.file_format == 'OPEN_EXR_MULTILAYER': - # slot_name = slot_name - # else: - # slot_name = f'{slot_name}/{slot_name}_' - # fo.file_slots.new(slot_name) - fs = fo.file_slots.new('tmp') # slot_name) - ls = fo.layer_slots.new('tmp') # slot_name + 'layer') + y_offset += -22 * len(tech_outputs) + # Create and connect crypto outputs with 32-bit depth + if crypto_outputs: + out_name = f'OUT_{out_base}_cryptos' + + fo_crypto = create_and_connect_file_output( + node, crypto_outputs, None, out_name, base_path, + out_base, scene, remap_names, file_format, + suffix='cryptos/', color_depth='32', + location_offset=(500, y_offset) + ) - ls = fo.layer_slots[-1] - ls.name = slot_name - - fs = fo.file_slots[-1] - fs.path = f'{slot_name}/{slot_name}_' # Error 'NodeSocketColor' object has no attribute 'path' - - - out_input = fo.inputs[-1] - links.new(o, out_input) - - clear_disconnected(fo) - fo.update() - - ## Create separate file out for cryptos - if cryptout: - out_name += '_cryptos' - fo = file_out - if not fo: - fo = nodes.get(out_name) - if not fo: - # color = (0.2,0.3,0.5) - fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+400, real_loc(node)[1]-200), width=220) - fo.inputs.remove(fo.inputs[0]) # Remove default image input - if file_format: - for k, v in file_format.items(): - setattr(fo.format, k, v) - else: - set_file_output_format(fo) # OPEN_EXR_MULTILAYER, RGBA, ZIP - fo.format.color_depth = '32' # For crypto force 32bit - - fo.name = out_name - if node.parent: - fo.parent = node.parent - - if base_path: - fo.base_path = base_path - else: - if fo.format.file_format == 'OPEN_EXR_MULTILAYER': - ## FIXME: find a better organization for separated crypto pass - fo.base_path = f'//render/{out_base}/cryptos/cryptos_' - else: - fo.base_path = f'//render/{out_base}' - - for o in cryptout: - ## Skip already connected - ## TODO Test recusively to find fo (some have interconnected sockets) - # if next((l for l in o.links if l.to_node == fo), None): - if next((l for l in o.links if recursive_node_connect_check(l, fo)), None): - continue - - # if remap_names and (custom_name := remap_names.get(o.name)): - if (socket_remaps := remap_names.get(node.name)) and (custom_name := socket_remaps.get(o.name)): - slot_name = bpy.path.clean_name(custom_name) # clean name ? - else: - slot_name = bpy.path.clean_name(o.name) # directly use name in multi layer exr - - # if fo.format.file_format == 'OPEN_EXR_MULTILAYER': - # slot_name = slot_name - # else: - # slot_name = f'{slot_name}/{slot_name}_' - # fo.file_slots.new(slot_name) - - # Setting both file_slots and layer_slots... - fs = fo.file_slots.new('tmp') - ls = fo.layer_slots.new('tmp') - - ls = fo.layer_slots[-1] - ls.name = slot_name - - fs = fo.file_slots[-1] - fs.path = f'{slot_name}/{slot_name}_' # Error 'NodeSocketColor' object has no attribute 'path' - - - out_input = fo.inputs[-1] - links.new(o, out_input) - clear_disconnected(fo) - fo.update() + if fo_crypto and fo_crypto not in created_nodes: + created_nodes.append(fo_crypto) + + + return created_nodes \ No newline at end of file diff --git a/setup_outputs.py b/setup_outputs.py index 71dd166..cc2580e 100755 --- a/setup_outputs.py +++ b/setup_outputs.py @@ -7,6 +7,8 @@ from bpy.props import (StringProperty, EnumProperty, CollectionProperty) +from .constant import TECH_PASS_KEYWORDS + ## -- search and replace (WIP) to batch rename class RT_OT_search_and_replace(bpy.types.Operator): @@ -132,6 +134,11 @@ class RT_OT_create_output_layers(bpy.types.Operator): name='Settings', default=False) + split_tech_passes : BoolProperty( + name='Separate Tech Passes', + default=True, + description='Create a separate file output for technical passes (32 bit)') + ## enum choice for naming: socket_name, node_name, node_and_socket_name, name_type : EnumProperty( name='Output Name From', @@ -259,6 +266,7 @@ class RT_OT_create_output_layers(bpy.types.Operator): box = layout.box() expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT' box.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon) + ## Settings if self.show_custom_settings: box.use_property_split = True @@ -268,6 +276,7 @@ class RT_OT_create_output_layers(bpy.types.Operator): col.prop(self, 'file_format') col.prop(self, 'exr_codec') col.row().prop(self, 'color_depth', expand=True) + col.prop(self, 'split_tech_passes') search_row = layout.row() op = search_row.operator("rt.search_and_replace", icon='BORDERMOVE') @@ -310,6 +319,9 @@ class RT_OT_create_output_layers(bpy.types.Operator): display_name = item.socket_name if 'crypto' in display_name.lower(): display_name = f'{display_name} -> 32bit output node' + elif self.split_tech_passes and display_name.lower() in TECH_PASS_KEYWORDS: + display_name = f'{display_name} -> Tech pass' + row.label(text=display_name) row.label(text='', icon='RIGHTARROW') @@ -375,7 +387,15 @@ class RT_OT_create_output_layers(bpy.types.Operator): # fn.connect_to_file_output(selected, outfile) for n in selected: - fn.connect_to_file_output(n, outfile, base_path=self.base_path, excludes=excludes, remap_names=remap_names, file_format=file_format) + fn.connect_to_file_output( + n, + outfile, + base_path=self.base_path, + excludes=excludes, + remap_names=remap_names, + file_format=file_format, + split_tech_passes=self.split_tech_passes) + return {"FINISHED"} classes=( diff --git a/ui.py b/ui.py index 2ac0517..998beba 100755 --- a/ui.py +++ b/ui.py @@ -6,7 +6,7 @@ from bpy.types import Panel class RT_PT_gp_node_ui(Panel): bl_space_type = "NODE_EDITOR" bl_region_type = "UI" - bl_category = "Render" + bl_category = "Render" # Wrangler bl_label = "Render Toolbox" def draw(self, context):