export anim cam 2d positionwithin bg cam

0.9.8

- feat: `Export Camera 2D Position To AE` to export 'anim cam' (or selected cam) frame center pixel coordinate within scene camera.
  - write txt file as after effects postion clipboard data
main
Pullusb 2022-02-09 12:27:29 +01:00
parent d43e59b7c7
commit c5ea98b4d0
4 changed files with 144 additions and 33 deletions

View File

@ -14,6 +14,11 @@ Activate / deactivate layer opaticty according to prefix
Activate / deactivate all masks using MA layers Activate / deactivate all masks using MA layers
--> -->
0.9.8
- feat: `Export Camera 2D Position To AE` to export 'anim cam' (or selected cam) frame center pixel coordinate within scene camera.
- write txt file as after effects postion clipboard data
0.9.7 0.9.7
- feat: `Select Nodes` added in Dopesheet. Select nodes associated with selected gp layers and report if there are errors - feat: `Select Nodes` added in Dopesheet. Select nodes associated with selected gp layers and report if there are errors

View File

@ -43,40 +43,33 @@ def correct_shift(vec, cam):
def export_AE_objects_position_keys(): def export_AE_objects_position_keys():
'''Export keys as paperclip to paste in after''' '''Export keys as paperclip to paste in after'''
C= bpy.context
scn = bpy.context.scene
result = {} result = {}
for fr in range(C.scene.frame_start,C.scene.frame_end + 1): for fr in range(scn.frame_start,scn.frame_end + 1):
C.scene.frame_set(fr) scn.frame_set(fr)
for o in C.selected_objects: for o in C.selected_objects:
if not result.get(o.name): if not result.get(o.name):
result[o.name] = [] result[o.name] = []
proj2d = world_to_camera_view(C.scene,C.scene.camera,o.matrix_world.to_translation())# + Vector((.5,.5,0)) proj2d = world_to_camera_view(scn, scn.camera, o.matrix_world.to_translation()) # + Vector((.5,.5,0))
# proj2d = correct_shift(proj2d,C.scene.camera) # needed ? # proj2d = correct_shift(proj2d, scn.camera) # needed ?
x = (proj2d[0]) * C.scene.render.resolution_x x = (proj2d[0]) * scn.render.resolution_x
y = -(proj2d[1]) * C.scene.render.resolution_y + C.scene.render.resolution_y y = -(proj2d[1]) * scn.render.resolution_y + scn.render.resolution_y
result[o.name].append((fr,x,y)) result[o.name].append((fr,x,y))
for name,value in result.items(): for name,value in result.items():
prefix = 'Adobe After Effects 8.0 Keyframe Data\n\n' txt = fn.get_ae_keyframe_clipboard_header(scn)
prefix += '\tUnits Per Second\t%s\n'%C.scene.render.fps
prefix += '\tSource Width\t%s\n'%C.scene.render.resolution_x
prefix += '\tSource Height\t%s\n'%C.scene.render.resolution_y
prefix += '\tSource Pixel Aspect Ratio\t1\n'
prefix += '\tComp Pixel Aspect Ratio\t1\n\n'
prefix += 'Transform\tPosition\n'
prefix += '\tFrame\tX pixels\tY pixels\tyZ pixels\t\n'
for v in value: for v in value:
prefix += '\t%s\t%s\t%s\t\n'%(v[0],v[1],v[2]) txt += '\t%s\t%s\t%s\t\n'%(v[0],v[1],v[2])
prefix += '\n\n' txt += '\n\nEnd of Keyframe Data\n' # keyframe txt footer
prefix += 'End of Keyframe Data\n'
blend = Path(bpy.data.filepath) blend = Path(bpy.data.filepath)
keyfile = blend.parent / 'render' / f'pos_{name}.txt' keyfile = blend.parent / 'render' / f'pos_{name}.txt'
@ -84,11 +77,12 @@ def export_AE_objects_position_keys():
print(f'exporting keys for {name}') print(f'exporting keys for {name}')
with open(keyfile, 'w') as fd: with open(keyfile, 'w') as fd:
fd.write(prefix) fd.write(txt)
class GPEXP_OT_export_keys_to_ae(bpy.types.Operator): class GPEXP_OT_export_keys_to_ae(bpy.types.Operator):
bl_idname = "gp.export_keys_to_ae" bl_idname = "gp.export_keys_to_ae"
bl_label = "Export 2D position to AE" bl_label = "Export 2D Position To AE"
bl_description = "Export selected objects positions as text file containing key paperclip for AfterEffects layers" bl_description = "Export selected objects positions as text file containing key paperclip for AfterEffects layers"
bl_options = {"REGISTER"} bl_options = {"REGISTER"}
@ -101,6 +95,53 @@ class GPEXP_OT_export_keys_to_ae(bpy.types.Operator):
return {"FINISHED"} return {"FINISHED"}
def export_anim_cam_position(camera=None, context=None):
context = context or bpy.context
scn = context.scene
camera = camera or bpy.data.objects.get('anim_cam')
if not camera:
return 'Abort: No "anim_cam" found!'
text = fn.get_ae_keyframe_clipboard_header(scn)
for i in range(scn.frame_start, scn.frame_end + 1):
scn.frame_set(i)
center = fn.get_cam_frame_center_world(camera)
coord = fn.get_coord_in_cam_space(scn, scn.camera, center, ae=True)
# text += f'\t{i}\t{coord[0]}\t{coord[1]}\t\n'
text += f' {i} {coord[0]} {coord[1]}\n'
text += '\n\nEnd of Keyframe Data\n' # Ae Frame ending
blend = Path(bpy.data.filepath)
keyfile = blend.parent / 'render' / f'anim_cam_pos.txt'
keyfile.parent.mkdir(parents=False, exist_ok=True)
print(f'Exporting anim cam positions keys at: {keyfile}')
with open(keyfile, 'w') as fd:
fd.write(text)
class GPEXP_OT_export_cam_keys_to_ae(bpy.types.Operator):
bl_idname = "gp.export_cam_keys_to_ae"
bl_label = "Export Camera 2D Position To AE"
bl_description = "Export anim cam positions as text file containing key paperclip for AfterEffects layers"
bl_options = {"REGISTER"}
@classmethod
def poll(cls, context):
return context.selected_objects
def execute(self, context):
cam = None
if context.object and context.object.type == 'CAMERA' and context.object != context.scene.camera:
cam = context.object
err = export_anim_cam_position(camera=cam, context=context)
if err:
self.report({'ERROR'}, err)
return {"CANCELLED"}
return {"FINISHED"}
class GPEXP_OT_fix_overscan_shift(bpy.types.Operator): class GPEXP_OT_fix_overscan_shift(bpy.types.Operator):
bl_idname = "gp.fix_overscan_shift" bl_idname = "gp.fix_overscan_shift"
bl_label = "Fix Cam Shift Value With Overscan" bl_label = "Fix Cam Shift Value With Overscan"
@ -196,8 +237,10 @@ class GPEXP_PT_extra_gprender_func(bpy.types.Panel):
def draw(self, context): def draw(self, context):
layout = self.layout layout = self.layout
layout.operator("gp.fix_overscan_shift") col = layout.column()
layout.operator("gp.export_keys_to_ae") col.operator("gp.fix_overscan_shift")
col.operator("gp.export_keys_to_ae")
col.operator("gp.export_cam_keys_to_ae")
@ -208,6 +251,7 @@ class GPEXP_PT_extra_gprender_func(bpy.types.Panel):
classes=( classes=(
GPEXP_OT_export_keys_to_ae, GPEXP_OT_export_keys_to_ae,
GPEXP_OT_export_cam_keys_to_ae,
GPEXP_OT_fix_overscan_shift, GPEXP_OT_fix_overscan_shift,
GPEXP_PT_extra_gprender_func GPEXP_PT_extra_gprender_func
) )

View File

@ -2,7 +2,7 @@ bl_info = {
"name": "GP Render", "name": "GP Render",
"description": "Organise export of gp layers through compositor output", "description": "Organise export of gp layers through compositor output",
"author": "Samuel Bernou", "author": "Samuel Bernou",
"version": (0, 9, 7), "version": (0, 9, 8),
"blender": (2, 93, 0), "blender": (2, 93, 0),
"location": "View3D", "location": "View3D",
"warning": "", "warning": "",

80
fn.py
View File

@ -9,6 +9,9 @@ from collections import defaultdict
from time import time from time import time
import json import json
### -- node basic
def create_node(type, tree=None, **kargs): def create_node(type, tree=None, **kargs):
'''Get a type, a tree to add in, and optionnaly multiple attribute to set '''Get a type, a tree to add in, and optionnaly multiple attribute to set
return created node return created node
@ -70,6 +73,8 @@ def create_aa_nodegroup(tree):
return ng return ng
## -- object and scene settings
def copy_settings(obj_a, obj_b): def copy_settings(obj_a, obj_b):
exclusion = ['bl_rna', 'id_data', 'identifier','name_property','rna_type','properties', 'stamp_note_text','use_stamp_note', 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', 'settingsFilePath', 'settingsStamp', 'select', 'matrix_local', 'matrix_parent_inverse',
@ -95,7 +100,6 @@ def copy_settings(obj_a, obj_b):
# print(f"can't set {attr}") # print(f"can't set {attr}")
pass pass
def set_file_output_format(fo): def set_file_output_format(fo):
fo.format.file_format = 'OPEN_EXR' fo.format.file_format = 'OPEN_EXR'
fo.format.color_mode = 'RGBA' fo.format.color_mode = 'RGBA'
@ -108,7 +112,6 @@ def set_file_output_format(fo):
# fo.format.color_depth = '8' # fo.format.color_depth = '8'
# fo.format.compression = 15 # fo.format.compression = 15
def set_scene_aa_settings(scene=None, aa=True): def set_scene_aa_settings(scene=None, aa=True):
'''aa == using native AA, else disable scene AA''' '''aa == using native AA, else disable scene AA'''
if not scene: if not scene:
@ -229,7 +232,8 @@ def get_view_layer(name, scene=None):
pass_vl = scene.view_layers.new(name) pass_vl = scene.view_layers.new(name)
return pass_vl return pass_vl
### node location tweaks
## -- node location tweaks
def real_loc(n): def real_loc(n):
if not n.parent: if not n.parent:
@ -260,7 +264,7 @@ def get_frame_transform(f, node_tree=None):
return loc, dim return loc, dim
## get all frames with their real transform. ## -- get all frames with their real transform.
def bbox(f, frames): def bbox(f, frames):
xs=[] xs=[]
@ -322,7 +326,7 @@ def get_frames_bbox(node_tree):
return frames_bbox return frames_bbox
## nodes helper functions ## -- nodes helper functions
def clear_nodegroup(name, full_clear=False): def clear_nodegroup(name, full_clear=False):
'''remove duplication of a nodegroup (.???) '''remove duplication of a nodegroup (.???)
@ -359,7 +363,6 @@ def rearrange_rlayers_in_frames(node_tree):
rl.location.y = top rl.location.y = top
top -= rl.dimensions.y + 20 # place next down by height + gap of 20 top -= rl.dimensions.y + 20 # place next down by height + gap of 20
def rearrange_frames(node_tree): def rearrange_frames(node_tree):
frame_d = get_frames_bbox(node_tree) # dic : {frame_node:(loc vector, dimensions vector), ...} frame_d = get_frames_bbox(node_tree) # dic : {frame_node:(loc vector, dimensions vector), ...}
if not frame_d: if not frame_d:
@ -616,7 +619,7 @@ def nodegroup_merge_inputs(ngroup):
out = ngroup.outputs.new('NodeSocketColor', ngroup.inputs[0].name) out = ngroup.outputs.new('NodeSocketColor', ngroup.inputs[0].name)
ngroup.links.new(aa.outputs[0], ng_out.inputs[0]) ngroup.links.new(aa.outputs[0], ng_out.inputs[0])
## --- renumbering funcs --- ## -- renumbering funcs
def get_numbered_output(out, slot_name): def get_numbered_output(out, slot_name):
'''Return output slot name without looking for numbering ???_ '''Return output slot name without looking for numbering ???_
@ -911,6 +914,8 @@ def show_message_box(_message = "", _title = "Message Box", _icon = 'INFO'):
bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon) bpy.context.window_manager.popup_menu(draw, title = _title, icon = _icon)
## -- camera framing and object anim checks
def get_bbox_3d(ob): def get_bbox_3d(ob):
bbox_coords = ob.bound_box bbox_coords = ob.bound_box
return [ob.matrix_world @ Vector(b) for b in bbox_coords] return [ob.matrix_world @ Vector(b) for b in bbox_coords]
@ -1032,7 +1037,6 @@ def set_border_region_from_coord(coords, scn=None, margin=30, export_json=True):
# export_crop_to_json(scn) # export_crop_to_json(scn)
return pixel_bbox2d_coords return pixel_bbox2d_coords
def get_gp_box_all_frame(ob, cam=None): 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) '''set crop to object bounding box considering whole animation. Cam should not be animated (render in bg_cam)
return 2d bbox in pixels return 2d bbox in pixels
@ -1159,7 +1163,6 @@ def get_bbox_2d(ob, cam=None):
return [Vector(b) for b in bbox2d_coords] return [Vector(b) for b in bbox2d_coords]
def set_box_from_selected_objects(scn=None, cam=None, export_json=False): def set_box_from_selected_objects(scn=None, cam=None, export_json=False):
scn = scn or bpy.context.scene scn = scn or bpy.context.scene
cam = cam or scn.camera cam = cam or scn.camera
@ -1171,6 +1174,65 @@ def set_box_from_selected_objects(scn=None, cam=None, export_json=False):
_bbox_px = set_border_region_from_coord(coords, margin=30, scn=scn, export_json=export_json) _bbox_px = set_border_region_from_coord(coords, margin=30, scn=scn, export_json=export_json)
def get_cam_frame_center_world(cam):
'''get camera frame center world position in 3d space'''
## ortho cam note: scale must be 1,1,1 (parent too) to fit right in cam-frame rectangle
import numpy as np
frame = cam.data.view_frame()
mat = cam.matrix_world
frame = [mat @ v for v in frame]
# return np.add.reduce(frame) / 4
return Vector(np.sum(frame, axis=0) / 4)
def get_coord_in_cam_space(scene, cam_ob, co, ae=False):
'''Get 2d coordinate of vector in cam space
:scene: scene where camera is used (needed to get resolution)
:cam_ob: camera object
:co: the Vector3 coordinate to find in cam space
:ae: if True, Return after effects coord, top-left corner origin (blender is bottom-left)
'''
import bpy_extras
co_2d = bpy_extras.object_utils.world_to_camera_view(scene, cam_ob, co)
if ae:
# y coordinate from top
co_2d = Vector((co_2d.x, 1 - co_2d.y))
## Convert to pixel values based on scene resolution and percentage
render_scale = scene.render.resolution_percentage / 100
render_size = (
int(scene.render.resolution_x * render_scale),
int(scene.render.resolution_y * render_scale),
)
return (
round(co_2d.x * render_size[0]), # x
round(co_2d.y * render_size[1]), # y
)
## -- After effects exports
def get_ae_keyframe_clipboard_header(scn):
import textwrap
t = f'''\
Adobe After Effects 8.0 Keyframe Data
Units Per Second {scn.render.fps}
Source Width {scn.render.resolution_x}
Source Height {scn.render.resolution_y}
Source Pixel Aspect Ratio 1
Comp Pixel Aspect Ratio 1
Transform Position
Frame X pixels Y pixels Z pixels
'''
return textwrap.dedent(t)
## -- Collection handle
def get_collection_childs_recursive(col, cols=[], include_root=True): def get_collection_childs_recursive(col, cols=[], include_root=True):
'''return a list of all the sub-collections in passed col''' '''return a list of all the sub-collections in passed col'''