diff --git a/io_export_after_effects.py b/io_export_after_effects.py index 8d73d1936c2a8c50991154770ef490f42f6a1566..4861de9e1367d6eb74e1dd1cb8598b2b65447df1 100644 --- a/io_export_after_effects.py +++ b/io_export_after_effects.py @@ -22,8 +22,8 @@ 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, 0, 70), + "author": "Bartek Skorupa, Damien Picard (@pioverfour)", + "version": (0, 1, 0), "blender": (2, 80, 0), "location": "File > Export > Adobe After Effects (.jsx)", "warning": "", @@ -61,7 +61,7 @@ def get_comp_data(context): 'end': end, 'duration': (end - start + 1.0) / fps, 'active_cam_frames': active_cam_frames, - 'curframe': scene.frame_current, + 'frame_current': scene.frame_current, } @@ -97,40 +97,285 @@ def get_active_cam_for_each_frame(scene, start, end): 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 +class ObjectExport(): + """Base exporter class - for ob in obs: - if ob.type == 'CAMERA': - cameras.append(ob) + 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, context, prop_name, value, time): + """Set keyframe for given property""" + prop_keys = self.keyframes.setdefault(prop_name, []) + if not len(prop_keys) or value != prop_keys[-1][1]: + prop_keys.append((time, value)) + + def get_keyframe(self, context, data, time, ae_size): + """Store animation for the current frame""" + ae_transform = convert_transform_matrix(self.obj.matrix_world, + data['width'], data['height'], + data['aspect'], True, ae_size) + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, '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_prop_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_prop_script(self, include_animation): + """Get the part of the JSX script encoding animation""" + prop_script = "" - elif is_image_plane(ob): - images.append(ob) + # 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(" ", "") + prop_script += ( + f'{self.name_ae}.property("{prop}").setValuesAtTimes([{times}],[{values}]);\n') + else: + value = str(keys[0][1]).replace(" ", "") + prop_script += ( + f'{self.name_ae}.property("{prop}").setValue({value});\n') + prop_script += '\n' + + return prop_script + + def get_post_script(self): + """This is only used in lights as a post-treatment after animation""" + return "" + +class CameraExport(ObjectExport): + def get_keyframe(self, context, data, time, ae_size): + ae_transform = convert_transform_matrix(self.obj.matrix_world, + data['width'], data['height'], + data['aspect'], True, ae_size) + zoom = convert_lens(self.obj, data['width'], data['height'], + data['aspect']) + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'zoom', zoom, time) + + def get_type_script(self): + type_script = f'var {self.name_ae} = newComp.layers.addCamera("{self.name_ae}",[0,0]);\n' + type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n' + return type_script + + +class LightExport(ObjectExport): + def get_keyframe(self, context, data, time, ae_size): + ae_transform = convert_transform_matrix(self.obj.matrix_world, + data['width'], data['height'], + data['aspect'], True, 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(context, 'position', ae_transform[0:3], time) + if self.type in {'SPOT', 'SUN'}: + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'intensity', intensity, time) + self.get_prop_keyframe(context, 'Color', color, time) + if self.type == 'SPOT': + cone_angle = degrees(self.obj.data.spot_size) + self.get_prop_keyframe(context, 'Cone Angle', cone_angle, time) + cone_feather = self.obj.data.spot_blend * 100.0 + self.get_prop_keyframe(context, '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, data, 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 / data['width'], 4) + + ae_transform = convert_transform_matrix(plane_matrix, data['width'], + data['height'], data['aspect'], + True, 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 / data['width'] + scale = ae_transform[6:9] + scale[0] /= ratio_to_comp + scale[1] = scale[1] / ratio_to_comp * image_width / image_height + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'scale', scale, time) + self.get_prop_keyframe(context, '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, data, 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 / data['width'], 4) + + ae_transform = convert_transform_matrix(plane_matrix, data['width'], + data['height'], data['aspect'], + True, 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 = data['width'] + if not hasattr(self, 'height'): + self.height = data['height'] + + scale = ae_transform[6:9] + scale[1] *= data['width'] / data['height'] + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], time) + self.get_prop_keyframe(context, 'orientation', ae_transform[3:6], time) + self.get_prop_keyframe(context, 'scale', scale, time) + self.get_prop_keyframe(context, '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, data, time, ae_size): + # Bundles are in camera space. + # Transpose to world space + matrix = Matrix.Translation(self.obj.matrix_basis + @ self.track.bundle) + # Convert the position into AE space + ae_transform = convert_transform_matrix(matrix, data['width'], + data['height'], + data['aspect'], False, + ae_size) + + self.get_prop_keyframe(context, 'position', ae_transform[0:3], 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 - elif is_plane(ob): - solids.append(ob) + # 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)) - elif ob.type == 'LIGHT': - lights.append(ob) + return cam_bundles - else: - nulls.append(ob) - - selection = { - 'cameras': cameras, - 'images': images, - 'solids': solids, - 'lights': lights, - 'nulls': nulls, - } - return selection +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 + + if context.scene.camera is not None: + if include_active_cam: + cameras.append(CameraExport(context.scene.camera)) + if include_cam_bundles: + cam_bundles.extend(get_camera_bundles(context.scene, context.scene.camera)) + + for obj in context.selected_objects: + if obj.type == 'CAMERA': + if (include_active_cam + and obj is context.scene.camera): + # Ignore active camera if already selected + continue + else: + 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): @@ -276,14 +521,8 @@ def get_image_plane_matrix(obj): def convert_name(name): """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(): + if not name[0].isalpha(): name = "_" + name - ''' name = bpy.path.clean_name(name) name = name.replace("-", "_") @@ -300,7 +539,7 @@ def convert_transform_matrix(matrix, width, height, aspect, scale_mat = Matrix.Scale(width, 4) - # Get blender transform data for ob + # 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() @@ -319,7 +558,7 @@ def convert_transform_matrix(matrix, width, height, aspect, 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 Blender, object 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. @@ -327,7 +566,8 @@ def convert_transform_matrix(matrix, width, height, aspect, sy = b_scale.y * ae_size sz = b_scale.z * ae_size - return x, y, z, rx, ry, rz, sx, sy, sz + 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 @@ -387,7 +627,6 @@ def convert_transform_matrix(matrix, width, height, aspect, # 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 @@ -411,612 +650,86 @@ def convert_lens(camera, width, height, aspect): # 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): +def write_jsx_file(context, file, data, selection, include_animation, 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': {}, - 'images': {}, - 'solids': {}, - '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': '', - 'orientation_static': '', - 'orientation_anim': False, - 'scale': '', - 'scale_static': '', - 'scale_anim': False, - 'opacity': '', - 'opacity_static': '', - 'opacity_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, - 'opacity': '', - 'opacity_static': '', - 'opacity_anim': False, - 'filepath': '', - } + print("\n---------------------------\n" + "- Export to After Effects -\n" + "---------------------------") - # 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, - 'opacity': '', - 'opacity_static': '', - 'opacity_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'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' + # Store the current frame to restore it at the end of export + frame_current = data['frame_current'] # 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)) + print("Working on frame: " + str(frame)) data['scn'].frame_set(frame) # Get time for this loop - js_data['times'] += str((frame - data['start']) / data['fps']) + ',' + time = (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'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - zoom = str(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'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - zoom = str(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'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - scale = f'[{ae_transform[6]},{ae_transform[7] * data["width"] / data["height"]},{ae_transform[8]}],' - opacity = '0.0,' if obj.hide_render else '100.0,' - 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 - js_solid['opacity'] += opacity - # 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 - if opacity != js_solid['opacity_static']: - js_solid['opacity_anim'] = True - js_solid['position_static'] = position - js_solid['orientation_static'] = orientation - js_solid['scale_static'] = scale - js_solid['opacity_static'] = opacity - - # 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'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - energy = f'[{obj.data.energy * 100.0}],' - color = f'[{color[0]},{color[1]},{color[2]}],' - opacity = '0.0,' if obj.hide_render else '100.0,' - js_light = js_data['lights'][name_ae] - js_light['position'] += position - js_light['orientation'] += orientation - js_light['intensity'] += energy - js_light['Color'] += color - js_light['opacity'] += opacity - # 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 - if opacity != js_light['opacity_static']: - js_light['opacity_anim'] = True - js_light['position_static'] = position - js_light['orientation_static'] = orientation - js_light['intensity_static'] = energy - js_light['Color_static'] = color - js_light['opacity_static'] = opacity - if type == 'SPOT': - 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'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = f'[{ae_transform[3]},{ae_transform[4]},{ae_transform[5]}],' - scale = 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'[{ae_transform[0]},{ae_transform[1]},{ae_transform[2]}],' - orientation = 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'[{ae_transform[6] / ratio_to_comp},{ae_transform[7] / ratio_to_comp * image_width / image_height},{ae_transform[8]}],' - opacity = '0.0,' if obj.hide_render else '100.0,' - js_image = js_data['images'][name_ae] - js_image['position'] += position - js_image['orientation'] += orientation - js_image['scale'] += scale - js_image['opacity'] += opacity - # 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 - if opacity != js_image['opacity_static']: - js_image['opacity_anim'] = True - js_image['position_static'] = position - js_image['orientation_static'] = orientation - js_image['scale_static'] = scale - js_image['opacity_static'] = opacity - js_image['filepath'] = get_image_filepath(obj) - - # keyframes for all object bundles. Not ready yet. - # - # - # + for obj_type in selection.values(): + for obj in obj_type: + obj.get_keyframe(context, data, time, ae_size) # ---- 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(f'Scene : {data["scn"].name}\n') - jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n') - jsx_file.write(f'Duration : {data["duration"]}\n') - jsx_file.write(f'FPS : {data["fps"]}\n') - jsx_file.write(f'Date : {datetime.datetime.now()}\n') - jsx_file.write(f'Exported with io_export_after_effects.py\n') - jsx_file.write(f'**************************************/\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, {data["width"]}, ' - f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});') - jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n") - - jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') - - # 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(f'var {name_ae} = newComp.layers.addNull();\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - jsx_file.write(f'{name_ae}.property("position").setValue({obj["position"]});\n\n') - 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(f'var {name_ae} = newComp.layers.addNull();\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - # 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( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write( - f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - 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( - f'var {name_ae} = newComp.layers.addSolid({obj["color"]},"{name_ae}",{obj["width"]},{obj["height"]},1.0);\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - jsx_file.write(f'{name_ae}.source.parentFolder = footageFolder;\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "scale", "opacity"): - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write( - f'{name_ae}.property("{prop}").setValue([{obj[prop + "_static"]});\n') - 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( - f'var newFootage = app.project.importFile(new ImportOptions(File("{obj["filepath"]}")));\n') - jsx_file.write('newFootage.parentFolder = footageFolder;\n') - jsx_file.write( - f'var {name_ae} = newComp.layers.add(newFootage);\n') - jsx_file.write(f'{name_ae}.threeDLayer = true;\n') - jsx_file.write(f'{name_ae}.source.name = "{name_ae}";\n') - # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "scale", "opacity"): - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - 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( - f'var {name_ae} = newComp.layers.addLight("{name_ae}", [0.0, 0.0]);\n') - jsx_file.write( - f'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n') - # Set values of properties, add keyframes only where needed - props = ["position", "orientation", "intensity", "Color", "opacity"] - if obj['type'] == 'SPOT': - props.extend(("Cone Angle", "Cone Feather")) - for prop in props: - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write( - f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - jsx_file.write('\n') - jsx_file.write('\n') - - # Create cameras - jsx_file.write('// ************** CAMERAS **************\n\n') - for name_ae, obj in js_data['cameras'].items(): - # More than one camera can be selected - jsx_file.write( - f'var {name_ae} = newComp.layers.addCamera("{name_ae}",[0,0]);\n') + 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 : {data["scn"].name}\n') + jsx_file.write(f'Resolution : {data["width"]} x {data["height"]}\n') + jsx_file.write(f'Duration : {data["duration"]}\n') + jsx_file.write(f'FPS : {data["fps"]}\n') + jsx_file.write(f'Date : {datetime.datetime.now()}\n') + jsx_file.write(f'Exported with io_export_after_effects.py\n') + jsx_file.write(f'**************************************/\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'{name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n') - - # Set values of properties, add keyframes only where needed - for prop in ("position", "orientation", "zoom"): - if include_animation and obj[prop + '_anim']: - jsx_file.write( - f'{name_ae}.property("{prop}").setValuesAtTimes([{js_data["times"]}],[{obj[prop]}]);\n') - else: - jsx_file.write(f'{name_ae}.property("{prop}").setValue({obj[prop + "_static"]});\n') - jsx_file.write('\n') - 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') - jsx_file.close() + f'\nvar newComp = app.project.items.addComp(compName, {data["width"]}, ' + f'{data["height"]}, {data["aspect"]}, {data["duration"]}, {data["fps"]});') + jsx_file.write(f"\nnewComp.displayStartTime = {(data['start']) / data['fps']};\n\n") + + jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n') + + 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') # Set current frame of animation in blender to state before export - data['scn'].frame_set(curframe) + data['scn'].frame_set(frame_current) ########################################## @@ -1066,6 +779,11 @@ class ExportJsx(bpy.types.Operator, ExportHelper): 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", @@ -1084,19 +802,24 @@ class ExportJsx(bpy.types.Operator, ExportHelper): box = layout.box() 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.prop(self, 'include_image_planes') + 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') - box = layout.box() - box.label(text='Include Tracking Data:') - box.prop(self, 'include_cam_bundles') -# box.prop(self, 'include_ob_bundles') @classmethod def poll(cls, context): @@ -1106,11 +829,14 @@ class ExportJsx(bpy.types.Operator, ExportHelper): def execute(self, context): data = get_comp_data(context) - selection = get_selected(context) - write_jsx_file(self.filepath, data, selection, self.include_animation, - self.include_active_cam, self.include_selected_cams, - self.include_selected_objects, self.include_cam_bundles, - self.include_image_planes, self.ae_size) + 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, data, selection, + self.include_animation, self.ae_size) print("\nExport to After Effects Completed") return {'FINISHED'}