diff --git a/CHANGELOG.md b/CHANGELOG.md index 9d97448..8f5809d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ Activate / deactivate layer opaticty according to prefix Activate / deactivate all masks using MA layers --> +0.6.0: + +- feat: button to generate a background rendering script to batch multi-scene +- fix: exposed checkbox to change scene AA settings, should be on except if there are NG_merges. (auto-off when using merge nodes buttons) +- fix: default generated scene have native AA +- fix: adding layers from object in other scene use active scene (stop always rerouting to 'Render' scene) + 0.5.9 - feat: Select which scene to render diff --git a/OP_add_layer.py b/OP_add_layer.py index 4f89892..e37953a 100644 --- a/OP_add_layer.py +++ b/OP_add_layer.py @@ -44,7 +44,7 @@ class GPEXP_OT_add_layer_to_render(bpy.types.Operator): return {"FINISHED"} -def export_gp_objects(oblist, exclude_list=[]): +def export_gp_objects(oblist, exclude_list=[], scene=None): # Skip layer containing element in excluyde list if not isinstance(oblist, list): oblist = [oblist] @@ -54,10 +54,10 @@ 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').name # assign "exclude" + l.viewlayer_render = fn.get_view_layer('exclude', scene=scene).name # assign "exclude" continue - _vl, _cp = gen_vlayer.get_set_viewlayer_from_gp(ob, l) # scene=fn.get_render_scene()) + _vl, _cp = gen_vlayer.get_set_viewlayer_from_gp(ob, l, scene=scene) # scene=fn.get_render_scene()) ## send operator with mode ALL or SELECTED to batch build @@ -75,17 +75,22 @@ class GPEXP_OT_add_objects_to_render(bpy.types.Operator): def execute(self, context): # create render scene - fn.get_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': - export_gp_objects([o for o in context.selected_objects if o.type == 'GPENCIL'], exclude_list=excludes) # excludes + 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"} - export_gp_objects([o for o in scn.objects if o.type == 'GPENCIL' and not o.hide_get()], exclude_list=excludes) # excludes + # scn = bpy.data.scenes.get('Scene') + # if not scn: + # self.report({'ERROR'}, 'Could not found default scene') + # return {"CANCELLED"} + export_gp_objects([o for o in scn.objects if o.type == 'GPENCIL' and not o.hide_get()], exclude_list=excludes, scene=scn) return {"FINISHED"} diff --git a/OP_manage_outputs.py b/OP_manage_outputs.py index 187cb38..f345377 100644 --- a/OP_manage_outputs.py +++ b/OP_manage_outputs.py @@ -211,12 +211,48 @@ class GPEXP_OT_activate_only_selected_layers(bpy.types.Operator): 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_nam(scn.name)}/preview_' + + 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 + + fn.scene_aa(scene=scn, toggle=use_native_aa) + + 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_OT_normalize_outnames, ) diff --git a/OP_merge_layers.py b/OP_merge_layers.py index 2eda9b8..2a2726c 100644 --- a/OP_merge_layers.py +++ b/OP_merge_layers.py @@ -110,6 +110,8 @@ 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) + return ng, out class GPEXP_OT_merge_viewlayers_to_active(bpy.types.Operator): @@ -265,7 +267,11 @@ class GPEXP_OT_merge_selected_viewlayer_nodes(bpy.types.Operator): disconnect : bpy.props.BoolProperty(default=True, options={'SKIP_SAVE'}) def execute(self, context): - render = bpy.data.scenes.get('Render') + 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"} diff --git a/OP_render_scenes.py b/OP_render_scenes.py index 47504d6..647bb4a 100644 --- a/OP_render_scenes.py +++ b/OP_render_scenes.py @@ -1,6 +1,8 @@ import bpy from . import fn -from time import time +from time import time, strftime +from pathlib import Path +import sys from bpy.types import Panel, UIList, Operator, PropertyGroup, Menu from bpy.props import PointerProperty, IntProperty, BoolProperty, StringProperty, EnumProperty, FloatProperty @@ -101,10 +103,98 @@ class GPEXP_OT_render_selected_scene(bpy.types.Operator): return {"FINISHED"} +class GPEXP_OT_bg_render_script_selected_scene(bpy.types.Operator): + bl_idname = "gp.bg_render_script_selected_scenes" + bl_label = "Create Selected Scene Render Batch " + bl_description = "Create a batch script to render all selected scenes in a selection popup" + bl_options = {"REGISTER"} + + @classmethod + def poll(cls, context): + return True + + def invoke(self, context, event): + context.scene.scenes_list.clear() + for s in bpy.data.scenes: + scn_item = context.scene.scenes_list.add() + scn_item.name = s.name + scn_item.select = s.name != 'Scene' + + return context.window_manager.invoke_props_dialog(self, width=500) + + def draw(self, context): + layout = self.layout + col = layout.column() + for si in context.scene.scenes_list: + row = col.row() + row.prop(si, 'select',text='') + row.label(text=si.name) + + ## Display warnings + scn = bpy.data.scenes.get(si.name) + # compare to existing Rlayers (overkill ?) + # vls = [scn.view_layers.get(n.layer) for n in rlayers_nodes if scn.view_layers.get(n.layer)] + + vls = [vl for vl in scn.view_layers if vl.name != 'View Layer'] + + if vls: + exclude_count = len([vl for vl in vls if not vl.use]) + if exclude_count: + row.label(text=f'{exclude_count}/{len(vls)} excluded viewlayers', icon='ERROR') + + if not scn.use_nodes: + row.label(text='use_node deactivated', icon='ERROR') + continue + + outfiles = [n for n in scn.node_tree.nodes if n.type == 'OUTPUT_FILE'] + if not outfiles: + row.label(text='No output files nodes', icon='ERROR') + continue + + outnum = len(outfiles) + muted = len([x for x in outfiles if x.mute]) + if muted == outnum: + row.label(text='All output file are muted', icon='ERROR') + continue + + elif muted: + row.label(text=f'{muted}/{outnum} output file muted', icon='ERROR') + continue + + + def execute(self, context): + platform = sys.platform + + blend = Path(bpy.data.filepath) + + scn_to_render = [si.name for si in context.scene.scenes_list if si.select] + batch_file = blend.parent / f'{blend.stem}--{len(scn_to_render)}batch_{strftime("%m-%d-%H")}.sh' + + if platform.startswith('win'): + script_text = ['@ECHO OFF'] + batch_file = batch_file.with_suffix('.bat') + else: + script_text = ['#!/bin/bash'] + + print('batch_file: ', batch_file) + for scn_name in scn_to_render: + cmd = f'"{bpy.app.binary_path}" -b "{bpy.data.filepath}" -S "{scn_name}" -a' + script_text.append(cmd) + + script_text.append('echo --- END BATCH ---') + script_text.append('pause') + + with batch_file.open('w') as fd: + fd.write('\n'.join(script_text)) + + self.report({'INFO'}, f'Batch script generated: {batch_file}') + return {"FINISHED"} + classes=( GPEXP_scene_select_prop, GPEXP_OT_render_selected_scene, GPEXP_OT_render_all_scenes, +GPEXP_OT_bg_render_script_selected_scene, ) def register(): diff --git a/__init__.py b/__init__.py index f5e49e2..0f57a80 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, 5, 9), + "version": (0, 6, 0), "blender": (2, 93, 0), "location": "View3D", "warning": "", @@ -25,6 +25,11 @@ from . import prefs from . import OP_setup_layers from . import ui +from .fn import scene_aa + +def update_scene_aa(context, scene): + scene_aa(toggle=bpy.context.scene.use_aa) + import bpy def register(): @@ -46,6 +51,14 @@ def register(): OP_setup_layers.register() ui.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: @@ -66,7 +79,7 @@ def unregister(): OP_add_layer.unregister() prefs.unregister() - # del bpy.types.Scene.pgroup_name + del bpy.types.Scene.use_aa if __name__ == "__main__": register() \ No newline at end of file diff --git a/fn.py b/fn.py index 1b37cf0..57c464a 100644 --- a/fn.py +++ b/fn.py @@ -107,12 +107,26 @@ def set_file_output_format(fo): # fo.format.color_depth = '8' # fo.format.compression = 15 -def set_settings(scene=None): + +def set_scene_aa_settings(scene=None, aa=True): + '''aa == using native AA, else disable scene AA''' if not scene: scene = bpy.context.scene + if aa: + scene.eevee.taa_render_samples = 32 + scene.grease_pencil_settings.antialias_threshold = 1 + else: + scene.eevee.taa_render_samples = 1 + scene.grease_pencil_settings.antialias_threshold = 0 + +def set_settings(scene=None, aa=True): + '''aa == using native AA, else disable scene AA''' + if not scene: + scene = bpy.context.scene + # specify scene settings for these kind of render - scene.eevee.taa_render_samples = 1 - scene.grease_pencil_settings.antialias_threshold = 0 + set_scene_aa_settings(scene=scene, aa=aa) + scene.render.film_transparent = True scene.render.use_compositing = True scene.render.use_sequencer = False @@ -121,11 +135,25 @@ def set_settings(scene=None): scene.render.resolution_percentage = 100 # output (fast write settings since this is just to delete afterwards...) - scene.render.filepath = '//render/preview/preview_' + scene.render.filepath = f'//render/preview/{scene.name}/preview_' scene.render.image_settings.file_format = 'JPEG' scene.render.image_settings.color_mode = 'RGB' scene.render.image_settings.quality = 0 +def scene_aa(scene=None, toggle=True): + '''Change scene AA settings and commute AA nodes according to toggle''' + if not scene: + scene=bpy.context.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: + if n.type == 'GROUP' and n.name.startswith('NG_'): + # n.mute = False # mute whole nodegroup ? + for gn in n.node_tree.nodes: + if gn.type == 'GROUP' and gn.node_tree.name == 'AA': + gn.mute = toggle def new_scene_from(name, src_scn=None, regen=True, crop=True, link_cam=True, link_light=True): '''Get / Create a scene from name and source scene to get settings from''' @@ -179,10 +207,11 @@ def get_render_scene(): render_scn.collection.objects.link(ob) render_scn.use_nodes = True + # TODO Clear node tree (initial view layer stuff) - # set adapted render settings (no AA) - set_settings(render_scn) + set_settings(render_scn, with_aa=False) # set adapted render settings (no AA by default) + render_scn.use_aa = True return render_scn def get_view_layer(name, scene=None): @@ -1054,8 +1083,8 @@ def split_object_to_scene(): coords = get_gp_box_all_frame_selection(oblist=gp_objs, scn=new, cam=new.camera) if not coords: return f'Scene "{scene_name}" created. But Border was not set (Timeout during GP analysis), should be done by hand if needed then use export crop to json' + set_border_region_from_coord(coords, margin=30, scn=new, export_json=True) - export_crop_to_json() @@ -1106,56 +1135,3 @@ def clear_frame_out_of_range_all_object(): ct += nct print(f'{ct} gp frames deleted') return ct - - -""" -def split_object_to_scene(): - '''Create a new scene from selection''' - - # send objects in a new render scene - ## define new scene name with active object names - active = bpy.context.object - scene_name = active.name - objs = [o for o in bpy.context.selected_objects] - - rd_scn = bpy.data.scenes.get('Render') - ## create scene and copy settings from render scene or current - # src_scn = bpy.data.scenes.get('Render') - # src_scn = src_scn or bpy.context.scene - # if src_scn.name == scene_name: - # print('! Problem ! Trying to to create new render scene without source') - # return - - - ## From current scene (might be Render OR Scene) - src_scn = bpy.context.scene - - new = new_scene_from(scene_name, src_scn=src_scn, regen=True) # crop=True, link_cam=True, link_light=True - - for ob in objs: - new.collection.objects.link(ob) - if ob.type == 'GPENCIL': - # recreate VL - vl_names = [l.viewlayer_render for l in ob.data.layers if l.viewlayer_render] - for names in vl_names: - new.view_layers.new(names) - # get_set_viewlayer_from_gp(ob, l, scene=new) - - -def set_crop_bbox_2d(ob, cam=None): - '''Basic crop using bouding box on current frame''' - from bpy_extras.object_utils import world_to_camera_view - - scn = bpy.context.scene - cam = cam or scn.camera - # bbox = [ob.matrix_world @ Vector(b) for b in bbox_coords] - coords2d = [world_to_camera_view(scn, cam, p) for p in get_bbox_3d(ob)] - - coords2d_x = sorted([c[0] for c in coords2d]) - coords2d_y = sorted([c[1] for c in coords2d]) - scn.render.border_min_x = coords2d_x[0] - scn.render.border_max_x = coords2d_x[-1] - scn.render.border_min_y = coords2d_y[0] - scn.render.border_max_y = coords2d_y[-1] - return -""" \ No newline at end of file diff --git a/gen_vlayer.py b/gen_vlayer.py index 42cdc40..9c09493 100644 --- a/gen_vlayer.py +++ b/gen_vlayer.py @@ -40,7 +40,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): - scene = fn.get_render_scene() + 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 @@ -147,6 +151,9 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): # ng_in.outputs[vl_name] 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 + fn.reorganise_NG_nodegroup(ng) # decorative @@ -210,8 +217,10 @@ def connect_render_layer(rlayer, ng=None, out=None, frame=None): def get_set_viewlayer_from_gp(ob, l, scene=None): '''setup ouptut from passed gp obj > layer''' if not scene: - # scene = bpy.context.scene - scene = fn.get_render_scene() # create if necessary + if bpy.context.scene.name != 'Scene': + scene = bpy.context.scene + else: + scene = fn.get_render_scene() node_tree = scene.node_tree nodes = node_tree.nodes diff --git a/ui.py b/ui.py index ff4c04f..63414f1 100644 --- a/ui.py +++ b/ui.py @@ -104,6 +104,7 @@ class GPEXP_PT_gp_node_ui(Panel): col.separator() col.operator('gp.clean_compo_tree', icon='BRUSHES_ALL', text='Clean Nodes') # NODE_CORNER + col.operator('gp.reset_render_settings', icon='SCENE', text='Reset All Scenes Render Settings') col.separator() @@ -129,17 +130,20 @@ class GPEXP_PT_gp_node_ui(Panel): col.operator('gp.clear_render_tree', icon='X', text='Clear & Delete Render Scene').mode = "COMPLETE" layout.separator() - layout.label(text='Sub Scenes:') + layout.label(text='Scenes:') layout.operator('gp.split_to_scene', icon='DUPLICATE', text='Split Selected Obj To Scene') row = layout.row(align=True) row.operator('gp.set_crop_from_selection', icon='CON_OBJECTSOLVER', text='Autoset Crop') row.operator('gp.export_crop_coord_to_json', icon='FILE', text='Export json') + layout.label(text='Render:') row = layout.row(align=True) row.operator('gp.render_selected_scenes', icon='RENDER_ANIMATION', text='Render Selected Scene') + row.operator('gp.bg_render_script_selected_scenes', icon='TEXT', text='Gen Batch') # row.operator('gp.render_all_scenes', icon='RENDER_ANIMATION', text='Render All') + layout.prop(context.scene, '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'