Split tech passes in separate 32 bits

hardcoded tech passes names : ['uv', 'normal', 'depth', 'position', 'vector', 'ao']
master
pullusb 2025-04-02 11:53:11 +02:00
parent 79406ca0a2
commit 7fd7f9ab27
5 changed files with 180 additions and 135 deletions

View File

@ -2,13 +2,13 @@ bl_info = {
"name": "Render Toolbox", "name": "Render Toolbox",
"description": "Perform checks and setup outputs", "description": "Perform checks and setup outputs",
"author": "Samuel Bernou", "author": "Samuel Bernou",
"version": (0, 2, 0), "version": (0, 3, 0),
"blender": (3, 0, 0), "blender": (3, 0, 0),
"location": "View3D", "location": "View3D",
"warning": "", "warning": "",
"doc_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox", "doc_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox",
"tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox/issues", "tracker_url": "https://git.autourdeminuit.com/autour_de_minuit/render_toolbox/issues",
"category": "Object" "category": "Render"
} }
from . import setup_outputs from . import setup_outputs

2
constant.py Normal file
View File

@ -0,0 +1,2 @@
# Technical pass names (names that should be in 32bit)
TECH_PASS_KEYWORDS = ['uv', 'normal', 'depth', 'position', 'vector', 'ao']

285
fn.py
View File

@ -3,6 +3,8 @@ import os
import re import re
import json import json
from .constant import TECH_PASS_KEYWORDS
### --- Manage nodes --- ### ### --- Manage nodes --- ###
def real_loc(n): def real_loc(n):
@ -60,7 +62,92 @@ def recursive_node_connect_check(l, target_node):
return False 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) """Connect selected nodes output to file output(s)
if a file output is selected, add intputs on it 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. Defaults toNone.
remap_names (dict, optionnal): List of output names to remap {node_name: {output_name: new_name}}. 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. split_tech_passes (bool, optional): When True, create a separate file output for technical passes
Defaults to None. Defaults to False.
Returns: Returns:
list[bpy.types.CompositorNode]: All nodes created. list[bpy.types.CompositorNode]: All nodes created.
""" """
scene = bpy.context.scene scene = bpy.context.scene
nodes = scene.node_tree.nodes
links = scene.node_tree.links
if not isinstance(node_list, list): if not isinstance(node_list, list):
node_list = [node_list] node_list = [node_list]
node_list = [n for n in node_list if n.type != 'OUTPUT_FILE'] node_list = [n for n in node_list if n.type != 'OUTPUT_FILE']
if not node_list: if not node_list:
return return []
created_nodes = []
excludes = excludes or {} excludes = excludes or {}
remap_names = remap_names or {}
for node in node_list: for node in node_list:
exclusions = excludes.get(node.name) or [] 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] # Get all available outputs excluding those 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] all_outputs = [o for o in node.outputs if not o.is_unavailable and o.name not in exclusions]
# Base name for output nodes
## 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
if node.label: if node.label:
out_base = node.label out_base = node.label
else: else:
out_base = node.name out_base = node.name
out_base = bpy.path.clean_name(out_base) 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: y_offset = 50
fo = file_out node_margin = 100
if not fo: # Create and connect regular outputs
fo = nodes.get(out_name) if regular_outputs:
if not fo: out_name = f'OUT_{out_base}'
# color = (0.2,0.3,0.5) fo_regular = create_and_connect_file_output(
fo = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+500, real_loc(node)[1]+50), width=600) node, regular_outputs, file_out, out_name, base_path,
fo.inputs.remove(fo.inputs[0]) # Remove default image input out_base, scene, remap_names, file_format,
if file_format: location_offset=(500, y_offset)
for k, v in file_format.items(): )
setattr(fo.format, k, v) if fo_regular and fo_regular not in created_nodes:
else: created_nodes.append(fo_regular)
set_file_output_format(fo)
fo.name = out_name y_offset += -22 * len(regular_outputs) - node_margin
if node.parent: # Create and connect tech outputs with 32-bit depth if split_tech_passes is True
fo.parent = node.parent if tech_outputs:
out_name = f'OUT_{out_base}_tech'
if base_path: fo_tech = create_and_connect_file_output(
fo.base_path = base_path node, tech_outputs, None, out_name, base_path,
else: out_base, scene, remap_names, file_format,
if fo.format.file_format == 'OPEN_EXR_MULTILAYER': suffix='tech/', color_depth='32',
fo.base_path = f'//render/{out_base}/{out_base}_' location_offset=(500, y_offset)
else: )
fo.base_path = f'//render/{out_base}'
for o in outs: if fo_tech and fo_tech not in created_nodes:
if next((l for l in o.links if recursive_node_connect_check(l, fo)), None): created_nodes.append(fo_tech)
continue
y_offset -= node_margin
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.format.file_format == 'OPEN_EXR_MULTILAYER': y_offset += -22 * len(tech_outputs)
# slot_name = slot_name # Create and connect crypto outputs with 32-bit depth
# else: if crypto_outputs:
# slot_name = f'{slot_name}/{slot_name}_' out_name = f'OUT_{out_base}_cryptos'
# fo.file_slots.new(slot_name)
fs = fo.file_slots.new('tmp') # slot_name) fo_crypto = create_and_connect_file_output(
ls = fo.layer_slots.new('tmp') # slot_name + 'layer') 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] if fo_crypto and fo_crypto not in created_nodes:
ls.name = slot_name created_nodes.append(fo_crypto)
fs = fo.file_slots[-1]
fs.path = f'{slot_name}/{slot_name}_' # Error 'NodeSocketColor' object has no attribute 'path' return created_nodes
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()

View File

@ -7,6 +7,8 @@ from bpy.props import (StringProperty,
EnumProperty, EnumProperty,
CollectionProperty) CollectionProperty)
from .constant import TECH_PASS_KEYWORDS
## -- search and replace (WIP) to batch rename ## -- search and replace (WIP) to batch rename
class RT_OT_search_and_replace(bpy.types.Operator): class RT_OT_search_and_replace(bpy.types.Operator):
@ -132,6 +134,11 @@ class RT_OT_create_output_layers(bpy.types.Operator):
name='Settings', name='Settings',
default=False) 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, ## enum choice for naming: socket_name, node_name, node_and_socket_name,
name_type : EnumProperty( name_type : EnumProperty(
name='Output Name From', name='Output Name From',
@ -259,6 +266,7 @@ class RT_OT_create_output_layers(bpy.types.Operator):
box = layout.box() box = layout.box()
expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT' 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) box.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon)
## Settings ## Settings
if self.show_custom_settings: if self.show_custom_settings:
box.use_property_split = True 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, 'file_format')
col.prop(self, 'exr_codec') col.prop(self, 'exr_codec')
col.row().prop(self, 'color_depth', expand=True) col.row().prop(self, 'color_depth', expand=True)
col.prop(self, 'split_tech_passes')
search_row = layout.row() search_row = layout.row()
op = search_row.operator("rt.search_and_replace", icon='BORDERMOVE') 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 display_name = item.socket_name
if 'crypto' in display_name.lower(): if 'crypto' in display_name.lower():
display_name = f'{display_name} -> 32bit output node' 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=display_name)
row.label(text='', icon='RIGHTARROW') 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) # fn.connect_to_file_output(selected, outfile)
for n in selected: 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"} return {"FINISHED"}
classes=( classes=(

2
ui.py
View File

@ -6,7 +6,7 @@ from bpy.types import Panel
class RT_PT_gp_node_ui(Panel): class RT_PT_gp_node_ui(Panel):
bl_space_type = "NODE_EDITOR" bl_space_type = "NODE_EDITOR"
bl_region_type = "UI" bl_region_type = "UI"
bl_category = "Render" bl_category = "Render" # Wrangler
bl_label = "Render Toolbox" bl_label = "Render Toolbox"
def draw(self, context): def draw(self, context):