From d7d97b77592d702aaac4ab7b7e08e509bd7c4387 Mon Sep 17 00:00:00 2001 From: Bartek Skorupa Date: Wed, 26 Oct 2011 09:40:10 +0000 Subject: adding After Effects Exporter to trunk --- io_export_after_effects.py | 403 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 io_export_after_effects.py (limited to 'io_export_after_effects.py') diff --git a/io_export_after_effects.py b/io_export_after_effects.py new file mode 100644 index 00000000..580cc203 --- /dev/null +++ b/io_export_after_effects.py @@ -0,0 +1,403 @@ +# ***** BEGIN GPL LICENSE BLOCK ***** +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# +# The Original Code is: all of this file. +# +# ***** END GPL LICENSE BLOCK ***** +# +bl_info = { + 'name': 'Export: Adobe After Effects (.jsx)', + 'description': 'Export selected cameras, objects & bundles to Adobe After Effects CS3 and above', + 'author': 'Bartek Skorupa', + 'version': (0, 55), + 'blender': (2, 6, 0), + 'api': 41098, + 'location': 'File > Export > Adobe After Effects (.jsx)', + 'category': 'Import-Export', + "warning": "", + "wiki_url": "http://wiki.blender.org/index.php/Extensions:2.5/Py/Scripts/Import-Export/Adobe_After_Effects" + } + + +from math import pi +import bpy +import datetime + + +# create list of static blender's data +def get_comp_data(context): + scene = context.scene + aspect_x = scene.render.pixel_aspect_x + aspect_y = scene.render.pixel_aspect_y + aspect = aspect_x / aspect_y + fps = scene.render.fps + + return { + 'scn': scene, + 'width': scene.render.resolution_x, + 'height': scene.render.resolution_y, + 'aspect': aspect, + 'fps': fps, + 'start': scene.frame_start, + 'end': scene.frame_end, + 'duration': (scene.frame_end - scene.frame_start + 1.0) / fps, + 'curframe': scene.frame_current, + } + + +# create managable list of selected objects +# (only selected objects will be analyzed and exported) +def get_selected(context, prefix): + cameras = [] # list of selected cameras + cams_names = [] # list of selected cameras' names (prevent from calling "ConvertName(ob)" function too many times) + nulls = [] # list of all selected objects exept cameras (will be used to create nulls in AE) + nulls_names = [] # list of above objects names (prevent from calling "ConvertName(ob)" function too many times) + obs = context.selected_objects + + for ob in obs: + if ob.type == 'CAMERA': + cameras.append(ob) + cams_names.append(convert_name(ob, prefix)) + else: + nulls.append(ob) + nulls_names.append(convert_name(ob, prefix)) + + selection = { + 'cameras': cameras, + 'cams_names': cams_names, + 'nulls': nulls, + 'nulls_names': nulls_names, + } + + return selection + + +# convert names of objects to avoid errors in AE. Add user specified prefix +def convert_name(ob, prefix): + ob_name = ob.name + for c in (" ", ".", ",", "-", "=", "+", "*"): + ob_name = ob_name.replace(c, "_") + + return prefix + ob_name + + +# get object's blender's location and rotation and return AE's Position and Rotation/Orientation +# this function will be called for every object for every frame +def convert_pos_rot_matrix(matrix, width, height, aspect, x_rot_correction=False): + + # get blender location for ob + b_loc_x, b_loc_y, b_loc_z = matrix.to_translation() + b_rot_x, b_rot_y, b_rot_z = matrix.to_euler() + + # get blender rotation for ob + if x_rot_correction: + b_rot_x = b_rot_x / pi * 180.0 - 90.0 + else: + b_rot_x = b_rot_x / pi * 180.0 + b_rot_y = b_rot_y / pi * 180.0 + b_rot_z = b_rot_z / pi * 180.0 + + # convert to AE Position and Rotation + # Axes in AE are different. AE's X is blender's X, AE's Y is negative Blender's Z, AE's Z is Blender's Y + x = (b_loc_x * 100.0) / aspect + width / 2.0 # calculate AE's X position + y = (-b_loc_z * 100.0) + (height / 2.0) # calculate AE's Y position + z = b_loc_y * 100.0 # calculate AE's Z position + rx = b_rot_x # calculate AE's X rotation. Will become AE's RotationX property + ry = -b_rot_z # calculate AE's Y rotation. Will become AE's OrientationY property + rz = b_rot_y # calculate AE's Z rotation. Will become AE's OrentationZ property + # Using AE's rotation combined with AE's orientation allows to compensate for different euler rotation order. + + return x, y, z, rx, ry, rz + + +def convert_pos_rot(obj, width, height, aspect, x_rot_correction=False): + matrix = obj.matrix_world.copy() + return convert_pos_rot_matrix(matrix, width, height, aspect, x_rot_correction) + + +# get camera's lens and convert to AE's "zoom" value in pixels +# this function will be called for every camera for every frame +# +# +# AE's lens is defined by "zoom" in pixels. Zoom determines focal angle or focal length. +# AE's camera's focal length is calculated basing on zoom value. +# +# Known values: +# - sensor (blender's sensor is 32mm) +# - lens (blender's lens in mm) +# - width (witdh of the composition/scene in pixels) +# +# zoom can be calculated from simple proportions. +# +# | +# / | +# / | +# / | w +# s |\ / | i +# e | \ / | d +# n | \ / | t +# s | / \ | h +# o | / \ | +# r |/ \ | +# \ | +# | | \ | +# | | \ | +# | | | +# lens | zoom +# +# zoom/width = lens/sensor => +# zoom = lens/sensor*width = lens*width * (1/sensor) +# sensor - sensor_width will be taken into account if version of blender supports it. If not - standard blender's 32mm will be caclulated. +# +# +# above is true if square pixels are used. If not - aspect compensation is needed, so final formula is: +# zoom = lens * width * (1/sensor) * aspect +# +def convert_lens(camera, width, aspect): + # wrap camera.data.sensor_width in 'try' to maintain compatibility with blender version not supporting camera.data.sensor_width + try: + sensor = camera.data.sensor_width # if camera.data.sensor_width is supported - it will be taken into account + except: + sensor = 32 # if version of blender doesn't yet support sensor_width - default blender's 32mm will be taken. + zoom = camera.data.lens * width * (1.0 / sensor) * aspect + + return zoom + + +# jsx script for AE creation +def write_jsx_file(file, data, selection, export_bundles, comp_name, prefix): + from mathutils import Matrix + + print("\n---------------------------\n- Export to After Effects -\n---------------------------") + #store the current frame to restore it at the enf of export + curframe = data['curframe'] + #create array which will contain all keyframes values + js_data = { + 'times': '', + 'cameras': {}, + 'objects': {}, + } + + # create camera structure + for i, cam in enumerate(selection['cameras']): # more than one camera can be selected + name_ae = selection['cams_names'][i] + js_data['cameras'][name_ae] = { + 'position': '', + 'pointOfInterest': '', + 'orientation': '', + 'rotationX': '', + 'zoom': '', + } + + # create object structure + for i, obj in enumerate(selection['nulls']): # nulls representing blender's obs except cameras + name_ae = selection['nulls_names'][i] + js_data['objects'][name_ae] = { + 'position': '', + 'orientation': '', + 'rotationX': '', + } + + # get all keyframes for each objects and store into dico + for frame in range(data['start'], data['end'] + 1): + print("working on frame: " + str(frame)) + data['scn'].frame_set(frame) + + #get time for this loop + js_data['times'] += '%f ,' % ((frame - data['start']) / data['fps']) + + # keyframes for all cameras + for i, cam in enumerate(selection['cameras']): + #get cam name + name_ae = selection['cams_names'][i] + #convert cam position to AE space + ae_pos_rot = convert_pos_rot(cam, data['width'], data['height'], data['aspect'], x_rot_correction=True) + #convert Blender's cam zoom to AE's + zoom = convert_lens(cam, data['width'], data['aspect']) + #store all the value into dico + js_data['cameras'][name_ae]['position'] += '[%f,%f,%f],' % (ae_pos_rot[0], ae_pos_rot[1], ae_pos_rot[2]) + js_data['cameras'][name_ae]['pointOfInterest'] += '[%f,%f,%f],' % (ae_pos_rot[0], ae_pos_rot[1], ae_pos_rot[2]) + js_data['cameras'][name_ae]['orientation'] += '[%f,%f,%f],' % (0, ae_pos_rot[4], ae_pos_rot[5]) + js_data['cameras'][name_ae]['rotationX'] += '%f ,' % (ae_pos_rot[3]) + js_data['cameras'][name_ae]['zoom'] += '[%f],' % (zoom) + + #keyframes for all nulls + for i, ob in enumerate(selection['nulls']): + #get object name + name_ae = selection['nulls_names'][i] + #convert ob position to AE space + ae_pos_rot = convert_pos_rot(ob, data['width'], data['height'], data['aspect'], x_rot_correction=False) + #store all datas into dico + js_data['objects'][name_ae]['position'] += '[%f,%f,%f],' % (ae_pos_rot[0], ae_pos_rot[1], ae_pos_rot[2]) + js_data['objects'][name_ae]['orientation'] += '[%f,%f,%f],' % (0, ae_pos_rot[4], ae_pos_rot[5]) + js_data['objects'][name_ae]['rotationX'] += '%f ,' % (ae_pos_rot[3]) + + # ---- write JSX file + jsx_file = open(file, 'w') + + # make the jsx executable in After Effects (enable double click on jsx) + jsx_file.write('#target AfterEffects\n\n') + jsx_file.write('/**************************************\n') + jsx_file.write('Scene : %s\n' % data['scn'].name) + jsx_file.write('Resolution : %i x %i\n' % (data['width'], data['height'])) + jsx_file.write('Duration : %f\n' % (data['duration'])) + jsx_file.write('FPS : %f\n' % (data['fps'])) + jsx_file.write('Date : %s\n' % datetime.datetime.now()) + jsx_file.write('Exported with io_export_after_effects.py\n') + jsx_file.write('**************************************/\n\n\n\n') + + #wrap in function + jsx_file.write("function compFromBlender(){\n") + # create new comp + jsx_file.write('\nvar compName = "%s";' % (comp_name)) + jsx_file.write('\nvar newComp = app.project.items.addComp(compName, %i, %i, %f, %f, %i);\n\n\n' % + (data['width'], data['height'], data['aspect'], data['duration'], data['fps'])) + + # create cameras + jsx_file.write('// ************** CAMERAS **************\n\n\n') + for i, cam in enumerate(js_data['cameras']): # more than one camera can be selected + name_ae = cam + jsx_file.write('var %s = newComp.layers.addCamera("%s",[0,0]);\n' % (name_ae, name_ae)) + jsx_file.write('%s.property("position").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['position'])) + jsx_file.write('%s.property("pointOfInterest").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['pointOfInterest'])) + jsx_file.write('%s.property("orientation").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['orientation'])) + jsx_file.write('%s.property("rotationX").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['rotationX'])) + jsx_file.write('%s.property("rotationY").setValue(0);\n' % name_ae) + jsx_file.write('%s.property("rotationZ").setValue(0);\n' % name_ae) + jsx_file.write('%s.property("zoom").setValuesAtTimes([%s],[%s]);\n\n\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['zoom'])) + + # create objects + jsx_file.write('// ************** OBJECTS **************\n\n\n') + for i, obj in enumerate(js_data['objects']): # more than one camera can be selected + name_ae = obj + jsx_file.write('var %s = newComp.layers.addNull();\n' % (name_ae)) + jsx_file.write('%s.threeDLayer = true;\n' % name_ae) + jsx_file.write('%s.source.name = "%s";\n' % (name_ae, name_ae)) + jsx_file.write('%s.property("position").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['objects'][obj]['position'])) + jsx_file.write('%s.property("orientation").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['objects'][obj]['orientation'])) + jsx_file.write('%s.property("rotationX").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['objects'][obj]['rotationX'])) + jsx_file.write('%s.property("rotationY").setValue(0);\n' % name_ae) + jsx_file.write('%s.property("rotationZ").setValue(0);\n\n\n' % name_ae) + + # create Bundles + if export_bundles: + + jsx_file.write('// ************** BUNDLES (3d tracks) **************\n\n\n') + + #Bundles are linked to MovieClip, so we have to find which MC is linked to our selected camera (if any?) + mc = '' + + #go through each selected Cameras + for cam in selection['cameras']: + #go through each constrains of this camera + for constrain in cam.constraints: + #does the camera have a Camera Solver constrain + if constrain.type == 'CAMERA_SOLVER': + #Which movie clip does it use ? + if constrain.use_default_clip: + mc = data['scn'].clip + else: + mc = constrain.clip + + #go throuhg each tracking point + for track in mc.tracking.tracks: + #is this tracking point has a Bundles (does it's 3D position has been solved) + if track.has_bundle: + # bundle are in camera space, so transpose it to world space + matrix = Matrix.Translation(cam.matrix_basis * track.bundle) + #convert the position into AE space + ae_pos_rot = convert_pos_rot_matrix(matrix, data['width'], data['height'], data['aspect'], x_rot_correction=False) + #get the name of the tracker + name_ae = convert_name(track, prefix) + #write JS script for this Bundle + jsx_file.write('var %s = newComp.layers.addNull();\n' % name_ae) + jsx_file.write('%s.threeDLayer = true;\n' % name_ae) + jsx_file.write('%s.source.name = "%s";\n' % (name_ae, name_ae)) + jsx_file.write('%s.property("position").setValue([%f,%f,%f]);\n\n\n' % (name_ae, ae_pos_rot[0], ae_pos_rot[1], ae_pos_rot[2])) + + jsx_file.write("}\n\n\n") + jsx_file.write('app.beginUndoGroup("Import Blender animation data");\n') + jsx_file.write('compFromBlender();\n') + jsx_file.write('app.endUndoGroup();\n\n\n') + jsx_file.close() + + data['scn'].frame_set(curframe) # set current frame of animation in blender to state before export + +########################################## +# DO IT +########################################## + + +def main(file, context, export_bundles, comp_name, prefix): + data = get_comp_data(context) + selection = get_selected(context, prefix) + write_jsx_file(file, data, selection, export_bundles, comp_name, prefix) + print ("\nExport to After Effects Completed") + return {'FINISHED'} + +########################################## +# ExportJsx class register/unregister +########################################## + +from bpy_extras.io_utils import ExportHelper +from bpy.props import StringProperty, BoolProperty + + +class ExportJsx(bpy.types.Operator, ExportHelper): + '''Export selected cameras and objects animation to After Effects''' + bl_idname = "export.jsx" + bl_label = "Export to Adobe After Effects" + filename_ext = ".jsx" + filter_glob = StringProperty(default="*.jsx", options={'HIDDEN'}) + + comp_name = StringProperty( + name="Comp Name", + description="Name of composition to be created in After Effects", + default="BlendComp" + ) + prefix = StringProperty( + name="Layer's Prefix", + description="Prefix to use before AE layer's name", + #default="bl_" + ) + export_bundles = BoolProperty( + name="Export Bundles", + description="Export 3D Tracking points of a selected camera", + default=False, + ) + + @classmethod + def poll(cls, context): + return context.active_object is not None + + def execute(self, context): + return main(self.filepath, context, self.export_bundles, self.comp_name, self.prefix) + + +def menu_func(self, context): + self.layout.operator(ExportJsx.bl_idname, text="Adobe After Effects (.jsx)") + + +def register(): + bpy.utils.register_class(ExportJsx) + bpy.types.INFO_MT_file_export.append(menu_func) + + +def unregister(): + bpy.utils.unregister_class(ExportJsx) + bpy.types.INFO_MT_file_export.remove(menu_func) + +if __name__ == "__main__": + register() -- cgit v1.2.3