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",
|
"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
|
||||||
|
|
|
@ -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 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()
|
|
|
@ -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
2
ui.py
|
@ -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):
|
||||||
|
|
Loading…
Reference in New Issue