File output - Selectable socket to connect in popup
1.7.2 - added: selectable output popup in `connect to file output` operatormain
parent
c6dfcb6655
commit
ad083e45b6
|
@ -14,6 +14,10 @@ Activate / deactivate layer opacity according to prefix
|
||||||
Activate / deactivate all masks using MA layers
|
Activate / deactivate all masks using MA layers
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
1.7.2
|
||||||
|
|
||||||
|
- added: selectable output popup in `connect to file output` operator
|
||||||
|
|
||||||
1.7.1
|
1.7.1
|
||||||
|
|
||||||
- fix: file output base path depending on file format
|
- fix: file output base path depending on file format
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import bpy
|
import bpy
|
||||||
import re
|
import re
|
||||||
|
import os
|
||||||
from . import fn
|
from . import fn
|
||||||
|
|
||||||
class GPEXP_OT_mute_toggle_output_nodes(bpy.types.Operator):
|
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"}
|
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):
|
class GPEXP_OT_connect_selected_to_file_out(bpy.types.Operator):
|
||||||
bl_idname = "gp.connect_selected_to_file_out"
|
bl_idname = "gp.connect_selected_to_file_out"
|
||||||
bl_label = "Connect Selected To File Output"
|
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"
|
\nIf a fileoutput node is selected, socket are added to it"
|
||||||
bl_options = {"REGISTER", "UNDO"}
|
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):
|
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
|
scn = context.scene
|
||||||
nodes = scn.node_tree.nodes
|
nodes = scn.node_tree.nodes
|
||||||
selected = [n for n in nodes if n.select]
|
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)
|
# fn.connect_to_file_output(selected, outfile)
|
||||||
for n in selected:
|
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"}
|
return {"FINISHED"}
|
||||||
|
|
||||||
classes=(
|
classes=(
|
||||||
|
@ -264,6 +404,7 @@ GPEXP_OT_number_outputs,
|
||||||
GPEXP_OT_enable_all_viewlayers,
|
GPEXP_OT_enable_all_viewlayers,
|
||||||
GPEXP_OT_activate_only_selected_layers,
|
GPEXP_OT_activate_only_selected_layers,
|
||||||
GPEXP_OT_reset_render_settings,
|
GPEXP_OT_reset_render_settings,
|
||||||
|
GPEXP_PG_selectable_prop,
|
||||||
GPEXP_OT_connect_selected_to_file_out,
|
GPEXP_OT_connect_selected_to_file_out,
|
||||||
# GPEXP_OT_normalize_outnames,
|
# GPEXP_OT_normalize_outnames,
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,7 +2,7 @@ bl_info = {
|
||||||
"name": "GP Render",
|
"name": "GP Render",
|
||||||
"description": "Organise export of gp layers through compositor output",
|
"description": "Organise export of gp layers through compositor output",
|
||||||
"author": "Samuel Bernou",
|
"author": "Samuel Bernou",
|
||||||
"version": (1, 7, 1),
|
"version": (1, 7, 2),
|
||||||
"blender": (3, 0, 0),
|
"blender": (3, 0, 0),
|
||||||
"location": "View3D",
|
"location": "View3D",
|
||||||
"warning": "",
|
"warning": "",
|
||||||
|
|
62
fn.py
62
fn.py
|
@ -1871,7 +1871,32 @@ def recursive_node_connect_check(l, target_node):
|
||||||
return True
|
return True
|
||||||
return False
|
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
|
scene = bpy.context.scene
|
||||||
nodes = scene.node_tree.nodes
|
nodes = scene.node_tree.nodes
|
||||||
links = scene.node_tree.links
|
links = scene.node_tree.links
|
||||||
|
@ -1881,10 +1906,13 @@ def connect_to_file_output(node_list, file_out=None):
|
||||||
if not node_list:
|
if not node_list:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
excludes = excludes or {}
|
||||||
|
|
||||||
for node in node_list:
|
for node in node_list:
|
||||||
|
exclusions = excludes.get(node.name) or []
|
||||||
## create one output facing node and connect all
|
## 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()]
|
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()]
|
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':
|
if node.type == 'R_LAYERS':
|
||||||
out_base = node.layer
|
out_base = node.layer
|
||||||
|
@ -1902,11 +1930,19 @@ def connect_to_file_output(node_list, file_out=None):
|
||||||
if not fo:
|
if not fo:
|
||||||
# color = (0.2,0.3,0.5)
|
# 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 = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+500, real_loc(node)[1]+50), width=600)
|
||||||
|
if file_format:
|
||||||
|
for k, v in file_format.items():
|
||||||
|
setattr(fo.format, k, v)
|
||||||
|
else:
|
||||||
set_file_output_format(fo)
|
set_file_output_format(fo)
|
||||||
|
|
||||||
fo.name = out_name
|
fo.name = out_name
|
||||||
if node.parent:
|
if node.parent:
|
||||||
fo.parent = node.parent
|
fo.parent = node.parent
|
||||||
|
|
||||||
|
if base_path:
|
||||||
|
fo.base_path = base_path
|
||||||
|
else:
|
||||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||||
fo.base_path = f'//render/{out_base}/{out_base}_'
|
fo.base_path = f'//render/{out_base}/{out_base}_'
|
||||||
else:
|
else:
|
||||||
|
@ -1923,6 +1959,7 @@ def connect_to_file_output(node_list, file_out=None):
|
||||||
fo.file_slots.new(slot_name)
|
fo.file_slots.new(slot_name)
|
||||||
out_input = fo.inputs[-1]
|
out_input = fo.inputs[-1]
|
||||||
links.new(o, out_input)
|
links.new(o, out_input)
|
||||||
|
|
||||||
clear_disconnected(fo)
|
clear_disconnected(fo)
|
||||||
fo.update()
|
fo.update()
|
||||||
|
|
||||||
|
@ -1935,18 +1972,25 @@ def connect_to_file_output(node_list, file_out=None):
|
||||||
if not fo:
|
if not fo:
|
||||||
# color = (0.2,0.3,0.5)
|
# 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 = create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(real_loc(node)[0]+400, real_loc(node)[1]-200), width=220)
|
||||||
|
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
|
set_file_output_format(fo) # OPEN_EXR_MULTILAYER, RGBA, ZIP
|
||||||
fo.format.color_depth = '32'
|
fo.format.color_depth = '32' # For crypto force 32bit
|
||||||
|
|
||||||
fo.name = out_name
|
fo.name = out_name
|
||||||
if node.parent:
|
if node.parent:
|
||||||
fo.parent = node.parent
|
fo.parent = node.parent
|
||||||
|
|
||||||
|
if base_path:
|
||||||
|
fo.base_path = base_path
|
||||||
|
else:
|
||||||
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
if fo.format.file_format == 'OPEN_EXR_MULTILAYER':
|
||||||
## FIXME: find a better organization for separated crypto pass
|
## FIXME: find a better organization for separated crypto pass
|
||||||
fo.base_path = f'//render/{out_base}/cryptos/cryptos_'
|
fo.base_path = f'//render/{out_base}/cryptos/cryptos_'
|
||||||
else:
|
else:
|
||||||
fo.base_path = f'//render/{out_base}'
|
fo.base_path = f'//render/{out_base}'
|
||||||
# fo.base_path = f'//render/{out_base}/cryptos'
|
|
||||||
|
|
||||||
for o in cryptout:
|
for o in cryptout:
|
||||||
## Skip already connected
|
## Skip already connected
|
||||||
|
@ -1961,8 +2005,14 @@ def connect_to_file_output(node_list, file_out=None):
|
||||||
slot_name = slot_name
|
slot_name = slot_name
|
||||||
else:
|
else:
|
||||||
slot_name = f'{slot_name}/{slot_name}_'
|
slot_name = f'{slot_name}/{slot_name}_'
|
||||||
|
|
||||||
fo.file_slots.new(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]
|
out_input = fo.inputs[-1]
|
||||||
links.new(o, out_input)
|
links.new(o, out_input)
|
||||||
clear_disconnected(fo)
|
clear_disconnected(fo)
|
||||||
|
|
Loading…
Reference in New Issue