UI: Move Extensions repositories popover to header
[blender-addons-contrib.git] / io_export_after_effects.py
blob0db40e8b7daa6db9bf1b9b88b5650edda5224845
1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
19 bl_info = {
20 "name": "Export: Adobe After Effects (.jsx)",
21 "description": "Export cameras, selected objects & camera solution "
22 "3D Markers to Adobe After Effects CS3 and above",
23 "author": "Bartek Skorupa, Damien Picard (@pioverfour)",
24 "version": (0, 1, 3),
25 "blender": (2, 80, 0),
26 "location": "File > Export > Adobe After Effects (.jsx)",
27 "warning": "",
28 "doc_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
29 "Scripts/Import-Export/Adobe_After_Effects",
30 "category": "Import-Export",
34 import bpy
35 import os
36 import datetime
37 from math import degrees
38 from mathutils import Matrix, Vector, Color
41 def get_camera_frame_ranges(scene, start, end):
42 """Get frame ranges for each marker in the timeline
44 For this, start at the end of the timeline,
45 iterate through each camera-bound marker in reverse,
46 and get the range from this marker to the end of the previous range.
47 """
48 markers = sorted((m for m in scene.timeline_markers if m.camera is not None),
49 key=lambda m:m.frame, reverse=True)
51 if len(markers) <= 1:
52 return [[[start, end], scene.camera],]
54 camera_frame_ranges = []
55 current_frame = end
56 for m in markers:
57 if m.frame < current_frame:
58 camera_frame_ranges.append([[m.frame, current_frame + 1], m.camera])
59 current_frame = m.frame - 1
60 camera_frame_ranges.reverse()
61 camera_frame_ranges[0][0][0] = start
62 return camera_frame_ranges
65 class ObjectExport():
66 """Base exporter class
68 Collects data about an object and outputs the proper JSX script for AE.
69 """
70 def __init__(self, obj):
71 self.obj = obj
72 self.name_ae = convert_name(self.obj.name)
73 self.keyframes = {}
75 def get_prop_keyframe(self, prop_name, value, time):
76 """Get keyframe for given property, only if different from previous value"""
77 prop_keys = self.keyframes.setdefault(prop_name, [])
78 if len(prop_keys) == 0:
79 prop_keys.append([time, value, False])
80 return
82 if value != prop_keys[-1][1]:
83 prop_keys.append([time, value, False])
84 # Store which keys should hold, that is, which are
85 # the first in a series of identical values
86 else:
87 prop_keys[-1][2] = True
89 def get_keyframe(self, context, width, height, aspect, time, ae_size):
90 """Store animation for the current frame"""
91 ae_transform = convert_transform_matrix(self.obj.matrix_world,
92 width, height, aspect, ae_size)
94 self.get_prop_keyframe('position', ae_transform[0:3], time)
95 self.get_prop_keyframe('orientation', ae_transform[3:6], time)
96 self.get_prop_keyframe('scale', ae_transform[6:9], time)
98 def get_obj_script(self, include_animation):
99 """Get the JSX script for the object"""
100 return self.get_type_script() + self.get_anim_script(include_animation) + self.get_post_script()
102 def get_type_script(self):
103 """Get the basic part of the JSX script"""
104 type_script = f'var {self.name_ae} = newComp.layers.addNull();\n'
105 type_script += f'{self.name_ae}.threeDLayer = true;\n'
106 type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
107 return type_script
109 def get_anim_script(self, include_animation):
110 """Get the part of the JSX script encoding animation"""
111 anim_script = ""
113 # Set values of properties, add keyframes only where needed
114 for prop, keys in self.keyframes.items():
115 if include_animation and len(keys) > 1:
116 times = ",".join(str(k[0]) for k in keys)
117 values = ",".join(str(k[1]) for k in keys).replace(" ", "")
118 anim_script += (
119 f'{self.name_ae}.property("{prop}").setValuesAtTimes([{times}],[{values}]);\n')
121 # Set to HOLD the frames after which animation is fixed
122 # for several frames, to avoid interpolation errors
123 if any(k[2] for k in keys):
124 anim_script += (
125 f'var hold_frames = {[i + 1 for i, k in enumerate(keys) if k[2]]};\n'
126 'for (var i = 0; i < hold_frames.length; i++) {\n'
127 f' {self.name_ae}.property("{prop}").setInterpolationTypeAtKey(hold_frames[i], KeyframeInterpolationType.HOLD);\n'
128 '}\n')
130 # No animation for this property
131 else:
132 value = str(keys[0][1]).replace(" ", "")
133 anim_script += (
134 f'{self.name_ae}.property("{prop}").setValue({value});\n')
136 anim_script += '\n'
138 return anim_script
140 def get_post_script(self):
141 """This is only used in lights as a post-treatment after animation"""
142 return ""
144 class CameraExport(ObjectExport):
145 def __init__(self, obj, start_time=None, end_time=None):
146 super().__init__(obj)
147 self.start_time = start_time
148 self.end_time = end_time
150 def get_keyframe(self, context, width, height, aspect, time, ae_size):
151 ae_transform = convert_transform_matrix(self.obj.matrix_world,
152 width, height, aspect, ae_size)
153 zoom = convert_lens(self.obj, width, height,
154 aspect)
156 self.get_prop_keyframe('position', ae_transform[0:3], time)
157 self.get_prop_keyframe('orientation', ae_transform[3:6], time)
158 self.get_prop_keyframe('zoom', zoom, time)
160 def get_type_script(self):
161 type_script = f'var {self.name_ae} = newComp.layers.addCamera("{self.name_ae}",[0,0]);\n'
162 # Restrict time range when multiple cameras are used (markers)
163 if self.start_time is not None:
164 type_script += f'{self.name_ae}.inPoint = {self.start_time};\n'
165 type_script += f'{self.name_ae}.outPoint = {self.end_time};\n'
166 type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n'
167 return type_script
170 class LightExport(ObjectExport):
171 def get_keyframe(self, context, width, height, aspect, time, ae_size):
172 ae_transform = convert_transform_matrix(self.obj.matrix_world,
173 width, height, aspect, ae_size)
174 self.type = self.obj.data.type
175 color = list(self.obj.data.color)
176 intensity = self.obj.data.energy * 10.0
178 self.get_prop_keyframe('position', ae_transform[0:3], time)
179 if self.type in {'SPOT', 'SUN'}:
180 self.get_prop_keyframe('orientation', ae_transform[3:6], time)
181 self.get_prop_keyframe('intensity', intensity, time)
182 self.get_prop_keyframe('Color', color, time)
183 if self.type == 'SPOT':
184 cone_angle = degrees(self.obj.data.spot_size)
185 self.get_prop_keyframe('Cone Angle', cone_angle, time)
186 cone_feather = self.obj.data.spot_blend * 100.0
187 self.get_prop_keyframe('Cone Feather', cone_feather, time)
189 def get_type_script(self):
190 type_script = f'var {self.name_ae} = newComp.layers.addLight("{self.name_ae}", [0.0, 0.0]);\n'
191 type_script += f'{self.name_ae}.autoOrient = AutoOrientType.NO_AUTO_ORIENT;\n'
192 type_script += f'{self.name_ae}.lightType = LightType.SPOT;\n'
193 return type_script
195 def get_post_script(self):
196 """Set light type _after_ the orientation, otherwise the property is hidden in AE..."""
197 if self.obj.data.type == 'SUN':
198 post_script = f'{self.name_ae}.lightType = LightType.PARALLEL;\n'
199 elif self.obj.data.type == 'SPOT':
200 post_script = f'{self.name_ae}.lightType = LightType.SPOT;\n'
201 else:
202 post_script = f'{self.name_ae}.lightType = LightType.POINT;\n'
203 return post_script
206 class ImageExport(ObjectExport):
207 def get_keyframe(self, context, width, height, aspect, time, ae_size):
208 # Convert obj transform properties to AE space
209 plane_matrix = get_image_plane_matrix(self.obj)
210 # Scale plane to account for AE's transforms
211 plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4)
213 ae_transform = convert_transform_matrix(plane_matrix,
214 width, height, aspect, ae_size)
215 opacity = 0.0 if self.obj.hide_render else 100.0
217 if not hasattr(self, 'filepath'):
218 self.filepath = get_image_filepath(self.obj)
220 image_width, image_height = get_image_size(self.obj)
221 ratio_to_comp = image_width / width
222 scale = ae_transform[6:9]
223 if image_height != 0.0:
224 scale[1] *= image_width / image_height
225 if ratio_to_comp != 0.0:
226 scale[0] /= ratio_to_comp
227 scale[1] /= ratio_to_comp
229 self.get_prop_keyframe('position', ae_transform[0:3], time)
230 self.get_prop_keyframe('orientation', ae_transform[3:6], time)
231 self.get_prop_keyframe('scale', scale, time)
232 self.get_prop_keyframe('opacity', opacity, time)
234 def get_type_script(self):
235 type_script = f'var newFootage = app.project.importFile(new ImportOptions(File("{self.filepath}")));\n'
236 type_script += 'newFootage.parentFolder = footageFolder;\n'
237 type_script += f'var {self.name_ae} = newComp.layers.add(newFootage);\n'
238 type_script += f'{self.name_ae}.threeDLayer = true;\n'
239 type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
240 return type_script
243 class SolidExport(ObjectExport):
244 def get_keyframe(self, context, width, height, aspect, time, ae_size):
245 # Convert obj transform properties to AE space
246 plane_matrix = get_plane_matrix(self.obj)
247 # Scale plane to account for AE's transforms
248 plane_matrix = plane_matrix @ Matrix.Scale(100.0 / width, 4)
250 ae_transform = convert_transform_matrix(plane_matrix,
251 width, height, aspect, ae_size)
252 opacity = 0.0 if self.obj.hide_render else 100.0
254 if not hasattr(self, 'color'):
255 self.color = get_plane_color(self.obj)
256 if not hasattr(self, 'width'):
257 self.width = width
258 if not hasattr(self, 'height'):
259 self.height = height
261 scale = ae_transform[6:9]
262 scale[1] *= width / height
264 self.get_prop_keyframe('position', ae_transform[0:3], time)
265 self.get_prop_keyframe('orientation', ae_transform[3:6], time)
266 self.get_prop_keyframe('scale', scale, time)
267 self.get_prop_keyframe('opacity', opacity, time)
269 def get_type_script(self):
270 type_script = f'var {self.name_ae} = newComp.layers.addSolid({self.color},"{self.name_ae}",{self.width},{self.height},1.0);\n'
271 type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
272 type_script += f'{self.name_ae}.source.parentFolder = footageFolder;\n'
273 type_script += f'{self.name_ae}.threeDLayer = true;\n'
274 return type_script
277 class CamBundleExport(ObjectExport):
278 def __init__(self, obj, track):
279 self.obj = obj
280 self.track = track
281 self.name_ae = convert_name(f'{obj.name}__{track.name}')
282 self.keyframes = {}
284 def get_keyframe(self, context, width, height, aspect, time, ae_size):
285 # Bundles are in camera space.
286 # Transpose to world space
287 matrix = self.obj.matrix_basis @ Matrix.Translation(self.track.bundle)
288 # Convert the position into AE space
289 ae_transform = convert_transform_matrix(matrix,
290 width, height, aspect, ae_size)
292 self.get_prop_keyframe('position', ae_transform[0:3], time)
293 self.get_prop_keyframe('orientation', ae_transform[3:6], time)
295 def get_type_script(self):
296 type_script = f'var {self.name_ae} = newComp.layers.addNull();\n'
297 type_script += f'{self.name_ae}.threeDLayer = true;\n'
298 type_script += f'{self.name_ae}.source.name = "{self.name_ae}";\n'
299 return type_script
302 def get_camera_bundles(scene, camera):
303 cam_bundles = []
305 for constraint in camera.constraints:
306 if constraint.type == 'CAMERA_SOLVER':
307 # Which movie clip does it use
308 if constraint.use_active_clip:
309 clip = scene.active_clip
310 else:
311 clip = constraint.clip
313 # Go through each tracking point
314 for track in clip.tracking.tracks:
315 # Does this tracking point have a bundle
316 # (has its 3D position been solved)
317 if track.has_bundle:
318 cam_bundles.append(CamBundleExport(camera, track))
320 return cam_bundles
323 def get_selected(context, include_active_cam, include_selected_cams,
324 include_selected_objects, include_cam_bundles,
325 include_image_planes, include_solids):
326 """Create manageable list of selected objects"""
327 cameras = []
328 solids = [] # Meshes exported as AE solids
329 images = [] # Meshes exported as AE AV layers
330 lights = [] # Lights exported as AE lights
331 cam_bundles = [] # Camera trackers exported as AE nulls
332 nulls = [] # Remaining objects exported as AE nulls
334 scene = context.scene
335 fps = scene.render.fps / scene.render.fps_base
337 if context.scene.camera is not None:
338 if include_active_cam:
339 for frame_range, camera in get_camera_frame_ranges(
340 context.scene,
341 context.scene.frame_start, context.scene.frame_end):
343 if (include_cam_bundles
344 and camera not in (cam.obj for cam in cameras)):
345 cam_bundles.extend(
346 get_camera_bundles(context.scene, camera))
348 cameras.append(
349 CameraExport(camera,
350 (frame_range[0] - scene.frame_start) / fps,
351 (frame_range[1] - scene.frame_start) / fps))
353 for obj in context.selected_objects:
354 if obj.type == 'CAMERA':
355 # Ignore camera if already selected
356 if obj in (cam.obj for cam in cameras):
357 continue
358 if include_selected_cams:
359 cameras.append(CameraExport(obj))
360 if include_cam_bundles:
361 cam_bundles.extend(get_camera_bundles(context.scene, obj))
363 elif include_image_planes and is_image_plane(obj):
364 images.append(ImageExport(obj))
366 elif include_solids and is_plane(obj):
367 solids.append(SolidExport(obj))
369 elif include_selected_objects:
370 if obj.type == 'LIGHT':
371 lights.append(LightExport(obj))
372 else:
373 nulls.append(ObjectExport(obj))
375 return {'cameras': cameras,
376 'images': images,
377 'solids': solids,
378 'lights': lights,
379 'nulls': nulls,
380 'cam_bundles': cam_bundles}
383 def get_first_material(obj):
384 for slot in obj.material_slots:
385 if slot.material is not None:
386 return slot.material
389 def get_image_node(mat):
390 for node in mat.node_tree.nodes:
391 if node.type == "TEX_IMAGE":
392 return node.image
395 def get_plane_color(obj):
396 """Get the object's emission and base color, or 0.5 gray if no color is found."""
397 if obj.active_material is None:
398 color = (0.5,) * 3
399 elif obj.active_material:
400 from bpy_extras import node_shader_utils
401 wrapper = node_shader_utils.PrincipledBSDFWrapper(obj.active_material)
402 color = Color(wrapper.base_color[:3]) + wrapper.emission_color
404 return str(list(color))
407 def is_plane(obj):
408 """Check if object is a plane
410 Makes a few assumptions:
411 - The mesh has exactly one quad face
412 - The mesh is a rectangle
414 For now this doesn't account for shear, which could happen e.g. if the
415 vertices are rotated, and the object is scaled non-uniformly...
417 if obj.type != 'MESH':
418 return False
420 if len(obj.data.polygons) != 1:
421 return False
423 if len(obj.data.polygons[0].vertices) != 4:
424 return False
426 v1, v2, v3, v4 = (obj.data.vertices[v].co for v in obj.data.polygons[0].vertices)
428 # Check that poly is a parallelogram
429 if -v1 + v2 + v4 != v3:
430 return False
432 # Check that poly has at least one right angle
433 if (v2-v1).dot(v4-v1) != 0.0:
434 return False
436 # If my calculations are correct, that should make it a rectangle
437 return True
440 def is_image_plane(obj):
441 """Check if object is a plane with an image
443 Makes a few assumptions:
444 - The mesh is a plane
445 - The mesh has exactly one material
446 - There is only one image in this material node tree
447 - The rectangle is UV unwrapped and its UV is occupying the whole space
449 if not is_plane(obj):
450 return False
452 if len(obj.material_slots) == 0:
453 return False
455 mat = get_first_material(obj)
456 if mat is None:
457 return False
459 img = get_image_node(mat)
460 if img is None:
461 return False
463 if len(obj.data.vertices) != 4:
464 return False
466 if not get_image_plane_matrix(obj):
467 return False
469 return True
471 def get_image_filepath(obj):
472 mat = get_first_material(obj)
473 img = get_image_node(mat)
474 filepath = img.filepath
475 filepath = bpy.path.abspath(filepath)
476 filepath = os.path.abspath(filepath)
477 filepath = filepath.replace('\\', '\\\\')
478 return filepath
481 def get_image_size(obj):
482 mat = get_first_material(obj)
483 img = get_image_node(mat)
484 return img.size
487 def get_plane_matrix(obj):
488 """Get object's polygon local matrix from vertices."""
489 v1, v2, v3, v4 = (obj.data.vertices[v].co for v in obj.data.polygons[0].vertices)
491 p0 = obj.matrix_world @ v1
492 px = obj.matrix_world @ v2 - p0
493 py = obj.matrix_world @ v4 - p0
495 rot_mat = Matrix((px, py, px.cross(py))).transposed().to_4x4()
496 trans_mat = Matrix.Translation(p0 + (px + py) / 2.0)
497 mat = trans_mat @ rot_mat
499 return mat
502 def get_image_plane_matrix(obj):
503 """Get object's polygon local matrix from uvs.
505 This will only work if uvs occupy all space, to get bounds
507 p0, px, py = None, None, None
508 for p_i, p in enumerate(obj.data.uv_layers.active.data):
509 if p.uv == Vector((0, 0)):
510 p0 = p_i
511 elif p.uv == Vector((1, 0)):
512 px = p_i
513 elif p.uv == Vector((0, 1)):
514 py = p_i
516 if None in (p0, px, py):
517 return False
519 verts = obj.data.vertices
520 loops = obj.data.loops
522 p0 = obj.matrix_world @ verts[loops[p0].vertex_index].co
523 px = obj.matrix_world @ verts[loops[px].vertex_index].co - p0
524 py = obj.matrix_world @ verts[loops[py].vertex_index].co - p0
526 rot_mat = Matrix((px, py, px.cross(py))).transposed().to_4x4()
527 trans_mat = Matrix.Translation(p0 + (px + py) / 2.0)
528 mat = trans_mat @ rot_mat
530 return mat
533 def convert_name(name):
534 """Convert names of objects to avoid errors in AE"""
535 if not name[0].isalpha():
536 name = "_" + name
537 name = bpy.path.clean_name(name)
538 name = name.replace("-", "_")
540 return name
543 def convert_transform_matrix(matrix, width, height, aspect, ae_size=100.0):
544 """Convert from Blender's Location, Rotation and Scale
545 to AE's Position, Rotation/Orientation and Scale
547 This function will be called for every object for every frame
550 # Get blender transform data for object
551 b_loc = matrix.to_translation()
552 b_rot = matrix.to_euler('ZYX') # ZYX euler matches AE's orientation and allows to use x_rot_correction
553 b_scale = matrix.to_scale()
555 # Convert to AE Position Rotation and Scale. Axes in AE are different:
556 # AE's X is Blender's X,
557 # AE's Y is Blender's -Z,
558 # AE's Z is Blender's Y
559 x = (b_loc.x * 100.0 / aspect + width / 2.0) * ae_size / 100.0
560 y = (-b_loc.z * 100.0 + height / 2.0) * ae_size / 100.0
561 z = (b_loc.y * 100.0) * ae_size / 100.0
563 # Convert rotations to match AE's orientation.
564 # In Blender, object of zero rotation lays on floor.
565 # In AE, layer of zero orientation "stands", so subtract 90 degrees
566 rx = degrees(b_rot.x) - 90.0 # AE's X orientation = blender's X rotation if 'ZYX' euler.
567 ry = -degrees(b_rot.y) # AE's Y orientation = -blender's Y rotation if 'ZYX' euler
568 rz = -degrees(b_rot.z) # AE's Z orientation = -blender's Z rotation if 'ZYX' euler
570 # Convert scale to AE scale. ae_size is a global multiplier.
571 sx = b_scale.x * ae_size
572 sy = b_scale.y * ae_size
573 sz = b_scale.z * ae_size
575 return [x, y, z, rx, ry, rz, sx, sy, sz]
578 # Get camera's lens and convert to AE's "zoom" value in pixels
579 # this function will be called for every camera for every frame
582 # AE's lens is defined by "zoom" in pixels.
583 # Zoom determines focal angle or focal length.
585 # ZOOM VALUE CALCULATIONS:
587 # Given values:
588 # - sensor width (camera.data.sensor_width)
589 # - sensor height (camera.data.sensor_height)
590 # - sensor fit (camera.data.sensor_fit)
591 # - lens (blender's lens in mm)
592 # - width (width of the composition/scene in pixels)
593 # - height (height of the composition/scene in pixels)
594 # - PAR (pixel aspect ratio)
596 # Calculations are made using sensor's size and scene/comp dimension (width or height).
597 # If camera.sensor_fit is set to 'HORIZONTAL':
598 # sensor = camera.data.sensor_width, dimension = width.
600 # If camera.sensor_fit is set to 'AUTO':
601 # sensor = camera.data.sensor_width
602 # (actually, it just means to use the first value)
603 # In AUTO, if the vertical size is greater than the horizontal size:
604 # dimension = width
605 # else:
606 # dimension = height
608 # If camera.sensor_fit is set to 'VERTICAL':
609 # sensor = camera.data.sensor_height, dimension = height
611 # Zoom can be calculated using simple proportions.
614 # / |
615 # / |
616 # / | d
617 # s |\ / | i
618 # e | \ / | m
619 # n | \ / | e
620 # s | / \ | n
621 # o | / \ | s
622 # r |/ \ | i
623 # \ | o
624 # | | \ | n
625 # | | \ |
626 # | | |
627 # lens | zoom
629 # zoom / dimension = lens / sensor =>
630 # zoom = lens * dimension / sensor
632 # Above is true if square pixels are used. If not,
633 # aspect compensation is needed, so final formula is:
634 # zoom = lens * dimension / sensor * aspect
636 def convert_lens(camera, width, height, aspect):
637 if camera.data.sensor_fit == 'VERTICAL':
638 sensor = camera.data.sensor_height
639 else:
640 sensor = camera.data.sensor_width
642 if (camera.data.sensor_fit == 'VERTICAL'
643 or camera.data.sensor_fit == 'AUTO'
644 and (width / height) * aspect < 1.0):
645 dimension = height
646 else:
647 dimension = width
649 zoom = camera.data.lens * dimension / sensor * aspect
651 return zoom
653 # convert object bundle's matrix. Not ready yet. Temporarily not active
654 # def get_ob_bundle_matrix_world(cam_matrix_world, bundle_matrix):
655 # matrix = cam_matrix_basis
656 # return matrix
659 def write_jsx_file(context, file, selection, include_animation, ae_size):
660 """jsx script for AE creation"""
662 print("\n---------------------------\n"
663 "- Export to After Effects -\n"
664 "---------------------------")
666 # Create list of static blender data
667 scene = context.scene
668 width = scene.render.resolution_x
669 height = scene.render.resolution_y
670 aspect_x = scene.render.pixel_aspect_x
671 aspect_y = scene.render.pixel_aspect_y
672 aspect = aspect_x / aspect_y
673 if include_animation:
674 frame_end = scene.frame_end + 1
675 else:
676 frame_end = scene.frame_start + 1
677 fps = scene.render.fps / scene.render.fps_base
678 duration = (frame_end - scene.frame_start) / fps
680 # Store the current frame to restore it at the end of export
681 frame_current = scene.frame_current
683 # Get all keyframes for each object
684 for frame in range(scene.frame_start, frame_end):
685 print("Working on frame: " + str(frame))
686 scene.frame_set(frame)
688 # Get time for this loop
689 time = (frame - scene.frame_start) / fps
691 for obj_type in selection.values():
692 for obj in obj_type:
693 obj.get_keyframe(context, width, height, aspect, time, ae_size)
695 # ---- write JSX file
696 with open(file, 'w') as jsx_file:
697 # Make the jsx executable in After Effects (enable double click on jsx)
698 jsx_file.write('#target AfterEffects\n\n')
699 # Script's header
700 jsx_file.write('/**************************************\n')
701 jsx_file.write(f'Scene : {scene.name}\n')
702 jsx_file.write(f'Resolution : {width} x {height}\n')
703 jsx_file.write(f'Duration : {duration}\n')
704 jsx_file.write(f'FPS : {fps}\n')
705 jsx_file.write(f'Date : {datetime.datetime.now()}\n')
706 jsx_file.write('Exported with io_export_after_effects.py\n')
707 jsx_file.write('**************************************/\n\n\n\n')
709 # Wrap in function
710 jsx_file.write("function compFromBlender(){\n")
712 # Create new comp
713 if bpy.data.filepath:
714 comp_name = convert_name(
715 os.path.splitext(os.path.basename(bpy.data.filepath))[0])
716 else:
717 comp_name = "BlendComp"
718 jsx_file.write(f'\nvar compName = prompt("Blender Comp\'s Name \\nEnter Name of newly created Composition","{comp_name}","Composition\'s Name");\n')
719 jsx_file.write('if (compName){')
720 # Continue only if comp name is given. If not - terminate
721 jsx_file.write(
722 f'\nvar newComp = app.project.items.addComp(compName, {width}, '
723 f'{height}, {aspect}, {duration}, {fps});')
724 jsx_file.write(f"\nnewComp.displayStartTime = {scene.frame_start / fps};\n\n")
726 jsx_file.write('var footageFolder = app.project.items.addFolder(compName + "_layers")\n\n\n')
728 # Write each object's creation script
729 for obj_type in ('cam_bundles', 'nulls', 'solids', 'images', 'lights', 'cameras'):
730 if len(selection[obj_type]):
731 type_name = 'CAMERA 3D MARKERS' if obj_type == 'cam_bundles' else obj_type.upper()
732 jsx_file.write(f'// ************** {type_name} **************\n\n')
733 for obj in selection[obj_type]:
734 jsx_file.write(obj.get_obj_script(include_animation))
735 jsx_file.write('\n')
737 # Exit import if no comp name given
738 jsx_file.write('\n}else{alert ("Exit Import Blender animation data \\nNo Comp name has been chosen","EXIT")};')
739 # Close function
740 jsx_file.write("}\n\n\n")
741 # Execute function. Wrap in "undo group" for easy undoing import process
742 jsx_file.write('app.beginUndoGroup("Import Blender animation data");\n')
743 jsx_file.write('compFromBlender();\n') # Execute function
744 jsx_file.write('app.endUndoGroup();\n\n\n')
746 # Restore current frame of animation in blender to state before export
747 scene.frame_set(frame_current)
750 ##########################################
751 # ExportJsx class register/unregister
752 ##########################################
755 from bpy_extras.io_utils import ExportHelper
756 from bpy.props import StringProperty, BoolProperty, FloatProperty
759 class ExportJsx(bpy.types.Operator, ExportHelper):
760 """Export selected cameras and objects animation to After Effects"""
761 bl_idname = "export.jsx"
762 bl_label = "Export to Adobe After Effects"
763 bl_options = {'PRESET', 'UNDO'}
764 filename_ext = ".jsx"
765 filter_glob: StringProperty(default="*.jsx", options={'HIDDEN'})
767 include_animation: BoolProperty(
768 name="Animation",
769 description="Animate Exported Cameras and Objects",
770 default=True,
772 include_active_cam: BoolProperty(
773 name="Active Camera",
774 description="Include Active Camera",
775 default=True,
777 include_selected_cams: BoolProperty(
778 name="Selected Cameras",
779 description="Add Selected Cameras",
780 default=True,
782 include_selected_objects: BoolProperty(
783 name="Selected Objects",
784 description="Export Selected Objects",
785 default=True,
787 include_cam_bundles: BoolProperty(
788 name="Camera 3D Markers",
789 description="Include 3D Markers of Camera Motion Solution for selected cameras",
790 default=True,
792 include_image_planes: BoolProperty(
793 name="Image Planes",
794 description="Include image mesh objects",
795 default=True,
797 include_solids: BoolProperty(
798 name="Solids",
799 description="Include rectangles as solids",
800 default=True,
802 # include_ob_bundles = BoolProperty(
803 # name="Objects 3D Markers",
804 # description="Include 3D Markers of Object Motion Solution for selected cameras",
805 # default=True,
807 ae_size: FloatProperty(
808 name="Scale",
809 description="Size of AE Composition (pixels per 1 BU)",
810 default=100.0,
811 min=0.0,
812 soft_max=10000,
815 def draw(self, context):
816 layout = self.layout
818 box = layout.box()
819 box.label(text='Include Cameras and Objects')
820 col = box.column(align=True)
821 col.prop(self, 'include_active_cam')
822 col.prop(self, 'include_selected_cams')
823 col.prop(self, 'include_selected_objects')
824 col.prop(self, 'include_image_planes')
825 col.prop(self, 'include_solids')
827 box = layout.box()
828 box.label(text='Include Tracking Data')
829 box.prop(self, 'include_cam_bundles')
830 # box.prop(self, 'include_ob_bundles')
832 box = layout.box()
833 box.prop(self, 'include_animation')
835 box = layout.box()
836 box.label(text='Transform')
837 box.prop(self, 'ae_size')
839 @classmethod
840 def poll(cls, context):
841 selected = context.selected_objects
842 camera = context.scene.camera
843 return selected or camera
845 def execute(self, context):
846 selection = get_selected(context, self.include_active_cam,
847 self.include_selected_cams,
848 self.include_selected_objects,
849 self.include_cam_bundles,
850 self.include_image_planes,
851 self.include_solids)
852 write_jsx_file(context, self.filepath, selection,
853 self.include_animation, self.ae_size)
854 print("\nExport to After Effects Completed")
855 return {'FINISHED'}
858 def menu_func(self, context):
859 self.layout.operator(
860 ExportJsx.bl_idname, text="Adobe After Effects (.jsx)")
863 def register():
864 bpy.utils.register_class(ExportJsx)
865 bpy.types.TOPBAR_MT_file_export.append(menu_func)
868 def unregister():
869 bpy.utils.unregister_class(ExportJsx)
870 bpy.types.TOPBAR_MT_file_export.remove(menu_func)
873 if __name__ == "__main__":
874 register()