Scene split and json export

0.5.0

- feat: add a render operator that render all scene
- feat: split selected object to a separated scene
- feat: crop border to objects
- feat: export AE position coordinates to replace layers
- change: ops gp.clean_compo_tree now take context.scene
- fix: tick `use_compositing` and untick `sequencer` in new scenes
main
Pullusb 2021-09-30 18:51:49 +02:00
parent 852b893f8a
commit 2aa4ecc00e
9 changed files with 529 additions and 10 deletions

View File

@ -14,6 +14,14 @@ Activate / deactivate layer opaticty according to prefix
Activate / deactivate all masks using MA layers
-->
0.5.0
- feat: add a render operator that render all scene
- feat: split selected object to a separated scene
- feat: crop border to objects
- feat: export AE position coordinates to replace layers
- change: ops gp.clean_compo_tree now take context.scene
- fix: tick `use_compositing` and untick `sequencer` in new scenes
0.4.1

View File

@ -89,9 +89,29 @@ class GPEXP_OT_add_objects_to_render(bpy.types.Operator):
return {"FINISHED"}
class GPEXP_OT_split_to_scene(bpy.types.Operator):
bl_idname = "gp.split_to_scene"
bl_label = "Split Objects To Scene"
bl_description = "Take selected objects and send them to separate scene"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.object and context.object.type == 'GPENCIL'
mode : bpy.props.StringProperty(default='ALL', options={'SKIP_SAVE'})
def execute(self, context):
fn.split_object_to_scene()
return {"FINISHED"}
classes=(
GPEXP_OT_add_layer_to_render,
GPEXP_OT_add_objects_to_render,
GPEXP_OT_split_to_scene,
)
def register():

View File

@ -98,11 +98,12 @@ class GPEXP_OT_clean_compo_tree(bpy.types.Operator):
# box.prop(self, 'fo_clear_disconnected')
def execute(self, context):
render = bpy.data.scenes.get('Render')
if not render:
print('SKIP, no Render scene')
return {"CANCELLED"}
# render = bpy.data.scenes.get('Render')
# if not render:
# print('SKIP, no Render scene')
# return {"CANCELLED"}
render = context.scene
nodes = render.node_tree.nodes
if self.clear_unused_view_layers:
used_rlayer_names = [n.layer for n in nodes if n.type == 'R_LAYERS']

51
OP_crop_to_object.py Normal file
View File

@ -0,0 +1,51 @@
import bpy
from . import fn
class GPEXP_OT_set_crop_from_selection(bpy.types.Operator):
bl_idname = "gp.set_crop_from_selection"
bl_label = "Set Crop"
bl_description = "Automatic set crop from selection"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
scn = context.scene
fn.set_box_from_selected_objects(scn=scn, cam=scn.camera)
scn.render.use_border = True
scn.render.use_crop_to_border = True
return {"FINISHED"}
class GPEXP_OT_export_crop_coord_to_json(bpy.types.Operator):
bl_idname = "gp.export_crop_coord_to_json"
bl_label = "Set Crop"
bl_description = "Export json of all scenes borders (when enabled)" # Automatic set crop from selection
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
# scn = context.scene
# if not scn.render.use_border or not scn.render.use_crop_to_border:
# self.report({'ERROR'}, 'Current scene have cropping disabled or use crop_to_border disabled!')
# return {'CANCELLED'}
fn.export_crop_to_json()
return {"FINISHED"}
classes=(
GPEXP_OT_set_crop_from_selection,
GPEXP_OT_export_crop_coord_to_json,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

47
OP_render_scenes.py Normal file
View File

@ -0,0 +1,47 @@
import bpy
from . import fn
from time import time
class GPEXP_OT_render_all_scenes(bpy.types.Operator):
bl_idname = "gp.render_all_scenes"
bl_label = "Render all scenes"
bl_description = "Render all scene except Render"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return True
def execute(self, context):
start = time()
ct = 0
for scn in bpy.data.scenes:
if scn.name == 'Scene':
continue
if not scn.use_nodes:
continue
if not [n for n in scn.node_tree.nodes if n.type == 'OUTPUT_FILE' and n.mute]:
# skip if no fileout
print(f'\n -!-> Skip {scn.name}, No output file, or all muted')
continue
print(f'\n --> Rendering {scn.name}')
# bpy.context.window.scene = scn
bpy.ops.render.render(animation=True, scene=scn.name)
ct += 1
print(f'\nDone. {ct} scenes rendered in {time()-start:.2f}s')
return {"FINISHED"}
classes=(
GPEXP_OT_render_all_scenes,
)
def register():
for cls in classes:
bpy.utils.register_class(cls)
def unregister():
for cls in reversed(classes):
bpy.utils.unregister_class(cls)

View File

@ -6,7 +6,7 @@
- on renaming, correct also names in GP modifiers !!!
- opt: multi-merge : also merge merged NG automatically disabling AA without group (or externalise AA node ?)
- opt : How to disable main output
- To add : Reconnect inside nodegroup for nodes when using clean nodes
## Done
- set exlude VL on non-used layers

View File

@ -2,7 +2,7 @@ bl_info = {
"name": "GP Render",
"description": "Organise export of gp layers through compositor output",
"author": "Samuel Bernou",
"version": (0, 4, 1),
"version": (0, 5, 0),
"blender": (2, 93, 0),
"location": "View3D",
"warning": "",
@ -17,6 +17,8 @@ from . import OP_clean
from . import OP_connect_toggle
from . import OP_manage_outputs
from . import OP_scene_switch
from . import OP_crop_to_object
from . import OP_render_scenes
# from . import OP_check_layer_status
from . import OP_render_pdf
from . import prefs
@ -37,6 +39,8 @@ def register():
OP_merge_layers.register()
OP_manage_outputs.register()
OP_scene_switch.register()
OP_crop_to_object.register()
OP_render_scenes.register()
# OP_check_layer_status.register()
OP_render_pdf.register()
OP_setup_layers.register()
@ -51,6 +55,8 @@ def unregister():
OP_setup_layers.unregister()
# OP_check_layer_status.unregister()
OP_render_pdf.unregister()
OP_render_scenes.unregister()
OP_crop_to_object.unregister()
OP_scene_switch.unregister()
OP_manage_outputs.unregister()
OP_merge_layers.unregister()

382
fn.py
View File

@ -1,10 +1,12 @@
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
@ -112,6 +114,8 @@ def set_settings(scene=None):
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
@ -119,10 +123,41 @@ def set_settings(scene=None):
# 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 = 'BW'
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'''
@ -677,4 +712,345 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
if isinstance(_message, str):
_message = [_message]
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
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
"""

10
ui.py
View File

@ -118,6 +118,16 @@ class GPEXP_PT_gp_node_ui(Panel):
col.operator('gp.clear_render_tree', icon='X', text='Clear Framed Nodes')
col.operator('gp.clear_render_tree', icon='X', text='Clear & Delete Render Scene').mode = "COMPLETE"
layout.separator()
layout.label(text='Sub Scenes:')
layout.operator('gp.split_to_scene', icon='DUPLICATE', text='Split To Scene')
row = layout.row(align=True)
row.operator('gp.set_crop_from_selection', icon='CON_OBJECTSOLVER', text='Set Crop')
row.operator('gp.export_crop_coord_to_json', icon='FILE', text='Export json')
layout.operator('gp.render_all_scenes', icon='RENDER_ANIMATION', text='Render All Sub-Scene')
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'