diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b5de6c..4aad74a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,12 @@ Activate / deactivate layer opacity according to prefix Activate / deactivate all masks using MA layers --> +1.2.0 + +- changed: enabled when launched in background +- added: autobuild: hide "invisible" material +- removed: timer to setup render scene + 1.1.4 - changed: force set color by prefix if autobuild option swiched on diff --git a/OP_auto_build.py b/OP_auto_build.py index 6e78ffd..71346b6 100644 --- a/OP_auto_build.py +++ b/OP_auto_build.py @@ -4,7 +4,7 @@ from pathlib import Path from . import gen_vlayer, fn -def batch_setup_render_scene(context=None): +def batch_setup_render_scene(context=None, render_scn=None): '''A series of setup actions for Render scene: - renumber fileout - Clean compo Tree @@ -14,8 +14,11 @@ def batch_setup_render_scene(context=None): if context is None: context = bpy.context - - render_scn = context.scene + if render_scn is None: + render_scn = bpy.data.scenes.get('Render') + if not render_scn: + print('"Render" scene not found in batch_setup_render_scene') + return ## Renumber File outputs print('Renumber File outputs') @@ -24,27 +27,36 @@ def batch_setup_render_scene(context=None): fn.renumber_keep_existing(fo) ## Swap to bg_cam (if any) - if render_scn.objects.get('bg_cam') and (not render_scn.camera or render_scn.camera.name != 'bg_cam'): - print('Swap to bg cam') - bpy.ops.gp.swap_render_cams() + # if render_scn.objects.get('bg_cam') and (not render_scn.camera or render_scn.camera.name != 'bg_cam'): + # print('Swap to bg cam') + # bpy.ops.gp.swap_render_cams() - ## Go to camera view in visible viewports - print('Go to camera view in visible viewports') - if render_scn.camera: - for window in bpy.context.window_manager.windows: - screen = window.screen - for area in screen.areas: - if area.type == 'VIEW_3D': - area.spaces.active.region_3d.view_perspective = 'CAMERA' + if render_scn.objects.get('bg_cam'): + render_scn.camera = render_scn.objects.get('bg_cam') + fn.set_resolution_from_cam_prop(scene=render_scn) + + ## Go to camera view in visible viewports (! Need timer + Already done in workspace script!) + # if not bpy.app.background: + # print('Go to camera view in visible viewports') + # if render_scn.camera: + # for window in bpy.context.window_manager.windows: + # screen = window.screen + # for area in screen.areas: + # if area.type == 'VIEW_3D': + # print('3D viewport found, Go in Camera') + # area.spaces.active.region_3d.view_perspective = 'CAMERA' ## 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) ## Trigger check file before finishing ? # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') + print('batch setup render scene Done') + class GPEXP_OT_render_auto_build(bpy.types.Operator): bl_idname = "gp_export.render_auto_build" bl_label = "Auto-Build" @@ -55,7 +67,7 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): def poll(cls, context): return context.object and context.object.type == 'GPENCIL' - timer : bpy.props.FloatProperty(default=0.1, options={'SKIP_SAVE'}) + # timer : bpy.props.FloatProperty(default=0.1, options={'SKIP_SAVE'}) excluded_prefix : bpy.props.StringProperty( name='Excluded Layer By Prefix', default='GP, RG, PO, MA', @@ -159,6 +171,11 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): for ob in all_gp_objects: fn.clean_mats_duplication(ob) + ## Hide "invisible" material + mat_invisible = bpy.data.materials.get('invisible') + if mat_invisible and mat_invisible.is_grease_pencil: + mat_invisible.grease_pencil.hide = True + ob_list = [o for o in all_gp_objects if not o.hide_get() and fn.is_valid_name(o.name)] if not ob_list: self.report({'ERROR'}, 'No GP object to render found') @@ -202,9 +219,9 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): if not render_scn: self.report({'ERROR'}, 'No render scene found') return {'CANCELLED'} - - context.window.scene = render_scn + context.window.scene = render_scn + ## Group all adjacent layer type if self.group_all_adjacent_layer_type: print('Group all adjacent layer type') @@ -226,149 +243,51 @@ class GPEXP_OT_render_auto_build(bpy.types.Operator): else: render_wkspace_filepath = Path(bpy.utils.user_resource('SCRIPTS'), 'startup', 'bl_app_templates_user', 'GP', 'startup.blend') ret = bpy.ops.workspace.append_activate(idname='GP Render', filepath=str(render_wkspace_filepath)) - print('ret: ', ret) + if ret != {'FINISHED'}: + print(f'Fallback to addon stored workspaces: (No "GP Render" found in {render_wkspace_filepath})') # Fallback to workspace template shipped with addon (TODO : add template blend file in addon) render_wkspace_filepath = Path(__file__).parent / 'workspaces' / 'startup.blend' ret = bpy.ops.workspace.append_activate(idname='GP Render', filepath=str(render_wkspace_filepath)) + if ret != {'FINISHED'}: print('No GP render workspace available') - - ## Batch setup render scene - if batch_setup_render_scene: - bpy.app.timers.register(batch_setup_render_scene, first_interval=self.timer) - - ## Trigger check file before finishing ? - # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') - ## Note: After all these operation, a ctrl+Z might crash - - print('\nDone.') - return {"FINISHED"} + + ## extra retry after append activate ?... + if ret == {'FINISHED'}: + render_wkspace = bpy.data.workspaces.get('GP Render') + if render_wkspace: + context.window.workspace = render_wkspace -''' -class GPEXP_OT_render_auto_build(bpy.types.Operator): - bl_idname = "gp_export.render_auto_build" - bl_label = "Auto-Build" - 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' - - excluded_prefix : bpy.props.StringProperty( - name='Excluded Layer By Prefix', default='GP,RG,PO', - description='Exclude layer to send to render by prefix (comma separated list)') - - timer : bpy.props.FloatProperty(default=0.01, options={'SKIP_SAVE'}) - - def execute(self, context): - print('-- Auto-build Render scene --\n') - - ## Prefix Filter - ## TODO : add to preferences / environment var - prefix_to_render = ['CO', 'CU', 'FX', 'TO', 'MA'] - - render_scn = bpy.data.scenes.get('Render') - if render_scn: - self.report({'ERROR'}, 'A "Render" scene already exists') - return {'CANCELLED'} - - ## clean name and visibility - for o in [o for o in context.scene.objects if o.type == 'GPENCIL']: - if o.hide_render: - print(f'skip: {o.name} hide render') - continue - for l in o.data.layers: - ## Clean name when layer has no name after prefix - if re.match(r'^[A-Z]{2}_$', l.info): - l.info = l.info + o.name.lower() - ## Make used prefix visible ? - if (res := re.search(r'^([A-Z]{2})_', l.info)): - if res.group(1) in prefix_to_render and l.hide == True: - print(f'{o.name} -> {l.info} : Switch visibility On') - l.hide = False - - ob_list = [o for o in context.scene.objects if o.type == 'GPENCIL' and not o.hide_get() and fn.is_valid_name(o.name)] - if not ob_list: - self.report({'ERROR'}, 'No GP object to render found') - return {'CANCELLED'} - - print('GP objects to send:') - for o in ob_list: - print(f' - {o.name}') - - ## Set layers colors (skip if colors were already set ?) - ## Option: Maybe find a way to create a color from prefix hash ? (always give unique color with same prefix on other project!) - fn.set_layer_colors(skip_if_colored=True) - - ## 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 - 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) - print('Export layer infos (skip if json already exists)') - bpy.ops.gp.export_infos_for_compo('INVOKE_DEFAULT', skip_check=True) - - ## Send all GP to render scene - print('Send all GP to render scene') - # 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 - - ## Switch to new Render Scene - print('Switch to new Render Scene') - render_scn = bpy.data.scenes.get('Render') - if not render_scn: - self.report({'ERROR'}, 'No render scene found') - return {'CANCELLED'} - - context.window.scene = render_scn - - ## Group all adjacent layer type - print('Group all adjacent layer type') - for ob in ob_list: - fn.group_adjacent_layer_prefix_rlayer(ob, excluded_prefix=['GP', 'PO', 'RG'], first_name=True) - - bpy.ops.gp_export.render_scene_setup() # next render scene setup at once - - ## attempt to refresh scene - # render_scn.node_tree.nodes.update() - # context.view_layer.update() + # context.workspace.update_tag() # context.scene.update_tag() - ## Change to GP workspace (if needed) - if context.window.workspace.name != 'GP Render': - print('Change to GP workspace') - if (render_wkspace := bpy.data.workspaces.get('GP Render')): - context.window.workspace = render_wkspace - else: - render_wkspace_filepath = Path(bpy.utils.user_resource('SCRIPTS'), 'startup', 'bl_app_templates_user', 'GP', 'startup.blend') - ret = bpy.ops.workspace.append_activate(idname='GP Render', filepath=str(render_wkspace_filepath)) - print('ret: ', ret) - if ret != {'FINISHED'}: - # Fallback to workspace template shipped with addon (TODO : add template blend file in addon) - render_wkspace_filepath = Path(__file__).parent / 'workspaces' / 'startup.blend' - ret = bpy.ops.workspace.append_activate(idname='GP Render', filepath=str(render_wkspace_filepath)) - if ret != {'FINISHED'}: - print('No GP render workspace available') + ## Batch setup render scene + batch_setup_render_scene(render_scn=render_scn) + + ## No need for timer anymore ! + # if batch_setup_render_scene: + # if self.timer > 0: + # print(f'batch_setup_render_scene: called with timer {self.timer}s') + # # add timer otherwise render scene setup don't do anything + # bpy.app.timers.register(batch_setup_render_scene, first_interval=self.timer) + # else: + # print('batch_setup_render_scene: Direct call') + # batch_setup_render_scene(render_scn=render_scn) - bpy.app.timers.register(batch_setup_render_scene, first_interval=self.timer) + ## set at least one GP object active + gp_ob = next((o for o in render_scn.objects if o.type == 'GPENCIL'), None) + if gp_ob: + context.view_layer.objects.active = gp_ob ## Trigger check file before finishing ? # bpy.ops.gp.check_render_scene('INVOKE_DEFAULT') ## Note: After all these operation, a ctrl+Z might crash - print('\nDone.') - return {"FINISHED"} -''' + print('\nDone.\n') + return {"FINISHED"} class GPEXP_OT_render_scene_setup(bpy.types.Operator): bl_idname = "gp_export.render_scene_setup" diff --git a/OP_scene_switch.py b/OP_scene_switch.py index 2ff5af9..cbd08d2 100644 --- a/OP_scene_switch.py +++ b/OP_scene_switch.py @@ -1,4 +1,5 @@ import bpy +from .import fn class GPEXP_OT_render_scene_switch(bpy.types.Operator): bl_idname = "gp.render_scene_switch" @@ -36,33 +37,12 @@ class GPEXP_OT_render_scene_switch(bpy.types.Operator): bpy.context.window.scene = scn return {"FINISHED"} -def set_resolution_from_cam_prop(cam=None): - if not cam: - cam = bpy.context.scene.camera - if not cam: - return ('ERROR', 'No active camera') - - res = cam.get('resolution') - if not res: - return ('ERROR', 'Cam has no resolution attribute') - - rd = bpy.context.scene.render - if rd.resolution_x == res[0] and rd.resolution_y == res[1]: - return ('INFO', f'Resolution already at {res[0]}x{res[1]}') - else: - rd.resolution_x, rd.resolution_y = res[0], res[1] - return ('INFO', f'Resolution to {res[0]}x{res[1]}') - class GPEXP_OT_swap_render_cams(bpy.types.Operator): bl_idname = "gp.swap_render_cams" bl_label = "Swap Cameras" bl_description = "Toggle between anim and bg cam" bl_options = {"REGISTER"} - @classmethod - def poll(cls, context): - return True - def execute(self, context): anim_cam = bpy.context.scene.objects.get('anim_cam') bg_cam = bpy.context.scene.objects.get('bg_cam') @@ -74,9 +54,8 @@ class GPEXP_OT_swap_render_cams(bpy.types.Operator): cam = context.scene.camera if not cam: context.scene.camera = anim_cam - set_resolution_from_cam_prop() + fn.set_resolution_from_cam_prop() return {"FINISHED"} - in_draw = False if cam.parent and cam.name in ('draw_cam', 'action_cam'): @@ -104,10 +83,10 @@ class GPEXP_OT_swap_render_cams(bpy.types.Operator): bg_cam.hide_viewport = anim_cam.hide_viewport = True # set res - ret = set_resolution_from_cam_prop(main) + ret = fn.set_resolution_from_cam_prop(main) if ret: self.report({ret[0]}, ret[1]) - + return {"FINISHED"} classes=( diff --git a/OP_setup_layers.py b/OP_setup_layers.py index a4637b4..e19bde6 100644 --- a/OP_setup_layers.py +++ b/OP_setup_layers.py @@ -82,6 +82,8 @@ class GPEXP_OT_export_infos_for_compo(bpy.types.Operator): 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: diff --git a/__init__.py b/__init__.py index d23fc8f..367dd9c 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": (1, 1, 4), + "version": (1, 2, 0), "blender": (2, 93, 0), "location": "View3D", "warning": "", @@ -57,8 +57,8 @@ def update_scene_aa(context, scene): import bpy def register(): - if bpy.app.background: - return + # if bpy.app.background: + # return for mod in bl_modules: mod.register() @@ -74,8 +74,8 @@ Toggle: AA settings of and muting AA nested-nodegroup', update=update_scene_aa) def unregister(): - if bpy.app.background: - return + # if bpy.app.background: + # return for mod in reversed(bl_modules): mod.unregister() diff --git a/fn.py b/fn.py index 7cb476b..b2c9457 100644 --- a/fn.py +++ b/fn.py @@ -214,8 +214,18 @@ def get_render_scene(): if render_scn: return render_scn + ## -- Create render scene current = bpy.context.scene + + ## With data render_scn = bpy.data.scenes.new('Render') + + ## With ops (goes directly into scene) + # bpy.ops.scene.new(type='NEW') + # render_scn = bpy.context.scene + # print('render_scn: ', render_scn) + # render_scn.name = 'Render' + ## copy original settings over to new scene # copy_settings(current, render_scn) # BAD for attr in ['frame_start', 'frame_end', 'frame_current', 'camera', 'world']: @@ -223,7 +233,7 @@ def get_render_scene(): copy_settings(current.render, render_scn.render) ## link cameras (and lights ?) - for ob in bpy.context.scene.objects: + for ob in current.objects: if ob.type in ('CAMERA', 'LIGHT'): render_scn.collection.objects.link(ob) @@ -235,7 +245,7 @@ def get_render_scene(): # render_scn.node_tree.nodes.remove(n) set_settings(render_scn) - render_scn['use_aa'] = True + render_scn['use_aa'] = True return render_scn def get_view_layer(name, scene=None): @@ -253,6 +263,24 @@ def get_view_layer(name, scene=None): pass_vl.use_pass_z = True return pass_vl +def set_resolution_from_cam_prop(cam=None, scene=None): + if scene is None: + scene = bpy.context.scene + if not cam: + cam = scene.camera + if not cam: + return ('ERROR', 'No active camera') + + res = cam.get('resolution') + if not res: + return ('ERROR', 'Cam has no resolution attribute') + + rd = scene.render + if rd.resolution_x == res[0] and rd.resolution_y == res[1]: + return ('INFO', f'Resolution already at {res[0]}x{res[1]}') + else: + rd.resolution_x, rd.resolution_y = res[0], res[1] + return ('INFO', f'Resolution to {res[0]}x{res[1]}') ## -- node location tweaks @@ -487,7 +515,13 @@ def rearrange_rlayers_in_frames(node_tree): for rl in rlayers: # move to top with equal size rl.location.y = top - top -= rl.dimensions.y + 20 # place next down by height + gap of 20 + + if rl.dimensions.y == 0: + # Newly created nodes + top -= 180 + 20 # down by probable size + gap of 20 + else: + top -= rl.dimensions.y + 20 # place next down by height + gap of 20 + def rearrange_frames(node_tree): frame_d = get_frames_bbox(node_tree) # dic : {frame_node:(loc vector, dimensions vector), ...}