from typing import Coroutine import bpy import re from mathutils import Vector from pathlib import Path from math import isclose from collections import defaultdict from time import time import json def create_node(type, tree=None, **kargs): '''Get a type, a tree to add in, and optionnaly multiple attribute to set return created node ''' tree = tree or bpy.context.scene.node_tree node = tree.nodes.new(type) for k,v in kargs.items(): setattr(node, k, v) return node def new_aa_node(tree, **kargs): '''create AA node''' aa = create_node('CompositorNodeAntiAliasing', tree) # type = ANTIALIASING aa.threshold = 1.0 # 0.5 aa.contrast_limit = 0.25 # 0.5 aa.corner_rounding = 0.25 aa.hide = True for k,v in kargs.items(): setattr(aa, k, v) return aa def create_aa_nodegroup(tree): ngroup = bpy.data.node_groups.get('AA') if not ngroup: ngroup = bpy.data.node_groups.new('AA', 'CompositorNodeTree') 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=(-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=(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]) ng = create_node('CompositorNodeGroup', tree=tree) ng.node_tree = ngroup ng.name = ngroup.name ng.hide=True return ng def copy_settings(obj_a, obj_b): exclusion = ['bl_rna', 'id_data', 'identifier','name_property','rna_type','properties', 'stamp_note_text','use_stamp_note', 'settingsFilePath', 'settingsStamp', 'select', 'matrix_local', 'matrix_parent_inverse', 'matrix_basis','location','rotation_euler', 'rotation_quaternion', 'rotation_axis_angle', 'scale'] for attr in dir(obj_a): if attr.startswith('__'): continue if attr in exclusion: continue # print('attr: ', attr) # if obj_a.is_property_readonly(attr): # block when things aren't attribute # continue try: val = getattr(obj_a, attr) except AttributeError: # print(f'cant get {attr}') pass try: setattr(obj_b, attr, val) except: # print(f"can't set {attr}") pass def set_file_output_format(fo): fo.format.file_format = 'OPEN_EXR' fo.format.color_mode = 'RGBA' fo.format.color_depth = '16' fo.format.exr_codec = 'ZIP' # fo.format.exr_codec = 'RLE' # fo.format.file_format = 'PNG' # fo.format.color_mode = 'RGBA' # fo.format.color_depth = '8' # fo.format.compression = 15 def set_settings(scene=None): 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 scene.render.film_transparent = True scene.render.use_compositing = True scene.render.use_sequencer = False scene.view_settings.view_transform = 'Standard' scene.render.resolution_percentage = 100 # output (fast write settings since this is just to delete afterwards...) scene.render.filepath = '//render/preview/preview_' scene.render.image_settings.file_format = 'JPEG' scene.render.image_settings.color_mode = 'RGB' scene.render.image_settings.quality = 0 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''' scn = bpy.data.scenes.get(name) if scn and not regen: return scn elif scn and regen: bpy.data.scenes.remove(scn) src_scn = src_scn or bpy.context.scene # given scene, or active scene scn = bpy.data.scenes.new(name) ## copy original settings over to new scene # copy_settings(current, scn) # BAD for attr in ['frame_start', 'frame_end', 'frame_current', 'camera', 'world']: setattr(scn, attr, getattr(src_scn, attr)) copy_settings(src_scn.render, scn.render) ## link cameras (and lights ?) for ob in src_scn.objects: if link_cam and ob.type == 'CAMERA': scn.collection.objects.link(ob) if link_light and ob.type == 'LIGHT': scn.collection.objects.link(ob) # set adapted render settings (no AA) set_settings(scn) if crop: scn.render.use_border = True scn.render.use_crop_to_border = True scn.use_nodes = True return scn def get_render_scene(): '''Get / Create a scene named Render''' render_scn = bpy.data.scenes.get('Render') if not render_scn: current = bpy.context.scene render_scn = bpy.data.scenes.new('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']: setattr(render_scn, attr, getattr(current, attr)) copy_settings(current.render, render_scn.render) ## link cameras (and lights ?) for ob in bpy.context.scene.objects: if ob.type in ('CAMERA', 'LIGHT'): render_scn.collection.objects.link(ob) # set adapted render settings (no AA) set_settings(render_scn) render_scn.use_nodes = 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() ### pass double letter prefix as suffix ## pass_name = re.sub(r'^([A-Z]{2})(_)(.*)', r'\3\2\1', 'name') ## pass_name = f'{name}_{passe}' pass_vl = scene.view_layers.get(name) if not pass_vl: pass_vl = scene.view_layers.new(name) return pass_vl ### node location tweaks def real_loc(n): if not n.parent: return n.location return n.location + real_loc(n.parent) def get_frame_transform(f, node_tree=None): '''Return real transform location of a frame node only works with one level of nesting (not recursive) ''' if not node_tree: node_tree = f.id_data if f.type != 'FRAME': return # return real_loc(f), f.dimensions childs = [n for n in node_tree.nodes if n.parent == f] # 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] ys = [n.location.y for n in childs] + [n.location.y - n.dimensions.y for n in childs] xs.sort(key=lambda loc: loc) # x val : ascending ys.sort(key=lambda loc: loc) # ascending # , reversed=True) # y val : descending loc = Vector((min(xs), max(ys))) dim = Vector((max(xs) - min(xs) + 60, max(ys) - min(ys) + 60)) return loc, dim ## get all frames with their real transform. def bbox(f, frames): xs=[] ys=[] for n in frames[f]: # nodes of passed frame # Better as Vectors ? if n.type == 'FRAME': if n not in frames.keys(): # print(f'frame {n.name} not in frame list') continue all_xs, all_ys = bbox(n, frames) # frames[n] xs += all_xs ys += all_ys else: loc = real_loc(n) xs += [loc.x, loc.x + n.dimensions.x] # + (n.dimensions.x/get_dpi_factor()) ys += [loc.y, loc.y - n.dimensions.y] # - (n.dimensions.y/get_dpi_factor()) # margin ~= 30 # return xs and ys return [min(xs)-30, max(xs)+30], [min(ys)-30, max(ys)+30] def get_frames_bbox(node_tree): '''Return a dic with all frames ex: {frame_node: (location, dimension), ...} ''' # create dic of frame object with his direct child nodes nodes frames = defaultdict(list) frames_bbox = {} for n in node_tree.nodes: if not n.parent: continue # also contains frames frames[n.parent].append(n) # Dic for bbox coord for f, nodes in frames.items(): if f.parent: continue xs, ys = bbox(f, frames) # xs, ys = bbox(nodes, frames) ## returning: list of corner coords # coords = [ # Vector((xs[0], ys[1])), # Vector((xs[1], ys[1])), # Vector((xs[1], ys[0])), # Vector((xs[0], ys[0])), # ] # frames_bbox[f] = coords ## returning: (loc vector, dimensions vector) frames_bbox[f] = Vector((xs[0], ys[1])), Vector((xs[1] - xs[0], ys[1] - ys[0])) return frames_bbox ## nodes helper functions def clear_nodegroup(name, full_clear=False): '''remove duplication of a nodegroup (.???) also remove the base one if full_clear True ''' for ng in reversed(bpy.data.node_groups): pattern = name + r'\.\d{3}' if not full_clear and ng.users: continue if re.search(pattern, ng.name): bpy.data.node_groups.remove(ng) if full_clear and ng.name == name: # if full clear bpy.data.node_groups.remove(ng) def rearrange_frames(node_tree): frame_d = get_frames_bbox(node_tree) # dic : {frame_node:(loc vector, dimensions vector), ...} if not frame_d: print('no frame found') return # print([f.name for f in frame_d.keys()]) ## order the dict by frame.y location frame_d = {key: value for key, value in sorted(frame_d.items(), key=lambda pair: pair[1][0].y - pair[1][1].y, reverse=True)} frames = [[f, v[0], v[1].y] for f, v in frame_d.items()] # [frame_node, real_loc, real dimensions] top = frames[0][1].y # upper node location.y # top = 0 #always start a 0 offset = 0 for f in frames: ## f[1] : real loc Vector ## f[0] : frame ## move frame by offset needed (delta between real_loc and "fake" loc , minus offset) f[0].location.y = (f[1].y - f[0].location.y) - offset # avoid offset when recalculating from 0 top # f[0].location.y = f[1].y - top - offset offset += f[2] + 200 # gap f[0].update() def reorder_inputs(ng): rl_nodes = [s.links[0].from_node for s in ng.inputs if s.is_linked and s.links and s.links[0].from_node.type == 'R_LAYERS'] rl_nodes.sort(key=lambda x: x.location.y, reverse=True) names = [n.layer for n in rl_nodes] inputs_names = [s.name for s in ng.inputs] filtered_names = [n for n in names if n in inputs_names] for dest, name in enumerate(filtered_names): ## rebuild list at each iteration so index are good inputs_names = [s.name for s in ng.inputs] src = inputs_names.index(name) # reorder on node_tree not directly on node! ng.node_tree.inputs.move(src, dest) def reorder_outputs(ng): ordered_out_name = [nis.name for nis in ng.inputs if nis.name in [o.name for o in ng.outputs]] for s_name in ordered_out_name: all_outnames = [o.name for o in ng.outputs] # reorder on nodetree, not on node ! ng.node_tree.outputs.move(all_outnames.index(s_name), ordered_out_name.index(s_name)) def clear_disconnected(fo): for inp in reversed(fo.inputs): if not inp.is_linked: print(f'Deleting unlinked fileout slot: {inp.name}') fo.inputs.remove(inp) def reorder_fileout(fo, ng=None): if not ng: # get connected nodegroup for s in fo.inputs: if s.is_linked and s.links and s.links[0].from_node.type == 'GROUP': ng = s.links[0].from_node break if not ng: print(f'No nodegroup to refer to filter {fo.name}') return ordered = [o.links[0].to_socket.name for o in ng.outputs if o.is_linked and o.is_linked and o.links[0].to_node == fo] for s_name in ordered: all_outnames = [s.name for s in fo.inputs] # same as [fs.path for fs in fo.file_slots] fo.inputs.move(all_outnames.index(s_name), ordered.index(s_name)) def reorganise_NG_nodegroup(ng): '''refit node content to avoid overlap''' ngroup = ng.node_tree ng_in = ngroup.nodes.get('Group Input') offset = 35 y = 0 for s in ng_in.outputs: if s.is_linked: s.links[0].to_node.location.y = y y -= offset def connect_to_group_output(n): for o in n.outputs: if o.is_linked: if o.links[0].to_node.type == 'GROUP_OUTPUT': return o.links[0].to_socket val = connect_to_group_output(o.links[0].to_node) if val: return val return False def connect_to_group_input(n): for i in n.inputs: if i.is_linked: if i.links[0].from_node.type == 'GROUP_INPUT': return i.links[0].from_socket val = connect_to_group_input(i.links[0].from_node) if val: return val return False def all_connected_forward(n, nlist=[]): '''return list of all forward connected nodes recursively (include passed nodes)''' for o in n.outputs: if o.is_linked: for lnk in o.links: if lnk.to_node.type == 'GROUP_OUTPUT': if n not in nlist: return nlist + [n] else: return nlist else: nlist = all_connected_forward(lnk.to_node, nlist) if n in nlist: return nlist return nlist + [n] def clear_nodegroup_content_if_disconnected(ngroup): '''Get a nodegroup.node_tree delete orphan nodes that are not connected from group input node ''' for n in reversed(ngroup.nodes): if n.type in ('GROUP_INPUT', 'GROUP_OUTPUT'): continue if not connect_to_group_input(n) and not connect_to_group_output(n): # is disconnected from both side ngroup.nodes.remove(n) def clean_nodegroup_inputs(ng, skip_existing_pass=True): '''Clear inputs to output of passed nodegroup if not connected''' ngroup = ng.node_tree rl_nodes = [n.layer for n in ng.id_data.nodes if n.type == 'R_LAYERS'] for i in range(len(ng.inputs))[::-1]: if not ng.inputs[i].is_linked: if skip_existing_pass and any(ng.inputs[i].name == x for x in rl_nodes): # a render layer of this name still exists continue ngroup.inputs.remove(ngroup.inputs[i]) # clear_nodegroup_content_if_disconnected(ngroup) def bridge_reconnect_nodegroup(ng, socket_name=None): ''' Reconnect group_in and group out that have been disconnected :socket: only use this specific socket type ''' ngroup = ng.node_tree ng_in = ngroup.nodes.get('Group Input') ng_out = ngroup.nodes.get('Group Output') for sockin in ng_in.outputs: if socket_name and sockin.name != socket_name: continue if not sockin.name: # last empty output is listed continue sockout = ng_out.inputs.get(sockin.name) if not sockout: continue if len(sockin.links) and connect_to_group_output(sockin.links[0].to_node): continue ## need reconnect aa = create_aa_nodegroup(ngroup) ngroup.links.new(sockin, aa.inputs[0]) ngroup.links.new(aa.outputs[0], sockout) print(f'{ng.name}: Bridged {sockin.name}') def random_color(alpha=False): import random if alpha: return (random.uniform(0,1), random.uniform(0,1), random.uniform(0,1), 1) return (random.uniform(0,1), random.uniform(0,1), random.uniform(0,1)) def nodegroup_merge_inputs(ngroup): '''Get a nodegroup merge every group inputs with alpha over then connect to antialias and a new output ''' ng_in = ngroup.nodes.get('Group Input') ng_out = ngroup.nodes.get('Group Output') x, y = ng_in.location.x + 200, 0 offset_x, offset_y = 150, -100 # merge all inputs in alphaover nodes prev = None for i in range(len(ng_in.outputs)-1): # skip waiting point inp = ng_in.outputs[i] if not prev: prev = ng_in continue # live connect ao = create_node('CompositorNodeAlphaOver', tree=ngroup, location=(x,y), hide=True) ngroup.links.new(prev.outputs[0], ao.inputs[1]) ngroup.links.new(inp, ao.inputs[2]) x += offset_x y += offset_y prev = ao ## create a merged name as output ?? 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 # create one input and link out = ngroup.outputs.new('NodeSocketColor', ngroup.inputs[0].name) ngroup.links.new(aa.outputs[0], ng_out.inputs[0]) ## --- renumbering funcs --- def get_numbered_output(out, slot_name): '''Return output slot name without looking for numbering ???_ ''' pattern = r'^(?:\d{3}_)?' # optional non capture group of 3 digits + _ pattern = f'{pattern}{slot_name}' for inp in out.inputs: if re.match(pattern, inp.name): return inp def add_fileslot_number(fs, number): elems = fs.path.split('/') for i, e in enumerate(elems): if re.match(r'^\d{3}_', e): elems[i] = re.sub(r'^(\d{3})', lambda x: str(number).zfill(3), e) else: elems[i] = f'{str(number).zfill(3)}_{e}' new = '/'.join(elems) fs.path = new return new def renumber(fo, offset=10): '''Force renumber all the slots with a 3''' if fo.type != 'OUTPUT_FILE': return ct = 10 # start at 10 for fs in fo.file_slots: add_fileslot_number(fs, ct) ct += offset def get_num(string) -> int: '''get a tring or a file_slot object return leading number or None ''' if not isinstance(string, str): string = string.path num = re.search(r'^(\d{3})_', string) if num: return int(num.group(1)) def delete_numbering(fo): # padding=3 '''Delete prefix numbering on all slots on passed file output''' if fo.type != 'OUTPUT_FILE': return for fs in fo.file_slots: elems = fs.path.split('/') for i, e in enumerate(elems): elems[i] = re.sub(r'^\d{3}_', '', e) new = '/'.join(elems) fs.path = new def reverse_fileout_inputs(fo): count = len(fo.inputs) for i in range(count): fo.inputs.move(count-1, i) def renumber_keep_existing(fo, offset=10, invert=True): '''Renumber by keeping existing numbers and inserting new one whenever possible Big and ugly function that do the trick nonetheless... ''' if fo.type != 'OUTPUT_FILE': return ct = 10 if invert: reverse_fileout_inputs(fo) fsl = fo.file_slots last_idx = len(fsl) - 1 prev = None prev_num = None for idx, fs in enumerate(fsl): # print('-->', idx, fs.path) if idx == last_idx: # handle last if get_num(fs) is not None: break if idx > 0: prev = fsl[idx-1] num = get_num(prev) if num is not None: add_fileslot_number(fs, num + offset) else: add_fileslot_number(fs, ct) else: add_fileslot_number(fs, 10) # there is only one slot (maybe don't number ?) break # update the ct with the current taken number if any number = get_num(fs) if number is not None: prev = fs ct = number + offset continue # skip already numbered # analyse all next slots until there is numbered divider = 0 # print(f'range(1, {len(fsl) - idx}') for i in range(1, len(fsl) - idx): next_num = get_num(fsl[idx + i]) if next_num is not None: divider = i+1 break if idx == 0: # handle first prev_num = 0 prev = None if next_num is None: add_fileslot_number(fs, 0) elif next_num == 0: print(f'Cannot insert value before 0 to {fsl.path}') continue else: add_fileslot_number(fs, int(next_num / 2)) else: prev = fsl[idx-1] test_prev = get_num(prev) if test_prev is not None: prev_num = test_prev if not divider: if prev_num is not None: add_fileslot_number(fs, prev_num + offset) else: add_fileslot_number(fs, ct) else: if prev_num is not None: # iterate rename gap_inc = int((next_num - prev_num) / divider) if gap_inc < 1: # same values ! print(f'cannot insert a median value at {fs.path} between {prev_num} and {next_num}') continue ct = prev_num for temp_id in range(idx, idx+i): ct += gap_inc add_fileslot_number(fsl[temp_id], ct) else: print("what's going on ?\n") # first check if it has a number (if not bas) prev = fs ct += offset if invert: reverse_fileout_inputs(fo) def has_channel_color(layer): '''Return True if gp_layer.channel_color is different than the default (0.2, 0.2, 0.2) ''' if not any(isclose(i, 0.2, abs_tol=0.001) for i in layer.channel_color): return True def normalize(text): return text.lower().replace('-', '_') PATTERN = r'^(?P-\s)?(?P[A-Z]{2}_)?(?P.*?)(?P_[A-Z]{2})?(?P\.\d{3})?$' # numering def normalize_layer_name(layer, prefix='', desc='', suffix='', lower=True, dash_to_underscore=True): '''GET a layer and argument 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 if lower: name = name.lower() if dash_to_underscore: name = name.replace('-', '_') new = f'{grp}{tag}{name}{sfix}' # lower suffix ? if new != layer.info: print(f'{layer.info} >> new') layer.info = new ## confirm pop-up message: def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'): def draw(self, context): for l in _message: if isinstance(l, str): self.layout.label(text=l) else: self.layout.label(text=l[0], icon=l[1]) if isinstance(_message, str): _message = [_message] bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) def get_bbox_3d(ob): bbox_coords = ob.bound_box return [ob.matrix_world @ Vector(b) for b in bbox_coords] def get_crop_pixel_coord(scn): # width height probably not needed. might need px_width = (scn.render.border_max_x - scn.render.border_min_x) * scn.render.resolution_x px_height = (scn.render.border_max_y - scn.render.border_min_y) * scn.render.resolution_y pos_x = (scn.render.border_min_x + ((scn.render.border_max_x - scn.render.border_min_x) / 2)) * scn.render.resolution_x ## coord y > image center coord from bottom-left (Blender) # pos_y = (scn.render.border_min_y + ((scn.render.border_max_y - scn.render.border_min_y) / 2)) * scn.render.resolution_y, ## image center coord from top-left (AE) pos_y = ((1 - scn.render.border_max_y) + ((scn.render.border_max_y - scn.render.border_min_y) / 2)) * scn.render.resolution_y coord = { 'position_x' : round(pos_x), 'position_y' : round(pos_y), 'width' : round(px_width), 'height' : round(px_height), } return coord def export_crop_to_json(): '''Export crop to json coords for AE ''' blend = Path(bpy.data.filepath) json_path = blend.parent / 'render' / f'{blend.stem}.json' #f'{ob.name}.json' ## per scene : json_path = Path(bpy.data.filepath).parent / 'render' / f'{scn.name}.json' # json_path = Path(bpy.data.filepath).parent / 'render' / f'{scn.name}.json' #f'{ob.name}.json' coord_dic = {} for scn in bpy.data.scenes: # if scn.name in {'Scene', 'Render'}: if scn.name == 'Scene': continue if scn.render.use_border: scn_border = get_crop_pixel_coord(scn) for ob in [o for o in scn.objects if o.type == 'GPENCIL']: coord_dic[ob.name] = scn_border # save bbox with json_path.open('w') as fd: json.dump(coord_dic, fd, indent='\t') print(f'coord saved at: {json_path}') return coord_dic def set_border_region_from_coord(coords, scn=None, margin=30, export_json=True): '''Get a list of point coord in worldcamera view space (0 to 1) on each axis''' scn = scn or bpy.context.scene coords2d_x = sorted([c[0] for c in coords]) coords2d_y = sorted([c[1] for c in coords]) margin_width = margin / scn.render.resolution_x margin_height = margin / scn.render.resolution_y # set crop scn.render.border_min_x = coords2d_x[0] - margin_width scn.render.border_max_x = coords2d_x[-1] + margin_width scn.render.border_min_y = coords2d_y[0] - margin_height scn.render.border_max_y = coords2d_y[-1] + margin_height ## get clamped relative value # relative_bbox2d_coords = [ # (scn.render.border_min_x, scn.render.border_min_y), # (scn.render.border_min_x, scn.render.border_max_y), # (scn.render.border_max_x, scn.render.border_max_y), # (scn.render.border_max_x, scn.render.border_min_y), # ] pixel_bbox2d_coords = [ (scn.render.border_min_x*scn.render.resolution_x, scn.render.border_min_y*scn.render.resolution_y), (scn.render.border_min_x*scn.render.resolution_x, scn.render.border_max_y*scn.render.resolution_y), (scn.render.border_max_x*scn.render.resolution_x, scn.render.border_max_y*scn.render.resolution_y), (scn.render.border_max_x*scn.render.resolution_x, scn.render.border_min_y*scn.render.resolution_y), ] # if export_json: # export_crop_to_json(scn) return pixel_bbox2d_coords def get_gp_box_all_frame(ob, cam=None): '''set crop to object bounding box considering whole animation. Cam should not be animated (render in bg_cam) return 2d bbox in pixels ''' from bpy_extras.object_utils import world_to_camera_view coords_cam_list = [] scn = bpy.context.scene cam = cam or scn.camera start = time() if ob.animation_data and ob.animation_data.action: # use frame set on all frames print(f'{ob.name} has anim') # frame_nums = sorted(list(set([f.frame_number for l in ob.data.layers if len(l.frames) for f in l.frames if len(f.strokes) and scn.frame_start <= f.frame_number <= scn.frame_end]))) for num in range(scn.frame_start, scn.frame_end+1): scn.frame_set(num) for l in ob.data.layers: if l.hide or l.opacity == 0.0: continue if l.active_frame: for s in l.active_frame.strokes: if len(s.points) == 1: # skip isolated points continue coords_cam_list += [world_to_camera_view(scn, cam, ob.matrix_world @ p.co) for p in s.points] else: # if object is not animated no need to frame_set to update object position print(f'{ob.name} no anim') for l in ob.data.layers: if l.hide or l.opacity == 0.0: continue for f in l.frames: if not (scn.frame_start <= f.frame_number <= scn.frame_end): continue for s in f.strokes: if len(s.points) == 1: # skip isolated points continue coords_cam_list += [world_to_camera_view(scn, cam, ob.matrix_world @ p.co) for p in s.points] print(time() - start) # Dbg-time return coords_cam_list def has_anim(ob): # TODO make a better check (check if there is only one key in each channel, count as not animated) return ob.animation_data and ob.animation_data.action def get_gp_box_all_frame_selection(oblist=None, scn=None, cam=None): ''' get points of all selection return 2d bbox in pixels ''' from bpy_extras.object_utils import world_to_camera_view coords_cam_list = [] scn = scn or bpy.context.scene oblist = oblist or [o for o in scn.objects if o.select_get()] cam = cam or scn.camera start = time() if any(has_anim(ob) for ob in oblist): print(f'at least one is animated: {oblist}') for num in range(scn.frame_start, scn.frame_end+1): scn.frame_set(num) for ob in oblist: for l in ob.data.layers: if l.hide or l.opacity == 0.0: continue if not l.active_frame: continue for s in l.active_frame.strokes: if len(s.points) == 1: # skip isolated points continue coords_cam_list += [world_to_camera_view(scn, cam, ob.matrix_world @ p.co) for p in s.points] else: print(f'No anim') for ob in oblist: # if object is not animated no need to frame_set to update object position for l in ob.data.layers: if l.hide or l.opacity == 0.0: continue for f in l.frames: if not (scn.frame_start <= f.frame_number <= scn.frame_end): continue for s in f.strokes: if len(s.points) == 1: # skip isolated points continue coords_cam_list += [world_to_camera_view(scn, cam, ob.matrix_world @ p.co) for p in s.points] print(f'{len(coords_cam_list)} gp points listed {time() - start:.1f}s') return coords_cam_list def get_bbox_2d(ob, cam=None): from bpy_extras.object_utils import world_to_camera_view scn = bpy.context.scene cam = cam or scn.camera 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]) bbox2d_coords = [ (coords2d_x[0], coords2d_y[0]), (coords2d_x[0], coords2d_y[-1]), (coords2d_x[-1], coords2d_y[-1]), (coords2d_x[-1], coords2d_y[0]), ] return [Vector(b) for b in bbox2d_coords] def set_box_from_selected_objects(scn=None, cam=None, export_json=False): scn = scn or bpy.context.scene cam = cam or scn.camera selection = [o for o in scn.objects if o.select_get()] # selected_objects coords = get_gp_box_all_frame_selection(oblist=selection, scn=scn, cam=cam) _bbox_px = set_border_region_from_coord(coords, margin=30, scn=scn, export_json=export_json) def get_collection_childs_recursive(col, cols=[]): '''return a list of all the sub-collections in passed col''' for sub in col.children: if sub not in cols: cols.append(sub) if len(sub.children): cols = get_collection_childs_recursive(sub, cols) return cols def unlink_objects_from_scene(oblist, scn): all_col = [scn.collection] all_col += get_collection_childs_recursive(scn.collection) for col in all_col: for ob in reversed(col.objects): if ob in oblist: col.objects.unlink(ob) def remove_scene_nodes_by_obj_names(scn, name_list, negative=False): for n in reversed(scn.node_tree.nodes): if negative: if (n.parent and n.parent.label not in name_list) or (n.type == 'FRAME' and n.label not in name_list): scn.node_tree.nodes.remove(n) else: if (n.parent and n.parent.label in name_list) or (n.type == 'FRAME' and n.label in name_list): scn.node_tree.nodes.remove(n) def split_object_to_scene(): '''Create a new scene from object selection''' active = bpy.context.object scene_name = active.name objs = [o for o in bpy.context.selected_objects] if bpy.data.scenes.get(scene_name): print(f'Scene "{scene_name}" Already Exists') raise Exception(f'Scene "{scene_name}" Already Exists') src = bpy.context.scene bpy.ops.scene.new(type='LINK_COPY') new = bpy.context.scene new.name = scene_name ## unlink unwanted objects from collection all_col = [new.collection] all_col += get_collection_childs_recursive(new.collection) for col in all_col: for sob in reversed(col.objects): if sob.type in ('CAMERA', 'LIGHT'): continue if sob not in objs: col.objects.unlink(sob) frame_names = [n.label for n in new.node_tree.nodes if n.type == 'FRAME' if new.objects.get(n.label)] remove_scene_nodes_by_obj_names(new, frame_names, negative=True) bpy.ops.gp.clean_compo_tree() # add crop new.render.use_border = True new.render.use_crop_to_border = True new.render.use_compositing = True new.render.use_sequencer = False ## remove asset from original scene #src_frame_names = [n.label for n in src.node_tree.nodes if n.type == 'FRAME' and n.label in [o.name for o in objs]] #remove_scene_nodes_by_obj_names(src, src_frame_names) remove_scene_nodes_by_obj_names(src, frame_names, negative=False) # unlink objects ? unlink_objects_from_scene(objs, src) # border to GP objects of the scene gp_objs = [o for o in new.objects if o.type == 'GPENCIL'] coords = get_gp_box_all_frame_selection(oblist=gp_objs, scn=new, cam=new.camera) set_border_region_from_coord(coords, margin=30, scn=new, export_json=True) export_crop_to_json() """ 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 """