diff --git a/CHANGELOG.md b/CHANGELOG.md index 952a91e..b9317db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,14 @@ Activate / deactivate layer opaticty according to prefix Activate / deactivate all masks using MA layers --> +0.3.0 + +- fix: viewlayer exclude attribution error +- fix: force PNG 8bit 15% compression for output settings +- change: GP dopesheet merge ops -> viewlayer merge instead of creating alphaover node +- feat: batch to_lower name +- feat: copy active output node format to selected + 0.2.8 - fix: added AA nodegroup diff --git a/OP_add_layer.py b/OP_add_layer.py index 806a16b..24c3b73 100644 --- a/OP_add_layer.py +++ b/OP_add_layer.py @@ -27,7 +27,7 @@ class GPEXP_OT_add_layer_to_render(bpy.types.Operator): if not l.select: if not l.viewlayer_render: # TODO : need to link, can reaise error if object is not linked in Render scene yet - l.viewlayer_render == fn.get_view_layer('exclude') + l.viewlayer_render == fn.get_view_layer('exclude').name continue gen_vlayer.get_set_viewlayer_from_gp(ob, l) @@ -54,7 +54,7 @@ def export_gp_objects(oblist, exclude_list=[]): # if l.hide: # continue if l.hide or any(x + '_' in l.info for x in exclude_list): # exclude hided ? - l.viewlayer_render = fn.get_view_layer('exclude') # assign "exclude" + l.viewlayer_render = fn.get_view_layer('exclude').name # assign "exclude" continue _vl, _cp = gen_vlayer.get_set_viewlayer_from_gp(ob, l) # scene=fn.get_render_scene()) diff --git a/OP_manage_outputs.py b/OP_manage_outputs.py index 4030767..9df1d8e 100644 --- a/OP_manage_outputs.py +++ b/OP_manage_outputs.py @@ -22,8 +22,48 @@ class GPEXP_OT_mute_toggle_output_nodes(bpy.types.Operator): self.report({"INFO"}, f'{ct} nodes {state}') 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): + # scene = bpy.data.scenes.get('Render') + 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 + color_mode = ref.format.color_mode + file_format = ref.format.file_format + color_depth = ref.format.color_depth + compression = ref.format.compression + + ct = 0 + for n in context.scene.node_tree.nodes: + if n.type != 'OUTPUT_FILE' or n == ref: + continue + + n.format.color_mode = color_mode + n.format.file_format = file_format + 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"} + + classes=( GPEXP_OT_mute_toggle_output_nodes, +GPEXP_OT_set_output_node_format, ) def register(): diff --git a/OP_merge_layers.py b/OP_merge_layers.py index f297a2f..2eda9b8 100644 --- a/OP_merge_layers.py +++ b/OP_merge_layers.py @@ -95,6 +95,7 @@ def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None) # create dedicated fileout out = fn.create_node('CompositorNodeOutputFile', tree=node_tree, location=(ng.location[0]+450, ng.location[1]+50), width=600) + fn.set_file_output_format(out) out_name = f'merge_OUT_{vl_name}' # or get output from frame out.name = out_name out.base_path = base_path @@ -111,9 +112,90 @@ def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None) return ng, out +class GPEXP_OT_merge_viewlayers_to_active(bpy.types.Operator): + bl_idname = "gp.merge_viewlayers_to_active" + bl_label = "Merge selected layers view_layers" + bl_description = "Merge view layers of selected gp layers to on the active one" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return context.object and context.object.type == 'GPENCIL' + + def execute(self, context): + ob = bpy.context.object + # layers = [l for l in ob.data.layers if l.select and not l.hide] + act = ob.data.layers.active + layers = [l for l in ob.data.layers if l.select and l != act] + + if not act.viewlayer_render: + self.report({'ERROR'}, f'Active layer {act.info} has no viewlayer assigned') + return {'CANCELLED'} + + rd_scn = bpy.data.scenes.get('Render') + if not rd_scn: + self.report({'ERROR'}, 'Viewlayers needs to be generated first!') + return {'CANCELLED'} + + # list layers and viewlayers + vls = [rd_scn.view_layers.get(l.viewlayer_render) for l in layers + if l.viewlayer_render and l.viewlayer_render != act.viewlayer_render and rd_scn.view_layers.get(l.viewlayer_render)] + + vl_names = [v.name for v in vls] + + for n in reversed(rd_scn.node_tree.nodes): + if n.type == 'R_LAYERS' and n.layer in vl_names: + for lnk in n.outputs[0].links: + grp = lnk.to_node + if grp.type != 'GROUP': + continue + if not grp.name.startswith('NG'): + continue + sockin = lnk.to_socket + sockout = grp.outputs.get(sockin.name) + if not sockout: + continue + + for grplink in sockout.links: + if grplink.to_node.type != 'OUTPUT_FILE': + continue + fo_socket = grplink.to_socket + fo = grplink.to_node + fo.file_slots.remove(fo_socket) + + # remove input and output from group + # grp.inputs.remove(sockin) # do not clear inside !! + # grp.outputs.remove(sockout) # do not clear inside !! + ngroup = grp.node_tree + for i in range(len(grp.inputs))[::-1]: + if grp.inputs[i].name == sockin.name: + ngroup.inputs.remove(ngroup.inputs[i]) + break + for i in range(len(grp.outputs))[::-1]: + if grp.outputs[i].name == sockout.name: + ngroup.outputs.remove(ngroup.outputs[i]) + break + + # remove render_layer node + rd_scn.node_tree.nodes.remove(n) + + # assign view layer from active to selected + for l in layers: + l.viewlayer_render = act.viewlayer_render + + ## delete unused_vl + + # used_vl_name = [n.layer for n in rd_scn.node_tree.nodes if n.type == 'R_LAYERS' and n.layer] + for vl in vls: + rd_scn.view_layers.remove(vl) + # if not vl.name in used_vl_name: + # rd_scn.view_layers.remove(vl) + + return {"FINISHED"} + class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): bl_idname = "gp.merge_selected_dopesheet_layers" - bl_label = "Merge selected layers view_layers " + bl_label = "Merge selected layers nodes" bl_description = "Merge view layers of selected gp layers to a new dedicated file output" bl_options = {"REGISTER"} @@ -124,10 +206,10 @@ class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): disconnect : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) def execute(self, context): - # merge_selected_layers() # function to merge from GP dopesheet ob = bpy.context.object layers = [l for l in ob.data.layers if l.select and not l.hide] act = ob.data.layers.active + # merge_selected_layers() # function to merge from GP dopesheet if not act: self.report({'ERROR'}, f'An active layer is needed to set merge output name') return {"CANCELLED"} @@ -199,14 +281,16 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator): if not all(selection[0].layer.split('.')[0] == n.layer.split('.')[0] for n in selection): print('/!\ Merge -> Not every nodes start with the same object') - # obname = selection[0].layer.split('.')[0] - merge_layers(selection, active=nodes.active, disconnect=self.disconnect) + color = None + if nodes.active.use_custom_color and nodes.active.color: + color = nodes.active.color + merge_layers(selection, active=nodes.active, disconnect=self.disconnect, color=color) return {"FINISHED"} - classes=( -GPEXP_OT_merge_selected_dopesheet_layers, +GPEXP_OT_merge_viewlayers_to_active, +GPEXP_OT_merge_selected_dopesheet_layers,# unused GPEXP_OT_merge_selected_viewlayer_nodes, ) diff --git a/OP_setup_layers.py b/OP_setup_layers.py index 0a5071c..242ee33 100644 --- a/OP_setup_layers.py +++ b/OP_setup_layers.py @@ -120,9 +120,119 @@ class GPEXP_OT_layers_state(bpy.types.Operator): # return {"CANCELLED"} return {"FINISHED"} + + +PATTERN = r'^(?P-\s)?(?P[A-Z]{2}_)?(?P.*?)(?P_[A-Z]{2})?(?P\.\d{3})?$' # numering +def lower_layer_name(layer, prefix='', desc='', suffix=''): + '''GET a layer and argumen to build and assign name''' + import re + + name = layer.info + pattern = PATTERN + sep = '_' + res = re.search(pattern, name.strip()) + + + grp = '' if res.group('grp') is None else res.group('grp') + tag = '' if res.group('tag') is None else res.group('tag') + # tag2 = '' if res.group('tag2') is None else res.group('tag2') + name = '' if res.group('name') is None else res.group('name') + sfix = '' if res.group('sfix') is None else res.group('sfix') + inc = '' if res.group('inc') is None else res.group('inc') + + if grp: + grp = ' ' + grp # name is strip(), so grp first spaces are gones. + + if prefix: + if prefix == 'prefixkillcode': + tag = '' + else: + tag = prefix.upper().strip() + sep + # if prefix2: + # tag2 = prefix2.upper().strip() + sep + if desc: + name = desc + + if suffix: + if suffix == 'suffixkillcode': + sfix = '' + else: + sfix = sep + suffix.upper().strip() + + # check if name is available without the increment ending + new = f'{grp}{tag}{name.lower()}{sfix}' # lower suffix ? + + if new != layer.info: + print(f'{layer.info} >> new') + layer.info = new + +class GPEXP_OT_lower_layers_name(bpy.types.Operator): + bl_idname = "gp.lower_layers_name" + bl_label = "Lower Layers Name" + bl_description = "Make the layer name lowercase without touching 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=False, description='On All object, else use selected objects') # , options={'SKIP_SAVE'} + + object_name : BoolProperty(name='Lower Object Name', + default=True, description='Make the object name lowercase') # , options={'SKIP_SAVE'} + + layer_name : BoolProperty(name='Lower Layers Names', + 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']) + 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 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'] + 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() + if rename_data: + ob.data.name = ob.data.name.lower() + + if self.layer_name: + for l in ob.data.layers: + lower_layer_name(l) + + return {"FINISHED"} + classes=( GPEXP_OT_layers_state, +GPEXP_OT_lower_layers_name ) def register(): diff --git a/__init__.py b/__init__.py index 33c4d4e..310566d 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": (0, 2, 8), + "version": (0, 3, 0), "blender": (2, 93, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index 14f920d..7febb47 100644 --- a/fn.py +++ b/fn.py @@ -37,22 +37,27 @@ def create_aa_nodegroup(tree): ng_in = create_node('NodeGroupInput', tree=ngroup, location=(-600,0)) ng_out = create_node('NodeGroupOutput', tree=ngroup, location=(600,0)) - sep = create_node('CompositorNodeSepRGBA', tree=ngroup, location=(-300,0)) - comb = create_node('CompositorNodeCombRGBA', tree=ngroup, location=(200,25)) - ngroup.links.new(ng_in.outputs[0], sep.inputs[0]) + sep = create_node('CompositorNodeSepRGBA', tree=ngroup, location=(-150,0)) + comb = create_node('CompositorNodeCombRGBA', tree=ngroup, location=(350,25)) + + # in AA + # ngroup.links.new(comb.outputs[0], ng_out.inputs[0]) # <- connect without out AA + aa = new_aa_node(ngroup, location=(-400, 0)) + # ngroup.links.new(ng_in.outputs[0], sep.inputs[0]) + ngroup.links.new(ng_in.outputs[0], aa.inputs[0]) + ngroup.links.new(aa.outputs[0], sep.inputs[0]) + + # ngroup.links.new(ng_in.outputs[0], sep.inputs[0]) for i in range(3): ngroup.links.new(sep.outputs[i], comb.inputs[i]) # alpha AA - alpha_aa = new_aa_node(ngroup, location=(-50,-150)) + alpha_aa = new_aa_node(ngroup, location=(100,-150)) ngroup.links.new(sep.outputs[3], alpha_aa.inputs[0]) ngroup.links.new(alpha_aa.outputs[0], comb.inputs[3]) + + ngroup.links.new(comb.outputs[0], ng_out.inputs[0]) - # outpout AA (maybe externalize ?) - # ngroup.links.new(comb.outputs[0], ng_out.inputs[0]) # <- connect without out AA - aa = new_aa_node(ngroup, location=(380, 0)) - ngroup.links.new(comb.outputs[0], aa.inputs[0]) - ngroup.links.new(aa.outputs[0], ng_out.inputs[0]) ng = create_node('CompositorNodeGroup', tree=tree) @@ -88,6 +93,12 @@ def copy_settings(obj_a, obj_b): pass +def set_file_output_format(fo): + fo.format.color_mode = 'RGBA' + fo.format.file_format = 'PNG' + fo.format.color_depth = '8' + fo.format.compression = 15 + def set_settings(scene=None): if not scene: scene = bpy.context.scene @@ -408,7 +419,7 @@ def nodegroup_merge_inputs(ngroup): prev = ao ## create a merged name as output ?? - aa = new_aa_node(ngroup) + aa = create_aa_nodegroup(ngroup) # new_aa_node(ngroup) aa.location = (ao.location.x + 200, ao.location.y) ngroup.links.new(ao.outputs[0], aa.inputs[0]) # node_tree diff --git a/gen_vlayer.py b/gen_vlayer.py index 6fdb375..42cdc40 100644 --- a/gen_vlayer.py +++ b/gen_vlayer.py @@ -181,6 +181,7 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): if not out: # color = (0.2,0.3,0.5) out = fn.create_node('CompositorNodeOutputFile', tree=scene.node_tree, location=(fn.real_loc(ng)[0]+500, fn.real_loc(ng)[1]+50), width=600) # =(ng.location[0]+600, ng.location[1]+50) + fn.set_file_output_format(out) out.name = out_name out.parent = frame out.base_path = f'//render/{bpy.path.clean_name(obname)}' diff --git a/ui.py b/ui.py index ac2d255..a8f6a0d 100644 --- a/ui.py +++ b/ui.py @@ -60,6 +60,9 @@ class GPEXP_PT_gp_node_ui(Panel): subcol.operator('gp.number_outputs', icon='LINENUMBERS_ON', text=txt).mode = 'SELECTED' # col.operator('gp.number_outputs', icon='LINENUMBERS_ON', text='Renumber all outputs').mode = 'ALL' + subcol.operator('gp.set_output_node_format', icon='OUTPUT', text='Copy Active Output Format') + + layout.separator() col=layout.column() @@ -91,6 +94,8 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): row = layout.row() row.label(text=f'Multiple users ({context.object.data.users})', icon='ERROR') row.operator("wm.call_menu", text="", icon='QUESTION').name = "GPEXP_MT_multi_user_doc" + layout.label(text=f'viewlayer: {context.object.data.layers.active.viewlayer_render}') + ## On layers if context.object and context.object.type == 'GPENCIL': @@ -105,7 +110,7 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): ct = len([l for l in context.object.data.layers if l.select]) txt = f'Merge {ct} layers' # merge layers from dopesheet - row.operator('gp.merge_selected_dopesheet_layers', text=txt, icon='SELECT_EXTEND') + row.operator('gp.merge_viewlayers_to_active', text=txt, icon='SELECT_EXTEND') row.enabled= ct > 1 @@ -121,6 +126,7 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): layout.separator() layout.operator('gp.layers_state', icon='CHECKMARK', text='Check layers') + layout.operator('gp.lower_layers_name', icon='SYNTAX_OFF', text='Rename Lowercase') # row = layout.row() layout.prop(bpy.context.preferences.edit, 'use_anim_channel_group_colors')