Skip to content
Snippets Groups Projects
io_export_after_effects.py 45.7 KiB
Newer Older
  • Learn to ignore specific revisions
  • # ##### 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 #####
    
    # <pep8 compliant>
    
    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",
    
        "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 datetime
    from math import degrees, floor
    
    from mathutils import Matrix, Vector, Color
    
    
    
    def get_comp_data(context):
    
        """Create list of static blender's data"""
    
        scene = context.scene
        aspect_x = scene.render.pixel_aspect_x
        aspect_y = scene.render.pixel_aspect_y
        aspect = aspect_x / aspect_y
        start = scene.frame_start
        end = scene.frame_end
        active_cam_frames = get_active_cam_for_each_frame(scene, start, end)
        fps = floor(scene.render.fps / (scene.render.fps_base) * 1000.0) / 1000.0
    
        return {
            'scn': scene,
            'width': scene.render.resolution_x,
            'height': scene.render.resolution_y,
            'aspect': aspect,
            'fps': fps,
            'start': start,
            'end': end,
            'duration': (end - start + 1.0) / fps,
            'active_cam_frames': active_cam_frames,
            'curframe': scene.frame_current,
            }
    
    
    def get_active_cam_for_each_frame(scene, start, end):
    
        """Create list of active camera for each frame in case active camera is set by markers"""
    
        active_cam_frames = []
        sorted_markers = []
        markers = scene.timeline_markers
        if markers:
            for marker in markers:
                if marker.camera:
                    sorted_markers.append([marker.frame, marker])
            sorted_markers = sorted(sorted_markers)
    
            if sorted_markers:
                for frame in range(start, end + 1):
                    for m, marker in enumerate(sorted_markers):
                        if marker[0] > frame:
                            if m != 0:
    
                                active_cam_frames.append(
                                    sorted_markers[m - 1][1].camera)
    
                            else:
                                active_cam_frames.append(marker[1].camera)
                            break
                        elif m == len(sorted_markers) - 1:
                            active_cam_frames.append(marker[1].camera)
        if not active_cam_frames:
            if scene.camera:
    
                # in this case active_cam_frames array will have length of 1. This
                # will indicate that there is only one active cam in all frames
    
                active_cam_frames.append(scene.camera)
    
        return(active_cam_frames)
    
    
    def get_selected(context):
    
        """Create manageable list of selected objects"""
        cameras = []  # List of selected cameras
        solids = []   # List of selected meshes exported as AE solids
        images = []   # List of selected meshes exported as AE AV layers
        lights = []   # List of selected lights exported as AE lights
        nulls = []    # List of selected objects except cameras (will be used to create nulls in AE)
    
        obs = context.selected_objects
    
        for ob in obs:
            if ob.type == 'CAMERA':
    
                cameras.append(ob)
    
            elif is_image_plane(ob):
                images.append(ob)
    
    NBurn's avatar
    NBurn committed
            elif ob.type == 'LIGHT':
    
    
        selection = {
            'cameras': cameras,
    
            'solids': solids,
            'lights': lights,
            'nulls': nulls,
            }
    
        return selection
    
    
    
    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 '[%f,%f,%f]' % (color[0], color[1], color[2])
    
    
    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 not len(obj.material_slots):
            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
    
        """Convert names of objects to avoid errors in AE"""
    
        name = "_" + name
        '''
        # Digits are not allowed at beginning of AE vars names.
        # This section is commented, as "_" is added at beginning of names anyway.
        # Placeholder for this name modification is left so that it's not ignored if needed
        if name[0].isdigit():
            name = "_" + name
        '''
        name = bpy.path.clean_name(name)
        name = name.replace("-", "_")
    
        return name
    
    
    
    def convert_transform_matrix(matrix, width, height, aspect,
                                 x_rot_correction=False, 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
        """
    
        scale_mat = Matrix.Scale(width, 4)
    
        # Get blender transform data for ob
    
        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.
    
        # If not x_rot_correction
        rx =  degrees(b_rot.x)  # 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
    
        if x_rot_correction:
    
            # In Blender, ob of zero rotation lays on floor.
            # In AE, layer of zero orientation "stands"
            rx -= 90.0
        # 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(file, data, selection, include_animation,
                       include_active_cam, include_selected_cams,
                       include_selected_objects, include_cam_bundles,
                       include_image_planes, ae_size):
        """jsx script for AE creation"""
    
    
        print("\n---------------------------\n- Export to After Effects -\n---------------------------")
    
        # Store the current frame to restore it at the end of export
    
        curframe = data['curframe']
    
        # Create array which will contain all keyframes values
    
        js_data = {
            'times': '',
            'cameras': {},
    
            'lights': {},
            'nulls': {},
            'bundles_cam': {},
            'bundles_ob': {},  # not ready yet
            }
    
    
        # Create structure for active camera/cameras
    
        active_cam_name = ''
    
        if include_active_cam and data['active_cam_frames']:
            # Check if more than one active cam exists
            # (True if active cams set by markers)
    
            if len(data['active_cam_frames']) == 1:
    
                # Take name of the only active camera in scene
                name_ae = convert_name(data['active_cam_frames'][0].name)
    
            else:
                name_ae = 'Active_Camera'
    
            # Store name to be used when creating keyframes for active cam
            active_cam_name = name_ae
    
            js_data['cameras'][name_ae] = {
                'position': '',
                'position_static': '',
                'position_anim': False,
                'orientation': '',
                'orientation_static': '',
                'orientation_anim': False,
                'zoom': '',
                'zoom_static': '',
                'zoom_anim': False,
                }
    
    
        # Create camera structure for selected cameras
    
        if include_selected_cams:
    
            for obj in selection['cameras']:
                # More than one camera can be selected
                if convert_name(obj.name) != active_cam_name:
                    name_ae = convert_name(obj.name)
    
                    js_data['cameras'][name_ae] = {
                        'position': '',
                        'position_static': '',
                        'position_anim': False,
                        'orientation': '',
                        'orientation_static': '',
                        'orientation_anim': False,
                        'zoom': '',
                        'zoom_static': '',
                        'zoom_anim': False,
                        }
    
    
        # Create structure for solids
        for obj in selection['solids']:
            name_ae = convert_name(obj.name)
    
            js_data['solids'][name_ae] = {
                'position': '',
    
                'position_static': '',
                'position_anim': False,
    
                'orientation_static': '',
                'orientation_anim': False,
    
                'scale_static': '',
                'scale_anim': False,
    
    
        # Create structure for images
        for obj in selection['images']:
            name_ae = convert_name(obj.name)
            js_data['images'][name_ae] = {
                'position': '',
                'position_static': '',
                'position_anim': False,
                'orientation': '',
                'orientation_static': '',
                'orientation_anim': False,
                'scale': '',
                'scale_static': '',
                'scale_anim': False,
                'filepath': '',
            }
    
        # Create structure for lights
        for obj in selection['lights']:
    
            if include_selected_objects:
    
                name_ae = obj.data.type + convert_name(obj.name)
    
                js_data['lights'][name_ae] = {
    
                    'type': obj.data.type,
                    'intensity': '',
                    'intensity_static': '',
                    'intensity_anim': False,
                    'Cone Angle': '',
                    'Cone Angle_static': '',
                    'Cone Angle_anim': False,
                    'Cone Feather': '',
                    'Cone Feather_static': '',
                    'Cone Feather_anim': False,
                    'Color': '',
                    'Color_static': '',
                    'Color_anim': False,
    
                    'position': '',
                    'position_static': '',
                    'position_anim': False,
                    'orientation': '',
                    'orientation_static': '',
                    'orientation_anim': False,
                    }
    
    
        # Create structure for nulls
        # nulls representing blender's obs except cameras, lights and solids
        for obj in selection['nulls']:
    
            if include_selected_objects:
    
                name_ae = convert_name(obj.name)
    
                js_data['nulls'][name_ae] = {
                    'position': '',
                    'position_static': '',
                    'position_anim': False,
                    'orientation': '',
                    'orientation_static': '',
                    'orientation_anim': False,
                    'scale': '',
                    'scale_static': '',
                    'scale_anim': False,
                    }
    
    
        # Create structure for cam bundles including positions
        # (cam bundles don't move)
    
        if include_cam_bundles:
    
            # Go through each selected camera and active cameras
    
            selected_cams = []
            active_cams = []
            if include_active_cam:
                active_cams = data['active_cam_frames']
            if include_selected_cams:
                for cam in selection['cameras']:
    
                    selected_cams.append(cam)
            # List of cameras that will be checked for 'CAMERA SOLVER'
    
            cams = list(set.union(set(selected_cams), set(active_cams)))
    
            for cam in cams:
    
                # Go through each constraints of this camera
    
                for constraint in cam.constraints:
    
                    # Does the camera have a Camera Solver constraint
    
                    if constraint.type == 'CAMERA_SOLVER':
                        # Which movie clip does it use
                        if constraint.use_active_clip:
                            clip = data['scn'].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:
    
                                # Get the name of the tracker
                                name_ae = convert_name(str(cam.name) + '__' +
                                                       str(track.name))
    
                                js_data['bundles_cam'][name_ae] = {
                                    'position': '',
                                    }
    
                                # Bundles are in camera space.
                                # Transpose to world space
                                matrix = Matrix.Translation(cam.matrix_basis.copy()
                                                            @ track.bundle)
                                # Convert the position into AE space
                                ae_transform = (convert_transform_matrix(
                                    matrix, data['width'], data['height'],
                                    data['aspect'], False, ae_size))
                                js_data['bundles_cam'][name_ae]['position'] += ('[%f,%f,%f],' % (ae_transform[0], ae_transform[1], ae_transform[2]))
    
        # Get all keyframes for each object and store in dico
    
        if include_animation:
            end = data['end'] + 1
        else:
            end = data['start'] + 1
        for frame in range(data['start'], end):
            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 active camera/cameras
    
            if include_active_cam and data['active_cam_frames'] != []:
                if len(data['active_cam_frames']) == 1:
                    cur_cam_index = 0
                else:
                    cur_cam_index = frame - data['start']
                active_cam = data['active_cam_frames'][cur_cam_index]
    
                name_ae = active_cam_name
    
                # Convert cam transform properties to AE space
                ae_transform = (convert_transform_matrix(
                    active_cam.matrix_world.copy(), data['width'], data['height'],
                    data['aspect'], True, ae_size))
                # Convert Blender's lens to AE's zoom in pixels
                zoom = convert_lens(active_cam, data['width'], data['height'],
                                    data['aspect'])
                # Store all values in dico
                position = '[%f,%f,%f],' % (ae_transform[0], ae_transform[1],
                                            ae_transform[2])
                orientation = '[%f,%f,%f],' % (ae_transform[3], ae_transform[4],
                                               ae_transform[5])
    
                zoom = '%f,' % (zoom)
    
                js_camera = js_data['cameras'][name_ae]
                js_camera['position'] += position
                js_camera['orientation'] += orientation
                js_camera['zoom'] += zoom
    
                # Check if properties change values compared to previous frame
    
                # If property don't change through out the whole animation,
                # keyframes won't be added
    
                if frame != data['start']:
    
                    if position != js_camera['position_static']:
                        js_camera['position_anim'] = True
                    if orientation != js_camera['orientation_static']:
                        js_camera['orientation_anim'] = True
                    if zoom != js_camera['zoom_static']:
                        js_camera['zoom_anim'] = True
                js_camera['position_static'] = position
                js_camera['orientation_static'] = orientation
                js_camera['zoom_static'] = zoom
    
            # Keyframes for selected cameras
    
            if include_selected_cams:
    
                for obj in selection['cameras']:
                    if convert_name(obj.name) != active_cam_name:
                        # Get cam name
                        name_ae = convert_name(obj.name)
                        # Convert cam transform properties to AE space
                        ae_transform = convert_transform_matrix(
                            obj.matrix_world.copy(), data['width'],
                            data['height'], data['aspect'], True, ae_size)
                        # Convert Blender's lens to AE's zoom in pixels
                        zoom = convert_lens(obj, data['width'], data['height'],
                                            data['aspect'])
                        # Store all values in dico
                        position = '[%f,%f,%f],' % (ae_transform[0],
                                                    ae_transform[1],
                                                    ae_transform[2])
                        orientation = '[%f,%f,%f],' % (ae_transform[3],
                                                       ae_transform[4],
                                                       ae_transform[5])
    
                        zoom = '%f,' % (zoom)
    
                        js_camera = js_data['cameras'][name_ae]
                        js_camera['position'] += position
                        js_camera['orientation'] += orientation
                        js_camera['zoom'] += zoom
    
                        # Check if properties change values compared to previous frame
    
                        # If property don't change through out the whole animation,
                        # keyframes won't be added
    
                        if frame != data['start']:
    
                            if position != js_camera['position_static']:
                                js_camera['position_anim'] = True
                            if orientation != js_camera['orientation_static']:
                                js_camera['orientation_anim'] = True
                            if zoom != js_camera['zoom_static']:
                                js_camera['zoom_anim'] = True
                        js_camera['position_static'] = position
                        js_camera['orientation_static'] = orientation
                        js_camera['zoom_static'] = zoom
    
            # Keyframes for all solids.
            if include_selected_objects:
                for obj in selection['solids']:
                    # Get object name
                    name_ae = convert_name(obj.name)
                    # Convert obj transform properties to AE space
                    plane_matrix = get_plane_matrix(obj)
    
                    # Scale plane to account for AE's transforms
                    plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4)
    
                    ae_transform = convert_transform_matrix(
                        plane_matrix, data['width'], data['height'],
                        data['aspect'], True, ae_size)
                    # Store all values in dico
                    position = '[%f,%f,%f],' % (ae_transform[0],
                                                ae_transform[1],
                                                ae_transform[2])
                    orientation = '[%f,%f,%f],' % (ae_transform[3],
                                                   ae_transform[4],
                                                   ae_transform[5])
                    # plane_width, plane_height, _ = plane_matrix.to_scale()
                    scale = '[%f,%f,%f],' % (ae_transform[6],
                                             ae_transform[7] * data['width'] / data['height'],
                                             ae_transform[8])
                    js_solid = js_data['solids'][name_ae]
                    js_solid['color'] = get_plane_color(obj)
                    js_solid['width'] = data['width']
                    js_solid['height'] = data['height']
                    js_solid['position'] += position
                    js_solid['orientation'] += orientation
                    js_solid['scale'] += scale
                    # Check if properties change values compared to previous frame
                    # If property don't change through out the whole animation,
                    # keyframes won't be added
                    if frame != data['start']:
                        if position != js_solid['position_static']:
                            js_solid['position_anim'] = True
                        if orientation != js_solid['orientation_static']:
                            js_solid['orientation_anim'] = True
                        if scale != js_solid['scale_static']:
                            js_solid['scale_anim'] = True
                    js_solid['position_static'] = position
                    js_solid['orientation_static'] = orientation
                    js_solid['scale_static'] = scale
    
            # Keyframes for all lights.
    
            if include_selected_objects:
    
                for obj in selection['lights']:
                    # Get object name
                    name_ae = obj.data.type + convert_name(obj.name)
                    type = obj.data.type
                    # Convert ob transform properties to AE space
                    ae_transform = convert_transform_matrix(
                        obj.matrix_world.copy(), data['width'], data['height'],
                        data['aspect'], True, ae_size)
                    color = obj.data.color
                    # Store all values in dico
                    position = '[%f,%f,%f],' % (ae_transform[0], ae_transform[1],
                                                ae_transform[2])
                    orientation = '[%f,%f,%f],' % (ae_transform[3],
                                                   ae_transform[4],
                                                   ae_transform[5])
                    energy = '[%f],' % (obj.data.energy * 100.0)
    
                    color = '[%f,%f,%f],' % (color[0], color[1], color[2])
    
                    js_light = js_data['lights'][name_ae]
                    js_light['position'] += position
                    js_light['orientation'] += orientation
                    js_light['intensity'] += energy
                    js_light['Color'] += color
    
                    # Check if properties change values compared to previous frame
    
                    # If property don't change through out the whole animation,
                    # keyframes won't be added
    
                    if frame != data['start']:
    
                        if position != js_light['position_static']:
                            js_light['position_anim'] = True
                        if orientation != js_light['orientation_static']:
                            js_light['orientation_anim'] = True
                        if energy != js_light['intensity_static']:
                            js_light['intensity_anim'] = True
                        if color != js_light['Color_static']:
                            js_light['Color_anim'] = True
                    js_light['position_static'] = position
                    js_light['orientation_static'] = orientation
                    js_light['intensity_static'] = energy
                    js_light['Color_static'] = color
    
                        cone_angle = '[%f],' % (degrees(obj.data.spot_size))
                        cone_feather = '[%f],' % (obj.data.spot_blend * 100.0)
                        js_light['Cone Angle'] += cone_angle
                        js_light['Cone Feather'] += cone_feather
    
                        # Check if properties change values compared to previous frame
    
                        # If property don't change through out the whole animation,
                        # keyframes won't be added
    
                        if frame != data['start']:
    
                            if cone_angle != js_light['Cone Angle_static']:
                                js_light['Cone Angle_anim'] = True
                            if cone_feather != js_light['Cone Feather_static']:
                                js_light['Cone Feather_anim'] = True
                        js_light['Cone Angle_static'] = cone_angle
                        js_light['Cone Feather_static'] = cone_feather
    
            # Keyframes for all nulls
    
            if include_selected_objects:
    
                for obj in selection['nulls']:
                    # Get object name
                    name_ae = convert_name(obj.name)
                    # Convert obj transform properties to AE space
                    ae_transform = convert_transform_matrix(obj.matrix_world.copy(), data['width'], data['height'], data['aspect'], True, ae_size)
                    # Store all values in dico
                    position = '[%f,%f,%f],' % (ae_transform[0], ae_transform[1],
                                                ae_transform[2])
                    orientation = '[%f,%f,%f],' % (ae_transform[3], ae_transform[4],
                                                   ae_transform[5])
                    scale = '[%f,%f,%f],' % (ae_transform[6], ae_transform[7],
                                             ae_transform[8])
                    js_null = js_data['nulls'][name_ae]
                    js_null['position'] += position
                    js_null['orientation'] += orientation
                    js_null['scale'] += scale
                    # Check if properties change values compared to previous frame
                    # If property don't change through out the whole animation,
                    # keyframes won't be added
                    if frame != data['start']:
                        if position != js_null['position_static']:
                            js_null['position_anim'] = True
                        if orientation != js_null['orientation_static']:
                            js_null['orientation_anim'] = True
                        if scale != js_null['scale_static']:
                            js_null['scale_anim'] = True
                    js_null['position_static'] = position
                    js_null['orientation_static'] = orientation
                    js_null['scale_static'] = scale
    
            # Keyframes for all images
            if include_image_planes:
                for obj in selection['images']:
                    # Get object name
                    name_ae = convert_name(obj.name)
                    # Convert obj transform properties to AE space
                    plane_matrix = get_image_plane_matrix(obj)
    
                    # Scale plane to account for AE's transforms
                    plane_matrix = plane_matrix @ Matrix.Scale(100.0 / data['width'], 4)
    
                    ae_transform = convert_transform_matrix(
                        plane_matrix, data['width'], data['height'],
                        data['aspect'], True, ae_size)
                    # Store all values in dico
                    position = '[%f,%f,%f],' % (ae_transform[0],
                                                ae_transform[1],
                                                ae_transform[2])
                    orientation = '[%f,%f,%f],' % (ae_transform[3],
                                                   ae_transform[4],
                                                   ae_transform[5])
                    image_width, image_height = get_image_size(obj)
                    ratio_to_comp = image_width / data['width']
                    scale = '[%f,%f,%f],' % (ae_transform[6] / ratio_to_comp,
                                             ae_transform[7] / ratio_to_comp
                                             * image_width / image_height,
                                             ae_transform[8])
                    js_image = js_data['images'][name_ae]
                    js_image['position'] += position
                    js_image['orientation'] += orientation
                    js_image['scale'] += scale
    
                    # Check if properties change values compared to previous frame
    
                    # If property don't change through out the whole animation,
                    # keyframes won't be added
    
                    if frame != data['start']:
    
                        if position != js_image['position_static']:
                            js_image['position_anim'] = True
                        if orientation != js_image['orientation_static']:
                            js_image['orientation_anim'] = True
                        if scale != js_image['scale_static']:
                            js_image['scale_anim'] = True
                    js_image['position_static'] = position
                    js_image['orientation_static'] = orientation
                    js_image['scale_static'] = scale
                    js_image['filepath'] = get_image_filepath(obj)
    
    
            # keyframes for all object bundles. Not ready yet.
            #
            #
            #
    
        # ---- 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')
        # Script's header
        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')
    
    
        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('\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","%s","Composition\'s Name");\n' % comp_name)
        jsx_file.write('if (compName){')
        # Continue only if comp name is given. If not - terminate
        jsx_file.write(
            '\nvar newComp = app.project.items.addComp(compName, %i, %i, %f, %f, %f);'
            % (data['width'], data['height'], data['aspect'],
               data['duration'], data['fps']))
        jsx_file.write('\nnewComp.displayStartTime = %f;\n\n\n'
                       % ((data['start'] + 1.0) / data['fps']))
    
        # Create camera bundles (nulls)
        jsx_file.write('// **************  CAMERA 3D MARKERS  **************\n\n')
        for name_ae, obj in js_data['bundles_cam'].items():
    
            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(%s);\n\n'
                           % (name_ae, obj['position']))
        jsx_file.write('\n')
    
        # Create object bundles (not ready yet)
    
        # Create objects (nulls)
        jsx_file.write('// **************  OBJECTS  **************\n\n')
        for name_ae, obj in js_data['nulls'].items():
    
            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))
    
            # Set values of properties, add keyframes only where needed
            for prop in ("position", "orientation", "scale"):
                if include_animation and obj[prop + '_anim']:
                    jsx_file.write(
                        '%s.property("%s").setValuesAtTimes([%s],[%s]);\n'
                        % (name_ae, prop, js_data['times'], obj[prop]))
                else:
                    jsx_file.write(
                        '%s.property("%s").setValue(%s);\n'
                        % (name_ae, prop, obj[prop + '_static']))
            jsx_file.write('\n')
        jsx_file.write('\n')
    
        # Create solids
        jsx_file.write('// **************  SOLIDS  **************\n\n')
        for name_ae, obj in js_data['solids'].items():
            jsx_file.write(
                'var %s = newComp.layers.addSolid(%s,"%s",%i,%i,%f);\n' % (
                    name_ae,
                    obj['color'],
                    name_ae,
                    obj['width'],
                    obj['height'],
                    1.0))
            jsx_file.write(
                '%s.threeDLayer = true;\n' % name_ae)
            jsx_file.write(
                '%s.source.name = "%s";\n' % (name_ae, name_ae))
            # Set values of properties, add keyframes only where needed
            for prop in ("position", "orientation", "scale"):
                if include_animation and obj[prop + '_anim']:
                    jsx_file.write(
                        '%s.property("%s").setValuesAtTimes([%s],[%s]);\n'
                        % (name_ae, prop, js_data['times'], obj[prop]))
                else:
                    jsx_file.write(
                        '%s.property("%s").setValue(%s);\n'
                        % (name_ae, prop, obj[prop + '_static']))
            jsx_file.write('\n')
        jsx_file.write('\n')
    
        # Create images
        jsx_file.write('// **************  IMAGES  **************\n\n')
        for name_ae, obj in js_data['images'].items():
            jsx_file.write(
                'var newFootage = app.project.importFile(new ImportOptions(File("%s")))\n'
                % (obj['filepath']))
            jsx_file.write(
                'var %s = newComp.layers.add(newFootage);\n' % (name_ae))
            jsx_file.write(
                '%s.threeDLayer = true;\n' % name_ae)
            jsx_file.write(
                '%s.source.name = "%s";\n' % (name_ae, name_ae))
            # Set values of properties, add keyframes only where needed
            for prop in ("position", "orientation", "scale"):
                if include_animation and obj[prop + '_anim']:
                    jsx_file.write(
                        '%s.property("%s").setValuesAtTimes([%s],[%s]);\n'
                        % (name_ae, prop, js_data['times'], obj[prop]))
                else:
                    jsx_file.write(
                        '%s.property("%s").setValue(%s);\n'
                        % (name_ae, prop, obj[prop + '_static']))
            jsx_file.write('\n')
        jsx_file.write('\n')
    
        # Create lights
        jsx_file.write('// **************  LIGHTS  **************\n\n')
        for name_ae, obj in js_data['lights'].items():
            jsx_file.write(
                'var %s = newComp.layers.addLight("%s", [0.0, 0.0]);\n'
                % (name_ae, name_ae))
            jsx_file.write(
                '%s.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n'
                % name_ae)
            # Set values of properties, add keyframes only where needed
            props = ["position", "orientation", "intensity", "Color"]
            if obj['type'] == 'SPOT':
                props.extend(("Cone Angle", "Cone Feather"))
            for prop in props:
                if include_animation and obj[prop + '_anim']: