gp_render/OP_setup_layers.py

629 lines
24 KiB
Python

import bpy
from bpy.props import (FloatProperty,
BoolProperty,
EnumProperty,
StringProperty,
IntProperty)
from . import fn
import math
import re
import json
from pathlib import Path
## TODO : export json info to re-setup layers in AE automagically
def check_outname(ob, l):
vl_name = l.viewlayer_render
if vl_name in {'exclude', 'View Layer'}:
return
ng_name = f'NG_{ob.name}'
## check in wich node tree this exists
for scn in bpy.data.scenes:
if scn.name == 'Scene':
continue
ng = scn.node_tree.nodes.get(ng_name)
if ng:
break
print(scn.name)
if not ng:
print(f'Skip {vl_name}: Not found nodegroup {ng_name}' )
return
ng_socket = ng.outputs.get(vl_name)
if not ng_socket:
print(f'Skip {vl_name}: Not found in nodegroup {ng_name} sockets')
return
if not len(ng_socket.links):
print(f' socket is disconnected in {ng_name} nodegroup')
return
fo_node = ng_socket.links[0].to_node
fo_socket = ng_socket.links[0].to_socket
if fo_node.type != 'OUTPUT_FILE':
print(f'Skip {vl_name}: node is not an output_file {fo_node.name}')
return
# fo_socket.name isn't right, have to iterate in paths
idx = [i for i in fo_node.inputs].index(fo_socket)
subpath = fo_node.file_slots[idx].path
# fp = Path(fo_node.base_path.rstrip('/')) / subpath
# fp = Path(bpy.path.abspath(str(fp)).rstrip("/")) # abspath on disk
outname = subpath.split('/')[0] # folder name on disk
return outname
class GPEXP_OT_export_infos_for_compo(bpy.types.Operator):
bl_idname = "gp.export_infos_for_compo"
bl_label = "Export Infos For Compo"
bl_description = "Export informations for compositing, including layers with masks, fusion mode, opacity"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
skip_check : bpy.props.BoolProperty(default=False, options={'SKIP_SAVE'})
def invoke(self, context, event):
self.l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
if self.skip_check:
if self.l_infos.exists():
# Preferably skip if already user defined ?
return {'FINISHED'}
return self.execute(context)
if self.l_infos.exists():
return context.window_manager.invoke_props_dialog(self)
return self.execute(context)
def draw(self, context):
layout = self.layout
layout.label(text='An infos Json already exists', icon = 'ERROR')
layout.label(text='Do you want to overwrite ?')
layout.label(text='Note: Must export before "Check Layers" step', icon='INFO')
def execute(self, context):
## Repeat because might not be registered if called with invoke_default
self.l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
dic = {}
pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
for o in pool:
# if not o.visible_get():
# continue
for l in o.data.layers:
# skip non rendered layers
if l.hide:
continue
if l.info.startswith('MA_'):
# No point in storing information of masking layers...
continue
## Can't check viewlayers and final fileout name if Render scene not even created...
""" if not l.viewlayer_render or l.viewlayer_render == 'exclude':
continue
fo_name = check_outname(o, l) # get name used for output file folder (same in AE)
if not fo_name:
print(f'! Could not found fileout name for {o.name} > {l.info}')
continue
"""
ldic = {}
## Check opacity, blend mode
if l.opacity < 1.0:
ldic['opacity'] = l.opacity
if l.blend_mode != 'REGULAR':
ldic['blend_mode'] = l.blend_mode
if l.use_mask_layer:
multi_mask = {}
## dict key as number for masks
# for i, ml in enumerate(l.mask_layers):
# mask = {}
# if ml.hide:
# continue
# mask['name'] = ml.name
# if ml.invert: # create key get only if inverted
# mask['invert'] = ml.invert
# # multi_mask[ml.name] = mask
# multi_mask[i] = mask
## dict key as mask name
for ml in l.mask_layers:
mask = {}
if ml.hide:
continue
# mask['name'] = ml.name
if ml.invert: # create key get only if inverted
mask['invert'] = ml.invert # ! no key if no invert
multi_mask[ml.name] = mask
if multi_mask:
ldic['masks'] = multi_mask
## add to full dic
if ldic:
# add source object ? might be usefull to pin point layer
ldic['object'] = o.name
dic[fn.normalize_layer_name(l, get_only=True)] = ldic
if dic:
self.l_infos.parent.mkdir(exist_ok=True) # create render folder if needed
with self.l_infos.open('w') as fd:
json.dump(dic, fd, indent='\t')
self.report({'INFO'}, f'Exported json at: {self.l_infos.as_posix()}')
else:
self.report({'WARNING'}, f'No custom data to write with those objects/layers')
return {"CANCELLED"}
return {"FINISHED"}
class GPEXP_OT_layers_state(bpy.types.Operator):
bl_idname = "gp.layers_state"
bl_label = "Set Layers State"
bl_description = "Display state of layer that migh need adjustement"
bl_options = {"REGISTER"} # , "UNDO"
# clear_unused_view_layers :BoolProperty(name="Clear unused view layers",
# description="Delete view layer that aren't used in the nodetree anymore",
# default=True)
all_objects : BoolProperty(name='On All Object',
default=True, description='On All object, else use selected objects') # , options={'SKIP_SAVE'}
set_full_opacity : BoolProperty(name='Set Full Opacity',
default=True, description='Check/Set full opacity') # , options={'SKIP_SAVE'}
set_use_lights : BoolProperty(name='Disable Use Light',
default=True, description='Check/Set use lights disabling') # , options={'SKIP_SAVE'}
set_blend_mode : BoolProperty(name='Set Regular Blend Mode',
default=True, description='Check/Set blend mode to regular') # , options={'SKIP_SAVE'}
clear_frame_out_of_range : BoolProperty(name='Clear Frames Out Of Scene Range',
default=False, description='Delete frames that before scene start and after scene end range\nWith a tolerance of one frame to avoid problem\nAffect all layers)') # , options={'SKIP_SAVE'}
opacity_exclude_list : StringProperty(name='Skip',
default='MA', description='Skip prefixes from this list when changing opacity\nSeparate multiple value with a comma (ex: MA,IN)') # , options={'SKIP_SAVE'}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def invoke(self, context, event):
## if no existing infos.json generated, call ops
l_infos = Path(bpy.data.filepath).parent / 'render' / 'infos.json'
if not l_infos.exists(): # only if infos not created
bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT')
# return self.execute(context)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, 'all_objects')
total = len([o for o in context.scene.objects if o.type == 'GPENCIL'])
target_num = total if self.all_objects else len([o for o in context.selected_objects if o.type == 'GPENCIL'])
layout.label(text=f'{target_num}/{total} targeted GP')
layout.separator()
layout.prop(self, 'clear_frame_out_of_range')
layout.separator()
layout.label(text='Set (or only perform a check):')
row = layout.row()
row.prop(self, 'set_full_opacity')
if self.set_full_opacity:
row.prop(self, 'opacity_exclude_list')
layout.prop(self, 'set_use_lights')
layout.prop(self, 'set_blend_mode')
# layout.prop(self, 'clear_unused_view_layers')
def execute(self, context):
if self.all_objects:
pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
else:
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
# pool = [context.object]
changes = []
for ob in pool:
changes.append(f'>> {ob.name}')
layers = ob.data.layers
if self.clear_frame_out_of_range:
ct = fn.clear_frame_out_of_range(ob, verbose=False)
if ct:
changes.append(f'{ct} out of range frame deleted')
for l in layers:
used = False
## mask check
# if l.mask_layers:
# print(f'-> masks')
# state = '' if l.use_mask_layer else ' (disabled)'
# print(f'{ob.name} > {l.info}{state}:')
# used = True
# for ml in l.mask_layers:
# mlstate = ' (disabled)' if ml.hide else ''
# mlinvert = ' <>' if ml.invert else ''
# print(f'{ml.info}{mlstate}{mlinvert}')
if l.opacity != 1:
# TODO Skip zeroed opacity ?
# check if there is an exclusion word
if any(x.strip() + '_' in l.info for x in self.opacity_exclude_list.strip(',').split(',') if x):
print(f'Skipped layer : {l.info}')
else:
full_opacity_state = '' if self.set_full_opacity else ' (check only)'
mess = f'{l.info} : opacity {l.opacity:.2f} >> 1.0{full_opacity_state}'
print(mess)
changes.append(mess)
if self.set_full_opacity:
l.opacity = 1.0
used = True
if l.use_lights:
use_lights_state = '' if self.set_use_lights else ' (check only)'
mess = f'{l.info} : disable use lights{use_lights_state}'
print(mess)
# changes.append(mess) # don't report disable use_light... too many messages
if self.set_use_lights:
l.use_lights = False
used = True
if l.blend_mode != 'REGULAR':
blend_mode_state = '' if self.set_blend_mode else ' (check only)'
mess = f'{l.info} : blend mode "{l.blend_mode}" >> regular{blend_mode_state}'
print(mess)
changes.append(mess)
if self.set_blend_mode:
l.blend_mode = 'REGULAR'
used = True
if len(l.frames) == 1 and len(l.frames[0].strokes) == 0 and not l.hide:
# probably used as separator
l.hide = True
mess = f'{l.info} : No frames. Hiding layer'
print(mess)
changes.append(mess)
used = True
if used:
print()
if changes:
changes.append('')
## disable multiframe editing on all GP (can cause artifacts on render)
gp_mu_edit_ct = 0
for gp in bpy.data.grease_pencils:
if gp.use_multiedit:
print(f'Disabling multi-edit on GP {gp.name}')
gp.use_multiedit = False
gp_mu_edit_ct += 1
if gp_mu_edit_ct:
changes.append(f'{gp_mu_edit_ct} multiframe-edit mode disabled')
fn.show_message_box(_message=changes, _title="Layers Check Report", _icon='INFO')
# render = bpy.data.scenes.get('Render')
# if not render:
# print('SKIP, no Render scene')
# return {"CANCELLED"}
return {"FINISHED"}
class GPEXP_OT_lower_layers_name(bpy.types.Operator):
bl_idname = "gp.lower_layers_name"
bl_label = "Normalize Layers Name"
bl_description = "Make the object and layers name lowercase with dashed converted to underscore (without touching layer prefix and suffix)"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
all_objects : BoolProperty(name='On All Object',
default=True, description='On All object, else use selected objects') # , options={'SKIP_SAVE'}
object_name : BoolProperty(name='Normalize Object Name',
default=True, description='Make the object name lowercase') # , options={'SKIP_SAVE'}
layer_name : BoolProperty(name='Normalize Layers Names',
default=True, description='Make the layers name lowercase') # , options={'SKIP_SAVE'}
# dash_to_undescore : BoolProperty(name='Dash To Underscore',
# default=True, description='Make the layers name lowercase') # , options={'SKIP_SAVE'}
def invoke(self, context, event):
# self.ctrl=event.ctrl
# self.alt=event.alt
if event.alt:
self.all_objects=True
# return self.execute(context)
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, 'all_objects')
if self.all_objects:
gp_ct = len([o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)])
else:
gp_ct = len([o for o in context.selected_objects if o.type == 'GPENCIL'])
layout.label(text=f'{gp_ct} to lower-case')
layout.separator()
layout.label(text=f'Choose what to rename:')
layout.prop(self, 'object_name')
layout.prop(self, 'layer_name')
# if self.layer_name:
# box = layout.box()
# box.prop(self, 'dash_to_undescore')
if not self.object_name and not self.layer_name:
layout.label(text=f'At least one choice!', icon='ERROR')
def execute(self, context):
if self.all_objects:
pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
else:
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
for ob in pool:
if self.object_name:
rename_data = ob.name == ob.data.name
ob.name = ob.name.lower().replace('-', '_')
if rename_data:
ob.data.name = ob.name
if self.layer_name:
for l in ob.data.layers:
# if self.dash_to_undescore:
l.info = l.info.replace('-', '_')
fn.normalize_layer_name(l) # default : lower=True, dash_to_underscore=self.dash_to_undescore
return {"FINISHED"}
class GPEXP_OT_auto_number_object(bpy.types.Operator):
bl_idname = "gp.auto_number_object"
bl_label = "Auto Number Object"
bl_description = "Automatic prefix number based on origin distance to camera and in_front values\nCtrl + Clic to delete name to delete numbering"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
all_objects : BoolProperty(name='On All GP Object',
default=True, description='On All object, else use selected Grease Pencil objects') # , options={'SKIP_SAVE'}
rename_data : BoolProperty(name='Rename Gpencil Data',
default=True, description='Rename Also the Grease Pencil data using same name as object') # , options={'SKIP_SAVE'}
delete : BoolProperty(default=False, options={'SKIP_SAVE'})
def invoke(self, context, event):
# if event.alt:
# self.all_objects=True
if event.ctrl or self.delete:
regex_num = re.compile(r'^(\d{3})_')
ct = 0
gps = [o for o in context.selected_objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
for o in gps:
if regex_num.match(o.name):
o.name = o.name[4:]
ct += 1
self.report({'INFO'}, f'{ct}/{len(gps)} number prefix removed from object names')
return {"FINISHED"}
return context.window_manager.invoke_props_dialog(self)
def draw(self, context):
layout = self.layout
layout.prop(self, 'all_objects')
if self.all_objects:
gp_ct = len([o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)])
else:
gp_ct = len([o for o in context.selected_objects if o.type == 'GPENCIL'])
layout.prop(self, 'rename_data')
layout.label(text=f'{gp_ct} objects to renumber')
if not gp_ct:
layout.label(text='No Gpencil object to renumber', icon = 'ERROR')
def execute(self, context):
if self.all_objects:
pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
else:
pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
def reversed_enumerate(collection: list):
for i in range(len(collection)-1, -1, -1):
yield i, collection[i]
fronts = []
## separate In Front objects:
for i, o in reversed_enumerate(pool):
if o.show_in_front:
fronts.append(pool.pop(i))
cam_loc = context.scene.camera.matrix_world.to_translation()
# filter by distance to camera object (considering origins)
pool.sort(key=lambda x: math.dist(x.matrix_world.to_translation(), cam_loc))
fronts.sort(key=lambda x: math.dist(x.matrix_world.to_translation(), cam_loc))
# re-insert fitlered infront object before others
pool = fronts + pool
ct = 10
regex_num = re.compile(r'^(\d{3})_')
for o in pool:
renum = regex_num.search(o.name)
if not renum:
o.name = f'{str(ct).zfill(3)}_{o.name}'
else:
## either replace or leave untouched
# continue
o.name = f'{str(ct).zfill(3)}_{o.name[4:]}'
ct += 10
if self.rename_data and o.name != o.data.name:
o.data.name = o.name
return {"FINISHED"}
class GPEXP_OT_check_masks(bpy.types.Operator):
bl_idname = "gp.check_masks"
bl_label = "Check Masks"
bl_description = "Check and report all masked GP layers"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
# if self.all_objects:
# pool = [o for o in context.scene.objects if o.type == 'GPENCIL']
# else:
# pool = [o for o in context.selected_objects if o.type == 'GPENCIL']
changes = []
pool = [o for o in context.scene.objects if o.type == 'GPENCIL' and fn.is_valid_name(o.name)]
for o in pool:
for l in o.data.layers:
if l.use_mask_layer:
obj_stat = f'{o.name} >>'
if not obj_stat in changes:
changes.append(obj_stat)
print(obj_stat)
hide_state = ' (hided)' if l.hide else ''
text = f' {l.info}{hide_state}:' # :masks:
changes.append(text)
print(text)
has_masks = False
for ml in l.mask_layers:
# 'hide', 'invert', 'name'
h = ' hided' if ml.hide else ''
i = ' (inverted)' if ml.invert else ''
text = f' - {ml.name}{h}{i}'
changes.append(text)
print(text)
has_masks = True
if not has_masks:
text = 'No masks!'
changes.append(text)
print(text)
changes.append('')
if changes:
fn.show_message_box(_message=changes, _title="Masks Check Report", _icon='INFO')
else:
fn.show_message_box(_message='No Masks!', _title="Masks Check Report", _icon='INFO')
return {"FINISHED"}
class GPEXP_OT_select_layer_in_comp(bpy.types.Operator):
bl_idname = "gp.select_layer_in_comp"
bl_label = "Select Layer In Compositor"
bl_description = "Select associated render_layer node in compositing"
bl_options = {"REGISTER", "UNDO"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
def execute(self, context):
gp = context.object.data
act = gp.layers.active
pool = fn.build_layers_targets_from_dopesheet(context)
if not pool:
self.report({'ERROR'}, 'No layers found in current GP dopesheet')
return {"CANCELLED"}
if not context.scene.node_tree:
self.report({'ERROR'}, 'No compo node-tree in active scene')
return {"CANCELLED"}
nodes = context.scene.node_tree.nodes
rl_nodes = [n for n in nodes if n.type == 'R_LAYERS']
if not rl_nodes:
self.report({'ERROR'}, 'No render layers nodes in active scene')
return {"CANCELLED"}
used_vl = [n.layer for n in rl_nodes]
selected = []
infos = []
for l in pool:
if not l.select:
continue
vl_name = l.viewlayer_render
if not vl_name:
mess = f'{l.info} has no viewlayers'
print(mess)
infos.append(mess)
continue
if not context.scene.view_layers.get(vl_name):
mess = f'/!\ {l.info}: view layer "{vl_name}" does not exists '
print(mess)
infos.append(mess)
continue
if not vl_name in used_vl:
mess = f'{l.info}: view layer "{vl_name}" not used in scene renderlayer nodes'
print(mess)
infos.append(mess)
continue
for n in rl_nodes:
if n.layer == vl_name:
print(f'{l.info} -> Select node {n.name}')
selected.append(n.name)
n.select = True
if not infos and not selected:
self.report({'ERROR'}, 'Nothing selected')
return {"CANCELLED"}
infos = infos + [f'-- Selected {len(selected)} nodes --'] + selected
fn.show_message_box(_message=infos, _title="Selected viewlayer in compo", _icon='INFO')
return {"FINISHED"}
classes=(
GPEXP_OT_auto_number_object,
GPEXP_OT_lower_layers_name,
GPEXP_OT_export_infos_for_compo,
GPEXP_OT_layers_state,
GPEXP_OT_check_masks,
GPEXP_OT_select_layer_in_comp,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)