gp_render/fn.py

1056 lines
36 KiB
Python
Raw Normal View History

from typing import Coroutine
2021-09-07 23:11:42 +02:00
import bpy
import re
from mathutils import Vector
from pathlib import Path
2021-09-17 18:36:15 +02:00
from math import isclose
2021-09-07 23:11:42 +02:00
from collections import defaultdict
from time import time
import json
2021-09-07 23:11:42 +02:00
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
2021-09-18 18:07:44 +02:00
def new_aa_node(tree, **kargs):
2021-09-07 23:11:42 +02:00
'''create AA node'''
aa = create_node('CompositorNodeAntiAliasing', tree) # type = ANTIALIASING
2021-09-18 18:07:44 +02:00
aa.threshold = 1.0 # 0.5
aa.contrast_limit = 0.25 # 0.5
2021-09-07 23:11:42 +02:00
aa.corner_rounding = 0.25
aa.hide = True
2021-09-18 18:07:44 +02:00
for k,v in kargs.items():
setattr(aa, k, v)
2021-09-07 23:11:42 +02:00
return aa
2021-09-18 18:07:44 +02:00
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])
2021-09-18 18:07:44 +02:00
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))
2021-09-18 18:07:44 +02:00
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])
2021-09-18 18:07:44 +02:00
ng = create_node('CompositorNodeGroup', tree=tree)
ng.node_tree = ngroup
ng.name = ngroup.name
ng.hide=True
return ng
2021-09-07 23:11:42 +02:00
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
2021-09-07 23:11:42 +02:00
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
2021-09-07 23:11:42 +02:00
def get_render_scene():
'''Get / Create a scene named Render'''
2021-09-18 18:07:44 +02:00
render_scn = bpy.data.scenes.get('Render')
if not render_scn:
current = bpy.context.scene
2021-09-18 18:07:44 +02:00
render_scn = bpy.data.scenes.new('Render')
## copy original settings over to new scene
2021-09-18 18:07:44 +02:00
# copy_settings(current, render_scn) # BAD
2021-09-17 18:36:15 +02:00
for attr in ['frame_start', 'frame_end', 'frame_current', 'camera', 'world']:
2021-09-18 18:07:44 +02:00
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'):
2021-09-18 18:07:44 +02:00
render_scn.collection.objects.link(ob)
# set adapted render settings (no AA)
2021-09-18 18:07:44 +02:00
set_settings(render_scn)
render_scn.use_nodes = True
return render_scn
2021-09-07 23:11:42 +02:00
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
2021-09-07 23:11:42 +02:00
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()])
2021-09-07 23:11:42 +02:00
## 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]
2021-09-16 00:19:57 +02:00
2021-09-07 23:11:42 +02:00
top = frames[0][1].y # upper node location.y
2021-09-16 00:19:57 +02:00
# top = 0 #always start a 0
2021-09-07 23:11:42 +02:00
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)
2021-09-16 00:19:57 +02:00
f[0].location.y = (f[1].y - f[0].location.y) - offset # avoid offset when recalculating from 0 top
2021-09-07 23:11:42 +02:00
# f[0].location.y = f[1].y - top - offset
2021-09-16 00:19:57 +02:00
offset += f[2] + 200 # gap
2021-09-07 23:11:42 +02:00
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
2021-09-07 23:11:42 +02:00
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 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:
2021-09-15 10:34:35 +02:00
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)
2021-09-17 18:36:15 +02:00
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<grp>-\s)?(?P<tag>[A-Z]{2}_)?(?P<name>.*?)(?P<sfix>_[A-Z]{2})?(?P<inc>\.\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
"""