first commit, transfer connect selected to file out from gp_render

master
pullusb 2025-03-26 17:43:16 +01:00
parent 016d3f827c
commit d56d32ad57
5 changed files with 576 additions and 2 deletions

View File

@ -1,5 +1,6 @@
# git_template
A Template from which to create new repository from.
# Render Toolbox
Blender addon for rendering setup and checks
## Development
### Cloning

50
__init__.py Normal file
View File

@ -0,0 +1,50 @@
bl_info = {
"name": "Render Toolbox",
"description": "Perform checks and setup outputs",
"author": "Samuel Bernou",
"version": (0, 1, 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"
}
## TODO:
## Transfer useful and generic render function from gp_render:
# - mute/unmute all file output nodes
# - export crop infos to json (hardcoded path in gp_render. make an export dialog instead, with eventually an environnement variable for the path)
# - Viewlayer list view
## Transfer useful and genereric render stuff from gp_toolbox:
# - ? Conflict visibility checker ? This also fit other tasks, but more often use to debug at precomp/rendering step.
## Improve existing:
# - Connect to file output node should have the option to add scene name as output prefix
# - Connect to file output node should have search and replace options for output names in window
# - Connect to file output node should not remove unlinked output by defaut (should be an option in window)
from . import setup_outputs
from . import ui
bl_modules = (
setup_outputs,
ui,
# prefs,
)
import bpy
def register():
for mod in bl_modules:
mod.register()
def unregister():
for mod in reversed(bl_modules):
mod.unregister()
if __name__ == "__main__":
register()

237
fn.py Normal file
View File

@ -0,0 +1,237 @@
import bpy
import os
import re
import json
### --- Manage nodes --- ###
def real_loc(n):
if not n.parent:
return n.location
return n.location + real_loc(n.parent)
def set_file_output_format(fo):
"""Default fileout format for output file node
Get from OUTPUT_RENDER_FILE_FORMAT environment variable
else fallback to multilayer EXR
"""
env_file_format = json.loads(os.environ.get('OUTPUT_RENDER_FILE_FORMAT', '{}'))
if not env_file_format:
env_file_format = {
'file_format': 'OPEN_EXR_MULTILAYER',
'exr_codec': 'ZIP',
'color_depth': '16',
'color_mode': 'RGBA'
}
for k, v in env_file_format.items():
setattr(fo.format, k, v)
def clear_disconnected(fo):
'''Remove unlinked inputs from file output node'''
for inp in reversed(fo.inputs):
if not inp.is_linked:
print(f'Deleting unlinked fileout slot: {inp.name}')
fo.inputs.remove(inp)
def create_node(type, tree=None, **kargs):
'''Get a type, a tree to add in, and optionnaly multiple attribute to set
return created node
'''
tree = tree or bpy.context.scene.node_tree
node = tree.nodes.new(type)
for k,v in kargs.items():
setattr(node, k, v)
return node
def recursive_node_connect_check(l, target_node):
'''Get a link and a node
return True if link is connected to passed target_node further in the node tree
'''
if l.to_node == target_node:
return True
for o in l.to_node.outputs:
for sl in o.links:
if recursive_node_connect_check(sl, target_node):
return True
return False
def connect_to_file_output(node_list, file_out=None, base_path='', excludes=None, remap_names=None, file_format=None):
"""Connect selected nodes output to file output(s)
if a file output is selected, add intputs on it
Args:
node_list (list[bpy.types.Nodes,]): Nodes to connect
file_out (bpy.types.CompositorNode, optional): File output node to connect to instead of new
Defaults to None
base_path (str, optional): Directory of images to render.
if not passed, will use source node layer name
Defaults to ''.
file_format (dict, optionnal): converts each dictionary key into a file output format
attribute and assigns the corresponding value.
Defaults to None.
excludes (dict, optionnal): List of output names to exclude {node_name: [outputs,]}.
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.
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
excludes = excludes 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]
if node.type == 'R_LAYERS':
out_base = node.layer
elif 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}'
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)
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}/{out_base}_'
else:
fo.base_path = f'//render/{out_base}'
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.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')
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()

259
setup_outputs.py Normal file
View File

@ -0,0 +1,259 @@
import bpy
import os
from . import fn
class RT_PG_selectable_prop(bpy.types.PropertyGroup):
node_name: bpy.props.StringProperty(name="Node Name")
node_label: bpy.props.StringProperty(name="Node Label")
name: bpy.props.StringProperty(name="Name or Path")
socket_name: bpy.props.StringProperty(name="Source socket Name") # Source socket name as reference
select: bpy.props.BoolProperty(name="Selected", default=True)
is_linked: bpy.props.BoolProperty(name="Linked", default=False)
# is_valid: bpy.props.BoolProperty(name="Valid", default=True)
## extra output naming options
name_from_node: bpy.props.StringProperty(name="Name From Node")
name_from_node_and_socket: bpy.props.StringProperty(name="Name From Node And Socket")
class RT_OT_connect_selected_to_file_out(bpy.types.Operator):
bl_idname = "rt.connect_selected_to_file_out"
bl_label = "Connect Selected To File Output"
bl_description = "Connect Selected Nodes to a new fileoutput node\
\nIf a fileoutput node is selected, socket are added to it"
bl_options = {"REGISTER", "UNDO"}
socket_collection : bpy.props.CollectionProperty(type=RT_PG_selectable_prop)
show_custom_settings : bpy.props.BoolProperty(
name='Settings',
default=False)
## enum choice for naming: socket_name, node_name, node_and_socket_name,
name_type : bpy.props.EnumProperty(
name='Output Name From',
description='Choose the output name\
\nNode name use Label (Use node name when there is no Label)',
default='node_and_socket_name',
items=(
('node_and_socket_name', 'Node_Socket Name', 'Use the node name prefix and socket name', 0),
('socket_name', 'Socket Name', 'Use the socket name as output name', 1),
('node_name', 'Node Name', 'Use the node name as output name', 2),
)
)
# prefix_with_node_name : bpy.props.BoolProperty(
# name='Prefix With Node Name',
# description='Add the node name as prefix to the output name',
# default=False)
base_path : bpy.props.StringProperty(
name='Custom base path',
default='',
description='Set the base path of created file_output (not if already exists)')
file_format : bpy.props.EnumProperty(
name='Output Format',
default='NONE',
items=(
('NONE', 'Default', 'Use default settings'),
('OPEN_EXR_MULTILAYER', 'OpenEXR MultiLayer', 'Output image in multilayer OpenEXR format'),
('OPEN_EXR', 'OpenEXR', 'Output image in OpenEXR format'),
)
)
exr_codec : bpy.props.EnumProperty(
name='Codec',
default='PIZ',
description='Codec settings for OpenEXR',
items=(
('PIZ', 'PIZ (lossless)', ''),
('ZIP', 'ZIP (lossless)', ''),
('RLE', 'RLE (lossless)', ''),
('ZIPS', 'ZIPS (lossless)', ''),
('PXR24', 'Pxr24 (lossy)', ''),
('B44', 'B44 (lossy)', ''),
('B44A', 'B44A (lossy)', ''),
('DWAA', 'DWAA (lossy)', ''),
('DWAB', 'DWAB (lossy)', ''),
),
)
color_depth : bpy.props.EnumProperty(
name='Color Depth',
default='16',
description='Bit depth per channel',
items=(
# ('8', '8', '8-bit color channels'),
# ('10', '10', '10-bit color channels'),
# ('12', '12', '12-bit color channels'),
('16', '16 (Half)', '16-bit color channels'),
('32', '32 (Full)', '32-bit color channels'),
),
)
def invoke(self, context, event):
self.socket_collection.clear()
if event.ctrl:
# Direct connect, do not use any options
self.base_path = ''
return self.execute(context)
selected = [n for n in context.scene.node_tree.nodes if n.select and n.type != 'OUTPUT_FILE']
if not selected:
self.report({'ERROR'}, 'No render layer nodes selected')
return {'CANCELLED'}
for n in selected:
for o in n.outputs:
if o.is_unavailable:
continue
item = self.socket_collection.add()
item.node_name = n.name
item.node_label = n.label.strip()
item.socket_name = o.name
## Set editable names
item.name = o.name
## Store other naming options (cleaned at exec with bpy.path.clean_name)
node_name = n.label.strip() if n.label.strip() else n.name
## Change node_name for render layers: scene_viewlayer_name
if n.type == 'R_LAYERS' and node_name != n.label: # skip if a label is set
node_name = f'{n.layer}'
# node_name = f'{n.scene.name}_{n.layer}'
item.name_from_node = node_name
if len(n.outputs) == 1:
## Only one output, just pick node name, no need to add socket name
item.name_from_node_and_socket = node_name
else:
item.name_from_node_and_socket = f'{node_name}_{o.name}'
## TODO: rename item.name according to template pairs in preferences (to add later)
if o.is_linked:
item.is_linked = True
item.select = False
return context.window_manager.invoke_props_dialog(self, width=500)
def draw(self, context):
layout = self.layout
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
box.row().prop(self, 'name_type', expand=False)
box.prop(self, 'base_path')
col = box.column()
col.prop(self, 'file_format')
col.prop(self, 'exr_codec')
col.row().prop(self, 'color_depth', expand=True)
## Node Sockets
layout.use_property_split = False
col = layout.column()
current_node_name = ''
for item in self.socket_collection:
if item.node_name != current_node_name:
current_node_name = item.node_name
col.separator()
## Display node label + name or name only
if item.node_label:
display_node_name = f'{item.node_label} ({item.node_name})'
else:
display_node_name = item.node_name
## A bit dirty: For render layer node, show render layer-node name.
if display_node_name.startswith('Render Layer'):
rln = context.scene.node_tree.nodes.get(item.node_name)
display_node_name = f'{rln.scene.name}/{rln.layer} ({display_node_name})'
col.label(text=display_node_name, icon='NODE_SEL')
row = col.row()
if item.is_linked:
row.label(text='', icon='LINKED') # NODETREE
else:
row.label(text='', icon='BLANK1')
row.prop(item, 'select', text='')
display_name = item.socket_name
if 'crypto' in display_name.lower():
display_name = f'{display_name} -> 32bit output node'
row.label(text=display_name)
row.label(text='', icon='RIGHTARROW')
if self.name_type == 'socket_name':
row.prop(item, 'name', text='')
elif self.name_type == 'node_name':
row.prop(item, 'name_from_node', text='')
elif self.name_type == 'node_and_socket_name':
row.prop(item, 'name_from_node_and_socket', text='')
def execute(self, context):
# Build exclude dict from selection
excludes = {}
remap_names = {}
## Old system
# if len(self.socket_collection):
# for item in self.socket_collection:
# if not item.select:
# # All deselected goes to exclude with {node_name: [socket1, ...]}
# excludes.setdefault(item.node_name, []).append(item.name)
# elif item.socket_name != item.name:
# remap_names[item.socket_name] = item.name
if len(self.socket_collection):
for item in self.socket_collection:
final_name = item.name
## change name if other options were used
if self.name_type == 'node_name':
final_name = item.name_from_node
elif self.name_type == 'node_and_socket_name':
final_name = item.name_from_node_and_socket
if not item.select:
# All deselected goes to exclude with {node_name: [socket1, ...]}
excludes.setdefault(item.node_name, []).append(item.socket_name)
elif item.socket_name != final_name:
remap_names.setdefault(item.node_name, {})[item.socket_name] = final_name
# remap_names[item.socket_name] = final_name
## Handle default file format
file_ext = self.file_format
if self.file_format == 'NONE':
env_file_format = os.environ.get('FILE_FORMAT')
file_ext = env_file_format if env_file_format else 'OPEN_EXR_MULTILAYER'
file_format = {
'file_format' : file_ext,
'exr_codec' : self.exr_codec,
'color_depth' : self.color_depth,
}
scn = context.scene
nodes = scn.node_tree.nodes
selected = [n for n in nodes if n.select]
outfile = next((n for n in selected if n.type == 'OUTPUT_FILE'), None)
# Exclude output file from
selected = [n for n in selected if n.type != 'OUTPUT_FILE']
# 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)
return {"FINISHED"}
classes=(
RT_PG_selectable_prop,
RT_OT_connect_selected_to_file_out,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

27
ui.py Normal file
View File

@ -0,0 +1,27 @@
import bpy
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_label = "Render Toolbox"
def draw(self, context):
layout = self.layout
layout.operator("rt.connect_selected_to_file_out", icon="NODE")
classes = (
RT_PT_gp_node_ui,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)