From c5ea98b4d0f3439fcef3cc334b301704abf00285 Mon Sep 17 00:00:00 2001 From: Pullusb Date: Wed, 9 Feb 2022 12:27:29 +0100 Subject: [PATCH] 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 --- CHANGELOG.md | 5 +++ OP_export_to_ae.py | 90 ++++++++++++++++++++++++++++++++++------------ __init__.py | 2 +- fn.py | 80 ++++++++++++++++++++++++++++++++++++----- 4 files changed, 144 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a3dfdb..c76aa62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,11 @@ Activate / deactivate layer opaticty according to prefix 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 - feat: `Select Nodes` added in Dopesheet. Select nodes associated with selected gp layers and report if there are errors diff --git a/OP_export_to_ae.py b/OP_export_to_ae.py index 2080261..97da4d8 100644 --- a/OP_export_to_ae.py +++ b/OP_export_to_ae.py @@ -43,40 +43,33 @@ def correct_shift(vec, cam): def export_AE_objects_position_keys(): '''Export keys as paperclip to paste in after''' - C= bpy.context + + scn = bpy.context.scene 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: if not result.get(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 ? - x = (proj2d[0]) * C.scene.render.resolution_x - y = -(proj2d[1]) * C.scene.render.resolution_y + C.scene.render.resolution_y + # proj2d = correct_shift(proj2d, scn.camera) # needed ? + x = (proj2d[0]) * scn.render.resolution_x + y = -(proj2d[1]) * scn.render.resolution_y + scn.render.resolution_y result[o.name].append((fr,x,y)) for name,value in result.items(): - - prefix = 'Adobe After Effects 8.0 Keyframe Data\n\n' - 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' + + txt = fn.get_ae_keyframe_clipboard_header(scn) 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' - prefix += 'End of Keyframe Data\n' + txt += '\n\nEnd of Keyframe Data\n' # keyframe txt footer blend = Path(bpy.data.filepath) keyfile = blend.parent / 'render' / f'pos_{name}.txt' @@ -84,11 +77,12 @@ def export_AE_objects_position_keys(): print(f'exporting keys for {name}') with open(keyfile, 'w') as fd: - fd.write(prefix) + fd.write(txt) + class GPEXP_OT_export_keys_to_ae(bpy.types.Operator): 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_options = {"REGISTER"} @@ -101,6 +95,53 @@ class GPEXP_OT_export_keys_to_ae(bpy.types.Operator): 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): bl_idname = "gp.fix_overscan_shift" 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): layout = self.layout - layout.operator("gp.fix_overscan_shift") - layout.operator("gp.export_keys_to_ae") + col = layout.column() + 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=( GPEXP_OT_export_keys_to_ae, +GPEXP_OT_export_cam_keys_to_ae, GPEXP_OT_fix_overscan_shift, GPEXP_PT_extra_gprender_func ) diff --git a/__init__.py b/__init__.py index cb87712..d8d3195 100644 --- a/__init__.py +++ b/__init__.py @@ -2,7 +2,7 @@ bl_info = { "name": "GP Render", "description": "Organise export of gp layers through compositor output", "author": "Samuel Bernou", - "version": (0, 9, 7), + "version": (0, 9, 8), "blender": (2, 93, 0), "location": "View3D", "warning": "", diff --git a/fn.py b/fn.py index 66236b5..b45a2be 100644 --- a/fn.py +++ b/fn.py @@ -9,6 +9,9 @@ from collections import defaultdict from time import time import json + +### -- node basic + def create_node(type, tree=None, **kargs): '''Get a type, a tree to add in, and optionnaly multiple attribute to set return created node @@ -70,6 +73,8 @@ def create_aa_nodegroup(tree): return ng +## -- object and scene settings + 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', @@ -95,7 +100,6 @@ def copy_settings(obj_a, obj_b): # print(f"can't set {attr}") pass - def set_file_output_format(fo): fo.format.file_format = 'OPEN_EXR' fo.format.color_mode = 'RGBA' @@ -108,7 +112,6 @@ def set_file_output_format(fo): # fo.format.color_depth = '8' # fo.format.compression = 15 - def set_scene_aa_settings(scene=None, aa=True): '''aa == using native AA, else disable scene AA''' if not scene: @@ -229,7 +232,8 @@ def get_view_layer(name, scene=None): pass_vl = scene.view_layers.new(name) return pass_vl -### node location tweaks + +## -- node location tweaks def real_loc(n): if not n.parent: @@ -260,7 +264,7 @@ def get_frame_transform(f, node_tree=None): return loc, dim -## get all frames with their real transform. +## -- get all frames with their real transform. def bbox(f, frames): xs=[] @@ -322,7 +326,7 @@ def get_frames_bbox(node_tree): return frames_bbox -## nodes helper functions +## -- nodes helper functions def clear_nodegroup(name, full_clear=False): '''remove duplication of a nodegroup (.???) @@ -359,7 +363,6 @@ def rearrange_rlayers_in_frames(node_tree): rl.location.y = top top -= rl.dimensions.y + 20 # place next down by height + gap of 20 - def rearrange_frames(node_tree): frame_d = get_frames_bbox(node_tree) # dic : {frame_node:(loc vector, dimensions vector), ...} if not frame_d: @@ -616,7 +619,7 @@ def nodegroup_merge_inputs(ngroup): out = ngroup.outputs.new('NodeSocketColor', ngroup.inputs[0].name) ngroup.links.new(aa.outputs[0], ng_out.inputs[0]) -## --- renumbering funcs --- +## -- renumbering funcs def get_numbered_output(out, slot_name): '''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) +## -- camera framing and object anim checks + def get_bbox_3d(ob): bbox_coords = ob.bound_box 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) 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 @@ -1159,7 +1163,6 @@ def get_bbox_2d(ob, cam=None): 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 @@ -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) +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): '''return a list of all the sub-collections in passed col'''