Split tech passes in separate 32 bits
hardcoded tech passes names : ['uv', 'normal', 'depth', 'position', 'vector', 'ao']master
parent
79406ca0a2
commit
7fd7f9ab27
|
@ -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
|
||||
|
|
|
@ -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
285
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
|
|
@ -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=(
|
||||
|
|
Loading…
Reference in New Issue