# ##### 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 2 # 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, write to the Free Software Foundation, # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. # # ##### END GPL LICENSE BLOCK ##### bl_info = { "name": "Export: Adobe After Effects (.jsx)", "description": "Export cameras, selected objects & camera solution " "3D Markers to Adobe After Effects CS3 and above", "author": "Bartek Skorupa, Damien Picard (@pioverfour)", "version": (0, 1, 2), "blender": (2, 80, 0), "location": "File > Export > Adobe After Effects (.jsx)", "warning": "", "doc_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/" "Scripts/Import-Export/Adobe_After_Effects", "category": "Import-Export", } import bpy import os import datetime from math import degrees from mathutils import Matrix, Vector, Color def get_camera_frame_ranges(scene, start, end): """Get frame ranges for each marker in the timeline For this, start at the end of the timeline, iterate through each camera-bound marker in reverse, and get the range from this marker to the end of the previous range. """ markers = sorted((m for m in scene.timeline_markers if m.camera is not None), key=lambda m:m.frame, reverse=True) if len(markers) <= 1: return [[[start, end], scene.camera],] camera_frame_ranges = [] current_frame = end for m in markers: if m.frame < current_frame: camera_frame_ranges.append([[m.frame, current_frame + 1], m.camera]) current_frame = m.frame - 1 camera_frame_ranges.reverse() camera_frame_ranges[0][0][0] = start return camera_frame_ranges class ObjectExport(): """Base exporter class Collects data about an object and outputs the proper JSX script for AE. """ def __init__(self, obj): self.obj = obj self.name_ae = convert_name(self.obj.name) self.keyframes = {} def get_prop_keyframe(self, prop_name, value, time): """Get keyframe for given property, only if different from previous value""" prop_keys = self.keyframes.setdefault(prop_name, []) if len(prop_keys) == 0: prop_keys.append([time, value, False]) return if value != prop_keys[-1][1]: prop_keys.append([time, value, False]) # Store which keys should hold, that is, which are # the first in a series of identical values else: prop_keys[-1][2] = True def get_keyframe(self, context, width, height, aspect, time, ae_size): """Store animation for the current frame""" ae_transform = convert_transform_matrix(self.obj.matrix_world, width, height, aspect, ae_size) self.get_prop_keyframe('position', ae_transform[0:3], time) self.get_prop_keyframe('orientation', ae_transform[3:6], time) self.get_prop_keyframe('scale', ae_transform[6:9], time) def get_obj_script(self, include_animation): """Get the JSX script for the object""" return self.get_type_script() + self.get_anim_script(include_animation) + self.get_post_script() def get_type_script(self): """Get the basic part of the JSX script""" type_script = f'var {self.name_ae} = newComp.layers.addNull();\n' type_script += f'{self.name_ae}.threeDLayer = true;\n' type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' return type_script def get_anim_script(self, include_animation): """Get the part of the JSX script encoding animation""" anim_script = "" # Set values of properties, add keyframes only where needed for prop, keys in self.keyframes.items(): if include_animation and len(keys) > 1: times = ",".join(str(k[0]) for k in keys) values = ",".join(str(k[1]) for k in keys).replace(" ", "") anim_script += ( f'{self.name_ae}.property("{prop}").setValuesAtTimes([{times}],[{values}]);\n') # Set to HOLD the frames after which animation is fixed # for several frames, to avoid interpolation errors if any(k[2] for k in keys): anim_script += ( f'var hold_frames = {[i + 1 for i, k in enumerate(keys) if k[2]]};\n' 'for (var i = 0; i < hold_frames.length; i++) {\n' f' {self.name_ae}.property("{prop}").setInterpolationTypeAtKey(hold_frames[i], KeyframeInterpolationType.HOLD);\n' '}\n') # No animation for this property else: value = str(keys[0][1]).replace(" ", "") anim_script += ( f'{self.name_ae}.property("{prop}").setValue({value});\n') anim_script += '\n' return anim_script def get_post_script(self): """This is only used in lights as a post-treatment after animation""" return "" class CameraExport(ObjectExport): def __init__(self, obj, start_time=None, end_time=None): super().__init__(obj) self.start_time = start_time self.end_time = end_time def get_keyframe(self, context, width, height, aspect, time, ae_size): ae_transform = convert_transform_matrix(self.obj.matrix_world, width, height, aspect, ae_size) zoom = convert_lens(self.obj, width, height, aspect) self.get_prop_keyframe('position', ae_transform[0:3], time) self.get_prop_keyframe('orientation', ae_transform[3:6], time) self.get_prop_keyframe('zoom', zoom, time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addCamera("{self.name_ae}",[0,0]);\n' # Restrict time range when multiple cameras are used (markers) if self.start_time is not None: type_script += f'{self.name_ae}.inPoint = {self.start_time};\n' type_script += f'{self.name_ae}.outPoint = {self.end_time};\n' type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' return type_script class LightExport(ObjectExport): def get_keyframe(self, context, width, height, aspect, time, ae_size): ae_transform = convert_transform_matrix(self.obj.matrix_world, width, height, aspect, ae_size) self.type = self.obj.data.type color = list(self.obj.data.color) intensity = self.obj.data.energy * 10.0 self.get_prop_keyframe('position', ae_transform[0:3], time) if self.type in {'SPOT', 'SUN'}: self.get_prop_keyframe('orientation', ae_transform[3:6], time) self.get_prop_keyframe('intensity', intensity, time) self.get_prop_keyframe('Color', color, time) if self.type == 'SPOT': cone_angle = degrees(self.obj.data.spot_size) self.get_prop_keyframe('Cone Angle', cone_angle, time) cone_feather = self.obj.data.spot_blend * 100.0 self.get_prop_keyframe('Cone Feather', cone_feather, time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addLight("{self.name_ae}", [0.0, 0.0]);\n' type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' type_script += f'{self.name_ae}.lightType = LightType.SPOT;\n' return type_script def get_post_script(self): """Set light type _after_ the orientation, otherwise the property is hidden in AE...""" if self.obj.data.type == 'SUN': post_script = f'{self.name_ae}.lightType = LightType.PARALLEL;\n' elif self.obj.data.type == 'SPOT': post_script = f'{self.name_ae}.lightType = LightType.SPOT;\n' else: post_script = f'{self.name_ae}.lightType = LightType.POINT;\n' return post_script class ImageExport(ObjectExport): def get_keyframe(self, context, width, height, aspect, time, ae_size): # Convert obj transform properties to AE space plane_matrix = get_image_plane_matrix(self.obj) # Scale plane to account for AE's transforms plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4) ae_transform = convert_transform_matrix(plane_matrix, width, height, aspect, ae_size) opacity = 0.0 if self.obj.hide_render else 100.0 if not hasattr(self, 'filepath'): self.filepath = get_image_filepath(self.obj) image_width, image_height = get_image_size(self.obj) ratio_to_comp = image_width / width scale = ae_transform[6:9] if image_height != 0.0: scale[1] *= image_width / image_height if ratio_to_comp != 0.0: scale[0] /= ratio_to_comp scale[1] /= ratio_to_comp self.get_prop_keyframe('position', ae_transform[0:3], time) self.get_prop_keyframe('orientation', ae_transform[3:6], time) self.get_prop_keyframe('scale', scale, time) self.get_prop_keyframe('opacity', opacity, time) def get_type_script(self): type_script = f'var newFootage = app.project.importFile(new ImportOptions(File("{self.filepath}")));\n' type_script += 'newFootage.parentFolder = footageFolder;\n' type_script += f'var {self.name_ae} = newComp.layers.add(newFootage);\n' type_script += f'{self.name_ae}.threeDLayer = true;\n' type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' return type_script class SolidExport(ObjectExport): def get_keyframe(self, context, width, height, aspect, time, ae_size): # Convert obj transform properties to AE space plane_matrix = get_plane_matrix(self.obj) # Scale plane to account for AE's transforms plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4) ae_transform = convert_transform_matrix(plane_matrix, width, height, aspect, ae_size) opacity = 0.0 if self.obj.hide_render else 100.0 if not hasattr(self, 'color'): self.color = get_plane_color(self.obj) if not hasattr(self, 'width'): self.width = width if not hasattr(self, 'height'): self.height = height scale = ae_transform[6:9] scale[1] *= width / height self.get_prop_keyframe('position', ae_transform[0:3], time) self.get_prop_keyframe('orientation', ae_transform[3:6], time) self.get_prop_keyframe('scale', scale, time) self.get_prop_keyframe('opacity', opacity, time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addSolid({self.color},"{self.name_ae}",{self.width},{self.height},1.0);\n' type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' type_script += f'{self.name_ae}.source.parentFolder = footageFolder;\n' type_script += f'{self.name_ae}.threeDLayer = true;\n' return type_script class CamBundleExport(ObjectExport): def __init__(self, obj, track): self.obj = obj self.track = track self.name_ae = convert_name(f'{obj.name}__{track.name}') self.keyframes = {} def get_keyframe(self, context, width, height, aspect, time, ae_size): # Bundles are in camera space. # Transpose to world space matrix = self.obj.matrix_basis @ Matrix.Translation(self.track.bundle) # Convert the position into AE space ae_transform = convert_transform_matrix(matrix, width, height, aspect, ae_size) self.get_prop_keyframe('position', ae_transform[0:3], time) self.get_prop_keyframe('orientation', ae_transform[3:6], time) def get_type_script(self): type_script = f'var {self.name_ae} = newComp.layers.addNull();\n' type_script += f'{self.name_ae}.threeDLayer = true;\n' type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n' return type_script def get_camera_bundles(scene, camera): cam_bundles = [] for constraint in camera.constraints: if constraint.type == 'CAMERA_SOLVER': # Which movie clip does it use if constraint.use_active_clip: clip = scene.active_clip else: clip = constraint.clip # Go through each tracking point for track in clip.tracking.tracks: # Does this tracking point have a bundle # (has its 3D position been solved) if track.has_bundle: cam_bundles.append(CamBundleExport(camera, track)) return cam_bundles def get_selected(context, include_active_cam, include_selected_cams, include_selected_objects, include_cam_bundles, include_image_planes, include_solids): """Create manageable list of selected objects""" cameras = [] solids = [] # Meshes exported as AE solids images = [] # Meshes exported as AE AV layers lights = [] # Lights exported as AE lights cam_bundles = [] # Camera trackers exported as AE nulls nulls = [] # Remaining objects exported as AE nulls scene = context.scene fps = scene.render.fps / scene.render.fps_base if context.scene.camera is not None: if include_active_cam: for frame_range, camera in get_camera_frame_ranges( context.scene, context.scene.frame_start, context.scene.frame_end): if (include_cam_bundles and camera not in (cam.obj for cam in cameras)): cam_bundles.extend( get_camera_bundles(context.scene, camera)) cameras.append( CameraExport(camera, (frame_range[0] - scene.frame_start) / fps, (frame_range[1] - scene.frame_start) / fps)) for obj in context.selected_objects: if obj.type == 'CAMERA': # Ignore camera if already selected if obj in (cam.obj for cam in cameras): continue if include_selected_cams: cameras.append(CameraExport(obj)) if include_cam_bundles: cam_bundles.extend(get_camera_bundles(context.scene, obj)) elif include_image_planes and is_image_plane(obj): images.append(ImageExport(obj)) elif include_solids and is_plane(obj): solids.append(SolidExport(obj)) elif include_selected_objects: if obj.type == 'LIGHT': lights.append(LightExport(obj)) else: nulls.append(ObjectExport(obj)) return {'cameras': cameras, 'images': images, 'solids': solids, 'lights': lights, 'nulls': nulls, 'cam_bundles': cam_bundles} def get_first_material(obj): for slot in obj.material_slots: if slot.material is not None: return slot.material def get_image_node(mat): for node in mat.node_tree.nodes: if node.type == "TEX_IMAGE": return node.image def get_plane_color(obj): """Get the object's emission and base color, or 0.5 gray if no color is found.""" if obj.active_material is None: color = (0.5,) * 3 elif obj.active_material: from bpy_extras import node_shader_utils wrapper = node_shader_utils.PrincipledBSDFWrapper(obj.active_material) color = Color(wrapper.base_color[:3]) + wrapper.emission_color return str(list(color)) def is_plane(obj): """Check if object is a plane Makes a few assumptions: - The mesh has exactly one quad face - The mesh is a rectangle For now this doesn't account for shear, which could happen e.g. if the vertices are rotated, and the object is scaled non-uniformly... """ if obj.type != 'MESH': return False if len(obj.data.polygons) != 1: return False if len(obj.data.polygons[0].vertices) != 4: return False v1, v2, v3, v4 = (obj.data.vertices[v].co for v in obj.data.polygons[0].vertices) # Check that poly is a parallelogram if -v1 + v2 + v4 != v3: return False # Check that poly has at least one right angle if (v2-v1).dot(v4-v1) != 0.0: return False # If my calculations are correct, that should make it a rectangle return True def is_image_plane(obj): """Check if object is a plane with an image Makes a few assumptions: - The mesh is a plane - The mesh has exactly one material - There is only one image in this material node tree """ if not is_plane(obj): return False if len(obj.material_slots) == 0: return False mat = get_first_material(obj) if mat is None: return False img = get_image_node(mat) if img is None: return False if len(obj.data.vertices) == 4: return True def get_image_filepath(obj): mat = get_first_material(obj) img = get_image_node(mat) filepath = img.filepath filepath = bpy.path.abspath(filepath) filepath = os.path.abspath(filepath) filepath = filepath.replace('\\', '\\\\') return filepath def get_image_size(obj): mat = get_first_material(obj) img = get_image_node(mat) return img.size def get_plane_matrix(obj): """Get object's polygon local matrix from vertices.""" v1, v2, v3, v4 = (obj.data.vertices[v].co for v in obj.data.polygons[0].vertices) p0 = obj.matrix_world @ v1 px = obj.matrix_world @ v2 - p0 py = obj.matrix_world @ v4 - p0 rot_mat = Matrix((px, py, px.cross(py))).transposed().to_4x4() trans_mat = Matrix.Translation(p0 + (px + py) / 2.0) mat = trans_mat @ rot_mat return mat def get_image_plane_matrix(obj): """Get object's polygon local matrix from uvs. This will only work if uvs occupy all space, to get bounds """ for p_i, p in enumerate(obj.data.uv_layers.active.data): if p.uv == Vector((0, 0)): p0 = p_i elif p.uv == Vector((1, 0)): px = p_i elif p.uv == Vector((0, 1)): py = p_i verts = obj.data.vertices loops = obj.data.loops p0 = obj.matrix_world @ verts[loops[p0].vertex_index].co px = obj.matrix_world @ verts[loops[px].vertex_index].co - p0 py = obj.matrix_world @ verts[loops[py].vertex_index].co - p0 rot_mat = Matrix((px, py, px.cross(py))).transposed().to_4x4() trans_mat = Matrix.Translation(p0 + (px + py) / 2.0) mat = trans_mat @ rot_mat return mat def convert_name(name): """Convert names of objects to avoid errors in AE""" if not name[0].isalpha(): name = "_" + name name = bpy.path.clean_name(name) name = name.replace("-", "_") return name def convert_transform_matrix(matrix, width, height, aspect, ae_size=100.0): """Convert from Blender's Location, Rotation and Scale to AE's Position, Rotation/Orientation and Scale This function will be called for every object for every frame """ # Get blender transform data for object b_loc = matrix.to_translation() b_rot = matrix.to_euler('ZYX') # ZYX euler matches AE's orientation and allows to use x_rot_correction b_scale = matrix.to_scale() # Convert to AE Position Rotation and Scale. Axes in AE are different: # AE's X is Blender's X, # AE's Y is Blender's -Z, # AE's Z is Blender's Y x = (b_loc.x * 100.0 / aspect + width / 2.0) * ae_size / 100.0 y = (-b_loc.z * 100.0 + height / 2.0) * ae_size / 100.0 z = (b_loc.y * 100.0) * ae_size / 100.0 # Convert rotations to match AE's orientation. # In Blender, object of zero rotation lays on floor. # In AE, layer of zero orientation "stands", so subtract 90 degrees rx = degrees(b_rot.x) - 90.0 # AE's X orientation = blender's X rotation if 'ZYX' euler. ry = -degrees(b_rot.y) # AE's Y orientation = -blender's Y rotation if 'ZYX' euler rz = -degrees(b_rot.z) # AE's Z orientation = -blender's Z rotation if 'ZYX' euler # Convert scale to AE scale. ae_size is a global multiplier. sx = b_scale.x * ae_size sy = b_scale.y * ae_size sz = b_scale.z * ae_size return [x, y, z, rx, ry, rz, sx, sy, sz] # 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. # # ZOOM VALUE CALCULATIONS: # # Given values: # - sensor width (camera.data.sensor_width) # - sensor height (camera.data.sensor_height) # - sensor fit (camera.data.sensor_fit) # - lens (blender's lens in mm) # - width (width of the composition/scene in pixels) # - height (height of the composition/scene in pixels) # - PAR (pixel aspect ratio) # # Calculations are made using sensor's size and scene/comp dimension (width or height). # If camera.sensor_fit is set to 'HORIZONTAL': # sensor = camera.data.sensor_width, dimension = width. # # If camera.sensor_fit is set to 'AUTO': # sensor = camera.data.sensor_width # (actually, it just means to use the first value) # In AUTO, if the vertical size is greater than the horizontal size: # dimension = width # else: # dimension = height # # If camera.sensor_fit is set to 'VERTICAL': # sensor = camera.data.sensor_height, dimension = height # # Zoom can be calculated using simple proportions. # # | # / | # / | # / | d # s |\ / | i # e | \ / | m # n | \ / | e # s | / \ | n # o | / \ | s # r |/ \ | i # \ | o # | | \ | n # | | \ | # | | | # lens | zoom # # zoom / dimension = lens / sensor => # zoom = lens * dimension / sensor # # Above is true if square pixels are used. If not, # aspect compensation is needed, so final formula is: # zoom = lens * dimension / sensor * aspect def convert_lens(camera, width, height, aspect): if camera.data.sensor_fit == 'VERTICAL': sensor = camera.data.sensor_height else: sensor = camera.data.sensor_width if (camera.data.sensor_fit == 'VERTICAL' or camera.data.sensor_fit == 'AUTO' and (width / height) * aspect < 1.0): dimension = height else: dimension = width zoom = camera.data.lens * dimension / sensor * aspect return zoom # convert object bundle's matrix. Not ready yet. Temporarily not active # def get_ob_bundle_matrix_world(cam_matrix_world, bundle_matrix): # matrix = cam_matrix_basis # return matrix def write_jsx_file(context, file, selection, include_animation, ae_size): """jsx script for AE creation""" print("\n---------------------------\n" "- Export to After Effects -\n" "---------------------------") # Create list of static blender data scene = context.scene width = scene.render.resolution_x height = scene.render.resolution_y aspect_x = scene.render.pixel_aspect_x aspect_y = scene.render.pixel_aspect_y aspect = aspect_x / aspect_y if include_animation: frame_end = scene.frame_end + 1 else: frame_end = scene.frame_start + 1 fps = scene.render.fps / scene.render.fps_base duration = (frame_end - scene.frame_start) / fps # Store the current frame to restore it at the end of export frame_current = scene.frame_current # Get all keyframes for each object for frame in range(scene.frame_start, frame_end): print("Working on frame: " + str(frame)) scene.frame_set(frame) # Get time for this loop time = (frame - scene.frame_start) / fps for obj_type in selection.values(): for obj in obj_type: obj.get_keyframe(context, width, height, aspect, time, ae_size) # ---- write JSX file with open(file, 'w') as jsx_file: # Make the jsx executable in After Effects (enable double click on jsx) jsx_file.write('#target AfterEffects\n\n') # Script's header jsx_file.write('/**************************************\n') jsx_file.write(f'Scene : {scene.name}\n') jsx_file.write(f'Resolution : {width} x {height}\n') jsx_file.write(f'Duration : {duration}\n') jsx_file.write(f'FPS : {fps}\n') jsx_file.write(f'Date : {datetime.datetime.now()}\n') 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 if bpy.data.filepath: comp_name = convert_name( os.path.splitext(os.path.basename(bpy.data.filepath))[0]) else: comp_name = "BlendComp" jsx_file.write(f'\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","{comp_name}","Composition\'s Name");\n') jsx_file.write('if (compName){') # Continue only if comp name is given. If not - terminate jsx_file.write( f'\nvar newComp = app.project.items.addComp(compName, {width}, ' f'{height}, {aspect}, {duration}, {fps});') jsx_file.write(f"\nnewComp.displayStartTime = {scene.frame_start / fps};\n\n") jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') # Write each object's creation script for obj_type in ('cam_bundles', 'nulls', 'solids', 'images', 'lights', 'cameras'): if len(selection[obj_type]): type_name = 'CAMERA 3D MARKERS' if obj_type == 'cam_bundles' else obj_type.upper() jsx_file.write(f'// ************** {type_name} **************\n\n') for obj in selection[obj_type]: jsx_file.write(obj.get_obj_script(include_animation)) jsx_file.write('\n') # Exit import if no comp name given jsx_file.write('\n}else{alert ("Exit Import Blender animation data \\nNo Comp name has been chosen","EXIT")};') # Close function jsx_file.write("}\n\n\n") # Execute function. Wrap in "undo group" for easy undoing import process jsx_file.write('app.beginUndoGroup("Import Blender animation data");\n') jsx_file.write('compFromBlender();\n') # Execute function jsx_file.write('app.endUndoGroup();\n\n\n') # Restore current frame of animation in blender to state before export scene.frame_set(frame_current) ########################################## # ExportJsx class register/unregister ########################################## from bpy_extras.io_utils import ExportHelper from bpy.props import StringProperty, BoolProperty, FloatProperty 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" bl_options = {'PRESET', 'UNDO'} filename_ext = ".jsx" filter_glob: StringProperty(default="*.jsx", options={'HIDDEN'}) include_animation: BoolProperty( name="Animation", description="Animate Exported Cameras and Objects", default=True, ) include_active_cam: BoolProperty( name="Active Camera", description="Include Active Camera", default=True, ) include_selected_cams: BoolProperty( name="Selected Cameras", description="Add Selected Cameras", default=True, ) include_selected_objects: BoolProperty( name="Selected Objects", description="Export Selected Objects", default=True, ) include_cam_bundles: BoolProperty( name="Camera 3D Markers", description="Include 3D Markers of Camera Motion Solution for selected cameras", default=True, ) include_image_planes: BoolProperty( name="Image Planes", description="Include image mesh objects", default=True, ) include_solids: BoolProperty( name="Solids", description="Include rectangles as solids", default=True, ) # include_ob_bundles = BoolProperty( # name="Objects 3D Markers", # description="Include 3D Markers of Object Motion Solution for selected cameras", # default=True, # ) ae_size: FloatProperty( name="Scale", description="Size of AE Composition (pixels per 1 BU)", default=100.0, min=0.0, soft_max=10000, ) def draw(self, context): layout = self.layout box = layout.box() box.label(text='Include Cameras and Objects') col = box.column(align=True) col.prop(self, 'include_active_cam') col.prop(self, 'include_selected_cams') col.prop(self, 'include_selected_objects') col.prop(self, 'include_image_planes') col.prop(self, 'include_solids') box = layout.box() box.label(text='Include Tracking Data') box.prop(self, 'include_cam_bundles') # box.prop(self, 'include_ob_bundles') box = layout.box() box.prop(self, 'include_animation') box = layout.box() box.label(text='Transform') box.prop(self, 'ae_size') @classmethod def poll(cls, context): selected = context.selected_objects camera = context.scene.camera return selected or camera def execute(self, context): selection = get_selected(context, self.include_active_cam, self.include_selected_cams, self.include_selected_objects, self.include_cam_bundles, self.include_image_planes, self.include_solids) write_jsx_file(context, self.filepath, selection, self.include_animation, self.ae_size) print("\nExport to After Effects Completed") return {'FINISHED'} 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.TOPBAR_MT_file_export.append(menu_func) def unregister(): bpy.utils.unregister_class(ExportJsx) bpy.types.TOPBAR_MT_file_export.remove(menu_func) if __name__ == "__main__": register()