First implementation of export helper for AE keyuframe data
parent
d6a102b044
commit
5c69bd0185
|
@ -1,12 +1,22 @@
|
|||
from encodings import utf_8
|
||||
import bpy
|
||||
import json
|
||||
|
||||
# import bpy_extras
|
||||
from bpy_extras.object_utils import world_to_camera_view # as cam_space
|
||||
from bpy_extras.io_utils import ExportHelper
|
||||
from mathutils import Vector
|
||||
from pathlib import Path
|
||||
import json
|
||||
from . import fn
|
||||
|
||||
from bpy.props import (
|
||||
StringProperty,
|
||||
BoolProperty,
|
||||
FloatProperty,
|
||||
EnumProperty,
|
||||
CollectionProperty,
|
||||
)
|
||||
|
||||
'''
|
||||
def Export_AE_2d_position_json_data():
|
||||
scn = bpy.context.scene
|
||||
|
@ -26,6 +36,378 @@ def Export_AE_2d_position_json_data():
|
|||
pix_pos = Vector((pos[0]*rx, (1-pos[1])*ry))
|
||||
'''
|
||||
|
||||
def export_ae_transforms(directory, selection=None, camera=None, exposition=True, prefix='ae_', suffix='', fr=False, export_format='txt'):
|
||||
"""
|
||||
Export After Effects transform data for selected objects and camera in Blender.
|
||||
|
||||
This function exports keyframe data for position, scale, and optional exposition for
|
||||
selected objects and the camera. The data can be exported in TXT and/or JSON formats,
|
||||
which can be imported into Adobe After Effects.
|
||||
|
||||
Parameters:
|
||||
directory (str): The directory path where the exported files will be saved.
|
||||
selection (list, optional): List of Blender objects or pose bones to export. Defaults to None.
|
||||
camera (bpy.types.Object, optional): The camera object to use for calculations. Defaults to the scene's active camera.
|
||||
exposition (bool, optional): Whether to export exposition data. Defaults to True.
|
||||
prefix (str, optional): Prefix for the exported filenames. Defaults to 'ae_'.
|
||||
suffix (str, optional): Suffix for the exported filenames. Defaults to ''.
|
||||
fr (bool, optional): If True, uses French labels for exposition data. Defaults to False.
|
||||
export_format (str, optional): The format(s) to export. Can be 'txt', 'json', or 'txt,json'. Defaults to 'txt'.
|
||||
|
||||
Example usage:
|
||||
export_ae_transforms('/path/to/export', selection=bpy.context.selected_objects, export_format='txt,json')
|
||||
"""
|
||||
|
||||
scn = bpy.context.scene
|
||||
res = Vector((scn.render.resolution_x, scn.render.resolution_y))
|
||||
|
||||
def get_keyframe_data(coords_2d, exposition=False):
|
||||
frame_start, org_2d, _ = coords_2d[0]
|
||||
org_distance = org_2d[-1]
|
||||
|
||||
keyframe_data = {'position': [], 'scale': []}
|
||||
|
||||
for frame, co_2d, is_animated in coords_2d:
|
||||
position = Vector((co_2d[0], 1-co_2d[1]))
|
||||
position *= res # Multiply by resolution to get pixels value
|
||||
keyframe_data['position'].append((frame-frame_start, (round(position[0]), round(position[1]))))
|
||||
|
||||
scale = (org_distance / co_2d[-1]) * 100
|
||||
keyframe_data['scale'].append((frame-frame_start, (round(scale, 5),)*3))
|
||||
|
||||
if is_animated:
|
||||
keyframe_data.setdefault('exposition', []).append(frame-frame_start)
|
||||
|
||||
return keyframe_data
|
||||
|
||||
def get_keyframe_data_txt(keyframe_data, exposition=True):
|
||||
keyframe_data_txt = (
|
||||
'Adobe After Effects 8.0 Keyframe Data\n\n'
|
||||
f'\tUnits Per Second\t{scn.render.fps}\n'
|
||||
f'\tSource Width\t{scn.render.resolution_x}\n'
|
||||
f'\tSource Height\t{scn.render.resolution_y}\n'
|
||||
'\tSource Pixel Aspect Ratio\t1\n'
|
||||
'\tComp Pixel Aspect Ratio\t1\n\n'
|
||||
)
|
||||
|
||||
# Position
|
||||
keyframe_data_txt += (
|
||||
'\nTransform\tPosition\n'
|
||||
'\tFrame\tX pixels\tY pixels\tZ pixels\t\n'
|
||||
)
|
||||
for frame, position in keyframe_data['position']:
|
||||
keyframe_data_txt += f'\t{frame}\t{position[0]}\t{position[1]}\t0\t\n'
|
||||
|
||||
# Scale
|
||||
keyframe_data_txt += (
|
||||
'\nTransform\tScale\n'
|
||||
'\tFrame\tX percent\tY percent\tZ percent\t\n'
|
||||
)
|
||||
for frame, scale in keyframe_data['scale']:
|
||||
keyframe_data_txt += f'\t{frame}\t{scale[0]}\t{scale[1]}\t{scale[2]}\t\n'
|
||||
|
||||
if exposition:
|
||||
if fr:
|
||||
keyframe_data_txt += '\nEffects\tParamètre case #1\tCase #2\t\n'
|
||||
else:
|
||||
keyframe_data_txt += '\nEffects\tCheckbox Control #1\tCheckbox #2\t\n'
|
||||
|
||||
keyframe_data_txt += '\tFrame\t\t\n'
|
||||
|
||||
for i, frame in enumerate(keyframe_data['exposition']):
|
||||
keyframe_data_txt += f'\t{frame}\t{i%2}\t\n'
|
||||
|
||||
keyframe_data_txt += '\nEnd of Keyframe Data\n'
|
||||
|
||||
return keyframe_data_txt
|
||||
|
||||
|
||||
if selection is None:
|
||||
selection = []
|
||||
|
||||
if camera is None:
|
||||
camera = scn.camera
|
||||
|
||||
use_simplify = scn.render.use_simplify
|
||||
simplify_subdivision = scn.render.simplify_subdivision
|
||||
scn.render.use_simplify = True
|
||||
scn.render.simplify_subdivision = 0
|
||||
|
||||
# Get the camera at the first frame for reference
|
||||
scn.frame_set(scn.frame_start)
|
||||
cam_org_3d = camera.matrix_world @ Vector((0, 0, -100))
|
||||
|
||||
camera_coords = []
|
||||
ob_coords = {}
|
||||
animation = {}
|
||||
for i in range(scn.frame_start, scn.frame_end + 1):
|
||||
scn.frame_set(i)
|
||||
|
||||
if camera is not None:
|
||||
co_2d = world_to_camera_view(scn, camera, cam_org_3d)
|
||||
camera_coords.append((i, co_2d, False))
|
||||
|
||||
for item in selection:
|
||||
if isinstance(item, bpy.types.PoseBone):
|
||||
co_3d = item.id_data.matrix_world @ item.matrix.to_translation()
|
||||
name = f'{item.id_data.name}_{item.name}'
|
||||
rig_transforms = [tuple(round(x, 5) for v in b.matrix for x in v) for b in item.id_data.pose.bones]
|
||||
is_animated = rig_transforms != animation.get(name)
|
||||
animation[name] = rig_transforms
|
||||
elif isinstance(item, bpy.types.GreasePencil):
|
||||
co_3d = item.matrix_world.to_translation()
|
||||
## Check if there is keys on any layer
|
||||
is_animated = False
|
||||
## append with is_animated flag to True
|
||||
for l in item.data.layers:
|
||||
is_animated = next((True for f in l.frames if l.frame_number == i), False)
|
||||
if is_animated:
|
||||
break
|
||||
|
||||
else:
|
||||
name = item.name
|
||||
co_3d = item.matrix_world.to_translation()
|
||||
is_animated = co_3d != animation.get(name)
|
||||
animation[name] = co_3d
|
||||
|
||||
co_2d = world_to_camera_view(scn, camera, co_3d)
|
||||
ob_coords.setdefault(name, []).append((i, co_2d, is_animated))
|
||||
|
||||
cam_key_frame_data = get_keyframe_data(camera_coords)
|
||||
objs_keyframe_data = {name: get_keyframe_data(co, exposition=exposition) for name, co in ob_coords.items()}
|
||||
|
||||
if 'json' in export_format or export_format == 'json':
|
||||
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,
|
||||
'objects': objs_keyframe_data
|
||||
}
|
||||
if camera is not None:
|
||||
keyframe_data['camera'] = cam_key_frame_data
|
||||
|
||||
Path(directory, f'{prefix}keyframe_data{suffix}.json').write_text(json.dumps(keyframe_data, indent=4), encoding='utf-8')
|
||||
|
||||
if 'txt' in export_format or export_format == 'txt':
|
||||
for name, keyframe_data in objs_keyframe_data.items():
|
||||
name = name.replace('.', '_')
|
||||
keyframe_data_txt = get_keyframe_data_txt(keyframe_data, exposition=exposition)
|
||||
Path(directory, f'{prefix}{name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
|
||||
|
||||
if camera is not None:
|
||||
keyframe_data_txt = get_keyframe_data_txt(cam_key_frame_data, exposition=False)
|
||||
Path(directory, f'{prefix}{camera.name}{suffix}.txt').write_text(keyframe_data_txt, newline='\r\n')
|
||||
|
||||
scn.render.use_simplify = use_simplify
|
||||
scn.render.simplify_subdivision = simplify_subdivision
|
||||
|
||||
|
||||
class GPEXP_OT_export_anim_to_ae(bpy.types.Operator, ExportHelper):
|
||||
bl_idname = "gp.export_anim_to_ae"
|
||||
bl_label = "Export To After Effects"
|
||||
bl_description = "Export the animation to after effect, object, greasepencil keys exposition, camera"
|
||||
bl_options = {"REGISTER"}
|
||||
|
||||
# filter_glob: StringProperty(default='*.txt;*.json;', options={'HIDDEN'})# *.jpeg;*.png;*.tif;*.tiff;*.bmp
|
||||
filter_glob: StringProperty(default='*.*', options={'HIDDEN'})# *.jpeg;*.png;*.tif;*.tiff;*.bmp
|
||||
|
||||
filename_ext = ''
|
||||
|
||||
filepath : StringProperty(
|
||||
name="File Path",
|
||||
description="File path used for export",
|
||||
maxlen= 1024)
|
||||
|
||||
use_selection: BoolProperty(
|
||||
name="Selected Objects",
|
||||
description="Export selected and visible objects only",
|
||||
default=True,
|
||||
)
|
||||
use_visible: BoolProperty(
|
||||
name='Visible Objects',
|
||||
description='Export visible objects only',
|
||||
default=False
|
||||
)
|
||||
use_active_collection: BoolProperty(
|
||||
name="Active Collection",
|
||||
description="Export only objects from the active collection (and its children)",
|
||||
default=False,
|
||||
)
|
||||
# collection: StringProperty(
|
||||
# name="Source Collection",
|
||||
# description="Export only objects from this collection (and its children)",
|
||||
# default="",
|
||||
# )
|
||||
object_types: EnumProperty(
|
||||
name="Object Types",
|
||||
options={'ENUM_FLAG'},
|
||||
items=(('EMPTY', "Empty", ""),
|
||||
('CAMERA', "Camera", ""),
|
||||
('LIGHT', "Lamp", ""),
|
||||
('ARMATURE', "Armature", "WARNING: not supported in dupli/group instances"),
|
||||
('MESH', "Mesh", ""),
|
||||
('GPENCIL', "Grease Pencil", ""),
|
||||
('OTHER', "Other", "Other geometry types, like curve, metaball, etc. (converted to meshes)"),
|
||||
),
|
||||
description="Which kind of object to export",
|
||||
default={'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'},
|
||||
)
|
||||
exposition: BoolProperty(
|
||||
name='Exposition',
|
||||
description='Export the exposition of the keys',
|
||||
default=False
|
||||
)
|
||||
use_grease_pencil_keys: BoolProperty(
|
||||
name='Grease Pencil Keys',
|
||||
description='Consider grease pencil keys for animated exposition',
|
||||
default=True
|
||||
)
|
||||
use_object_keys: BoolProperty(
|
||||
name='Object Keys',
|
||||
description='Consider object transform keys for animated exposition',
|
||||
default=True
|
||||
)
|
||||
use_active_camera: BoolProperty(
|
||||
name='Active Camera',
|
||||
description='Export active camera keys',
|
||||
default=True
|
||||
)
|
||||
data_lang: EnumProperty(
|
||||
name="AE Language",
|
||||
# options={'ENUM_FLAG'},
|
||||
items=(('FR', "Français", ""),
|
||||
('EN', "Anglais", ""),
|
||||
),
|
||||
description="Language for the exported keyframe data (depend on After Effect language settings)",
|
||||
default='FR',
|
||||
)
|
||||
file_format: EnumProperty(
|
||||
name="File",
|
||||
options={'ENUM_FLAG'},
|
||||
items=(('txt', "txt", ""),
|
||||
('json', "json", ""),
|
||||
),
|
||||
description="File format to export (allow multiple at once)",
|
||||
default={'txt'},
|
||||
)
|
||||
|
||||
# prefix ? (ensure file is prefixed at export), but weird in the the context of an export field
|
||||
|
||||
def draw(self, context):
|
||||
layout = self.layout
|
||||
layout.use_property_split = True
|
||||
layout.use_property_decorate = False # No animation.
|
||||
|
||||
# Are we inside the File browser
|
||||
is_file_browser = context.space_data.type == 'FILE_BROWSER'
|
||||
|
||||
export_main(layout, self, is_file_browser)
|
||||
export_panel_include(layout, self, is_file_browser)
|
||||
|
||||
def execute(self, context):
|
||||
objects_selection = get_object_selection(use_selection=self.use_selection,
|
||||
use_visible=self.use_visible,
|
||||
use_active_collection=self.use_active_collection,
|
||||
object_types=self.object_types
|
||||
)
|
||||
|
||||
for o in objects_selection:
|
||||
print(o.name)
|
||||
# return {"FINISHED"}
|
||||
|
||||
## Find directory
|
||||
output_path = Path(self.filepath)
|
||||
if not output_path.is_dir():
|
||||
output_path = output_path.parent
|
||||
|
||||
print('output_path: ', output_path)
|
||||
cam = None
|
||||
if self.use_active_camera and context.scene.camera:
|
||||
cam = context.scene.camera
|
||||
export_ae_transforms(directory=output_path,
|
||||
selection=objects_selection,
|
||||
camera=cam,
|
||||
exposition=self.exposition,
|
||||
prefix='ae_',
|
||||
fr=self.data_lang == 'FR',
|
||||
export_format=self.file_format)
|
||||
|
||||
self.report({'INFO'}, f'File saved here: {self.filepath}')
|
||||
return {"FINISHED"}
|
||||
|
||||
|
||||
|
||||
def get_object_selection(use_selection=False, use_visible=False, use_active_collection=False, object_types=None):
|
||||
context = bpy.context
|
||||
## determine selection based on filters4
|
||||
source_collection = None
|
||||
if use_active_collection:
|
||||
source_collection = context.view_layer.active_layer_collection.collection
|
||||
# elif collection:
|
||||
# local_collection = bpy.data.collections.get((collection, None))
|
||||
# if local_collection:
|
||||
# source_collection = local_collection
|
||||
# else:
|
||||
# operator.report({'ERROR'}, "Collection '%s' was not found" % collection)
|
||||
# return {'CANCELLED'}
|
||||
|
||||
if source_collection:
|
||||
if use_selection:
|
||||
ctx_objects = tuple(obj for obj in source_collection.all_objects if obj.select_get())
|
||||
else:
|
||||
ctx_objects = source_collection.all_objects
|
||||
else:
|
||||
if use_selection:
|
||||
ctx_objects = context.selected_objects
|
||||
else:
|
||||
ctx_objects = context.view_layer.objects
|
||||
|
||||
if use_visible:
|
||||
ctx_objects = tuple(obj for obj in ctx_objects if obj.visible_get())
|
||||
|
||||
## Filter by object type
|
||||
if object_types is None:
|
||||
object_types = {'EMPTY', 'CAMERA', 'LIGHT', 'ARMATURE', 'MESH', 'GPENCIL', 'OTHER'}
|
||||
|
||||
ctx_objects = [obj for obj in ctx_objects if obj.type in object_types]
|
||||
## Constant for compatible other object type
|
||||
# BLENDER_OTHER_OBJECT_TYPES = {'CURVE', 'SURFACE', 'FONT', 'META'}
|
||||
|
||||
if 'OTHER' in object_types:
|
||||
## Any object that is not in proposed list
|
||||
ctx_objects += [obj for obj in ctx_objects if obj.type not in object_types]
|
||||
|
||||
return ctx_objects
|
||||
|
||||
|
||||
def export_main(layout, operator, is_file_browser):
|
||||
layout.prop(operator, 'exposition')
|
||||
layout.prop(operator, 'use_grease_pencil_keys')
|
||||
layout.prop(operator, 'use_object_keys')
|
||||
layout.prop(operator, 'use_active_camera')
|
||||
|
||||
## Format (language and file)
|
||||
layout.prop(operator, 'data_lang', expand=True)
|
||||
if is_file_browser:
|
||||
layout.column().prop(operator, 'file_format')
|
||||
|
||||
|
||||
def export_panel_include(layout, operator, is_file_browser):
|
||||
header, body = layout.panel("AE_export_include", default_closed=False)
|
||||
header.label(text="Include")
|
||||
if body:
|
||||
sublayout = body.column(heading="Limit to")
|
||||
# if is_file_browser:
|
||||
sublayout.prop(operator, "use_selection")
|
||||
sublayout.prop(operator, "use_visible")
|
||||
sublayout.prop(operator, "use_active_collection")
|
||||
|
||||
body.column().prop(operator, "object_types")
|
||||
# body.prop(operator, "use_custom_props")
|
||||
|
||||
### --- Old functions
|
||||
|
||||
# Unused (old func that might not be usefull at all...)
|
||||
def correct_shift(vec, cam):
|
||||
|
@ -252,18 +634,23 @@ class GPEXP_PT_extra_gprender_func(bpy.types.Panel):
|
|||
# layout = self.layout
|
||||
# layout.operator("gp.fix_overscan_shift")
|
||||
|
||||
def export_ae_anim_menu(self, context):
|
||||
self.layout.operator('gp.export_anim_to_ae', text='After Effects Keyframe Data (.txt|.json)')
|
||||
|
||||
|
||||
classes=(
|
||||
GPEXP_OT_export_keys_to_ae,
|
||||
GPEXP_OT_export_cam_keys_to_ae,
|
||||
GPEXP_OT_fix_overscan_shift,
|
||||
GPEXP_PT_extra_gprender_func
|
||||
GPEXP_PT_extra_gprender_func,
|
||||
GPEXP_OT_export_anim_to_ae,
|
||||
)
|
||||
|
||||
def register():
|
||||
for cls in classes:
|
||||
bpy.utils.register_class(cls)
|
||||
|
||||
bpy.types.TOPBAR_MT_file_export.append(export_ae_anim_menu)
|
||||
# if hasattr(bpy.types, 'RENDER_PT_overscan'):
|
||||
# bpy.types.RENDER_PT_overscan.append(overcan_shift_fix_ui)
|
||||
|
||||
|
@ -274,3 +661,4 @@ def unregister():
|
|||
|
||||
for cls in reversed(classes):
|
||||
bpy.utils.unregister_class(cls)
|
||||
bpy.types.TOPBAR_MT_file_export.remove(export_ae_anim_menu)
|
||||
|
|
Loading…
Reference in New Issue