File output - Selectable socket to connect in popup

1.7.2

- added: selectable output popup in `connect to file output` operator
main
pullusb 2024-04-18 15:11:59 +02:00
parent c6dfcb6655
commit ad083e45b6
4 changed files with 213 additions and 18 deletions

View File

@ -14,6 +14,10 @@ Activate / deactivate layer opacity according to prefix
Activate / deactivate all masks using MA layers
-->
1.7.2
- added: selectable output popup in `connect to file output` operator
1.7.1
- fix: file output base path depending on file format

View File

@ -1,5 +1,6 @@
import bpy
import re
import os
from . import fn
class GPEXP_OT_mute_toggle_output_nodes(bpy.types.Operator):
@ -237,6 +238,13 @@ class GPEXP_OT_reset_render_settings(bpy.types.Operator):
return {"FINISHED"}
class GPEXP_PG_selectable_prop(bpy.types.PropertyGroup):
node_name: bpy.props.StringProperty(name="Object Name")
name: bpy.props.StringProperty(name="Name or Path")
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)
class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
bl_idname = "gp.connect_selected_to_file_out"
bl_label = "Connect Selected To File Output"
@ -244,7 +252,139 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
\nIf a fileoutput node is selected, socket are added to it"
bl_options = {"REGISTER", "UNDO"}
socket_collection : bpy.props.CollectionProperty(type=GPEXP_PG_selectable_prop)
show_custom_settings : bpy.props.BoolProperty(
name='Settings',
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.name = o.name
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
expand_icon = 'DISCLOSURE_TRI_DOWN' if self.show_custom_settings else 'DISCLOSURE_TRI_RIGHT'
layout.prop(self, 'show_custom_settings', emboss=False, icon=expand_icon)
## Settings
if self.show_custom_settings:
layout.use_property_split = True
layout.prop(self, 'base_path')
col = layout.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()
col.label(text=item.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.name
if 'crypto' in display_name.lower():
display_name = f'{display_name} (-> separate 32bit output node)'
row.label(text=display_name)
def execute(self, context):
# Build exclude dict from selection
excludes = {}
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)
## 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]
@ -254,7 +394,7 @@ class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
# fn.connect_to_file_output(selected, outfile)
for n in selected:
fn.connect_to_file_output(n, outfile)
fn.connect_to_file_output(n, outfile, base_path=self.base_path, excludes=excludes, file_format=file_format)
return {"FINISHED"}
classes=(
@ -264,6 +404,7 @@ GPEXP_OT_number_outputs,
GPEXP_OT_enable_all_viewlayers,
GPEXP_OT_activate_only_selected_layers,
GPEXP_OT_reset_render_settings,
GPEXP_PG_selectable_prop,
GPEXP_OT_connect_selected_to_file_out,
# GPEXP_OT_normalize_outnames,
)

View File

@ -2,7 +2,7 @@ bl_info = {
"name": "GP Render",
"description": "Organise export of gp layers through compositor output",
"author": "Samuel Bernou",
"version": (1, 7, 1),
"version": (1, 7, 2),
"blender": (3, 0, 0),
"location": "View3D",
"warning": "",

82
fn.py
View File

@ -1871,7 +1871,32 @@ def recursive_node_connect_check(l, target_node):
return True
return False
def connect_to_file_output(node_list, file_out=None):
def connect_to_file_output(node_list, file_out=None, base_path='', excludes=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.
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
@ -1881,10 +1906,13 @@ def connect_to_file_output(node_list, file_out=None):
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()]
cryptout = [o for o in node.outputs if not o.is_unavailable and 'crypto' in o.name.lower()]
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
@ -1902,15 +1930,23 @@ def connect_to_file_output(node_list, file_out=None):
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)
set_file_output_format(fo)
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 fo.format.file_format == 'OPEN_EXR_MULTILAYER':
fo.base_path = f'//render/{out_base}/{out_base}_'
if base_path:
fo.base_path = base_path
else:
fo.base_path = f'//render/{out_base}'
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):
@ -1923,6 +1959,7 @@ def connect_to_file_output(node_list, file_out=None):
fo.file_slots.new(slot_name)
out_input = fo.inputs[-1]
links.new(o, out_input)
clear_disconnected(fo)
fo.update()
@ -1935,18 +1972,25 @@ def connect_to_file_output(node_list, file_out=None):
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)
set_file_output_format(fo) # OPEN_EXR_MULTILAYER, RGBA, ZIP
fo.format.color_depth = '32'
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 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_'
if base_path:
fo.base_path = base_path
else:
fo.base_path = f'//render/{out_base}'
# fo.base_path = f'//render/{out_base}/cryptos'
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
@ -1961,8 +2005,14 @@ def connect_to_file_output(node_list, file_out=None):
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(slot_name)
# fs.path = f'{slot_name}/{slot_name}_' # Error 'NodeSocketColor' object has no attribute 'path'
# ls = fo.layer_slots.new(slot_name)
# ls.name = slot_name
out_input = fo.inputs[-1]
links.new(o, out_input)
clear_disconnected(fo)