Blender(2.8) to After Effects Exporter

Hello everyone,

I’m looking for a working Blender(2.8) to After Effects Exporter.

This one exports everything, but unfortunately all Blender rectangles arrive in After Effects as squares. Besides, they’re in the wrong position.

# ##### 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",
    "version": (0, 65),
    "blender": (2, 80, 0),
    "location": "File > Export > Adobe After Effects (.jsx)",
    "warning": "",
    "wiki_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


# 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
    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,
        }


# create list of active camera for each frame in case active camera is set by markers
def get_active_cam_for_each_frame(scene, start, end):
    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 legth 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)


# create manageable list of selected objects
def get_selected(context):
    cameras = []  # list of selected cameras
    solids = []  # list of all selected meshes that can be exported as AE's solids
    lights = []  # list of all selected lamps that can be exported as AE's lights
    nulls = []  # list of all 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, convert_name(ob.name)])

        elif is_plane(ob):
            # not ready yet. is_plane(object) returns False in all cases. This is temporary
            solids.append([ob, convert_name(ob.name)])

        elif ob.type == 'LIGHT':
            lights.append([ob, ob.data.type + convert_name(ob.name)])  # Type of lamp added to name

        else:
            nulls.append([ob, convert_name(ob.name)])

    selection = {
        'cameras': cameras,
        'solids': solids,
        'lights': lights,
        'nulls': nulls,
        }

    return selection


# check if object is plane and can be exported as AE's solid
def is_plane(object):
    # work in progress. Not ready yet
    return False


# convert names of objects to avoid errors in AE.
def convert_name(name):
    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


# get object's blender's location rotation and scale and return AE's Position, Rotation/Orientation and scale
# this function will be called for every object for every frame
def convert_transform_matrix(matrix, width, height, aspect, x_rot_correction=False, ae_size=100.0):

    # 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 negative Blender's Z, AE's Z is Blender's Y
    x = (b_loc.x * ae_size) / aspect + width / 2.0  # calculate AE's X position
    y = (-b_loc.z * ae_size) + (height / 2.0)  # calculate AE's Y position
    z = b_loc.y * ae_size  # calculate AE's Z position
    # Convert rotations to match AE's orientation.
    rx = degrees(b_rot.x)  # if not x_rot_correction - AE's X orientation = blender's X rotation if 'ZYX' euler.
    ry = -degrees(b_rot.y)  # AE's Y orientation is negative blender's Y rotation if 'ZYX' euler
    rz = -degrees(b_rot.z)  # AE's Z orientation is negative blender's Z rotation if 'ZYX' euler
    if x_rot_correction:
        rx -= 90.0  # In blender - ob of zero rotation lay on floor. In AE layer of zero orientation "stands"
    # Convert scale to AE scale
    sx = b_scale.x * 100.0  # scale of 1.0 is 100% in AE
    sy = b_scale.z * 100.0  # scale of 1.0 is 100% in AE
    sz = b_scale.y * 100.0  # scale of 1.0 is 100% in AE

    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 'AUTO' or 'HORIZONTAL' - sensor = camera.data.sensor_width, dimension = width.
# 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
        dimension = height
    else:
        sensor = camera.data.sensor_width
        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


# jsx script for AE creation
def write_jsx_file(file, data, selection, include_animation, include_active_cam, include_selected_cams, include_selected_objects, include_cam_bundles, ae_size):

    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': {},
        'solids': {},  # not ready yet
        '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 that one active cam exist (true if active cams set by markers)
        if len(data['active_cam_frames']) is 1:
            name_ae = convert_name(data['active_cam_frames'][0].name)  # take name of the only active camera in scene
        else:
            name_ae = 'Active_Camera'
        active_cam_name = name_ae  # store name to be used when creating keyframes for active cam.
        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 i, cam in enumerate(selection['cameras']):  # more than one camera can be selected
            if cam[1] != active_cam_name:
                name_ae = selection['cameras'][i][1]
                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. Not ready yet. Temporarily not active
    for i, obj in enumerate(selection['solids']):
        name_ae = selection['solids'][i][1]
        js_data['solids'][name_ae] = {
            'position': '',
            'orientation': '',
            'rotationX': '',
            'scale': '',
            }
    '''
    # create structure for lights
    for i, obj in enumerate(selection['lights']):
        if include_selected_objects:
            name_ae = selection['lights'][i][1]
            js_data['lights'][name_ae] = {
                'type': selection['lights'][i][0].data.type,
                'energy': '',
                'energy_static': '',
                'energy_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
    for i, obj in enumerate(selection['nulls']):  # nulls representing blender's obs except cameras, lamps and solids
        if include_selected_objects:
            name_ae = selection['nulls'][i][1]
            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[0])
        # 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]
            # get cam name
            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_data['cameras'][name_ae]['position'] += position
            js_data['cameras'][name_ae]['orientation'] += orientation
            js_data['cameras'][name_ae]['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_data['cameras'][name_ae]['position_static']:
                    js_data['cameras'][name_ae]['position_anim'] = True
                if orientation != js_data['cameras'][name_ae]['orientation_static']:
                    js_data['cameras'][name_ae]['orientation_anim'] = True
                if zoom != js_data['cameras'][name_ae]['zoom_static']:
                    js_data['cameras'][name_ae]['zoom_anim'] = True
            js_data['cameras'][name_ae]['position_static'] = position
            js_data['cameras'][name_ae]['orientation_static'] = orientation
            js_data['cameras'][name_ae]['zoom_static'] = zoom

        # keyframes for selected cameras
        if include_selected_cams:
            for i, cam in enumerate(selection['cameras']):
                if cam[1] != active_cam_name:
                    # get cam name
                    name_ae = selection['cameras'][i][1]
                    # convert cam transform properties to AE space
                    ae_transform = convert_transform_matrix(cam[0].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(cam[0], 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_data['cameras'][name_ae]['position'] += position
                    js_data['cameras'][name_ae]['orientation'] += orientation
                    js_data['cameras'][name_ae]['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_data['cameras'][name_ae]['position_static']:
                            js_data['cameras'][name_ae]['position_anim'] = True
                        if orientation != js_data['cameras'][name_ae]['orientation_static']:
                            js_data['cameras'][name_ae]['orientation_anim'] = True
                        if zoom != js_data['cameras'][name_ae]['zoom_static']:
                            js_data['cameras'][name_ae]['zoom_anim'] = True
                    js_data['cameras'][name_ae]['position_static'] = position
                    js_data['cameras'][name_ae]['orientation_static'] = orientation
                    js_data['cameras'][name_ae]['zoom_static'] = zoom

        '''
        # keyframes for all solids. Not ready yet. Temporarily not active
        for i, ob in enumerate(selection['solids']):
            #get object name
            name_ae = selection['solids'][i][1]
            #convert ob position to AE space
        '''

        # keyframes for all lights.
        if include_selected_objects:
            for i, ob in enumerate(selection['lights']):
                #get object name
                name_ae = selection['lights'][i][1]
                type = selection['lights'][i][0].data.type
                # convert ob transform properties to AE space
                ae_transform = convert_transform_matrix(ob[0].matrix_world.copy(), data['width'], data['height'], data['aspect'], True, ae_size)
                color = ob[0].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],' % (ob[0].data.energy * 100.0)
                color = '[%f,%f,%f],' % (color[0], color[1], color[2])
                js_data['lights'][name_ae]['position'] += position
                js_data['lights'][name_ae]['orientation'] += orientation
                js_data['lights'][name_ae]['energy'] += energy
                js_data['lights'][name_ae]['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_data['lights'][name_ae]['position_static']:
                        js_data['lights'][name_ae]['position_anim'] = True
                    if orientation != js_data['lights'][name_ae]['orientation_static']:
                        js_data['lights'][name_ae]['orientation_anim'] = True
                    if energy != js_data['lights'][name_ae]['energy_static']:
                        js_data['lights'][name_ae]['energy_anim'] = True
                    if color != js_data['lights'][name_ae]['color_static']:
                        js_data['lights'][name_ae]['color_anim'] = True
                js_data['lights'][name_ae]['position_static'] = position
                js_data['lights'][name_ae]['orientation_static'] = orientation
                js_data['lights'][name_ae]['energy_static'] = energy
                js_data['lights'][name_ae]['color_static'] = color
                if type == 'SPOT':
                    cone_angle = '[%f],' % (degrees(ob[0].data.spot_size))
                    cone_feather = '[%f],' % (ob[0].data.spot_blend * 100.0)
                    js_data['lights'][name_ae]['cone_angle'] += cone_angle
                    js_data['lights'][name_ae]['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_data['lights'][name_ae]['cone_angle_static']:
                            js_data['lights'][name_ae]['cone_angle_anim'] = True
                        if orientation != js_data['lights'][name_ae]['cone_feather_static']:
                            js_data['lights'][name_ae]['cone_feather_anim'] = True
                    js_data['lights'][name_ae]['cone_angle_static'] = cone_angle
                    js_data['lights'][name_ae]['cone_feather_static'] = cone_feather

        # keyframes for all nulls
        if include_selected_objects:
            for i, ob in enumerate(selection['nulls']):
                # get object name
                name_ae = selection['nulls'][i][1]
                # convert ob transform properties to AE space
                ae_transform = convert_transform_matrix(ob[0].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_data['nulls'][name_ae]['position'] += position
                js_data['nulls'][name_ae]['orientation'] += orientation
                js_data['nulls'][name_ae]['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_data['nulls'][name_ae]['position_static']:
                        js_data['nulls'][name_ae]['position_anim'] = True
                    if orientation != js_data['nulls'][name_ae]['orientation_static']:
                        js_data['nulls'][name_ae]['orientation_anim'] = True
                    if scale != js_data['nulls'][name_ae]['scale_static']:
                        js_data['nulls'][name_ae]['scale_anim'] = True
                js_data['nulls'][name_ae]['position_static'] = position
                js_data['nulls'][name_ae]['orientation_static'] = orientation
                js_data['nulls'][name_ae]['scale_static'] = scale

        # 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')

    # wrap in function
    jsx_file.write("function compFromBlender(){\n")
    # create new comp
    jsx_file.write('\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","BlendComp","Composition\'s Name");\n')
    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\n')
    for i, obj in enumerate(js_data['bundles_cam']):
        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").setValue(%s);\n\n\n' % (name_ae, js_data['bundles_cam'][obj]['position']))

    # create object bundles (not ready yet)

    # create objects (nulls)
    jsx_file.write('// **************  OBJECTS  **************\n\n\n')
    for i, obj in enumerate(js_data['nulls']):
        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))
        # Set values of properties, add kyeframes only where needed
        if include_animation and js_data['nulls'][name_ae]['position_anim']:
            jsx_file.write('%s.property("position").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['nulls'][obj]['position']))
        else:
            jsx_file.write('%s.property("position").setValue(%s);\n' % (name_ae, js_data['nulls'][obj]['position_static']))
        if include_animation and js_data['nulls'][name_ae]['orientation_anim']:
            jsx_file.write('%s.property("orientation").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['nulls'][obj]['orientation']))
        else:
            jsx_file.write('%s.property("orientation").setValue(%s);\n' % (name_ae, js_data['nulls'][obj]['orientation_static']))
        if include_animation and js_data['nulls'][name_ae]['scale_anim']:
            jsx_file.write('%s.property("scale").setValuesAtTimes([%s],[%s]);\n\n\n' % (name_ae, js_data['times'], js_data['nulls'][obj]['scale']))
        else:
            jsx_file.write('%s.property("scale").setValue(%s);\n\n\n' % (name_ae, js_data['nulls'][obj]['scale_static']))
    # create solids (not ready yet)

    # create lights
    jsx_file.write('// **************  LIGHTS  **************\n\n\n')
    for i, obj in enumerate(js_data['lights']):
        name_ae = obj
        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 kyeframes only where needed
        if include_animation and js_data['lights'][name_ae]['position_anim']:
            jsx_file.write('%s.property("position").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['lights'][obj]['position']))
        else:
            jsx_file.write('%s.property("position").setValue(%s);\n' % (name_ae, js_data['lights'][obj]['position_static']))
        if include_animation and js_data['lights'][name_ae]['orientation_anim']:
            jsx_file.write('%s.property("orientation").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['lights'][obj]['orientation']))
        else:
            jsx_file.write('%s.property("orientation").setValue(%s);\n' % (name_ae, js_data['lights'][obj]['orientation_static']))
        if include_animation and js_data['lights'][name_ae]['energy_anim']:
            jsx_file.write('%s.property("intensity").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['lights'][obj]['energy']))
        else:
            jsx_file.write('%s.property("intensity").setValue(%s);\n' % (name_ae, js_data['lights'][obj]['energy_static']))
        if include_animation and js_data['lights'][name_ae]['color_anim']:
            jsx_file.write('%s.property("Color").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['lights'][obj]['color']))
        else:
            jsx_file.write('%s.property("Color").setValue(%s);\n' % (name_ae, js_data['lights'][obj]['color_static']))
            if js_data['lights'][obj]['type'] == 'SPOT':
                if include_animation and js_data['lights'][name_ae]['cone_angle_anim']:
                    jsx_file.write('%s.property("Cone Angle").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['lights'][obj]['cone_angle']))
                else:
                    jsx_file.write('%s.property("Cone Angle").setValue(%s);\n' % (name_ae, js_data['lights'][obj]['cone_angle_static']))
                if include_animation and js_data['lights'][name_ae]['cone_feather_anim']:
                    jsx_file.write('%s.property("Cone Feather").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['lights'][obj]['cone_feather']))
                else:
                    jsx_file.write('%s.property("Cone Feather").setValue(%s);\n' % (name_ae, js_data['lights'][obj]['cone_feather_static']))
        jsx_file.write('\n\n')

    # 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.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' % name_ae)
        # Set values of properties, add kyeframes only where needed
        if include_animation and js_data['cameras'][name_ae]['position_anim']:
            jsx_file.write('%s.property("position").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['position']))
        else:
            jsx_file.write('%s.property("position").setValue(%s);\n' % (name_ae, js_data['cameras'][cam]['position_static']))
        if include_animation and js_data['cameras'][name_ae]['orientation_anim']:
            jsx_file.write('%s.property("orientation").setValuesAtTimes([%s],[%s]);\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['orientation']))
        else:
            jsx_file.write('%s.property("orientation").setValue(%s);\n' % (name_ae, js_data['cameras'][cam]['orientation_static']))
        if include_animation and js_data['cameras'][name_ae]['zoom_anim']:
            jsx_file.write('%s.property("zoom").setValuesAtTimes([%s],[%s]);\n\n\n' % (name_ae, js_data['times'], js_data['cameras'][cam]['zoom']))
        else:
            jsx_file.write('%s.property("zoom").setValue(%s);\n\n\n' % (name_ae, js_data['cameras'][cam]['zoom_static']))

    # Exit import if no comp name given
    jsx_file.write('\n}else{alert ("Exit Import Blender animation data \\nNo Comp\'s 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')
    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, include_animation, include_active_cam, include_selected_cams, include_selected_objects, include_cam_bundles, ae_size):
    data = get_comp_data(context)
    selection = get_selected(context)
    write_jsx_file(file, data, selection, include_animation, include_active_cam, include_selected_cams, include_selected_objects, include_cam_bundles, ae_size)
    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, 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"
    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_ob_bundles = BoolProperty(
#            name="Objects 3D Markers",
#            description="Include 3D Markers of Object Motion Solution for selected cameras",
#            default=True,
#            )
    ae_size: FloatProperty(
            name="AE Size",
            description="Size of AE Composition (pixels per 1BU)",
            default=100.0,
            )

    def draw(self, context):
        layout = self.layout

        box = layout.box()
        box.label(text='Size fo AE Comp (pixels per 1 BU)')
        box.prop(self, 'ae_size')
        box.label(text='Animation:')
        box.prop(self, 'include_animation')
        box.label(text='Include Cameras and Objects:')
        box.prop(self, 'include_active_cam')
        box.prop(self, 'include_selected_cams')
        box.prop(self, 'include_selected_objects')
        box.label(text="Include Tracking Data:")
        box.prop(self, 'include_cam_bundles')
#        box.prop(self, 'include_ob_bundles')

    @classmethod
    def poll(cls, context):
        active = context.active_object
        selected = context.selected_objects
        camera = context.scene.camera
        ok = selected or camera
        return ok

    def execute(self, context):
        return main(self.filepath, context, self.include_animation, self.include_active_cam, self.include_selected_cams, self.include_selected_objects, self.include_cam_bundles, self.ae_size)


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()

Does anyone have an working exporter that is a little more current?

6 Likes

I was hoping this would be updated also, I sent a tweet to the author but had no response, I hope somebody capable picks up on this in time for 2.8 release, I find it super handy.

1 Like

hi, could you please test the version here: https://developer.blender.org/T63856
let us know how it goes either here or in the main task.
Thanks

Hi Meta-Androcto,

great that someone took care of the script.
Unfortunately there is no difference to the old version.
Only 25 out of 100 images are imported.
The position and aspect ratio of the rectangles are wrong.

Blender from today (e80cbdac331a-windows64)
After Effects CS5


Here are the parts:
BlenderToAE-Project-ExportScript-MP4.7z (1.3 MB)

Edit:
I forgot the Blender file.
Blender-to-AE-2.8.7z (224.1 KB)

io_export_after_effects_Blender_2_8.py (37.3 KB)

I changed the version from 2.79. Works well - rectangles are out of place. Checked in CS6

It got a little better.
In your version, all 100 “images” are imported (and not just 25).

This isn’t working for scene with tracking data (if there is a Camera solver applied to camera).

The Error messagge looks like this: https://i.imgur.com/ZL9rh5p.png

This error is from using the old matrix multiplication syntax. There also was some outdated syntax for handling light sources (the term “lamp” was replaced by “light” in the API for 2.80). I fixed those two issues in the addons-contrib-repo, but to make things simpler I have also attached an updated version below (v0.0.66):
io_export_after_effects_v_0_0_66.zip (7.5 KB).

5 Likes

Thank you!

There used to be a scene scale field on the exporter but that appears to be missing from this version?

I always found that handy if I was dealing with other peoples scenes of incorrect world scale.

Could anyone reinstate that in a future update?

1 Like

hey, im going to be honest i dont know how to execute the converter, can somebody help me if they got time on thier hands rn? (i would use the built in export in 2.81 but its broken for me/ i cant seem to fix it)

Are you still having issues?

Today I tested this (the add-on linked by nBurn) in blender 2.81a.

I was able to export a scene with an animated camera, a light, and about 50 placeholders, each standing in for a cube mesh (it’s just a quick test scene). Seems to be importing into AE CC 2018 without noticeable problems.

If you use it, and you find your objects aren’t exported, try selecting them first. I did this a few times before I realized that the “Selected Objects” checkbox was checked, meaning that only objects currently selected will be exported.

I’ve been working with the script linked by nBurn above, and it works, except for one thing…
The camera exporter does not take into consideration if you have a “percentage scale” set to other than 100% for the Resolution x/y dimensions in the Output tab.
It will always export with the Resolution numbers you have and does not adjust based on a percentage value you have set.
Just something to keep in mind, and/or submit for an update.

Im doing a smartphone commercial at the moment and after doing my screens in Blender with Emission Nodes (since I only want 1 file for the whole commercial, I mixed the Inputs for the screen with mixRGB Nodes and animated the FACs…), I got fed up with it and wanted do the screens in After Effects, a little bit like it is possible in Cinema4D combined with AE.

Okay, so here is what I have tested: I exported the scene with that “Export to After Effects” unofficial add-on and got the null point of my smartphone animation and the camera animations into After effects. Then i rendered my animation as exr with cryptomatte. I tried to match my UI screen with the smartphone, didn’t work tho.

Then I placed the camera with a top-down view to my smartphone to match the UI Screen properly in After Effects. Then linked the UI with the null.

Then went back to my animation, placed the cryptomatte as a mask of my animation and: it didnt work because the information of the Null Object or the camera from the imported blender data is wrong as it seems

This is how it should look like when i do it with blender

There must be some kind of way to change what the screen is showing without changing materials and then render the rest. Why is this such a pain in the ass (Blender 2.82 and AE19)

To anyone looking for the Blender-to-After-Effects addon if it isn’t showing up in Blender 2.8x:
The script probably hasn’t been released with official builds yet (it still hasn’t been at the time that I write this). You can download it and install it manually at https://developer.blender.org/diffusion/BAC/ (this is where not-ready-for-official-release addons are housed and worked on). Go to that link and search “after” and you’ll find it. Download the .py file and install it through the blender preferences window.

4 Likes

Wait a minute… Did you actually just post your comment 1 hour ago? I literally just started Googling for a way to import the Blender camera to AE and stumbled upon this thread and it turns out after months of silence the solution was posted just a moment before I was starting to look for it :astonished:

Oh thankyouthankyouthankyou! I needed that for work today.

For anyone still interested, I made an update for this add-on, which fixes a couple of bugs, brings back the scale option, and most interestingly enables plane meshes export to solids or images. (Compare the footage in the two following screenshots: one was rendered in Blender’s 3D view, the other is the AE composition.)

This version may be found here as a diff to apply on the addons_contrib repository, and directly as a script to download.

Feel free to report bugs and inconsistencies, or leave feedback at the Blender Developer website! If this is robust enough I may try to submit it again for inclusion in the main add-on repository shipped with Blender.

1 Like

Hahaa, first when i looked at the word Exporter, Exploiter came into my mind.