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 #####
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)",
25 "blender": (2, 80, 0),
26 "location": "File > Export > Adobe After Effects (.jsx)",
28 "doc_url": "http://wiki.blender.org/index.php/Extensions:2.6/Py/"
29 "Scripts/Import-Export/Adobe_After_Effects",
30 "category": "Import-Export",
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.
48 markers
= sorted((m
for m
in scene
.timeline_markers
if m
.camera
is not None),
49 key
=lambda m
:m
.frame
, reverse
=True)
52 return [[[start
, end
], scene
.camera
],]
54 camera_frame_ranges
= []
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
66 """Base exporter class
68 Collects data about an object and outputs the proper JSX script for AE.
70 def __init__(self
, obj
):
72 self
.name_ae
= convert_name(self
.obj
.name
)
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])
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
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'
109 def get_anim_script(self
, include_animation
):
110 """Get the part of the JSX script encoding animation"""
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(" ", "")
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
):
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'
130 # No animation for this property
132 value
= str(keys
[0][1]).replace(" ", "")
134 f
'{self.name_ae}.property("{prop}").setValue({value});\n')
140 def get_post_script(self
):
141 """This is only used in lights as a post-treatment after animation"""
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
,
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'
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'
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'
202 post_script
= f
'{self.name_ae}.lightType = LightType.POINT;\n'
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'
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'):
258 if not hasattr(self
, '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'
277 class CamBundleExport(ObjectExport
):
278 def __init__(self
, obj
, track
):
281 self
.name_ae
= convert_name(f
'{obj.name}__{track.name}')
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'
302 def get_camera_bundles(scene
, camera
):
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
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)
318 cam_bundles
.append(CamBundleExport(camera
, track
))
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"""
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(
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
)):
346 get_camera_bundles(context
.scene
, 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
):
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
))
373 nulls
.append(ObjectExport(obj
))
375 return {'cameras': cameras
,
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:
389 def get_image_node(mat
):
390 for node
in mat
.node_tree
.nodes
:
391 if node
.type == "TEX_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:
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
))
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':
420 if len(obj
.data
.polygons
) != 1:
423 if len(obj
.data
.polygons
[0].vertices
) != 4:
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
:
432 # Check that poly has at least one right angle
433 if (v2
-v1
).dot(v4
-v1
) != 0.0:
436 # If my calculations are correct, that should make it a rectangle
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
):
452 if len(obj
.material_slots
) == 0:
455 mat
= get_first_material(obj
)
459 img
= get_image_node(mat
)
463 if len(obj
.data
.vertices
) != 4:
466 if not get_image_plane_matrix(obj
):
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('\\', '\\\\')
481 def get_image_size(obj
):
482 mat
= get_first_material(obj
)
483 img
= get_image_node(mat
)
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
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)):
511 elif p
.uv
== Vector((1, 0)):
513 elif p
.uv
== Vector((0, 1)):
516 if None in (p0
, px
, py
):
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
533 def convert_name(name
):
534 """Convert names of objects to avoid errors in AE"""
535 if not name
[0].isalpha():
537 name
= bpy
.path
.clean_name(name
)
538 name
= name
.replace("-", "_")
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:
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:
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.
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
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):
649 zoom
= camera
.data
.lens
* dimension
/ sensor
* aspect
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
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
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():
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')
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')
710 jsx_file
.write("function compFromBlender(){\n")
713 if bpy
.data
.filepath
:
714 comp_name
= convert_name(
715 os
.path
.splitext(os
.path
.basename(bpy
.data
.filepath
))[0])
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
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
))
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")};')
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(
769 description
="Animate Exported Cameras and Objects",
772 include_active_cam
: BoolProperty(
773 name
="Active Camera",
774 description
="Include Active Camera",
777 include_selected_cams
: BoolProperty(
778 name
="Selected Cameras",
779 description
="Add Selected Cameras",
782 include_selected_objects
: BoolProperty(
783 name
="Selected Objects",
784 description
="Export Selected Objects",
787 include_cam_bundles
: BoolProperty(
788 name
="Camera 3D Markers",
789 description
="Include 3D Markers of Camera Motion Solution for selected cameras",
792 include_image_planes
: BoolProperty(
794 description
="Include image mesh objects",
797 include_solids
: BoolProperty(
799 description
="Include rectangles as solids",
802 # include_ob_bundles = BoolProperty(
803 # name="Objects 3D Markers",
804 # description="Include 3D Markers of Object Motion Solution for selected cameras",
807 ae_size
: FloatProperty(
809 description
="Size of AE Composition (pixels per 1 BU)",
815 def draw(self
, context
):
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')
828 box
.label(text
='Include Tracking Data')
829 box
.prop(self
, 'include_cam_bundles')
830 # box.prop(self, 'include_ob_bundles')
833 box
.prop(self
, 'include_animation')
836 box
.label(text
='Transform')
837 box
.prop(self
, 'ae_size')
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
,
852 write_jsx_file(context
, self
.filepath
, selection
,
853 self
.include_animation
, self
.ae_size
)
854 print("\nExport to After Effects Completed")
858 def menu_func(self
, context
):
859 self
.layout
.operator(
860 ExportJsx
.bl_idname
, text
="Adobe After Effects (.jsx)")
864 bpy
.utils
.register_class(ExportJsx
)
865 bpy
.types
.TOPBAR_MT_file_export
.append(menu_func
)
869 bpy
.utils
.unregister_class(ExportJsx
)
870 bpy
.types
.TOPBAR_MT_file_export
.remove(menu_func
)
873 if __name__
== "__main__":