From ad083e45b6a00e2974c0f20cdc7ab221167399eb Mon Sep 17 00:00:00 2001 From: pullusb Date: Thu, 18 Apr 2024 15:11:59 +0200 Subject: [PATCH] File output - Selectable socket to connect in popup 1.7.2 - added: selectable output popup in `connect to file output` operator --- CHANGELOG.md | 4 ++ OP_manage_outputs.py | 143 ++++++++++++++++++++++++++++++++++++++++++- __init__.py | 2 +- fn.py | 82 ++++++++++++++++++++----- 4 files changed, 213 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12334e1..1309bc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/OP_manage_outputs.py b/OP_manage_outputs.py index 8e53521..8d30da6 100644 --- a/OP_manage_outputs.py +++ b/OP_manage_outputs.py @@ -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, ) diff --git a/__init__.py b/__init__.py index fd5c622..0cd4303 100644 --- a/__init__.py +++ b/__init__.py @@ -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": "", diff --git a/fn.py b/fn.py index 84c2602..83dfad2 100644 --- a/fn.py +++ b/fn.py @@ -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)