diff --git a/.gitignore b/.gitignore index 17ae4fe..244aaee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ __pycache__ -*.py[cod] \ No newline at end of file +*.py[cod] +pyrightconfig.json +*.vscode \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d5235c6..55a6458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ Activate / deactivate layer opacity according to prefix Activate / deactivate all masks using MA layers --> +1.7.0 + +- fix: problem when removing render layers +- changed: node distribution refactor, allow separate compositing scene + - Compositing scene (holding nodes) can be separated from render scene (holding GP objects and related viewlayers) + - Default render named changed from `Render` to `RenderGP` + - New properties in exposed Dopesheet N panel to manually set Render scene and Compo scene + - Operator expose a `node_scene` parameter to separate where to send nodes + - Switch scene button can have an extra button to go in compo scene if found + 1.6.4 - changed: remove poll checking gp active on some operators limiting use in script call diff --git a/OP_add_layer.py b/OP_add_layer.py index 103de88..5d1def0 100644 --- a/OP_add_layer.py +++ b/OP_add_layer.py @@ -1,6 +1,40 @@ import bpy from . import gen_vlayer, fn + +def add_layer_to_render(ob, node_scene=None): + '''Send GP object to render layer + return a tuple with report message''' + + # ob = ob or bpy.context.object + layer = ob.data.layers.active + if not layer: + return ('ERROR', 'No active layer') + + node_scene = fn.get_compo_scene(scene_name=node_scene, create=True) + + ct = 0 + # send scene ? + hidden = 0 + for l in ob.data.layers: + if not l.select: + if not l.viewlayer_render: + # TODO : need to link, can raise error if object is not linked in Render scene yet + l.viewlayer_render = fn.get_view_layer('exclude').name + continue + + gen_vlayer.get_set_viewlayer_from_gp(ob, l, node_scene=node_scene) + + if l.hide: + hidden += 1 + ct += 1 + + if hidden: + return ('WARNING', f'{hidden}/{ct} layers are hidden!') + + else: + return ('INFO', f'{ct} layer(s) added') + class GPEXP_OT_add_layer_to_render(bpy.types.Operator): bl_idname = "gp.add_layer_to_render" bl_label = "Add Gp Layer as render nodes" @@ -12,73 +46,76 @@ class GPEXP_OT_add_layer_to_render(bpy.types.Operator): return context.object and context.object.type == 'GPENCIL' # mode : bpy.props.StringProperty(options={'SKIP_SAVE'}) + node_scene : bpy.props.StringProperty(default='', + description='Scene where to add nodes, Abort if not found', options={'SKIP_SAVE'}) def execute(self, context): - ob = context.object - layer = ob.data.layers.active - if not layer: - self.report({'ERROR'}, 'No active layer') - return {"CANCELLED"} - - ct = 0 - # send scene ? - hided = 0 - for l in ob.data.layers: - if not l.select: - if not l.viewlayer_render: - # TODO : need to link, can raise error if object is not linked in Render scene yet - l.viewlayer_render = fn.get_view_layer('exclude').name - continue - gen_vlayer.get_set_viewlayer_from_gp(ob, l) - - if l.hide: - hided += 1 - ct += 1 - - if hided: - self.report({'WARNING'}, f'{hided}/{ct} layers are hided !') - - else: - self.report({'INFO'}, f'{ct} layer(s) added to scene "Render"') - + ret = add_layer_to_render(context.object, node_scene=self.node_scene) + if isinstance(ret, tuple): + self.report({ret[0]}, ret[1]) + if ret[0] == 'ERROR': + return {'CANCELLED'} return {"FINISHED"} + +def add_object_to_render(mode='ALL', scene='', node_scene=''): + context = bpy.context + + if scene: + scn = fn.get_render_scene(scene) + else: + scn = fn.get_render_scene() + + if node_scene: + node_scn = fn.get_compo_scene(scene_name=node_scene, create=True) + if not node_scn: + return ('ERROR', f'/!\ Node Scene "{node_scene}" not found ! Abort "Add object to Render" !') + else: + # if not passed add in render scene + node_scn = scn + + excludes = [] # ['MA', 'IN'] # Get list dynamically + if mode == 'SELECTED': + gen_vlayer.export_gp_objects([o for o in context.selected_objects if o.type == 'GPENCIL'], exclude_list=excludes, scene=scn, node_scene=node_scn) + + elif mode == 'ALL': + gen_vlayer.export_gp_objects([o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)], exclude_list=excludes, scene=scn, node_scene=node_scn) + + ## send operator with mode ALL or SELECTED to batch build class GPEXP_OT_add_objects_to_render(bpy.types.Operator): bl_idname = "gp.add_object_to_render" bl_label = "Add all Gp Layer of active object as render nodes" bl_description = "Setup GP object in render scene\ - \nNote: 'send all' mode skip hided objects" + \nNote: 'send all' mode skip hidden objects" bl_options = {"REGISTER"} @classmethod def poll(cls, context): return context.object and context.object.type == 'GPENCIL' - mode : bpy.props.StringProperty(default='ALL', options={'SKIP_SAVE'}) + mode : bpy.props.EnumProperty( + items=( + ('ALL', 'All', 'All objects', 0), + ('SELECTED', 'Selected', 'Selected objects', 0), + ), + default='ALL', options={'SKIP_SAVE'}, + description='Choice to send all or only selected objects') + + scene : bpy.props.StringProperty(default='', + description='Scene where to link object and create viewlayer (create if not exists)', options={'SKIP_SAVE'}) + + node_scene : bpy.props.StringProperty(default='', + description='Scene where to add nodes, Abort if not found', options={'SKIP_SAVE'}) def execute(self, context): - # create render scene - if context.scene.name == 'Scene': - scn = fn.get_render_scene() - else: - scn = context.scene - - - excludes = [] # ['MA', 'IN'] # Get list dynamically - if self.mode == 'SELECTED': - gen_vlayer.export_gp_objects([o for o in context.selected_objects if o.type == 'GPENCIL'], exclude_list=excludes, scene=scn) - - elif self.mode == 'ALL': - # scn = bpy.data.scenes.get('Scene') - # if not scn: - # self.report({'ERROR'}, 'Could not found default scene') - # return {"CANCELLED"} - gen_vlayer.export_gp_objects([o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)], exclude_list=excludes, scene=scn) - + ret = add_object_to_render(mode=self.mode, scene=self.scene, node_scene=self.node_scene) + if isinstance(ret, tuple): + self.report({ret[0]}, ret[1]) + if ret[0] == 'ERROR': + return {'CANCELLED'} return {"FINISHED"} - class GPEXP_OT_split_to_scene(bpy.types.Operator): bl_idname = "gp.split_to_scene" bl_label = "Split Objects To Scene" diff --git a/OP_auto_build.py b/OP_auto_build.py index 56be3dc..7dd83c7 100644 --- a/OP_auto_build.py +++ b/OP_auto_build.py @@ -2,9 +2,10 @@ import bpy import re from pathlib import Path from . import gen_vlayer, fn +from bpy.props import (BoolProperty, StringProperty) -def batch_setup_render_scene(context=None, render_scn=None, preview=True): +def batch_setup_render_scene(context=None, render_scn=None, node_scene=None, preview=True): '''A series of setup actions for Render scene: - renumber fileout - Clean compo Tree @@ -15,10 +16,12 @@ def batch_setup_render_scene(context=None, render_scn=None, preview=True): if context is None: context = bpy.context if render_scn is None: - render_scn = bpy.data.scenes.get('Render') + render_scn = fn.get_render_scene(create=False) if not render_scn: - print('"Render" scene not found in batch_setup_render_scene') + print('Render scene not found in batch_setup_render_scene') return + if node_scene is None: + node_scene = render_scn ## Renumber File outputs print('Renumber File outputs') @@ -49,8 +52,7 @@ def batch_setup_render_scene(context=None, render_scn=None, preview=True): ## Clean compo Tree print('Clean compo Tree') - bpy.ops.gp.clean_compo_tree('EXEC_DEFAULT', use_render_scene=True) - # bpy.ops.gp.clean_compo_tree('INVOKE_DEFAULT', use_render_scene=True) + bpy.ops.gp.clean_compo_tree('EXEC_DEFAULT', scene=render_scn.name, node_scene=node_scene.name) ## Trigger check file before finishing ? # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') @@ -63,55 +65,57 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): bl_description = "Trigger all operation to make build render scene with default settings" bl_options = {"REGISTER"} - # @classmethod - # def poll(cls, context): - # return context.object and context.object.type == 'GPENCIL' + scene : StringProperty(name="Target Scene Name", + description="Render scene to send GP to on a named scene, abort if not exists (not exposed)", + default='', options={'SKIP_SAVE'}) - # timer : bpy.props.FloatProperty(default=0.1, options={'SKIP_SAVE'}) + node_scene : StringProperty(name="Compositing Scene", + description="Name of the scene holding compositing nodes", + default='', options={'SKIP_SAVE'}) - make_gp_single_user : bpy.props.BoolProperty(name='Set Single User Data', default=True, + make_gp_single_user : BoolProperty(name='Set Single User Data', default=True, description='Set single user on all objects GP data') - excluded_prefix : bpy.props.StringProperty( + excluded_prefix : StringProperty( name='Excluded Layer By Prefix', default='GP, RG, PO, MA', description='Exclude layer to send to render by prefix (comma separated list)') - clean_name_and_visibility : bpy.props.BoolProperty(name='Clean Name And Visibility', default=True, + clean_name_and_visibility : BoolProperty(name='Clean Name And Visibility', default=True, description='Add object name to layer name when there is only prefix (ex: "CO_")\ \nEnable visibility for layer with prefix included in Prefix Filter') - clean_material_duplication : bpy.props.BoolProperty(name='Clean Material Duplication', default=True, + clean_material_duplication : BoolProperty(name='Clean Material Duplication', default=True, description='Clean material stack. i.e: Replace "mat.001" in material stack if "mat" exists and has same color') - prefix_filter : bpy.props.StringProperty(name='Prefix Filter', default='CO, CU, FX, TO', # , MA # exclude MA if mask are applied + prefix_filter : StringProperty(name='Prefix Filter', default='CO, CU, FX, TO', # , MA # exclude MA if mask are applied description='Comma separated prefix to render. Set the other prefix and non-prefixed layer to exluded viewlayer') - set_layers_colors : bpy.props.BoolProperty(name='Set Layers Colors', default=True, + set_layers_colors : BoolProperty(name='Set Layers Colors', default=True, description='Set colors for on layers according to prefix (hadrcoded color set)') - trigger_rename_lowercase : bpy.props.BoolProperty(name='Trigger Rename Lowercase', default=True, + trigger_rename_lowercase : BoolProperty(name='Trigger Rename Lowercase', default=True, description='Rename all layer names lowercase') - trigger_renumber_by_distance : bpy.props.BoolProperty(name='Trigger Renumber By Distance', default=True, + trigger_renumber_by_distance : BoolProperty(name='Trigger Renumber By Distance', default=True, description='Renumber object accordind to distance from camera and In-Front value') - export_layer_infos : bpy.props.BoolProperty(name='Export Layer Infos', default=True, + export_layer_infos : BoolProperty(name='Export Layer Infos', default=True, description='Export layers infos to a Json file') - group_all_adjacent_layer_type : bpy.props.BoolProperty(name='Group All Adjacent Layer Type', default=False, + group_all_adjacent_layer_type : BoolProperty(name='Group All Adjacent Layer Type', default=False, description='Fuse output Viewlayer according to adjacent Prefix in layer stack') - change_to_gp_workspace : bpy.props.BoolProperty(name='Change To Gp Workspace', default=True, + change_to_gp_workspace : BoolProperty(name='Change To Gp Workspace', default=True, description='Switch to "GP Render" workspace shipped with addon') - batch_setup_render_scene : bpy.props.BoolProperty(name='Batch Setup Render Scene', default=True, + batch_setup_render_scene : BoolProperty(name='Batch Setup Render Scene', default=True, description='- Renumber fileoutputs\ \n- Clean compo Tree\ \n- Go to camera view in visible viewports\ \n- Swap to bg cam' ) - add_preview : bpy.props.BoolProperty(name='Add Preview', default=True, + add_preview : BoolProperty(name='Add Preview', default=True, description='Create preview with stacked alpha over on render layers') def invoke(self, context, event): @@ -153,10 +157,21 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): prefix_to_render = [p.strip() for p in self.prefix_filter.split(',')] print('prefix_to_render: ', prefix_to_render) - render_scn = bpy.data.scenes.get('Render') - if render_scn: - self.report({'ERROR'}, 'A "Render" scene already exists') - return {'CANCELLED'} + render_scn = fn.get_render_scene(create=False) + if self.scene: + render_scn = bpy.data.scenes.get(self.scene) + if render_scn: + self.report({'ERROR'}, f'Abort, scene "{render_scn.name}" already exists') + return {'CANCELLED'} + + if self.node_scene: + node_scene = fn.get_compo_scene(scene_name=self.node_scene, create=True) # create if not exists + ## Set scene target in source scene + context.scene.gp_render_settings.node_scene = node_scene.name + else: + node_scene = fn.get_compo_scene(create=True) + if not node_scene: + node_scene = render_scn all_gp_objects = [o for o in context.scene.objects if o.type == 'GPENCIL'] @@ -206,13 +221,11 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): if self.trigger_rename_lowercase: print('Trigger rename lowercase') bpy.ops.gp.lower_layers_name('EXEC_DEFAULT') - # bpy.ops.gp.lower_layers_name('INVOKE_DEFAULT') ## Trigger renumber by distance if self.trigger_renumber_by_distance: print('Trigger renumber by distance') bpy.ops.gp.auto_number_object('EXEC_DEFAULT') - # bpy.ops.gp.auto_number_object('INVOKE_DEFAULT') ## Export layer infos ? (skip if json already exists) if self.export_layer_infos: @@ -226,13 +239,13 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): o.data = o.data.copy() ## Send all GP to render scene - print('Send all GP to render scene') + print('Send all GP to render scene (Create render scene if needed)') # bpy.ops.gp.add_object_to_render(mode="ALL") # Ops to send all - gen_vlayer.export_gp_objects(ob_list, exclude_list=self.excluded_prefix) # Create render scene OTF + gen_vlayer.export_gp_objects(ob_list, exclude_list=self.excluded_prefix, scene=render_scn, node_scene=node_scene) # Create render scene OTF ## Switch to new Render Scene print('Switch to new Render Scene') - render_scn = bpy.data.scenes.get('Render') + render_scn = fn.get_render_scene(create=False) if not render_scn: self.report({'ERROR'}, 'No render scene found') return {'CANCELLED'} @@ -268,13 +281,13 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): # context.scene.update_tag() ## Batch setup render scene - batch_setup_render_scene(render_scn=render_scn) + batch_setup_render_scene(render_scn=render_scn, node_scene=node_scene) ## create preview if self.add_preview: from .OP_merge_layers import merge_compositor_preview - merge_compositor_preview(scene=render_scn) + merge_compositor_preview(scene=node_scene) ## No need for timer anymore ! # if batch_setup_render_scene: @@ -295,7 +308,6 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') ## Note: After all these operation, a ctrl+Z might crash - print('\nDone.\n') return {"FINISHED"} diff --git a/OP_check_scene.py b/OP_check_scene.py index a30d011..17ba1c0 100644 --- a/OP_check_scene.py +++ b/OP_check_scene.py @@ -18,7 +18,6 @@ def check_broken_modifier_target(pool=None, reports=None): reports.append(f'Broken modifier target :{o.name} > {m.name} > {m.layer}') # else: # print(f'Modifier target :{o.name} > {m.name} > ok') - return reports @@ -72,10 +71,19 @@ def check_file_output_numbering(reports=None): if not prenum.match(fo.base_path.split('/')[-1]): reports.append(f'No object numbering : node {fo.name}') pct = 0 - slots = fo.layer_slots if fo.format.file_format == 'OPEN_EXR_MULTILAYER' else fo.file_slots - for fs in slots: - if not prenum.match(fs.path.split('/')[0]): - pct += 1 + if fo.format.file_format == 'OPEN_EXR_MULTILAYER': + ## multilayer use layer_slots > slot.name + slots = fo.layer_slots + for fs in slots: + if not prenum.match(fs.name.split('/')[0]): + pct += 1 + else: + ## classic use file_slots > path + slots = fo.file_slots + for fs in slots: + if not prenum.match(fs.path.split('/')[0]): + pct += 1 + if pct: reports.append(f'{pct}/{len(slots)} slots not numbered: node {fo.name}') @@ -96,11 +104,11 @@ class GPEXP_OT_check_render_scene(bpy.types.Operator): def invoke(self, context, event): return self.execute(context) - return context.window_manager.invoke_props_dialog(self) + # return context.window_manager.invoke_props_dialog(self) - def draw(self, context): - layout = self.layout - # layout.prop(self, 'clear_unused_view_layers') + # def draw(self, context): + # layout = self.layout + # # layout.prop(self, 'clear_unused_view_layers') def execute(self, context): reports = [] diff --git a/OP_clean.py b/OP_clean.py index dcec00f..8a0e648 100644 --- a/OP_clean.py +++ b/OP_clean.py @@ -1,4 +1,5 @@ import bpy +from bpy.props import BoolProperty, StringProperty from . import fn class GPEXP_OT_clean_compo_tree(bpy.types.Operator): @@ -8,31 +9,35 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator): bl_options = {"REGISTER", "UNDO"} # Internal prop (use when launching from python) - use_render_scene : bpy.props.BoolProperty(name="Use Render Scene", - description="Force the clean on scene named Render, abort if not exists (not exposed)", - default=False, options={'SKIP_SAVE'}) + scene : StringProperty(name="Rener Scene", + description="Scene to clear node from", + default='', options={'SKIP_SAVE'}) - clear_unused_view_layers : bpy.props.BoolProperty(name="Clear unused view layers", + node_scene : StringProperty(name="Compositing Scene", + description="Scene containing nodes, using current scene when calling operator from UI to clean node", + default='', options={'SKIP_SAVE'}) + + clear_unused_view_layers : BoolProperty(name="Clear unused view layers", description="Delete view layer that aren't used in the nodetree anymore", default=True) - arrange_rl_nodes : bpy.props.BoolProperty(name="Arrange Render Node In Frames", + arrange_rl_nodes : BoolProperty(name="Arrange Render Node In Frames", description="Re-arrange Render Layer Nodes Y positions within each existing frames" , default=True) - arrange_frames : bpy.props.BoolProperty(name="Arrange Frames", + arrange_frames : BoolProperty(name="Arrange Frames", description="Re-arrange all frames Y positions" , default=True) - reorder_inputs : bpy.props.BoolProperty(name="Reorder I/O Sockets", + reorder_inputs : BoolProperty(name="Reorder I/O Sockets", description="Reorder inputs/outputs of all 'NG_' nodegroup and their connected file output", default=True) - clear_isolated_node_in_groups : bpy.props.BoolProperty(name="Clear Isolated Node In Groups", + clear_isolated_node_in_groups : BoolProperty(name="Clear Isolated Node In Groups", description="Clean content of 'NG_' nodegroup bpy deleting isolated nodes)", default=True) - fo_clear_disconnected : bpy.props.BoolProperty(name="Remove Disconnected Export Inputs", + fo_clear_disconnected : BoolProperty(name="Remove Disconnected Export Inputs", description="Clear any disconnected intput of every 'file output' node", default=False) @@ -62,29 +67,39 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator): # box.prop(self, 'fo_clear_disconnected') def execute(self, context): - if self.use_render_scene: - render = bpy.data.scenes.get('Render') - if not render: - print('SKIP clean_compo_tree, No "Render" scene !') + + if self.scene: + scn = bpy.data.scenes.get(self.scene) + if not scn: + print(f'SKIP clean_compo_tree, No "{self.scene}" render scene found!') return {"CANCELLED"} else: - render = context.scene + scn = fn.get_render_scene(create=False) + + if self.node_scene: + node_scene = bpy.data.scenes.get(self.node_scene) + if not scn: + print(f'SKIP clean_compo_tree, No "{self.node_scene}" compo scene found!') + return {"CANCELLED"} + else: + node_scene = context.scene - nodes = render.node_tree.nodes + nodes = node_scene.node_tree.nodes for n in nodes: n.update() + if self.clear_unused_view_layers: used_rlayer_names = [n.layer for n in nodes if n.type == 'R_LAYERS'] - for rl in reversed(render.view_layers): + for rl in reversed(scn.view_layers): if rl.name in used_rlayer_names or rl.name == 'View Layer': continue - render.view_layers.remove(rl) + scn.view_layers.remove(rl) if self.arrange_rl_nodes: - fn.rearrange_rlayers_in_frames(render.node_tree) + fn.rearrange_rlayers_in_frames(node_scene.node_tree) if self.arrange_frames: - fn.rearrange_frames(render.node_tree) + fn.rearrange_frames(node_scene.node_tree) if self.reorder_inputs: for n in nodes: diff --git a/OP_clear.py b/OP_clear.py index 11d5e59..762ef48 100644 --- a/OP_clear.py +++ b/OP_clear.py @@ -1,5 +1,6 @@ import bpy - +from . import fn +from .constant import RD_SCENE_NAME class GPEXP_OT_clear_render_tree(bpy.types.Operator): bl_idname = "gp.clear_render_tree" @@ -15,43 +16,47 @@ class GPEXP_OT_clear_render_tree(bpy.types.Operator): def execute(self, context): - render = bpy.data.scenes.get('Render') + render = fn.get_render_scene(create=False) if not render: - print('SKIP, no Render scene') + print(f'SKIP, no {RD_SCENE_NAME} scene') return {"CANCELLED"} - # clear all nodes in frames - if render.use_nodes: - for i in range(len(render.node_tree.nodes))[::-1]: + scn = context.scene - # skip frames to delete later - if render.node_tree.nodes[i].type == 'FRAME': + # Clear all nodes in frames + if scn.use_nodes: + for i in range(len(scn.node_tree.nodes))[::-1]: + + # Skip frames to delete later + if scn.node_tree.nodes[i].type == 'FRAME': continue - # skip unparented nodes - if not render.node_tree.nodes[i].parent: + # Skip unparented nodes + if not scn.node_tree.nodes[i].parent: continue - render.node_tree.nodes.remove(render.node_tree.nodes[i]) + scn.node_tree.nodes.remove(scn.node_tree.nodes[i]) - # delete all framesWorki - if render.use_nodes: - for i in range(len(render.node_tree.nodes))[::-1]: - if render.node_tree.nodes[i].type == 'FRAME': - render.node_tree.nodes.remove(render.node_tree.nodes[i]) + # Delete all frames + if scn.use_nodes: + for i in range(len(scn.node_tree.nodes))[::-1]: + if scn.node_tree.nodes[i].type == 'FRAME': + scn.node_tree.nodes.remove(scn.node_tree.nodes[i]) - # clear all view_layers - for vl in reversed(render.view_layers): - if ' / ' in vl.name: - render.view_layers.remove(vl) - - # clear all "NG_" nodegroups + # Clear all "NG_" nodegroups for ng in reversed(bpy.data.node_groups): if ng.name.startswith('NG_'): ng.use_fake_user = False bpy.data.node_groups.remove(ng) + if self.mode == 'COMPLETE': bpy.data.scenes.remove(render) + return {"FINISHED"} + + # Clear all view_layers + for vl in reversed(render.view_layers): + if ' / ' in vl.name: + render.view_layers.remove(vl) return {"FINISHED"} diff --git a/OP_connect_toggle.py b/OP_connect_toggle.py index 455f7a7..a254e3a 100644 --- a/OP_connect_toggle.py +++ b/OP_connect_toggle.py @@ -66,16 +66,17 @@ class GPEXP_OT_delete_render_layer(bpy.types.Operator): def poll(cls, context): return True - # mode : bpy.props.StringProperty(default='NORMAL', options={'SKIP_SAVE'}) + def execute(self, context): + ## Compositing scene where nodes lives + compo_scene = context.scene - def execute(self, context): - - rd_scn = bpy.data.scenes.get('Render') + ## render scene where viewlayer lives + rd_scn = fn.get_render_scene(create=False) if not rd_scn: - self.report({'ERROR'}, 'Viewlayers needs to be generated first!') + self.report({'ERROR'}, 'No render scene found') return {'CANCELLED'} - nodes = rd_scn.node_tree.nodes + nodes = compo_scene.node_tree.nodes # 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)] @@ -112,25 +113,44 @@ class GPEXP_OT_delete_render_layer(bpy.types.Operator): inside_nodes = [] 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]) + + if bpy.app.version < (4,0,0): + for i in range(len(grp.inputs))[::-1]: + if grp.inputs[i].name == sockin.name: + ngroup.inputs.remove(ngroup.inputs[i]) - gp_in_socket = ngroup.nodes['Group Input'].outputs[i] - for lnk in gp_in_socket.links: - inside_nodes += fn.all_connected_forward(lnk.to_node) - list(set(inside_nodes)) - break - for i in range(len(grp.outputs))[::-1]: - if grp.outputs[i].name == sockout.name: - ngroup.outputs.remove(ngroup.outputs[i]) - break + gp_in_socket = ngroup.nodes['Group Input'].outputs[i] + for lnk in gp_in_socket.links: + inside_nodes += fn.all_connected_forward(lnk.to_node) + list(set(inside_nodes)) + break + for i in range(len(grp.outputs))[::-1]: + if grp.outputs[i].name == sockout.name: + ngroup.outputs.remove(ngroup.outputs[i]) + break + else: + g_inputs = [s for s in ngroup.interface.items_tree if s.in_out == 'INPUT'] + g_outputs = [s for s in ngroup.interface.items_tree if s.in_out == 'OUTPUT'] + for i in range(len(grp.inputs))[::-1]: + if grp.inputs[i].name == sockin.name: + ngroup.interface.remove(g_inputs[i]) + + gp_in_socket = ngroup.nodes['Group Input'].outputs[i] + for lnk in gp_in_socket.links: + inside_nodes += fn.all_connected_forward(lnk.to_node) + list(set(inside_nodes)) + break + + for i in range(len(grp.outputs))[::-1]: + if grp.outputs[i].name == sockout.name: + ngroup.interface.remove(g_outputs[i]) + break for sub_n in reversed(inside_nodes): ngroup.nodes.remove(sub_n) - # remove render_layer node - rd_scn.node_tree.nodes.remove(n) + # Remove render_layer node + nodes.remove(n) return {"FINISHED"} diff --git a/OP_manage_outputs.py b/OP_manage_outputs.py index 6f149f8..8e53521 100644 --- a/OP_manage_outputs.py +++ b/OP_manage_outputs.py @@ -228,12 +228,12 @@ class GPEXP_OT_reset_render_settings(bpy.types.Operator): use_native_aa = False break - if scn.use_aa != use_native_aa: + 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['use_aa'] = use_native_aa + scn.gp_render_settings['use_aa'] = use_native_aa return {"FINISHED"} diff --git a/OP_merge_layers.py b/OP_merge_layers.py index 908400e..8182d26 100644 --- a/OP_merge_layers.py +++ b/OP_merge_layers.py @@ -9,6 +9,7 @@ from . import gen_vlayer # --> need to delete/mute AA internal node def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None): + '''merge render layers, node_tree is found using first render layer (with id_data)''' print(f'Merging {len(rlayers)} layers') print('->', [r.layer for r in rlayers]) @@ -111,7 +112,7 @@ def merge_layers(rlayers, obname=None, active=None, disconnect=True, color=None) # fn.clean_nodegroup_inputs(dg) # # fn.clear_nodegroup_content_if_disconnected(dg.node_tree) - bpy.context.scene.use_aa = False # trigger fn.scene_aa(toggle=False) + bpy.context.scene.gp_render_settings.use_aa = False # trigger fn.scene_aa(toggle=False) return ng, out @@ -420,20 +421,20 @@ class GPEXP_OT_merge_selected_dopesheet_layers(bpy.types.Operator): self.report({'ERROR'}, f'Should select multiple layers for merging') return {"CANCELLED"} - render = bpy.data.scenes.get('Render') - if render: - nodes = render.node_tree.nodes + rd_scene = fn.get_render_scene(create=False) + if rd_scene: + nodes = rd_scene.node_tree.nodes clean_ob_name = bpy.path.clean_name(ob.name) rlayers = [] for l in layers: idname = f'{clean_ob_name} / {l.info}' rlayer = rl = None - # check the render layer that have a parent frame - if not render: + # check the rd_scene layer that have a parent frame + if not rd_scene: _vl, rl = gen_vlayer.get_set_viewlayer_from_gp(ob, l) - render = bpy.data.scenes.get('Render') - nodes = render.node_tree.nodes + rd_scene = fn.get_render_scene(create=False) + nodes = rd_scene.node_tree.nodes if not rl: rlayer = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == idname and n.parent] @@ -467,16 +468,8 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator): disconnect : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) def execute(self, context): - if context.scene.name == 'Scene': - render = bpy.data.scenes.get('Render') - else: - render = context.scene - if not render: - self.report({'ERROR'}, 'No render scene') - return {"CANCELLED"} - - nodes = render.node_tree.nodes + nodes = context.scene.node_tree.nodes selection = [n for n in nodes if n.select and n.type == 'R_LAYERS'] if not nodes.active in selection: @@ -490,8 +483,8 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator): 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) + merge_layers(selection, active=nodes.active, disconnect=self.disconnect, color=color) return {"FINISHED"} classes=( diff --git a/OP_scene_switch.py b/OP_scene_switch.py index cc32725..0ca9cc1 100644 --- a/OP_scene_switch.py +++ b/OP_scene_switch.py @@ -1,5 +1,6 @@ import bpy from .import fn +from .constant import RD_SCENE_NAME class GPEXP_OT_render_scene_switch(bpy.types.Operator): bl_idname = "gp.render_scene_switch" @@ -11,7 +12,7 @@ class GPEXP_OT_render_scene_switch(bpy.types.Operator): def poll(cls, context): return True - # mode : bpy.props.StringProperty(default='NORMAL', options={'SKIP_SAVE'}) + scene : bpy.props.StringProperty(default='', options={'SKIP_SAVE'}) def execute(self, context): scenes = bpy.data.scenes @@ -19,7 +20,17 @@ class GPEXP_OT_render_scene_switch(bpy.types.Operator): self.report({'ERROR'},'No other scene to go to') return {"CANCELLED"} - if context.scene.name == 'Render': + ## Case where a scene name is passed + if self.scene: + scn = bpy.data.scenes.get(self.scene) + if not scn: + self.report({'ERROR'},f'No scene "{self.scene}"') + return {"CANCELLED"} + self.report({'INFO'},f'Switched to scene "{scn.name}"') + bpy.context.window.scene = scn + return {"FINISHED"} + + if context.scene.name == RD_SCENE_NAME: scn = scenes.get('Scene') if not scn: # get the next available scene self.report({'WARNING'},'No scene named "Scene"') @@ -27,12 +38,11 @@ class GPEXP_OT_render_scene_switch(bpy.types.Operator): scn = scenes[(slist.index(bpy.context.scene.name) + 1) % len(scenes)] else: - scn = scenes.get('Render') + scn = fn.get_render_scene(create=False) if not scn: - self.report({'ERROR'},'No "Render" scene yet') + self.report({'ERROR'},f'No "{RD_SCENE_NAME}" scene yet') return {"CANCELLED"} - self.report({'INFO'},f'Switched to scene "{scn.name}"') bpy.context.window.scene = scn return {"FINISHED"} diff --git a/OP_setup_layers.py b/OP_setup_layers.py index a9ce2c3..8404ae6 100644 --- a/OP_setup_layers.py +++ b/OP_setup_layers.py @@ -562,6 +562,11 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator): def poll(cls, context): return context.object and context.object.type == 'GPENCIL' + + def invoke(self, context, event): + self.additive = event.shift + return self.execute(context) + def execute(self, context): gp = context.object.data act = gp.layers.active @@ -574,12 +579,19 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator): self.report({'ERROR'}, 'No compo node-tree in active scene') return {"CANCELLED"} - nodes = context.scene.node_tree.nodes + scn = context.scene + node_scene = fn.get_compo_scene(create=False) or scn + nodes = node_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"} + # Deselect all nodes if shift is not pressed + if not self.additive: + for n in nodes: + n.select = False + used_vl = [n.layer for n in rl_nodes] selected = [] infos = [] @@ -619,6 +631,9 @@ class GPEXP_OT_select_layer_in_comp(bpy.types.Operator): infos = infos + [f'-- Selected {len(selected)} nodes --'] + selected fn.show_message_box(_message=infos, _title="Selected viewlayer in compo", _icon='INFO') + # Change viewed scene if not in current scene + if selected and scn != node_scene: + context.window.scene = node_scene return {"FINISHED"} classes=( diff --git a/__init__.py b/__init__.py index 417ba91..71c9c34 100644 --- a/__init__.py +++ b/__init__.py @@ -11,6 +11,7 @@ bl_info = { "category": "Object" } +from . import properties from . import OP_add_layer from . import OP_merge_layers from . import OP_clear @@ -29,9 +30,10 @@ from . import OP_setup_layers from . import OP_auto_build from . import ui -from .fn import scene_aa +# from .fn import scene_aa bl_modules = ( + properties, prefs, OP_add_layer, OP_clear, @@ -51,36 +53,18 @@ bl_modules = ( ui, ) -def update_scene_aa(context, scene): - scene_aa(toggle=bpy.context.scene.use_aa) import bpy def register(): - # if bpy.app.background: - # return - for mod in bl_modules: mod.register() - # bpy.types.Scene.pgroup_name = bpy.props.PointerProperty(type = PROJ_PGT_settings) - bpy.types.Scene.use_aa = bpy.props.BoolProperty( - name='Use Native Anti Aliasing', - default=True, - description='\ -Should be Off only if tree contains a merge_NG or alpha-over-combined renderlayers.\n\ -Auto-set to Off when using node merge button\n\ -Toggle: AA settings of and muting AA nested-nodegroup', - update=update_scene_aa) def unregister(): - # if bpy.app.background: - # return - for mod in reversed(bl_modules): mod.unregister() - del bpy.types.Scene.use_aa if __name__ == "__main__": register() \ No newline at end of file diff --git a/constant.py b/constant.py new file mode 100644 index 0000000..89284a5 --- /dev/null +++ b/constant.py @@ -0,0 +1 @@ +RD_SCENE_NAME = 'RenderGP' \ No newline at end of file diff --git a/fn.py b/fn.py index 86d91df..21b4abe 100644 --- a/fn.py +++ b/fn.py @@ -9,6 +9,7 @@ from collections import defaultdict from time import time import json +from . constant import RD_SCENE_NAME ### -- rules @@ -208,10 +209,20 @@ def scene_aa(scene=None, toggle=True): if not scene: scene=bpy.context.scene - # enable/disable native anti-alias on active scene + # Enable/disable native anti-alias on active scene set_scene_aa_settings(scene=scene, aa=toggle) - # mute/unmute AA nodegroups - for n in scene.node_tree.nodes: + + ## Set AA on scene where object and viewlayers exists + local_nodes = scene.node_tree.nodes + if (group_node := next((n for n in local_nodes if n.name.startswith('NG_')), None)): + # Get a viewlayer connected to a NG_ and check which scene is referred + if (rlayer := next((i.links[0].from_node for i in group_node.inputs if i.links and i.links[0].from_node.type == 'R_LAYERS'), None)): + if rlayer.scene and rlayer.scene != scene: + print(f'Set AA to {toggle} on scene "{rlayer.scene.name}"') + set_scene_aa_settings(scene=rlayer.scene, aa=toggle) + + ## Mute/Unmute AA nodegroups + for n in local_nodes: if n.type == 'GROUP' and n.name.startswith('NG_'): # n.mute = False # mute whole nodegroup ? for gn in n.node_tree.nodes: @@ -250,17 +261,58 @@ def new_scene_from(name, src_scn=None, regen=True, crop=True, link_cam=True, lin scn.use_nodes = True return scn -def get_render_scene(): - '''Get / Create a scene named Render''' - render_scn = bpy.data.scenes.get('Render') - if render_scn: - return render_scn +def get_compo_scene(scene_name=None, create=True): + '''Get / Create a dedicated compositing scene to link GP + use passed scene name, if no name is passed fall back to compo_scene propertie + return None if field is empty''' + + scene_name = scene_name or bpy.context.scene.gp_render_settings.node_scene + if not scene_name: + # return None or render scene + return + + scn = bpy.data.scenes.get(scene_name) + if scn: + return scn + + if not create: + return ## -- Create render scene current = bpy.context.scene ## With data - render_scn = bpy.data.scenes.new('Render') + scn = bpy.data.scenes.new(scene_name) + + ## copy original settings over to new scene + for attr in ['frame_start', 'frame_end', 'frame_current']: # , 'camera', 'world' + setattr(scn, attr, getattr(current, attr)) + copy_settings(current.render, scn.render) + + scn.use_nodes = True + + ## Clear node tree + scn.node_tree.nodes.clear() + set_settings(scn) + scn.gp_render_settings['use_aa'] = True + return scn + +def get_render_scene(scene_name=None, create=True): + '''Get / Create a dedicated render scene to link GP''' + + scene_name = scene_name or RD_SCENE_NAME + + render_scn = bpy.data.scenes.get(scene_name) + if render_scn: + return render_scn + if not create: + return + + ## -- Create render scene + current = bpy.context.scene + + ## With data + render_scn = bpy.data.scenes.new(scene_name) ## copy original settings over to new scene # copy_settings(current, render_scn) # BAD @@ -281,15 +333,16 @@ def get_render_scene(): # render_scn.node_tree.nodes.remove(n) set_settings(render_scn) - render_scn['use_aa'] = True + render_scn.gp_render_settings['use_aa'] = True return render_scn def get_view_layer(name, scene=None): '''get viewlayer name return existing/created viewlayer ''' - if not scene: - scene = get_render_scene() + + scene = scene or get_render_scene() + ### pass double letter prefix as suffix ## pass_name = re.sub(r'^([A-Z]{2})(_)(.*)', r'\3\2\1', 'name') ## pass_name = f'{name}_{passe}' @@ -336,6 +389,9 @@ def get_frame_transform(f, node_tree=None): # return real_loc(f), f.dimensions childs = [n for n in node_tree.nodes if n.parent == f] + if not childs: + return f.location, f.dimensions + # real_locs = [f.location + n.location for n in childs] xs = [n.location.x for n in childs] + [n.location.x + n.dimensions.x for n in childs] @@ -470,7 +526,7 @@ def remove_nodes_by_viewlayer(viewlayer_list, scene=None): # Remove render_layer node scene.node_tree.nodes.remove(n) -def merge_gplayer_viewlayers(ob=None, act=None, layers=None): +def merge_gplayer_viewlayers(ob=None, act=None, layers=None, scene=None): '''ob is not needed if active and layers are passed''' if ob is None: ob = bpy.context.object @@ -482,9 +538,8 @@ def merge_gplayer_viewlayers(ob=None, act=None, layers=None): if act is None: return ({'ERROR'}, 'Active layer not found. Should be active layer on active object!') - rd_scn = bpy.data.scenes.get('Render') - if not rd_scn: - return ({'ERROR'}, 'Viewlayers needs to be generated first!') + rd_scn = scene or get_render_scene(create=False) # bpy.context.scene + node_scene = get_compo_scene(create=False) or rd_scn if not act.viewlayer_render: return ({'ERROR'}, f'Active layer {act.info} has no viewlayer assigned') @@ -495,8 +550,10 @@ def merge_gplayer_viewlayers(ob=None, act=None, layers=None): # Remove duplication vls = list(set(vls)) + # Remove viewlayer related nodes - remove_nodes_by_viewlayer(vls, rd_scn) + # FIXME it's possible nodes are nodes searched in the right scene if launched from RenderGP + remove_nodes_by_viewlayer(vls, node_scene) # send compositing scene # Assign view layer from active to selected for l in layers: diff --git a/gen_vlayer.py b/gen_vlayer.py index e2aea53..e2842ef 100644 --- a/gen_vlayer.py +++ b/gen_vlayer.py @@ -2,19 +2,30 @@ import bpy from mathutils import Vector from . import fn +def add_rlayer(layer_name, scene=None, node_scene=None, location=None, color=None, node_name=None, width=400): + '''Create a render layer node -def add_rlayer(layer_name, scene=None, location=None, color=None, node_name=None, width=400): - '''create a render layer node - if node_name is not specified, use passed layer name + args: + layer_name (str): Name of the viewlayer + scene (Scene): Scene holding the viewlayer + node_scene (Scene): scene where to add render layer + if None: fallback scene pointed by compo scene property + if compo scene property is empty: fallback to render scene. + location (Vector2): Location of the node + color (tuple): Color of the node + node_name (str): if not specified, use layer_name + width (int): width of the node ''' if not node_name: node_name = layer_name # 'RL_' + - if not scene: - scene=bpy.context.scene + if not node_scene: + node_scene=fn.get_compo_scene(create=False) + if not node_scene: + node_scene=fn.get_render_scene(create=False) - nodes = scene.node_tree.nodes + nodes = node_scene.node_tree.nodes comp = nodes.get(node_name) if comp: @@ -40,13 +51,11 @@ def add_rlayer(layer_name, scene=None, location=None, color=None, node_name=None return comp def connect_render_layer(rlayer, ng=None, out=None, frame=None): - if bpy.context.scene.name == 'Scene': - scene = fn.get_render_scene() - else: - scene = bpy.context.scene - nodes = scene.node_tree.nodes - links = scene.node_tree.links + node_tree = rlayer.id_data # get node_tree from rlayer + + nodes = node_tree.nodes + links = node_tree.links vl_name = rlayer.layer if not vl_name or vl_name == 'View Layer': @@ -88,7 +97,7 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): print(f'create nodegroup {ng_name}') ngroup = bpy.data.node_groups.new(ng_name, 'CompositorNodeTree') - ng = fn.create_node('CompositorNodeGroup', tree=scene.node_tree, location=(fn.real_loc(rlayer)[0] + 600, fn.real_loc(rlayer)[1]), width=400) # (rlayer.location[0] + 600, rlayer.location[1]) + ng = fn.create_node('CompositorNodeGroup', tree=node_tree, location=(fn.real_loc(rlayer)[0] + 600, fn.real_loc(rlayer)[1]), width=400) # (rlayer.location[0] + 600, rlayer.location[1]) if frame: ng.parent= frame ng.node_tree = ngroup @@ -163,8 +172,17 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): ngroup.links.new(ng_in.outputs[vl_name], aa.inputs[0]) # node_tree ngroup.links.new(aa.outputs[0], ng_out.inputs[vl_name]) # node_tree - aa.mute = scene.use_aa # mute if native AA is used + ## Get use_aa prop from render scene ? + # rd_scn = fn.get_render_scene(create=False) + # if rd_scn: + # aa.mute = rd_scn.gp_render_settings.use_aa # mute if native AA is used + scene = next((s for s in bpy.data.scenes if s.node_tree == node_tree), None) + if scene: + print(f'set AA from scene {scene.name}') + aa.mute = scene.gp_render_settings.use_aa # mute if native AA is used + else: + print('/!\ Scene could not be found to define if internal AA should be muted') fn.reorganise_NG_nodegroup(ng) # decorative @@ -206,7 +224,7 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): out = nodes.get(out_name) 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) + out = fn.create_node('CompositorNodeOutputFile', tree=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 @@ -233,16 +251,20 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): return ng, out -def get_set_viewlayer_from_gp(ob, l, scene=None): - '''setup ouptut from passed gp obj > layer''' - if not scene: - if bpy.context.scene.name != 'Scene': - scene = bpy.context.scene - else: - scene = fn.get_render_scene() +def get_set_viewlayer_from_gp(ob, l, scene=None, node_scene=None): + '''setup ouptut from passed gp obj > layer + scene: scene to set viewlayer + node_scene: where to add compo node (use scene if not passed)''' - # print('Set viewlayer Scene: ', scene.name) - node_tree = scene.node_tree + scene = scene or fn.get_render_scene() + node_scene = node_scene or fn.get_compo_scene() or scene + + print('Viewlayer Scene: ', scene.name) + print('Compo Scene: ', node_scene.name) + + ## If not passed, identical to scene holding viewlayers + + node_tree = node_scene.node_tree nodes = node_tree.nodes in_rds = scene.collection.all_objects.get(ob.name) @@ -265,18 +287,26 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): if nob: nob.select_set(True) - # create viewlayer + # Create viewlayer vl_name = f'{ob.name} / {l.info}' vl = fn.get_view_layer(vl_name, scene=scene) vl_name = vl.name - # affect layer to this vl + + ## To avoid potential error, transfer compo scene prop to renderscene. + scn = bpy.context.scene + if scn.gp_render_settings.node_scene and scn != scene: + if scn.gp_render_settings.node_scene != scene.gp_render_settings.node_scene: + print(f'Transfer compo scene target prop to render scene: "{scn.gp_render_settings.node_scene}"') + scene.gp_render_settings.node_scene = scn.gp_render_settings.node_scene + + # Affect layer to this vl l.viewlayer_render = vl_name - # check if already exists + # Check if already exists rlayer_list = [n for n in nodes if n.type == 'R_LAYERS' and n.layer == vl_name] - # get frame object and their contents - # dict like : {objname : [layer_nodeA, layer_nodeB,...]} + # Get frame object and their contents + # dict model : {objname : [layer_nodeA, layer_nodeB,...]} frame_dic = {f.label: [n for n in nodes if n.type == 'R_LAYERS' and n.parent and n.parent.name == f.name and '/' in n.layer] # n.layer != 'View Layer' for f in nodes if f.type == 'FRAME'} @@ -323,7 +353,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): frame.label_size = 50 frame.location = (loc[0], loc[1] + 20) - cp = add_rlayer(vl_name, scene=scene, location=loc) + cp = add_rlayer(vl_name, scene=scene, node_scene=node_scene, location=loc) cp.parent = frame # use same color as layer if fn.has_channel_color(l): @@ -341,23 +371,24 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): print(f'\n {ob.name} -> {l.info} (connect to existing)') ## object frame exists: get framing and insert - cp = add_rlayer(vl_name, scene=scene, location=(0,0)) + cp = add_rlayer(vl_name, scene=scene, node_scene=node_scene, location=(0,0)) if cp.layer != vl_name: print(f'problem with {cp}: {cp.layer} != {vl_name}') return if fn.has_channel_color(l): - cp.use_custom_color = True - cp.color = l.channel_color + cp.use_custom_color = True + cp.color = l.channel_color - frame = [f for f in nodes if f.type == 'FRAME' and f.label == ob.name][0] + frame = next((f for f in nodes if f.type == 'FRAME' and f.label == ob.name), None) rl_nodes = frame_dic[frame.label] # get nodes from if rl_nodes: # get nodes order to insert rl_nodes.sort(key=lambda n: fn.real_loc(n).y, reverse=True) # descending top_loc = fn.real_loc(rl_nodes[0]) else: - top_loc = fn.get_frame_transform(frame[1], node_tree) - 60 + print('!! gen_viewlayer: No Render layers nodes !!') + top_loc = fn.get_frame_transform(frame, node_tree) - 60 # cp.location = (top_loc[0], top_loc[1] + 100) # temp location to adjust x loc @@ -381,10 +412,9 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): # set x loc from first node in list (maybe use leftmost ?) n.location = Vector((fn.real_loc(ref_node)[0], top_loc[1] - offset)) - n.parent.location offset += 180 - n.update() - # reorder render layers nodes within frame + # reorder render layers nodes within frame connect_render_layer(cp, frame=frame) # re-arrange all frames (since the offset probably overlapped) @@ -392,7 +422,7 @@ def get_set_viewlayer_from_gp(ob, l, scene=None): return vl, cp -def export_gp_objects(oblist, exclude_list=[], scene=None): +def export_gp_objects(oblist, exclude_list=[], scene=None, node_scene=None): # Skip layer containing element in exclude list if not isinstance(oblist, list): oblist = [oblist] @@ -410,4 +440,4 @@ def export_gp_objects(oblist, exclude_list=[], scene=None): l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name # assign "exclude" continue - _vl, _cp = get_set_viewlayer_from_gp(ob, l, scene=scene) # scene=fn.get_render_scene()) \ No newline at end of file + _vl, _cp = get_set_viewlayer_from_gp(ob, l, scene=scene, node_scene=node_scene) # scene=fn.get_render_scene()) \ No newline at end of file diff --git a/properties.py b/properties.py new file mode 100644 index 0000000..983024d --- /dev/null +++ b/properties.py @@ -0,0 +1,44 @@ +import bpy +from .fn import scene_aa + +def update_scene_aa(context, scene): + scene_aa(toggle=bpy.context.scene.gp_render_settings.use_aa) + +class GPRENDER_PGT_settings(bpy.types.PropertyGroup) : + + show_scene_setup :bpy.props.BoolProperty( + name='Show scene setup', + default=False, + description='Show scene setup, options to tweak render scene and compo scene') + + render_scene : bpy.props.StringProperty( + name="Render Scene", description="Link object and create viewlayers in render scene, create if necessary", + default="", maxlen=0, options={'HIDDEN'}) + + node_scene : bpy.props.StringProperty( + name="Compo Scene", description="Add nodes in compositing scene, if empty, add in Render scene nodeTree", + default="", maxlen=0, options={'HIDDEN'}) + + use_aa : bpy.props.BoolProperty( + name='Use Native Anti Aliasing', + default=True, + description='\ +Should be Off only if tree contains a merge_NG or alpha-over-combined renderlayers.\n\ +Auto-set to Off when using node merge button\n\ +Toggle: AA settings of and muting AA nested-nodegroup', + update=update_scene_aa) + +classes=( +GPRENDER_PGT_settings, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + bpy.types.Scene.gp_render_settings = bpy.props.PointerProperty(type=GPRENDER_PGT_settings) + +def unregister(): + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + + del bpy.types.Scene.gp_render_settings diff --git a/ui.py b/ui.py index e5bc558..3465bd1 100644 --- a/ui.py +++ b/ui.py @@ -2,6 +2,7 @@ import bpy from bpy.types import Panel from pathlib import Path from .prefs import get_addon_prefs +from .constant import RD_SCENE_NAME # from .preferences import get_addon_prefs # Node view panel @@ -16,7 +17,17 @@ class GPEXP_PT_gp_node_ui(Panel): prefs = get_addon_prefs() advanced = prefs.advanced layout = self.layout - layout.operator('gp.render_scene_switch', icon='SCENE_DATA', text='Switch Scene') + row = layout.row(align=True) + row.operator('gp.render_scene_switch', icon='SCENE_DATA', text='Switch Scene') + + ## Check if there is another scene reference current scene in it's comp renderlayers + ## If so, expose a button to go in (expensive check ?) + node_scn = next((s for s in bpy.data.scenes + if s != context.scene and s.use_nodes + and next((n for n in s.node_tree.nodes if n.type == 'R_LAYERS' and n.scene == context.scene), None) + ),None) + if node_scn: + row.operator('gp.render_scene_switch', icon='NODETREE', text='Node Scene').scene = node_scn.name scn = context.scene @@ -166,7 +177,7 @@ class GPEXP_PT_gp_node_ui(Panel): col.label(text='Post-Render:') col.operator('gp.renumber_files_on_disk', icon='FILE', text='Renumber Files On Disk') - layout.prop(context.scene, 'use_aa', text='Use Native AA Settings') + layout.prop(context.scene.gp_render_settings, 'use_aa', text='Use Native AA Settings') layout.prop(prefs, 'advanced', text='Show Advanced Options') # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'ALL' # layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='Layer To Render').mode = 'SELECTED' @@ -186,8 +197,20 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): def draw(self, context): layout = self.layout + settings = context.scene.gp_render_settings + + icon = 'DISCLOSURE_TRI_DOWN' if settings.show_scene_setup else 'DISCLOSURE_TRI_RIGHT' + col = layout.column() + col.prop(settings, 'show_scene_setup', icon=icon, text='Scenes Setup', emboss=False ) + if settings.show_scene_setup: + col.prop(settings, 'render_scene', icon='SCENE_DATA', placeholder=RD_SCENE_NAME) + + nodetree_placeholder = settings.render_scene or RD_SCENE_NAME + col.prop(settings, 'node_scene', icon='NODETREE', placeholder=nodetree_placeholder) + + op = layout.operator('gp_export.render_auto_build') + op.node_scene = settings.node_scene - layout.operator('gp_export.render_auto_build') if context.object: layout.label(text=f'Object: {context.object.name}') if context.object.data.users > 1: @@ -207,7 +230,9 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): txt = f'{len([l for l in context.object.data.layers if l.select])} Layer(s) To Render' else: txt = 'Layer To Render' - col.operator('gp.add_layer_to_render', icon='RENDERLAYERS', text=txt) + + op = col.operator('gp.add_layer_to_render', icon='RENDERLAYERS', text=txt) + op.node_scene = settings.node_scene # merge (only accessible if multiple layers selected) @@ -232,8 +257,14 @@ class GPEXP_PT_gp_dopesheet_ui(Panel): col = layout.column() col.label(text='Whole Objects:') txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render' - col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt).mode='SELECTED' - col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All Visible GP To Render').mode='ALL' + + op = col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt) + op.mode='SELECTED' + op.node_scene = settings.node_scene + + op = col.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All Visible GP To Render') + op.mode='ALL' + op.node_scene = settings.node_scene layout.separator() col = layout.column() @@ -357,9 +388,10 @@ def manager_ui(self, context): ## On objects # txt = 'Selected Object To Render' - if context.scene.name != 'Render': - txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render' - layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt).mode='SELECTED' + # if context.scene.name != RD_SCENE_NAME: + txt = f'{len([o for o in context.selected_objects if o.type == "GPENCIL" and o.select_get()])} Selected Object(s) To Render' + layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text=txt).mode='SELECTED' + layout.operator('gp.add_object_to_render', icon='RENDERLAYERS', text='All GP at once').mode='ALL'