[Bf-extensions-cvs] [a140f06] master: After Effects export: add solid and image plane export

Damien Picard noreply at git.blender.org
Sat Feb 20 19:24:39 CET 2021


Commit: a140f066ac99e1860af38080f008507029a34706
Author: Damien Picard
Date:   Wed Jul 8 14:28:04 2020 +0200
Branches: master
https://developer.blender.org/rBACa140f066ac99e1860af38080f008507029a34706

After Effects export: add solid and image plane export

This squashed commit also:
- sets the default AE comp name to the blend file name

- reverts "io_export_after_effects: update to 2.8 T63856"
(commit 5781362d811bb4a99bee4e38a9d3a69813a8dd04)
I don’t know what happened there, but it actually removed some
features, most notably rBA31e5c6b9.

- does a bit of cleanup, formatting, and refactoring:
  - include main() function in operator
  - use loop for prop animation writing
  - fix long-standing bug where spot lights wouldn't export cone angle
    and blend
  - simplify selection data structure

===================================================================

M	io_export_after_effects.py

===================================================================

diff --git a/io_export_after_effects.py b/io_export_after_effects.py
index e5a2643..7aec1d1 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, (Adi Samsonoff)",
-    "version": (0, 0, 66),
+    "author": "Bartek Skorupa",
+    "version": (0, 0, 67),
     "blender": (2, 80, 0),
     "location": "File > Export > Adobe After Effects (.jsx)",
     "warning": "",
@@ -34,13 +34,14 @@ bl_info = {
 
 
 import bpy
+import os
 import datetime
 from math import degrees, floor
-from mathutils import Matrix
+from mathutils import Matrix, Vector, Color
 
 
-# create list of static blender's data
 def get_comp_data(context):
+    """Create list of static blender's data"""
     scene = context.scene
     aspect_x = scene.render.pixel_aspect_x
     aspect_y = scene.render.pixel_aspect_y
@@ -64,8 +65,8 @@ def get_comp_data(context):
         }
 
 
-# create list of active camera for each frame in case active camera is set by markers
 def get_active_cam_for_each_frame(scene, start, end):
+    """Create list of active camera for each frame in case active camera is set by markers"""
     active_cam_frames = []
     sorted_markers = []
     markers = scene.timeline_markers
@@ -80,7 +81,8 @@ def get_active_cam_for_each_frame(scene, start, end):
                 for m, marker in enumerate(sorted_markers):
                     if marker[0] > frame:
                         if m != 0:
-                            active_cam_frames.append(sorted_markers[m - 1][1].camera)
+                            active_cam_frames.append(
+                                sorted_markers[m - 1][1].camera)
                         else:
                             active_cam_frames.append(marker[1].camera)
                         break
@@ -88,36 +90,41 @@ def get_active_cam_for_each_frame(scene, start, end):
                         active_cam_frames.append(marker[1].camera)
     if not active_cam_frames:
         if scene.camera:
-            # in this case active_cam_frames array will have legth of 1. This will indicate that there is only one active cam in all frames
+            # in this case active_cam_frames array will have length of 1. This
+            # will indicate that there is only one active cam in all frames
             active_cam_frames.append(scene.camera)
 
     return(active_cam_frames)
 
 
-# create managable list of selected objects
 def get_selected(context):
-    cameras = []  # list of selected cameras
-    solids = []  # list of all selected meshes that can be exported as AE's solids
-    lights = []  # list of all selected lamps that can be exported as AE's lights
-    nulls = []  # list of all selected objects exept cameras (will be used to create nulls in AE)
+    """Create manageable list of selected objects"""
+    cameras = []  # List of selected cameras
+    solids = []   # List of selected meshes exported as AE solids
+    images = []   # List of selected meshes exported as AE AV layers
+    lights = []   # List of selected lights exported as AE lights
+    nulls = []    # List of selected objects except cameras (will be used to create nulls in AE)
     obs = context.selected_objects
 
     for ob in obs:
         if ob.type == 'CAMERA':
-            cameras.append([ob, convert_name(ob.name)])
+            cameras.append(ob)
+
+        elif is_image_plane(ob):
+            images.append(ob)
 
         elif is_plane(ob):
-            # not ready yet. is_plane(object) returns False in all cases. This is temporary
-            solids.append([ob, convert_name(ob.name)])
+            solids.append(ob)
 
         elif ob.type == 'LIGHT':
-            lights.append([ob, ob.data.type + convert_name(ob.name)])  # Type of lamp added to name
+            lights.append(ob)
 
         else:
-            nulls.append([ob, convert_name(ob.name)])
+            nulls.append(ob)
 
     selection = {
         'cameras': cameras,
+        'images': images,
         'solids': solids,
         'lights': lights,
         'nulls': nulls,
@@ -126,14 +133,149 @@ def get_selected(context):
     return selection
 
 
-# check if object is plane and can be exported as AE's solid
-def is_plane(object):
-    # work in progress. Not ready yet
-    return False
+def get_first_material(obj):
+    for slot in obj.material_slots:
+        if slot.material is not None:
+            return slot.material
+
+
+def get_image_node(mat):
+    for node in mat.node_tree.nodes:
+        if node.type == "TEX_IMAGE":
+            return node.image
+
+
+def get_plane_color(obj):
+    """Get the object's emission and base color, or 0.5 gray if no color is found."""
+    if obj.active_material is None:
+        color = (0.5,) * 3
+    elif obj.active_material:
+        from bpy_extras import node_shader_utils
+        wrapper = node_shader_utils.PrincipledBSDFWrapper(obj.active_material)
+        color = Color(wrapper.base_color[:3]) + wrapper.emission_color
+
+    return '[%f,%f,%f]' % (color[0], color[1], color[2])
+
+
+def is_plane(obj):
+    """Check if object is a plane
+
+    Makes a few assumptions:
+    - The mesh has exactly one quad face
+    - The mesh is a rectangle
+
+    For now this doesn't account for shear, which could happen e.g. if the
+    vertices are rotated, and the object is scaled non-uniformly...
+    """
+    if obj.type != 'MESH':
+        return False
+
+    if len(obj.data.polygons) != 1:
+        return False
+
+    if len(obj.data.polygons[0].vertices) != 4:
+        return False
+
+    v1, v2, v3, v4 = (obj.data.vertices[v].co for v in obj.data.polygons[0].vertices)
+
+    # Check that poly is a parallelogram
+    if -v1 + v2 + v4 != v3:
+        return False
+
+    # Check that poly has at least one right angle
+    if (v2-v1).dot(v4-v1) != 0.0:
+        return False
+
+    # If my calculations are correct, that should make it a rectangle
+    return True
+
+
+def is_image_plane(obj):
+    """Check if object is a plane with an image
+
+    Makes a few assumptions:
+    - The mesh is a plane
+    - The mesh has exactly one material
+    - There is only one image in this material node tree
+    """
+    if not is_plane(obj):
+        return False
+
+    if not len(obj.material_slots):
+        return False
+
+    mat = get_first_material(obj)
+    if mat is None:
+        return False
+
+    img = get_image_node(mat)
+    if img is None:
+        return False
+
+    if len(obj.data.vertices) == 4:
+        return True
+
+
+def get_image_filepath(obj):
+    mat = get_first_material(obj)
+    img = get_image_node(mat)
+    filepath = img.filepath
+    filepath = bpy.path.abspath(filepath)
+    filepath = os.path.abspath(filepath)
+    filepath = filepath.replace('\\', '\\\\')
+    return filepath
+
+
+def get_image_size(obj):
+    mat = get_first_material(obj)
+    img = get_image_node(mat)
+    return img.size
+
+
+def get_plane_matrix(obj):
+    """Get object's polygon local matrix from vertices."""
+    v1, v2, v3, v4 = (obj.data.vertices[v].co for v in obj.data.polygons[0].vertices)
+
+    p0 = obj.matrix_world @ v1
+    px = obj.matrix_world @ v2 - p0
+    py = obj.matrix_world @ v4 - p0
+
+    rot_mat = Matrix((px, py, px.cross(py))).transposed().to_4x4()
+    trans_mat = Matrix.Translation(p0 + (px + py) / 2.0)
+    mat = trans_mat @ rot_mat
+
+    return mat
+
+
+def get_image_plane_matrix(obj):
+    """Get object's polygon local matrix from uvs.
+
+    This will only work if uvs occupy all space, to get bounds
+    """
+    for p_i, p in enumerate(obj.data.uv_layers.active.data):
+        if p.uv == Vector((0, 0)):
+            p0 = p_i
+        elif p.uv == Vector((1, 0)):
+            px = p_i
+        elif p.uv == Vector((0, 1)):
+            py = p_i
+
+    verts = obj.data.vertices
+    loops = obj.data.loops
+
+    p0 = obj.matrix_world @ verts[loops[p0].vertex_index].co
+    px = obj.matrix_world @ verts[loops[px].vertex_index].co - p0
+    py = obj.matrix_world @ verts[loops[py].vertex_index].co - p0
+
+    rot_mat = Matrix((px, py, px.cross(py))).transposed().to_4x4()
+    trans_mat = Matrix.Translation(p0 + (px + py) / 2.0)
+    mat = trans_mat @ rot_mat
+
+    return mat
 
 
-# convert names of objects to avoid errors in AE.
 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.
@@ -148,38 +290,51 @@ def convert_name(name):
     return name
 
 
-# get object's blender's location rotation and scale and return AE's Position, Rotation/Orientation and scale
-# this function will be called for every object for every frame
-def convert_transform_matrix(matrix, width, height, aspect, x_rot_correction=False):
+def convert_transform_matrix(matrix, width, height, aspect,
+                             x_rot_correction=False, ae_size=100.0):
+    """Convert from Blender's Location, Rotation and Scale
+    to AE's Position, Rotation/Orientation and Scale
 
-    # get blender transform data for ob
-    b_loc = matrix.to_translation()
+    This function will be called for every object for every frame
+    """
+
+    scale_mat = Matrix.Scale(width, 4)
+
+    # Get blender transform data for ob
+    b_loc = (scale_mat @ matrix).to_translation()
     b_rot = matrix.to_euler('ZYX')  # ZYX euler matches AE's orientation and allows to use x_rot_correction
     b_scale = matrix.to_scale()
 
-    # convert to AE Position Rotation and Scale
-    # Axes in AE are different. AE's X is blender's X, AE's Y is negative Blender's Z, AE's Z is Blender's Y
-    x = (b_loc.x * 100.0) / aspect + width / 2.0  # calculate AE's X position
-    y = (-b_loc.z * 100.0) + (height / 2.0)  # calculate AE's Y position
-    z = b_loc.y * 100.0  # calculate AE's Z position
+    # Convert to AE Position Rotation and Scale. Axes in AE are different:
+    # AE's X is Blender's X,
+    # AE's Y is Blender's -Z,
+    # AE's Z is Blender's Y
+    x = (b_loc.x * ae_size / 100.0) / aspect + width / 2.0
+    y = (-b_loc.z * a

@@ Diff output truncated at 10240 characters. @@



More information about the Bf-extensions-cvs mailing list