gp_render/OP_manage_outputs.py

425 lines
15 KiB
Python

import bpy
import re
import os
from . import fn
class GPEXP_OT_mute_toggle_output_nodes(bpy.types.Operator):
bl_idname = "gp.mute_toggle_output_nodes"
bl_label = "Mute Toggle output nodes"
bl_description = "Mute / Unmute all output nodes"
bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
def execute(self, context):
ct = 0
for n in context.scene.node_tree.nodes:
if n.type != 'OUTPUT_FILE':
continue
n.mute = self.mute
ct += 1
state = 'muted' if self.mute else 'unmuted'
self.report({"INFO"}, f'{ct} nodes {state}')
return {"FINISHED"}
class GPEXP_OT_number_outputs(bpy.types.Operator):
bl_idname = "gp.number_outputs"
bl_label = "Number Outputs"
bl_description = "(Re)Number the outputs to have ordered file by name in export directories\
\nCtrl+Clic : Delete numbering"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
mode : bpy.props.StringProperty(default='SELECTED', options={'SKIP_SAVE'})
clear : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
def invoke(self, context, event):
# use clear with Ctrl + Click
if event.ctrl:
self.clear = True
return self.execute(context)
def execute(self, context):
scn = context.scene
ct = 0
nodes = scn.node_tree.nodes
for fo in nodes:
if fo.type != 'OUTPUT_FILE':
continue
if self.mode == 'SELECTED' and not fo.select:
continue
# print(f'numbering {fo.name}')
ct += 1
if self.clear:
fn.delete_numbering(fo)
else:
fn.renumber_keep_existing(fo)
txt = 'de-numbered' if self.clear else 're-numbered'
if ct:
self.report({'INFO'}, f'{ct} output nodes {txt}')
else:
self.report({'ERROR'}, f'No output nodes {txt}')
return {"FINISHED"}
class GPEXP_OT_set_output_node_format(bpy.types.Operator):
bl_idname = "gp.set_output_node_format"
bl_label = "Set output format from active"
bl_description = "Change all selected output node to match active output node format"
bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
def execute(self, context):
nodes = context.scene.node_tree.nodes
if not nodes.active or nodes.active.type != 'OUTPUT_FILE':
self.report({"ERROR"}, f'Active node should be an output file to use as reference for output format')
return {"CANCELLED"}
ref = nodes.active
# file_format = ref.format.file_format
# color_mode = ref.format.color_mode
# color_depth = ref.format.color_depth
# compression = ref.format.compression
ct = 0
for n in nodes:
if n.type != 'OUTPUT_FILE' or n == ref or not n.select:
continue
for attr in dir(ref.format):
if attr.startswith('__') or attr in {'rna_type','bl_rna', 'view_settings', 'display_settings','stereo_3d_format'}: # views_format
continue
try:
setattr(n.format, attr, getattr(ref.format, attr))
except Exception as e:
print(f"can't set attribute : {attr}")
# n.format.file_format = file_format
# n.format.color_mode = color_mode
# n.format.color_depth = color_depth
# n.format.compression = compression
ct += 1
# state = 'muted' if self.mute else 'unmuted'
self.report({"INFO"}, f'{ct} output format copied from {ref.name}')
return {"FINISHED"}
def out_norm(x):
a = x.group(1) if x.group(1) else ''
b = x.group(2) if x.group(2) else ''
c = x.group(3) if x.group(3) else ''
d = x.group(4) if x.group(4) else ''
e = x.group(5) if x.group(5) else ''
return f'{a}{b}{fn.normalize(c)}{d}{e}'
## does not match the right thing yet
class GPEXP_OT_normalize_outnames(bpy.types.Operator):
bl_idname = "gp.normalize_outnames"
bl_label = "Normalize Output names"
bl_description = "Normalize output names with lowercase and replace dash to underscore"
bl_options = {"REGISTER"}
mute : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'})
def execute(self, context):
nodes = context.scene.node_tree.nodes
reslash = re.compile('\\/')
ct = 0
for n in nodes:
if n.type != 'OUTPUT_FILE' or not n.select:
continue
# Normalize last part of the file out names
base_path_l = reslash.split(n.base_path)
base_path_l[-1] = fn.normalize(base_path_l[-1])
n.base_path = '/'.join(base_path_l)
for fs in n.file_slots:
fp = fs.path
fp_l = reslash.split(fp)
for i, part in enumerate(fp_l):
fp_l[1] = re.sub(r'(^\d{3}_)?([A-Z]{2}_)?(.*?)(_[A-Z]{2})?(_)?', out_norm, part)
fs.path = '/'.join(fp_l)
ct += 1
# state = 'muted' if self.mute else 'unmuted'
self.report({"INFO"}, f'{ct} output nodes normalized')
return {"FINISHED"}
class GPEXP_OT_enable_all_viewlayers(bpy.types.Operator):
bl_idname = "gp.enable_all_viewlayers"
bl_label = "Enable All Viewlayers"
bl_description = "Enable all View layers except those named 'exclude' 'View Layer'"
bl_options = {"REGISTER"}
def execute(self, context):
scn = context.scene
vl_list = [vl for vl in scn.view_layers if not vl.use and vl.name not in {'View Layer', 'exclude'}]
for v in vl_list:
v.use = True
self.report({"INFO"}, f'{len(vl_list)} ViewLayers Reactivated')
return {"FINISHED"}
class GPEXP_OT_activate_only_selected_layers(bpy.types.Operator):
bl_idname = "gp.activate_only_selected_layers"
bl_label = "Activate Only Selected Layers"
bl_description = "Activate only selected node view layer , excluding all others"
bl_options = {"REGISTER"}
def execute(self, context):
scn = context.scene
nodes = scn.node_tree.nodes
rlayers_nodes = [n for n in nodes if n.select and n.type == 'R_LAYERS']
vls = [scn.view_layers.get(n.layer) for n in rlayers_nodes if scn.view_layers.get(n.layer)]
for v in scn.view_layers:
v.use = v in vls
self.report({"INFO"}, f'Now only {len(vls)} viewlayer active (/{len(scn.view_layers)})')
return {"FINISHED"}
### TODO reset scene settings (set settings )
class GPEXP_OT_reset_render_settings(bpy.types.Operator):
bl_idname = "gp.reset_render_settings"
bl_label = "Reset Render Settings"
bl_description = "Reset render settings on all scene, disabling AA nodes when there is no Merge nodegroup"
bl_options = {"REGISTER"}
def execute(self, context):
for scn in bpy.data.scenes:
if scn.name == 'Scene':
# don't touch original scene
continue
# set a unique preview output
# - avoid possible write/sync overlap (point to tmp on linux ?)
# - allow to monitor output of a scene and possibly use Overwrite
if scn.render.filepath.startswith('//render/preview/'):
scn.render.filepath = f'//render/preview/{bpy.path.clean_name(scn.name.lower())}/preview_'
print(f'Scene {scn.name}: change output to {scn.render.filepath}')
if not scn.use_nodes:
continue
# set the settings depending on merges node presences
use_native_aa = True
for n in scn.node_tree.nodes:
if n.name.startswith('merge_NG_'):
use_native_aa = False
break
if scn.gp_render_settings.use_aa != use_native_aa:
print(f'Scene {scn.name}: changed scene AA settings, native AA = {use_native_aa}')
fn.scene_aa(scene=scn, toggle=use_native_aa)
# set propertie on scn to reflect changes (without triggering update)
scn.gp_render_settings['use_aa'] = use_native_aa
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")
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)
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"
bl_description = "Connect Selected Nodes to a fileoutput node\
\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.socket_name = item.name = o.name
## TODO: rename item.name according to tamplate 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
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.socket_name
if 'crypto' in display_name.lower():
display_name = f'{display_name} (-> separate 32bit output node)'
row.label(text=display_name)
row.label(text='', icon='RIGHTARROW')
row.prop(item, 'name', text='')
def execute(self, context):
# Build exclude dict from selection
excludes = {}
remap_names = {}
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
## 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=(
GPEXP_OT_mute_toggle_output_nodes,
GPEXP_OT_set_output_node_format,
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,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)