diff --git a/CHANGELOG.md b/CHANGELOG.md index 161ff33..0bd1d26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,13 @@ Activate / deactivate layer opaticty according to prefix Activate / deactivate all masks using MA layers --> + +0.6.5 + +- feat: AE key exporter (not exposed). + - Add a basic 2D positions-keyframes exporter (in camera view space). export 2d position of selected object origin to copy-paste on AE layer. +- feat: If 'Camera Overscan' addon is activated, append a button to fix camera shift. + 0.6.4 - ui: render selected scene has hints on popup panekl like gen batch diff --git a/OP_export_to_ae.py b/OP_export_to_ae.py new file mode 100644 index 0000000..5e8173c --- /dev/null +++ b/OP_export_to_ae.py @@ -0,0 +1,206 @@ +import bpy +# import bpy_extras +from bpy_extras.object_utils import world_to_camera_view # as cam_space +from mathutils import Vector +from pathlib import Path +import json +from . import fn + +''' +def Export_AE_2d_position_json_data(): + scn = bpy.context.scene + cam = scn.objects.get('anim_cam') + if not cam: + print('Active camera not "anim_cam"') + cam = scn.camera + + rx = scn.render + rx, ry = rd.resolution_x, rd.resolution_y + + targets = [o for o in bpy.context.selected_objects if o.type != 'CAMERA'] + + for ob in targets: + # get an idea of the scale relative to image res ? (too complex since it's not even the right resolution...) + pos = world_to_camera_view(scn, scn.objects['anim_cam'], ob.matrix_world.to_translation())[:-1] + pix_pos = Vector((pos[0]*rx, (1-pos[1])*ry)) +''' + + +# Unused (old func that might not be usefull at all...) +def correct_shift(vec, cam): + resX = bpy.context.scene.render.resolution_x + resY = bpy.context.scene.render.resolution_y + ratio = resX/resY + shiftX = 2*cam.data.shift_x + shiftY = 2*cam.data.shift_y + + if ratio<1: + return vec - Vector((shiftX*(1/ratio), shiftY, 0)) + elif ratio>1: + return vec - Vector((shiftX, shiftY*ratio, 0)) + else: + return vec - Vector((shiftX, shiftY, 0)) + +def export_AE_objects_position_keys(): + '''Export keys as paperclip to paste in after''' + C= bpy.context + result = {} + + for fr in range(C.scene.frame_start,C.scene.frame_end + 1): + + C.scene.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 = 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 + + 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' + + for v in value: + prefix += '\t%s\t%s\t%s\t\n'%(v[0],v[1],v[2]) + + prefix += '\n\n' + prefix += 'End of Keyframe Data\n' + + blend = Path(bpy.data.filepath) + keyfile = blend.parent / 'render' / f'pos_{name}.txt' + + print(f'exporting keys for {name}') + with open(keyfile, 'w') as fd: + fd.write(prefix) + +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_description = "Export selected objects 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): + export_AE_objects_position_keys() + 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" + bl_description = "(Gp render operator) change shift values to re-center overscan" + bl_options = {"REGISTER"} + + # @classmethod + # def poll(cls, context): + # return context.object + + init_rx : bpy.props.IntProperty(name='pre-overscan res x') + init_ry : bpy.props.IntProperty(name='pre-overscan res y') + + def invoke(self, context, event): + # if not context.object: + # self.report({'ERROR'}, 'No object selected') + # return {'CANCELLED'} + + self.use_selection = False + if context.active_object and context.active_object.type == 'CAMERA' and context.active_object != context.scene.camera: + self.use_selection = True + self.cam_ob = context.active_object + else: + self.cam_ob = context.scene.camera + + self.init_rx = context.scene.render.resolution_x + self.init_ry = context.scene.render.resolution_y + + return context.window_manager.invoke_props_dialog(self) + + def draw(self, context): + layout = self.layout + + if self.use_selection: + col = layout.column() + col.label(text=f'Camera "{self.cam_ob.name}" selected', icon='INFO') + col.label(text='Change in shifts will apply on this one', icon='BLANK1') + + col = layout.column() + col.label(text='Overscan res (current)') + col.label(text=f'{context.scene.render.resolution_x} x {context.scene.render.resolution_y}') + + col = layout.column() + col.label(text='Enter Initial (pre-overscan) resolution:') + row = col.row(align=True) + row.prop(self, 'init_rx', text='') + row.prop(self, 'init_ry', text='') + + def execute(self, context): + cam = self.cam_ob.data + + ratio_x = self.init_rx / context.scene.render.resolution_x + ratio_y = self.init_ry / context.scene.render.resolution_y + + if ratio_x == 1 and ratio_y == 1: + self.report({'ERROR'}, 'Same init and overscan resolution, nothing to change') + return {'CANCELLED'} + + if ratio_x != 1: + if fn.has_keyframe(cam, 'shift_x'): + fcu = cam.animation_data.action.fcurves.find('shift_x') + for k in fcu.keyframe_points: + k.y = k.y * ratio_x + else: + if cam.shift_x != 1: + cam.shift_x = cam.shift_x * ratio_x + + if ratio_y != 1: + if fn.has_keyframe(cam, 'shift_y'): + fcu = cam.animation_data.action.fcurves.find('shift_y') + for k in fcu.keyframe_points: + k.y = k.y * ratio_y + else: + if cam.shift_y != 1: + cam.shift_y = cam.shift_y * ratio_y + + return {"FINISHED"} + + +def overcan_shift_fix_ui(self, context): + layout = self.layout + layout.operator("gp.fix_overscan_shift") + + +classes=( +GPEXP_OT_export_keys_to_ae, +GPEXP_OT_fix_overscan_shift, +) + +def register(): + for cls in classes: + bpy.utils.register_class(cls) + + if hasattr(bpy.types, 'RENDER_PT_overscan'): + bpy.types.RENDER_PT_overscan.append(overcan_shift_fix_ui) + + +def unregister(): + if hasattr(bpy.types, 'RENDER_PT_overscan'): + bpy.types.RENDER_PT_overscan.remove(overcan_shift_fix_ui) + + for cls in reversed(classes): + bpy.utils.unregister_class(cls) + \ No newline at end of file diff --git a/__init__.py b/__init__.py index 57b95dc..0f02356 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, 6, 4), + "version": (0, 6, 5), "blender": (2, 93, 0), "location": "View3D", "warning": "", @@ -21,6 +21,7 @@ from . import OP_crop_to_object from . import OP_render_scenes # from . import OP_check_layer_status from . import OP_render_pdf +from . import OP_export_to_ae from . import prefs from . import OP_setup_layers from . import ui @@ -48,6 +49,7 @@ def register(): OP_render_scenes.register() # OP_check_layer_status.register() OP_render_pdf.register() + OP_export_to_ae.register() OP_setup_layers.register() ui.register() # bpy.types.Scene.pgroup_name = bpy.props.PointerProperty(type = PROJ_PGT_settings) @@ -67,6 +69,7 @@ def unregister(): ui.unregister() OP_setup_layers.unregister() # OP_check_layer_status.unregister() + OP_export_to_ae.unregister() OP_render_pdf.unregister() OP_render_scenes.unregister() OP_crop_to_object.unregister() diff --git a/fn.py b/fn.py index 55da25c..101298e 100644 --- a/fn.py +++ b/fn.py @@ -920,6 +920,14 @@ 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 has_keyframe(ob, attr): + anim = ob.animation_data + if anim is not None and anim.action is not None: + for fcu in anim.action.fcurves: + if fcu.data_path == attr: + return len(fcu.keyframe_points) > 0 + return False + def get_gp_box_all_frame_selection(oblist=None, scn=None, cam=None, timeout=40): ''' get points of all selection