瀏覽代碼

Merge pull request #91 from Jason0214/support_nla_track

Refactor animation module, basic support for nla strips
Lu Jiacheng 6 年之前
父節點
當前提交
b24fc36f84
共有 26 個文件被更改,包括 1327 次插入900 次删除
  1. 6 0
      io_scene_godot/__init__.py
  2. 0 866
      io_scene_godot/converters/animation.py
  3. 4 0
      io_scene_godot/converters/animation/__init__.py
  4. 379 0
      io_scene_godot/converters/animation/action.py
  5. 248 0
      io_scene_godot/converters/animation/animation_data.py
  6. 98 0
      io_scene_godot/converters/animation/constraint_baking.py
  7. 486 0
      io_scene_godot/converters/animation/serializer.py
  8. 17 28
      io_scene_godot/converters/simple_nodes.py
  9. 11 2
      io_scene_godot/structures.py
  10. 0 0
      tests/reference_exports/action_with_constraint/constraint_external_IK.escn
  11. 0 0
      tests/reference_exports/action_with_constraint/constraint_internal_IK.escn
  12. 2 2
      tests/reference_exports/action_with_constraint/stashed_constraint.escn
  13. 11 2
      tests/reference_exports/light/animation_point_light_shadow.escn
  14. 29 0
      tests/reference_exports/nla_animation/animation_multi_strip.escn
  15. 8 0
      tests/reference_exports/nla_animation/nla_with_active_action.escn
  16. 10 0
      tests/reference_exports/nla_animation/nla_with_no_active_action.escn
  17. 8 0
      tests/reference_exports/nla_animation/nla_with_stashed_action.escn
  18. 10 0
      tests/reference_exports/shape_key/animation_shapekey_with_transform.escn
  19. 0 0
      tests/test_scenes/action_with_constraint/constraint_external_IK.blend
  20. 0 0
      tests/test_scenes/action_with_constraint/constraint_internal_IK.blend
  21. 0 0
      tests/test_scenes/action_with_constraint/stashed_constraint.blend
  22. 二進制
      tests/test_scenes/nla_animation/animation_multi_strip.blend
  23. 二進制
      tests/test_scenes/nla_animation/nla_with_active_action.blend
  24. 二進制
      tests/test_scenes/nla_animation/nla_with_no_active_action.blend
  25. 二進制
      tests/test_scenes/nla_animation/nla_with_stashed_action.blend
  26. 二進制
      tests/test_scenes/shape_key/animation_shapekey_with_transform.blend

+ 6 - 0
io_scene_godot/__init__.py

@@ -106,6 +106,12 @@ class ExportGodot(bpy.types.Operator, ExportHelper):
         description="Export only objects on the active layers.",
         description="Export only objects on the active layers.",
         default=True,
         default=True,
     )
     )
+    use_stashed_action = BoolProperty(
+        name="Export Stashed Actions",
+        description="Export stashed actions and muted nla_strip as separate "
+                    "animation and place into AnimationPlayer",
+        default=True,
+    )
     animation_modes = EnumProperty(
     animation_modes = EnumProperty(
         name="Animation Modes",
         name="Animation Modes",
         description="Configuration of how blender animation data being "
         description="Configuration of how blender animation data being "

+ 0 - 866
io_scene_godot/converters/animation.py

@@ -1,866 +0,0 @@
-"""Export animation into Godot scene tree"""
-import collections
-import re
-import math
-import copy
-import logging
-from functools import partial
-import bpy
-import bpy_extras.anim_utils
-import mathutils
-from ..structures import (NodeTemplate, NodePath, fix_directional_transform,
-                          InternalResource, Array, Map, fix_matrix,
-                          fix_bone_attachment_transform)
-
-NEAREST_INTERPOLATION = 0
-LINEAR_INTERPOLATION = 1
-
-
-# attribute converted as a bool, no interpolation
-CONVERT_AS_BOOL = 0
-# attribute converted as a float
-CONVERT_AS_FLOAT = 1
-# attribute is a vec or mat, mapping to several fcurves in animation
-CONVERT_AS_MULTI_VALUE = 2
-# a quad tuple contains information to convert an attribute
-# or a fcurve of blender to godot things
-AttributeConvertInfo = collections.namedtuple(
-    'AttributeConvertInfo',
-    ['bl_name', 'gd_name', 'converter_function', 'attribute_type']
-)
-
-# a suffix append to action need baking to avoid name collision
-# with baked action's name
-BAKING_SUFFIX = '--being-baking'
-
-
-class Track:
-    """Animation track, has track type, track path, interpolation
-    method, a list of frames and a list of frame values.
-
-    Note that element in value_list is not strictly typed, for example,
-    a transform track would have value with type mathutils.Matrix(),
-    while some track would just have a float value"""
-    def __init__(self, track_type, track_path,
-                 frames=(), values=()):
-        self.type = track_type
-        self.path = track_path
-        # default to linear
-        self.interp = LINEAR_INTERPOLATION
-        self.frames = list()
-        self.values = list()
-
-        for frame in frames:
-            self.frames.append(frame)
-        for value in values:
-            self.values.append(value)
-
-    def add_frame_data(self, frame, value):
-        """Add add frame to track"""
-        self.frames.append(frame)
-        self.values.append(value)
-
-    def frame_end(self):
-        """The frame number of last frame"""
-        if not self.frames:
-            return 0
-        return self.frames[-1]
-
-    def frame_begin(self):
-        """The frame number of first frame"""
-        if not self.frames:
-            return 0
-        return self.frames[0]
-
-
-class AnimationResource(InternalResource):
-    """Internal resource with type Animation"""
-    def __init__(self, name):
-        super().__init__('Animation', name)
-        self['step'] = 0.1
-        self['length'] = 0
-        self.track_count = 0
-
-    def add_track(self, track):
-        """add a track to animation resource"""
-        track_length = track.frame_end() / bpy.context.scene.render.fps
-        if track_length > self['length']:
-            self['length'] = track_length
-
-        track_id_str = 'tracks/{}'.format(self.track_count)
-        self.track_count += 1
-
-        self[track_id_str + '/type'] = '"{}"'.format(track.type)
-        self[track_id_str + '/path'] = track.path
-        self[track_id_str + '/interp'] = track.interp
-        if track.type == 'transform':
-            self[track_id_str + '/keys'] = transform_frames_to_keys(
-                track.frames, track.values, track.interp
-            )
-        elif track.type == 'value':
-            self[track_id_str + '/keys'] = value_frames_to_keys(
-                track.frames, track.values, track.interp
-            )
-
-    def add_track_via_attr_mapping(self, fcurves, conv_quad_tuple_list,
-                                   base_node_path):
-        """Accepts some attribute mapping relation between blender and godot,
-        and further call `add_simple_value_track` export tracks.
-
-        `conv_quad_tuple_list` is a list of quad tuple which compose of
-        of(bl_attr_name, gd_attr_name, converter_lambda, attr_type)"""
-        for item in conv_quad_tuple_list:
-            bl_attr, gd_attr, converter, val_type = item
-            # vector vallue animation need special treatment
-            if val_type in (CONVERT_AS_FLOAT, CONVERT_AS_BOOL):
-                if val_type == CONVERT_AS_FLOAT:
-                    track_builder = build_linear_interp_value_track
-                else:
-                    track_builder = build_const_interp_value_track
-                self.add_simple_value_track(
-                    fcurves,
-                    bl_attr,
-                    partial(
-                        track_builder,
-                        base_node_path.new_copy(gd_attr),
-                        converter
-                    )
-                )
-
-    def add_simple_value_track(self, fcurves, fcurve_data_path,
-                               fcurve_to_track_func):
-        """Add a simple value track into AnimationResource, simple value
-        track means it have a one-one mapping to fcurve.
-
-        Note that the fcurve_to_track_func is a partial of
-        function like build_linear_interp_value_track and
-        build_const_interp_value_track which create a track
-        from fcurve"""
-        fcurve = fcurves.find(fcurve_data_path)
-        if fcurve is not None:
-            new_track = fcurve_to_track_func(fcurve)
-            self.add_track(new_track)
-
-
-class AnimationPlayer(NodeTemplate):
-    """Godot scene node with type AnimationPlayer"""
-    def __init__(self, name, parent):
-        super().__init__(name, "AnimationPlayer", parent)
-        # use parent node as the animation root node
-        self['root_node'] = NodePath(self.get_path(), parent.get_path())
-        # blender actions not in nla_tracks are treated as default
-        self.default_animation = None
-
-    def add_default_animation_resource(self, escn_file, action):
-        """Default animation resource may hold animation from children
-        objects, parameter action is used as hash key of resource"""
-        self.default_animation = self.create_animation_resource(
-            escn_file, action
-        )
-
-    def create_animation_resource(self, escn_file, action):
-        """Create a new animation resource and add it into escn file"""
-        resource_name = action.name
-        new_anim_resource = AnimationResource(resource_name)
-
-        # add animation resource without checking hash,
-        # blender action is in world space, while godot animation
-        # is in local space (parent space),  so identical actions
-        # are not necessarily generates identical godot animations
-        resource_id = escn_file.force_add_internal_resource(new_anim_resource)
-
-        self['anims/{}'.format(resource_name)] = (
-            "SubResource({})".format(resource_id))
-
-        return new_anim_resource
-
-
-def value_frames_to_keys(frame_list, value_list, interp):
-    """Serialize a value list to a track keys object"""
-    time_array = Array(prefix='PoolRealArray(', suffix=')')
-    transition_array = Array(prefix='PoolRealArray(', suffix=')')
-    value_array = Array(prefix='[', suffix=']')
-    for index, frame in enumerate(frame_list):
-        if (interp == LINEAR_INTERPOLATION and index > 0 and
-                value_list[index] == value_list[index - 1]):
-            continue
-
-        time = frame / bpy.context.scene.render.fps
-        time_array.append(time)
-        transition_array.append(1)
-        value_array.append(value_list[index])
-
-    keys_map = Map()
-    keys_map["times"] = time_array.to_string()
-    keys_map["transitions"] = transition_array.to_string()
-    keys_map["update"] = 0
-    keys_map["values"] = value_array.to_string()
-
-    return keys_map
-
-
-def transform_frames_to_keys(frame_list, value_list, interp):
-    """Convert a list of transform matrix to the keyframes
-    of an animation track"""
-    array = Array(prefix='[', suffix=']')
-    for index, frame in enumerate(frame_list):
-        if (interp == LINEAR_INTERPOLATION and index > 0 and
-                value_list[index] == value_list[index - 1]):
-            # do not export same keyframe
-            continue
-
-        array.append(frame / bpy.context.scene.render.fps)
-
-        # transition default 1.0
-        array.append(1.0)
-
-        # convert from z-up to y-up
-        mat = value_list[index]
-        transform_mat = fix_matrix(mat)
-        location = transform_mat.to_translation()
-        quaternion = transform_mat.to_quaternion()
-        scale = transform_mat.to_scale()
-
-        array.append(location.x)
-        array.append(location.y)
-        array.append(location.z)
-        array.append(quaternion.x)
-        array.append(quaternion.y)
-        array.append(quaternion.z)
-        array.append(quaternion.w)
-        array.append(scale.x)
-        array.append(scale.y)
-        array.append(scale.z)
-
-    return array
-
-
-def get_animation_player(escn_file, export_settings, godot_node):
-    """Get a AnimationPlayer node, its return value depends
-    on animation exporting settings"""
-    animation_player = None
-    # the parent of AnimationPlayer
-    animation_base = None
-
-    if export_settings['animation_modes'] == 'ACTIONS':
-        animation_base = godot_node
-    elif export_settings['animation_modes'] == 'SCENE_ANIMATION':
-        node_ptr = godot_node
-        while node_ptr.parent is not None:
-            node_ptr = node_ptr.parent
-        scene_root = node_ptr
-        animation_base = scene_root
-        for child in scene_root.children:
-            if child.get_type() == 'AnimationPlayer':
-                animation_player = child
-                break
-    else:  # export_settings['animation_modes'] == 'SQUASHED_ACTIONS':
-        animation_base = godot_node
-        node_ptr = godot_node
-        while node_ptr is not None:
-            for child in node_ptr.children:
-                if child.get_type() == 'AnimationPlayer':
-                    animation_player = child
-                    break
-            if animation_player is not None:
-                break
-            node_ptr = node_ptr.parent
-
-    if animation_player is None:
-        animation_player = AnimationPlayer(
-            name='AnimationPlayer',
-            parent=animation_base,
-        )
-
-        escn_file.add_node(animation_player)
-
-    return animation_player
-
-
-def blender_path_to_bone_name(blender_object_path):
-    """Find the bone name inside a fcurve data path,
-    the parameter blender_object_path is part of
-    the fcurve.data_path generated through
-    split_fcurve_data_path()"""
-    return re.search(r'pose.bones\["([^"]+)"\]',
-                     blender_object_path).group(1)
-
-
-def split_fcurve_data_path(data_path):
-    """Split fcurve data path into a blender
-    object path and an attribute name"""
-    path_list = data_path.rsplit('.', 1)
-
-    if len(path_list) == 1:
-        return '', path_list[0]
-    return path_list[0], path_list[1]
-
-
-def get_action_frame_range(action):
-    """Return the a tuple denoting the frame range of action"""
-    # in blender `last_frame` is included, here plus one to make it
-    # excluded to fit python convention
-    return int(action.frame_range[0]), int(action.frame_range[1]) + 1
-
-
-def get_fcurve_frame_range(fcurve):
-    """Return the a tuple denoting the frame range of fcurve"""
-    return int(fcurve.range()[0]), int(fcurve.range()[1]) + 1
-
-
-def build_const_interp_value_track(track_path, map_func, fcurve):
-    """Build a godot value track from a Blender const interpolation fcurve"""
-    track = Track('value', track_path)
-    track.interp = NEAREST_INTERPOLATION
-
-    if map_func is None:
-        for keyframe in fcurve.keyframe_points:
-            track.add_frame_data(int(keyframe.co[0]), keyframe.co[1])
-    else:
-        for keyframe in fcurve.keyframe_points:
-            track.add_frame_data(int(keyframe.co[0]), map_func(keyframe.co[1]))
-
-    return track
-
-
-def build_linear_interp_value_track(track_path, map_func, fcurve):
-    """Build a godot value track by evaluate every frame of Blender fcurve"""
-    track = Track('value', track_path)
-
-    frame_range = get_fcurve_frame_range(fcurve)
-    if map_func is None:
-        for frame in range(frame_range[0], frame_range[1]):
-            track.add_frame_data(frame, fcurve.evaluate(frame))
-    else:
-        for frame in range(frame_range[0], frame_range[1]):
-            track.add_frame_data(frame, map_func(fcurve.evaluate(frame)))
-
-    return track
-
-
-def has_object_constraint(blender_object):
-    """Return bool indicate if object has constraint"""
-    if isinstance(blender_object, bpy.types.Object):
-        return True if blender_object.constraints else False
-    return False
-
-
-def has_pose_constraint(blender_object):
-    """Return bool indicate if object has pose constraint"""
-    if (isinstance(blender_object, bpy.types.Object) and
-            isinstance(blender_object.data, bpy.types.Armature)):
-        for pose_bone in blender_object.pose.bones:
-            if pose_bone.constraints:
-                return True
-    return False
-
-
-def bake_constraint_to_action(blender_object, base_action,
-                              bake_type, in_place):
-    """Bake pose or object constrainst (e.g. IK) to action"""
-    if base_action is not None:
-        blender_object.animation_data.action = base_action
-        frame_range = get_action_frame_range(base_action)
-    else:
-        frame_range = (1, 250)  # default, can be improved
-
-    # if action_bake_into is None, it would create a new one
-    # and baked into it
-    if in_place:
-        action_bake_into = base_action
-    else:
-        action_bake_into = None
-
-    do_pose = bake_type == "POSE"
-    do_object = not do_pose
-
-    if bpy.app.version <= (2, 79, 0):
-        active_obj_backup = bpy.context.scene.objects.active
-
-        # the object to bake is the current active object
-        bpy.context.scene.objects.active = blender_object
-        baked_action = bpy_extras.anim_utils.bake_action(
-            frame_start=frame_range[0],
-            frame_end=frame_range[1],
-            frame_step=1,
-            only_selected=False,
-            action=action_bake_into,
-            do_pose=do_pose,
-            do_object=do_object,
-            do_visual_keying=True,
-        )
-
-        bpy.context.scene.objects.active = active_obj_backup
-    else:
-        baked_action = bpy_extras.anim_utils.bake_action(
-            obj=blender_object,
-            frame_start=frame_range[0],
-            frame_end=frame_range[1],
-            frame_step=1,
-            only_selected=False,
-            action=action_bake_into,
-            do_pose=do_pose,
-            do_object=do_object,
-            do_visual_keying=True,
-        )
-
-    if in_place:
-        return action_bake_into
-
-    if base_action is not None:
-        baked_action.name = base_action.name[:-len(BAKING_SUFFIX)]
-    else:
-        baked_action.name = blender_object.name + 'Action'
-    return baked_action
-
-
-def export_transform_action(godot_node, animation_player,
-                            blender_object, action, animation_resource):
-    """Export a action with bone and object transform"""
-
-    class TransformFrame:
-        """A data structure hold transform values of an animation key,
-        it is used as an intermedia data structure, being updated during
-        parsing the fcurve data and finally being converted to a transform
-        matrix, notice itself uses location, scale, rotation not matrix"""
-        ATTRIBUTES = {
-            'location', 'scale', 'rotation_quaternion', 'rotation_euler'}
-
-        def __init__(self, default_transform, rotation_mode):
-            self.location = default_transform.to_translation()
-            # fixme: lose negative scale
-            self.scale = default_transform.to_scale()
-
-            # quaternion and euler fcurves may both exist in fcurves
-            self.rotation_mode = rotation_mode
-            self.rotation_quaternion = default_transform.to_quaternion()
-            if rotation_mode == 'QUATERNION':
-                self.rotation_euler = default_transform.to_euler()
-            else:
-                self.rotation_euler = default_transform.to_euler(
-                    rotation_mode
-                )
-
-        def update(self, attribute, array_index, value):
-            """Use fcurve data to update the frame"""
-            if attribute == 'location':
-                self.location[array_index] = value
-            elif attribute == 'scale':
-                self.scale[array_index] = value
-            elif attribute == 'rotation_quaternion':
-                self.rotation_quaternion[array_index] = value
-            elif attribute == 'rotation_euler':
-                self.rotation_euler[array_index] = value
-
-        def to_matrix(self):
-            """Convert location, scale, rotation to a transform matrix"""
-            if self.rotation_mode == 'QUATERNION':
-                rot_mat = self.rotation_quaternion.to_matrix().to_4x4()
-            else:
-                rot_mat = self.rotation_euler.to_matrix().to_4x4()
-            loc_mat = mathutils.Matrix.Translation(self.location)
-            sca_mat = mathutils.Matrix((
-                (self.scale[0], 0, 0),
-                (0, self.scale[1], 0),
-                (0, 0, self.scale[2]),
-            )).to_4x4()
-            return loc_mat * rot_mat * sca_mat
-
-    def init_transform_frame_values(object_path, blender_object, godot_node,
-                                    first_frame, last_frame):
-        """Initialize a list of TransformFrame for every animated object"""
-        if object_path.startswith('pose'):
-            bone_name = blender_path_to_bone_name(object_path)
-
-            # bone fcurve in a non armature object
-            if godot_node.get_type() != 'Skeleton':
-                logging.warning(
-                    "Skip a bone fcurve in a non-armature "
-                    "object '%s'",
-                    blender_object.name
-                )
-                return None
-
-            # if the correspond bone of this track not exported, skip
-            if godot_node.find_bone_id(bone_name) == -1:
-                return None
-
-            pose_bone = blender_object.pose.bones[
-                blender_object.pose.bones.find(bone_name)
-            ]
-
-            default_frame = TransformFrame(
-                pose_bone.matrix_basis,
-                pose_bone.rotation_mode
-            )
-        else:
-            # the fcurve location is matrix_basis.to_translation()
-            default_frame = TransformFrame(
-                blender_object.matrix_basis,
-                blender_object.rotation_mode
-            )
-
-        return [
-            copy.deepcopy(default_frame)
-            for _ in range(last_frame - first_frame)
-        ]
-
-    first_frame, last_frame = get_action_frame_range(action)
-
-    transform_frame_values_map = collections.OrderedDict()
-    for fcurve in action.fcurves:
-        # fcurve data are seperated into different channels,
-        # for example a transform action would have several fcurves
-        # (location.x, location.y, rotation.x ...), so here fcurves
-        # are aggregated to object while being evaluted
-        object_path, attribute = split_fcurve_data_path(fcurve.data_path)
-
-        if (object_path not in transform_frame_values_map and
-                attribute in TransformFrame.ATTRIBUTES):
-
-            frame_values = init_transform_frame_values(
-                object_path, blender_object,
-                godot_node, first_frame, last_frame
-            )
-
-            # unsuccessfully initialize frames, then skip this fcurve
-            if not frame_values:
-                continue
-
-            transform_frame_values_map[object_path] = frame_values
-
-        if attribute in TransformFrame.ATTRIBUTES:
-            for frame in range(first_frame, last_frame):
-                transform_frame_values_map[
-                    object_path][frame - first_frame].update(
-                        attribute,
-                        fcurve.array_index,
-                        fcurve.evaluate(frame)
-                    )
-
-    for object_path, frame_value_list in transform_frame_values_map.items():
-        if object_path == '':
-            # object_path equals '' represents node itself
-            if godot_node.parent.get_type() == 'BoneAttachment':
-                transform_mtx_list = [
-                    fix_bone_attachment_transform(
-                        blender_object,
-                        blender_object.matrix_parent_inverse * x.to_matrix
-                    ) for x in frame_value_list
-                ]
-            else:
-                transform_mtx_list = [
-                    blender_object.matrix_parent_inverse *
-                    x.to_matrix() for x in frame_value_list
-                ]
-
-            # convert matrix_basis to matrix_local(parent space transform)
-            if (godot_node.get_type()
-                    in ("SpotLight", "DirectionalLight", "Camera")):
-                transform_mtx_list = [
-                    fix_directional_transform(mtx)
-                    for mtx in transform_mtx_list
-                ]
-
-            track_path = NodePath(
-                animation_player.parent.get_path(),
-                godot_node.get_path()
-            )
-
-        elif object_path.startswith('pose'):
-            track_path = NodePath(
-                animation_player.parent.get_path(),
-                godot_node.get_path(),
-                godot_node.find_bone_name(
-                    blender_path_to_bone_name(object_path)
-                ),
-            )
-
-            transform_mtx_list = [x.to_matrix() for x in frame_value_list]
-
-        animation_resource.add_track(
-            Track(
-                'transform',
-                track_path,
-                range(first_frame, last_frame),
-                transform_mtx_list
-            )
-        )
-
-
-def export_shapekey_action(godot_node, animation_player,
-                           blender_object, action, animation_resource):
-    """Export shapekey value action"""
-    first_frame, last_frame = get_action_frame_range(action)
-
-    for fcurve in action.fcurves:
-
-        object_path, attribute = split_fcurve_data_path(fcurve.data_path)
-
-        if attribute == 'value':
-            shapekey_name = re.search(r'key_blocks\["([^"]+)"\]',
-                                      object_path).group(1)
-
-            track_path = NodePath(
-                animation_player.parent.get_path(),
-                godot_node.get_path(),
-                "blend_shapes/{}".format(shapekey_name)
-            )
-
-            value_track = Track(
-                'value',
-                track_path,
-            )
-
-            for frame in range(first_frame, last_frame):
-                value_track.add_frame_data(frame, fcurve.evaluate(frame))
-
-            animation_resource.add_track(value_track)
-
-
-def export_light_action(light_node, animation_player,
-                        blender_lamp, action, animation_resource):
-    """Export light(lamp in Blender) action"""
-    first_frame, last_frame = get_action_frame_range(action)
-    base_node_path = NodePath(
-        animation_player.parent.get_path(), light_node.get_path()
-    )
-
-    animation_resource.add_simple_value_track(
-        action.fcurves, 'use_negative',
-        partial(
-            build_const_interp_value_track,
-            base_node_path.new_copy('light_negative'),
-            lambda x: x > 0.0,
-        )
-    )
-
-    animation_resource.add_track_via_attr_mapping(
-        action.fcurves,
-        light_node.attribute_conversion,
-        base_node_path
-    )
-
-    # color tracks is not one-one mapping to fcurve, they
-    # need to be treated like transform track
-    color_frame_values_map = collections.OrderedDict()
-
-    for fcurve in action.fcurves:
-        _, attribute = split_fcurve_data_path(fcurve.data_path)
-
-        if attribute in ('color', 'shadow_color'):
-            if attribute not in color_frame_values_map:
-                color_frame_values_map[attribute] = [
-                    mathutils.Color()
-                    for _ in range(first_frame, last_frame)
-                ]
-            color_list = color_frame_values_map[attribute]
-            for frame in range(first_frame, last_frame):
-                color_list[frame - first_frame][
-                    fcurve.array_index] = fcurve.evaluate(frame)
-
-    for attribute, frame_value_list in color_frame_values_map.items():
-        if attribute == 'color':
-            track_path = base_node_path.new_copy('light_color')
-        else:
-            track_path = base_node_path.new_copy('shadow_color')
-
-        animation_resource.add_track(
-            Track(
-                'value',
-                track_path,
-                range(first_frame, last_frame),
-                frame_value_list
-            )
-        )
-
-
-def export_camera_action(camera_node, animation_player,
-                         blender_cam, action, animation_resource):
-    """Export camera action"""
-    first_frame, last_frame = get_action_frame_range(action)
-    base_node_path = NodePath(
-        animation_player.parent.get_path(), camera_node.get_path()
-    )
-
-    animation_resource.add_track_via_attr_mapping(
-        action.fcurves,
-        camera_node.attribute_conversion,
-        base_node_path
-    )
-
-    animation_resource.add_simple_value_track(
-        action.fcurves, 'type',
-        partial(
-            build_const_interp_value_track,
-            base_node_path.new_copy('projection'),
-            lambda x: 0 if x == 0.0 else 1,
-        )
-    )
-
-    # blender use sensor_width and f_lens to animate fov
-    # while godot directly use fov
-    fov_animated = False
-    focal_len_list = list()
-    sensor_size_list = list()
-
-    if action.fcurves.find('lens') is not None:
-        fcurve = action.fcurves.find('lens')
-        fov_animated = True
-        for frame in range(first_frame, last_frame):
-            focal_len_list.append(fcurve.evaluate(frame))
-    if action.fcurves.find('sensor_width') is not None:
-        fcurve = action.fcurves.find('sensor_width')
-        fov_animated = True
-        for frame in range(first_frame, last_frame):
-            sensor_size_list.append(fcurve.evaluate(frame))
-
-    if fov_animated:
-        # export fov track
-        if not focal_len_list:
-            focal_len_list = [blender_cam.lens
-                              for _ in range(first_frame, last_frame)]
-        if not sensor_size_list:
-            sensor_size_list = [blender_cam.sensor_width
-                                for _ in range(first_frame, last_frame)]
-
-        fov_list = list()
-        for index, flen in enumerate(focal_len_list):
-            fov_list.append(2 * math.degrees(
-                math.atan(
-                    sensor_size_list[index]/2/flen
-                )
-            ))
-
-        animation_resource.add_track(Track(
-            'value',
-            base_node_path.new_copy('fov'),
-            range(first_frame, last_frame),
-            fov_list
-        ))
-
-
-# ----------------------------------------------
-
-
-ACTION_EXPORTER_MAP = {
-    'transform': export_transform_action,
-    'shapekey': export_shapekey_action,
-    'light': export_light_action,
-    'camera': export_camera_action,
-}
-
-
-def export_animation_data(escn_file, export_settings, godot_node,
-                          blender_object, action_type):
-    """Export the action and nla_tracks in blender_object.animation_data,
-    it will further call the action exporting function in AnimationDataExporter
-    given by `func_name`"""
-    if not export_settings['use_export_animation']:
-        return
-    has_obj_cst = has_object_constraint(blender_object)
-    has_pose_cst = has_pose_constraint(blender_object)
-    need_bake = action_type == 'transform' and (has_obj_cst or has_pose_cst)
-
-    if blender_object.animation_data is None and not need_bake:
-        return
-
-    def action_baker(action_to_bake):
-        """A quick call to bake OBJECT and POSE action"""
-        # note it used variable outside its scope
-        if has_obj_cst and has_pose_cst:
-            tmp = bake_constraint_to_action(
-                blender_object, action_to_bake, "OBJECT", False
-            )
-            ret = bake_constraint_to_action(
-                blender_object, tmp, "POSE", True
-            )
-        elif has_pose_cst:
-            ret = bake_constraint_to_action(
-                blender_object, action_to_bake, "POSE", False
-            )
-        elif has_obj_cst:
-            ret = bake_constraint_to_action(
-                blender_object, action_to_bake, "POSE", False
-            )
-        return ret
-
-    animation_player = get_animation_player(
-        escn_file, export_settings, godot_node
-    )
-    exporter_func = ACTION_EXPORTER_MAP[action_type]
-    # avoid duplicated export, same actions may exist in different nla_strip
-    exported_actions = set()
-
-    # back up active action to reset back after finish exporting
-    if blender_object.animation_data:
-        active_action = blender_object.animation_data.action
-    else:
-        active_action = None
-
-    def export_active_action(active_action):
-        """Export the active action, if needed would call bake,
-        note that active_action maybe None, which means the object
-        has constraint and need to bake to action"""
-        if need_bake:
-            if active_action is not None:
-                active_action.name = active_action.name + BAKING_SUFFIX
-                exported_actions.add(active_action)
-            action_active_to_export = action_baker(active_action)
-        else:
-            action_active_to_export = active_action
-
-        if animation_player.default_animation is None:
-            animation_player.add_default_animation_resource(
-                escn_file, action_active_to_export
-            )
-
-        exporter_func(
-            godot_node, animation_player, blender_object,
-            action_active_to_export, animation_player.default_animation
-        )
-
-        if need_bake:
-            # remove new created action
-            bpy.data.actions.remove(action_active_to_export)
-            if active_action is not None:
-                # set back active action name
-                active_action.name = active_action.name[:-len(BAKING_SUFFIX)]
-
-    if (active_action is not None or
-            not blender_object.animation_data and need_bake):
-        export_active_action(active_action)
-
-    def export_nla_action(nla_action):
-        """Export an action in nla_tracks, would call baking if needed"""
-        exported_actions.add(nla_action)
-        if need_bake:
-            nla_action.name = nla_action.name + BAKING_SUFFIX
-            # nla_action_to_export is new created, need to be removed later
-            nla_action_to_export = action_baker(nla_action)
-        else:
-            nla_action_to_export = nla_action
-
-        anim_resource = animation_player.create_animation_resource(
-            escn_file, nla_action_to_export
-        )
-
-        exporter_func(godot_node, animation_player, blender_object,
-                      nla_action_to_export, anim_resource)
-
-        if need_bake:
-            # remove baked action
-            bpy.data.actions.remove(nla_action_to_export)
-            nla_action.name = nla_action.name[:-len(BAKING_SUFFIX)]
-
-    # export actions in nla_tracks, each exported to seperate
-    # animation resources
-    for nla_track in blender_object.animation_data.nla_tracks:
-        for nla_strip in nla_track.strips:
-            # here make sure no duplicate action exported
-            if (nla_strip.action is not None and
-                    nla_strip.action not in exported_actions):
-                export_nla_action(nla_strip.action)
-
-    if active_action is not None:
-        blender_object.animation_data.action = active_action

+ 4 - 0
io_scene_godot/converters/animation/__init__.py

@@ -0,0 +1,4 @@
+"""Convert blender animation_data to godot AnimationPlayer"""
+
+from .animation_data import export_animation_data
+from .action import AttributeConvertInfo

+ 379 - 0
io_scene_godot/converters/animation/action.py

@@ -0,0 +1,379 @@
+"""Convert Blender action into some intermedia class,
+then serialized to godot escn"""
+
+import re
+import collections
+import logging
+import math
+import copy
+import bpy
+import mathutils
+from .serializer import FloatTrack, TransformTrack, ColorTrack, TransformFrame
+from ...structures import (NodePath, fix_bone_attachment_location)
+
+
+# a triple contains information to convert an attribute
+# or a fcurve of blender to godot data structures
+AttributeConvertInfo = collections.namedtuple(
+    'AttributeConvertInfo',
+    ['bl_name', 'gd_name', 'converter_function']
+)
+
+
+def get_action_frame_range(action):
+    """Return a tuple which is the frame range of action"""
+    # in blender `last_frame` is included, here plus one to make it
+    # excluded to fit python convention
+    return int(action.frame_range[0]), int(action.frame_range[1]) + 1
+
+
+def get_strip_frame_range(strip):
+    """Return a tuple which is the frame of a NlaStrip"""
+    return int(strip.frame_start), int(strip.frame_end) + 1
+
+
+class ActionStrip:
+    """Abstract of blender action strip, it may override attributes
+    of an action object"""
+    def __init__(self, action_or_strip, action_override=None):
+        self.action = None
+        self.frame_range = (0, 0)
+
+        # blender strip does a linear transformation to its
+        # wrapped action frame range, so we need a k, b
+        # to store the linear function
+        self._fk = 1
+        self._fb = 0
+
+        if isinstance(action_or_strip, bpy.types.NlaStrip):
+            strip = action_or_strip
+            if action_override:
+                self.action = action_override
+            else:
+                self.action = strip.action
+            self._fk = (
+                (strip.frame_end - strip.frame_start) /
+                (self.action.frame_range[1] - self.action.frame_range[0])
+            )
+            self._fb = self.action.frame_range[1] - self._fk * strip.frame_end
+            self.frame_range = get_strip_frame_range(strip)
+        else:
+            if action_override:
+                assert False
+            self.action = action_or_strip
+            self.frame_range = get_action_frame_range(self.action)
+
+    def evaluate_fcurve(self, fcurve, frame):
+        """Evaluate a value of fcurve, DO NOT use fcurve.evalute, as
+        action may wrapped inside an action strip"""
+        return fcurve.evaluate(self._fk * frame + self._fb)
+
+    def evalute_keyframe(self, keyframe):
+        """Evaluate a key frame point and return the point in tuple,
+        DO NOT directly use keyframe.co, as action may wrapped in a strip"""
+        return int(self._fk * keyframe.co[0] + self._fb), keyframe.co[1]
+
+
+def blender_path_to_bone_name(blender_object_path):
+    """Find the bone name inside a fcurve data path,
+    the parameter blender_object_path is part of
+    the fcurve.data_path generated through
+    split_fcurve_data_path()"""
+    return re.search(r'pose.bones\["([^"]+)"\]',
+                     blender_object_path).group(1)
+
+
+def split_fcurve_data_path(data_path):
+    """Split fcurve data path into a blender
+    object path and an attribute name"""
+    path_list = data_path.rsplit('.', 1)
+
+    if len(path_list) == 1:
+        return '', path_list[0]
+    return path_list[0], path_list[1]
+
+
+def export_transform_action(godot_node, animation_player, blender_object,
+                            action_strip, animation_resource):
+    """Export a action with bone and object transform"""
+    def init_transform_frame_values(object_path, blender_object, godot_node,
+                                    first_frame, last_frame):
+        """Initialize a list of TransformFrame for every animated object"""
+        if object_path.startswith('pose'):
+            bone_name = blender_path_to_bone_name(object_path)
+
+            # bone fcurve in a non armature object
+            if godot_node.get_type() != 'Skeleton':
+                logging.warning(
+                    "Skip a bone fcurve in a non-armature "
+                    "object '%s'",
+                    blender_object.name
+                )
+                return None
+
+            # if the correspond bone of this track not exported, skip
+            if godot_node.find_bone_id(bone_name) == -1:
+                return None
+
+            pose_bone = blender_object.pose.bones[
+                blender_object.pose.bones.find(bone_name)
+            ]
+
+            default_frame = TransformFrame.factory(
+                pose_bone.matrix_basis,
+                pose_bone.rotation_mode
+            )
+        else:
+            # the fcurve location is matrix_basis.to_translation()
+            default_frame = TransformFrame.factory(
+                blender_object.matrix_basis,
+                blender_object.rotation_mode
+            )
+
+        return [
+            copy.deepcopy(default_frame)
+            for _ in range(last_frame - first_frame)
+        ]
+
+    first_frame, last_frame = action_strip.frame_range
+    transform_frame_values_map = collections.OrderedDict()
+    for fcurve in action_strip.action.fcurves:
+        # fcurve data are seperated into different channels,
+        # for example a transform action would have several fcurves
+        # (location.x, location.y, rotation.x ...), so here fcurves
+        # are aggregated to object while being evaluted
+        object_path, attribute = split_fcurve_data_path(fcurve.data_path)
+
+        if attribute in TransformFrame.ATTRIBUTES:
+            if object_path not in transform_frame_values_map:
+
+                frame_values = init_transform_frame_values(
+                    object_path, blender_object,
+                    godot_node, first_frame, last_frame
+                )
+
+                # unsuccessfully initialize frames, then skip this fcurve
+                if not frame_values:
+                    continue
+
+                transform_frame_values_map[object_path] = frame_values
+
+            for frame in range(first_frame, last_frame):
+                transform_frame_values_map[
+                    object_path][frame - first_frame].update(
+                        attribute,
+                        fcurve.array_index,
+                        action_strip.evaluate_fcurve(fcurve, frame)
+                    )
+
+    for object_path, frame_value_list in transform_frame_values_map.items():
+        if object_path == '':
+            # object_path equals '' represents node itself
+
+            track_path = NodePath(
+                animation_player.parent.get_path(),
+                godot_node.get_path()
+            )
+
+            if godot_node.parent.get_type() == 'BoneAttachment':
+                frame_value_list = [
+                    fix_bone_attachment_location(blender_object, x.location)
+                    for x in frame_value_list
+                ]
+
+            track = TransformTrack(
+                track_path,
+                frames_iter=range(first_frame, last_frame),
+                values_iter=frame_value_list,
+            )
+            track.set_parent_inverse(blender_object.matrix_parent_inverse)
+            if (godot_node.get_type()
+                    in ("SpotLight", "DirectionalLight", "Camera")):
+                track.is_directional = True
+            animation_resource.add_track(track)
+
+        elif object_path.startswith('pose'):
+            track_path = NodePath(
+                animation_player.parent.get_path(),
+                godot_node.get_path(),
+                godot_node.find_bone_name(
+                    blender_path_to_bone_name(object_path)
+                ),
+            )
+            animation_resource.add_track(
+                TransformTrack(
+                    track_path,
+                    frames_iter=range(first_frame, last_frame),
+                    values_iter=frame_value_list,
+                )
+            )
+
+
+def export_shapekey_action(godot_node, animation_player, blender_object,
+                           action_strip, animation_resource):
+    """Export shapekey value action"""
+    first_frame, last_frame = action_strip.frame_range
+    for fcurve in action_strip.action.fcurves:
+
+        object_path, attribute = split_fcurve_data_path(fcurve.data_path)
+
+        if attribute == 'value':
+            shapekey_name = re.search(r'key_blocks\["([^"]+)"\]',
+                                      object_path).group(1)
+
+            track_path = NodePath(
+                animation_player.parent.get_path(),
+                godot_node.get_path(),
+                "blend_shapes/{}".format(shapekey_name)
+            )
+
+            track = FloatTrack(track_path)
+
+            for frame in range(first_frame, last_frame):
+                track.add_frame_data(
+                    frame,
+                    action_strip.evaluate_fcurve(fcurve, frame)
+                )
+
+            animation_resource.add_track(track)
+
+
+def export_light_action(light_node, animation_player, blender_lamp,
+                        action_strip, animation_resource):
+    """Export light(lamp in Blender) action"""
+    # pylint: disable-msg=R0914
+    base_node_path = NodePath(
+        animation_player.parent.get_path(), light_node.get_path()
+    )
+
+    fcurves = action_strip.action.fcurves
+    animation_resource.add_attribute_track(
+        action_strip,
+        fcurves.find('use_negative'),
+        lambda x: x > 0.0,
+        base_node_path.new_copy('light_negative'),
+    )
+
+    animation_resource.add_attribute_track(
+        action_strip,
+        fcurves.find('shadow_method'),
+        lambda x: x > 0.0,
+        base_node_path.new_copy('shadow_enabled'),
+    )
+
+    for item in light_node.attribute_conversion:
+        bl_attr, gd_attr, converter = item
+        if bl_attr not in ('color', 'shadow_color'):
+            animation_resource.add_attribute_track(
+                action_strip,
+                fcurves.find(bl_attr),
+                converter,
+                base_node_path.new_copy(gd_attr)
+            )
+
+    # color tracks is not one-one mapping to fcurve, they
+    # need to be treated like transform track
+    color_frame_values_map = collections.OrderedDict()
+
+    first_frame, last_frame = action_strip.frame_range
+    for fcurve in fcurves:
+        _, attribute = split_fcurve_data_path(fcurve.data_path)
+
+        if attribute in ('color', 'shadow_color'):
+            if attribute not in color_frame_values_map:
+                color_frame_values_map[attribute] = [
+                    mathutils.Color()
+                    for _ in range(first_frame, last_frame)
+                ]
+            color_list = color_frame_values_map[attribute]
+            for frame in range(first_frame, last_frame):
+                color_list[frame - first_frame][fcurve.array_index] = (
+                    action_strip.evaluate_fcurve(fcurve, frame)
+                )
+
+    for attribute, frame_value_list in color_frame_values_map.items():
+        if attribute == 'color':
+            track_path = base_node_path.new_copy('light_color')
+        else:
+            track_path = base_node_path.new_copy('shadow_color')
+
+        animation_resource.add_track(
+            ColorTrack(
+                track_path,
+                frames_iter=range(first_frame, last_frame),
+                values_iter=frame_value_list
+            )
+        )
+
+
+def export_camera_action(camera_node, animation_player, blender_cam,
+                         action_strip, animation_resource):
+    """Export camera action"""
+    # pylint: disable-msg=R0914
+    first_frame, last_frame = action_strip.frame_range
+    base_node_path = NodePath(
+        animation_player.parent.get_path(), camera_node.get_path()
+    )
+
+    fcurves = action_strip.action.fcurves
+    for item in camera_node.attribute_conversion:
+        bl_attr, gd_attr, converter = item
+        animation_resource.add_attribute_track(
+            action_strip,
+            fcurves.find(bl_attr),
+            converter,
+            base_node_path.new_copy(gd_attr)
+        )
+
+    animation_resource.add_attribute_track(
+        action_strip,
+        fcurves.find('type'),
+        lambda x: 0 if x == 0.0 else 1,
+        base_node_path.new_copy('projection'),
+    )
+
+    # blender use sensor_width and f_lens to animate fov
+    # while godot directly use fov
+    fov_animated = False
+    focal_len_list = list()
+    sensor_size_list = list()
+
+    lens_fcurve = fcurves.find('lens')
+    if lens_fcurve is not None:
+        fov_animated = True
+        for frame in range(first_frame, last_frame):
+            focal_len_list.append(
+                action_strip.evaluate_fcurve(lens_fcurve, frame)
+            )
+    sensor_width_fcurve = fcurves.find('sensor_width')
+    if sensor_width_fcurve is not None:
+        fov_animated = True
+        for frame in range(first_frame, last_frame):
+            sensor_size_list.append(
+                action_strip.evaluate_fcurve(sensor_width_fcurve, frame)
+            )
+
+    if fov_animated:
+        # export fov track
+        if not focal_len_list:
+            focal_len_list = [blender_cam.lens
+                              for _ in range(first_frame, last_frame)]
+        if not sensor_size_list:
+            sensor_size_list = [blender_cam.sensor_width
+                                for _ in range(first_frame, last_frame)]
+
+        fov_list = list()
+        for index, flen in enumerate(focal_len_list):
+            fov_list.append(2 * math.degrees(
+                math.atan(
+                    sensor_size_list[index]/2/flen
+                )
+            ))
+
+        animation_resource.add_track(
+            FloatTrack(
+                base_node_path.new_copy('fov'),
+                frames_iter=range(first_frame, last_frame),
+                values_iter=fov_list
+            )
+        )

+ 248 - 0
io_scene_godot/converters/animation/animation_data.py

@@ -0,0 +1,248 @@
+"""Parsing Blender animation_data to create appropriate Godot
+AnimationPlayer as well as distribute Blender action into various
+action exporting functions"""
+
+import bpy
+from .action import (
+    export_camera_action,
+    export_shapekey_action,
+    export_light_action,
+    export_transform_action
+)
+from .constraint_baking import (
+    bake_constraint_to_action,
+    check_object_constraint,
+    check_pose_constraint,
+    action_baking_finalize,
+    action_baking_initialize
+)
+from .serializer import get_animation_player
+from .action import ActionStrip
+
+
+ACTION_EXPORTER_MAP = {
+    'transform': export_transform_action,
+    'shapekey': export_shapekey_action,
+    'light': export_light_action,
+    'camera': export_camera_action,
+}
+
+
+class ObjectAnimationExporter:
+    """A helper class holding states while exporting
+    animation data from a blender object"""
+    def __init__(self, godot_node, blender_object, action_type):
+        self.godot_node = godot_node
+        self.blender_object = blender_object
+
+        self.action_exporter_func = ACTION_EXPORTER_MAP[action_type]
+        self.animation_player = None
+
+        self.has_object_constraint = False
+        self.has_pose_constraint = False
+        self.need_baking = False
+
+        self.unmute_nla_tracks = []
+        self.mute_nla_tracks = []
+
+        self.check_baking_condition(action_type)
+        self.preprocess_nla_tracks(blender_object)
+
+    def check_baking_condition(self, action_type):
+        """Check whether the animated object has any constraint and
+        thus need to do baking, if needs, some states would be set"""
+        has_obj_cst = check_object_constraint(self.blender_object)
+        has_pose_cst = check_pose_constraint(self.blender_object)
+        self.need_baking = (
+            action_type == 'transform' and (has_obj_cst or has_pose_cst)
+        )
+        self.has_object_constraint = has_obj_cst
+        self.has_pose_constraint = has_pose_cst
+
+    def preprocess_nla_tracks(self, blender_object):
+        """Iterative through nla tracks, separately store mute and unmuted
+        tracks"""
+        if blender_object.animation_data:
+            for nla_track in blender_object.animation_data.nla_tracks:
+                if not nla_track.strips:
+                    # skip empty tracks
+                    continue
+                if not nla_track.mute:
+                    self.unmute_nla_tracks.append(nla_track)
+                else:
+                    self.mute_nla_tracks.append(nla_track)
+
+    def bake_to_new_action(self, action_to_bake):
+        """Baking object and pose constraint altogether.
+
+        Note that it accept a action to bake (which would not be modified)
+        and always return a new created baked actiony"""
+        if self.has_object_constraint and self.has_pose_constraint:
+            tmp = bake_constraint_to_action(
+                self.blender_object, action_to_bake, "OBJECT", False
+            )
+            ret = bake_constraint_to_action(
+                self.blender_object, action_to_bake, "POSE", True
+            )
+        elif self.has_pose_constraint:
+            ret = bake_constraint_to_action(
+                self.blender_object, action_to_bake, "POSE", False
+            )
+        elif self.has_object_constraint:
+            ret = bake_constraint_to_action(
+                self.blender_object, action_to_bake, "OBJECT", False
+            )
+        return ret
+
+    def export_active_action(self, escn_file, active_action):
+        """Export the active action, if needed, would call bake.
+
+        Note that active_action maybe None, which would happen when object has
+        some constraint (so even no action it is still animated)"""
+        if self.need_baking:
+            action_baking_initialize(active_action)
+            action_active_to_export = self.bake_to_new_action(active_action)
+        else:
+            action_active_to_export = active_action
+
+        if self.animation_player.active_animation is None:
+            self.animation_player.add_active_animation_resource(
+                escn_file, action_active_to_export.name
+            )
+
+        self.action_exporter_func(
+            self.godot_node,
+            self.animation_player,
+            self.blender_object,
+            ActionStrip(action_active_to_export),
+            self.animation_player.active_animation
+        )
+
+        if self.need_baking:
+            # remove new created action
+            bpy.data.actions.remove(action_active_to_export)
+            action_baking_finalize(active_action)
+        else:
+            # here export unmuted nla_tracks into animation resource,
+            # this is not needed for baking, as baking has applied to
+            # active action
+            for track in self.unmute_nla_tracks:
+                for strip in track.strips:
+                    self.action_exporter_func(
+                        self.godot_node,
+                        self.animation_player,
+                        self.blender_object,
+                        ActionStrip(strip),
+                        self.animation_player.active_animation
+                    )
+
+    def export_active_action_from_nla(self, escn_file):
+        """Export all unmute nla_tracks into an active action.
+        Note that it would not do baking for constraint"""
+        if self.animation_player.active_animation is None:
+            self.animation_player.add_active_animation_resource(
+                escn_file, self.blender_object.name + 'Action'
+            )
+
+        for track in self.unmute_nla_tracks:
+            for strip in track.strips:
+                self.action_exporter_func(
+                    self.godot_node,
+                    self.animation_player,
+                    self.blender_object,
+                    ActionStrip(strip),
+                    self.animation_player.active_animation
+                )
+
+    def export_stashed_track(self, escn_file, stashed_track):
+        """Export a muted nla_track, track with all its contained action
+        is exported to a single animation_resource.
+
+        It works as an action lib"""
+        if not stashed_track.strips:
+            return
+
+        # if only one action in nla_track, user may not editted
+        # nla_track name, thus this would make exported name nicer
+        if len(stashed_track.strips) > 1:
+            anim_name = stashed_track.name
+        else:
+            anim_name = stashed_track.strips[0].name
+
+        anim_resource = self.animation_player.create_animation_resource(
+            escn_file, anim_name
+        )
+
+        for strip in stashed_track.strips:
+            if self.need_baking:
+                action_baking_initialize(strip.action)
+                action_to_export = self.bake_to_new_action(strip.action)
+            else:
+                action_to_export = strip.action
+
+            self.action_exporter_func(
+                self.godot_node,
+                self.animation_player,
+                self.blender_object,
+                ActionStrip(strip, action_to_export),
+                anim_resource
+            )
+
+            if self.need_baking:
+                # remove baked action
+                bpy.data.actions.remove(action_to_export)
+                action_baking_finalize(strip.action)
+
+        if not self.need_baking:
+            # if baking, nla_tracks are already baked into strips
+            for nla_track in self.unmute_nla_tracks:
+                for strip in nla_track.strips:
+                    self.action_exporter_func(
+                        self.godot_node,
+                        self.animation_player,
+                        self.blender_object,
+                        ActionStrip(strip),
+                        anim_resource
+                    )
+
+
+def export_animation_data(escn_file, export_settings, godot_node,
+                          blender_object, action_type):
+    """Export the action and nla_tracks in blender_object.animation_data,
+    it will further call the action exporting function in AnimationDataExporter
+    given by `func_name`"""
+    if not export_settings['use_export_animation']:
+        return
+
+    anim_exporter = ObjectAnimationExporter(
+        godot_node, blender_object, action_type
+    )
+
+    if (blender_object.animation_data is None and
+            not anim_exporter.need_baking):
+        return
+
+    anim_exporter.animation_player = get_animation_player(
+        escn_file, export_settings, godot_node
+    )
+
+    # back up active action to reset back after finish exporting
+    if blender_object.animation_data:
+        active_action = blender_object.animation_data.action
+    else:
+        active_action = None
+
+    if (active_action is not None or anim_exporter.need_baking):
+        anim_exporter.export_active_action(escn_file, active_action)
+    elif anim_exporter.unmute_nla_tracks:
+        # if has effective nla_tracks but no active action, fake one
+        anim_exporter.export_active_action_from_nla(escn_file)
+
+    # export actions in nla_tracks, each exported to seperate
+    # animation resources
+    if export_settings['use_stashed_action']:
+        for stashed_track in anim_exporter.mute_nla_tracks:
+            anim_exporter.export_stashed_track(escn_file, stashed_track)
+
+    if active_action is not None:
+        blender_object.animation_data.action = active_action

+ 98 - 0
io_scene_godot/converters/animation/constraint_baking.py

@@ -0,0 +1,98 @@
+"""Collection of helper functions to baking constraints into action"""
+
+import bpy
+import bpy_extras.anim_utils
+from .action import get_action_frame_range
+
+# a suffix append to action need baking to avoid name collision
+# with baked action's name
+BAKING_SUFFIX = '--being-baking'
+
+
+def action_baking_initialize(action):
+    """Intialize steps before an action going to baking"""
+    if action is not None:
+        action.name = action.name + BAKING_SUFFIX
+
+
+def action_baking_finalize(action):
+    """Clear up some baking information for an action having
+    going through baking"""
+    if action is not None:
+        action.name = action.name[:-len(BAKING_SUFFIX)]
+
+
+def check_object_constraint(blender_object):
+    """Return bool indicate if object has constraint"""
+    if isinstance(blender_object, bpy.types.Object):
+        return True if blender_object.constraints else False
+    return False
+
+
+def check_pose_constraint(blender_object):
+    """Return bool indicate if object has pose constraint"""
+    if (isinstance(blender_object, bpy.types.Object) and
+            isinstance(blender_object.data, bpy.types.Armature)):
+        for pose_bone in blender_object.pose.bones:
+            if pose_bone.constraints:
+                return True
+    return False
+
+
+def bake_constraint_to_action(blender_object, base_action, bake_type,
+                              in_place):
+    """Bake pose or object constrainst (e.g. IK) to action"""
+    if base_action is not None:
+        blender_object.animation_data.action = base_action
+        frame_range = get_action_frame_range(base_action)
+    else:
+        frame_range = (1, 250)  # default, can be improved
+
+    # if action_bake_into is None, it would create a new one
+    # and baked into it
+    if in_place:
+        action_bake_into = base_action
+    else:
+        action_bake_into = None
+
+    do_pose = bake_type == "POSE"
+    do_object = not do_pose
+
+    if bpy.app.version <= (2, 79, 0):
+        active_obj_backup = bpy.context.scene.objects.active
+
+        # the object to bake is the current active object
+        bpy.context.scene.objects.active = blender_object
+        baked_action = bpy_extras.anim_utils.bake_action(
+            frame_start=frame_range[0],
+            frame_end=frame_range[1],
+            frame_step=1,
+            only_selected=False,
+            action=action_bake_into,
+            do_pose=do_pose,
+            do_object=do_object,
+            do_visual_keying=True,
+        )
+
+        bpy.context.scene.objects.active = active_obj_backup
+    else:
+        baked_action = bpy_extras.anim_utils.bake_action(
+            obj=blender_object,
+            frame_start=frame_range[0],
+            frame_end=frame_range[1],
+            frame_step=1,
+            only_selected=False,
+            action=action_bake_into,
+            do_pose=do_pose,
+            do_object=do_object,
+            do_visual_keying=True,
+        )
+
+    if in_place:
+        return action_bake_into
+
+    if base_action is not None:
+        baked_action.name = base_action.name[:-len(BAKING_SUFFIX)]
+    else:
+        baked_action.name = blender_object.name + 'Action'
+    return baked_action

+ 486 - 0
io_scene_godot/converters/animation/serializer.py

@@ -0,0 +1,486 @@
+"""Export animation into Godot scene tree"""
+import collections
+import re
+import bpy
+import mathutils
+from ...structures import (NodeTemplate, NodePath, Array, Map,
+                           InternalResource, fix_matrix,
+                           fix_directional_transform)
+
+NEAREST_INTERPOLATION = 0
+LINEAR_INTERPOLATION = 1
+
+
+class TransformFrame:
+    """A data structure hold transform values of an animation key,
+    it is used as an intermedia data structure, being updated during
+    parsing the fcurve data and finally being converted to a transform
+    matrix."""
+    ATTRIBUTES = {'location', 'scale', 'rotation_quaternion', 'rotation_euler'}
+
+    def __init__(self):
+        self.location = mathutils.Vector((0, 0, 0))
+        self.scale = mathutils.Vector((1, 1, 1))
+
+        self.rotation_mode = 'QUATERNION'
+        self.rotation_euler = mathutils.Euler((0, 0, 0))
+        self.rotation_quaternion = mathutils.Quaternion()
+
+    @classmethod
+    def factory(cls, trans_mat, rotation_mode):
+        """Factory function, create cls from a transform matrix"""
+        ret = cls()
+        ret.location = trans_mat.to_translation()
+        # fixme: lose negative scale
+        ret.scale = trans_mat.to_scale()
+
+        # quaternion and euler fcurves may both exist in fcurves
+        ret.rotation_mode = rotation_mode
+        ret.rotation_quaternion = trans_mat.to_quaternion()
+        if rotation_mode == 'QUATERNION':
+            ret.rotation_euler = trans_mat.to_euler()
+        else:
+            ret.rotation_euler = trans_mat.to_euler(
+                rotation_mode
+            )
+        return ret
+
+    def update(self, attribute, array_index, value):
+        """Use fcurve data to update the frame"""
+        if attribute == 'location':
+            self.location[array_index] = value
+        elif attribute == 'scale':
+            self.scale[array_index] = value
+        elif attribute == 'rotation_quaternion':
+            self.rotation_quaternion[array_index] = value
+        elif attribute == 'rotation_euler':
+            self.rotation_euler[array_index] = value
+
+    def to_matrix(self):
+        """Convert location, scale, rotation to a transform matrix"""
+        if self.rotation_mode == 'QUATERNION':
+            rot_mat = self.rotation_quaternion.to_matrix().to_4x4()
+        else:
+            rot_mat = self.rotation_euler.to_matrix().to_4x4()
+        loc_mat = mathutils.Matrix.Translation(self.location)
+        sca_mat = mathutils.Matrix((
+            (self.scale[0], 0, 0),
+            (0, self.scale[1], 0),
+            (0, 0, self.scale[2]),
+        )).to_4x4()
+        return loc_mat * rot_mat * sca_mat
+
+
+class Track:
+    """Animation track base type"""
+    def __init__(self, track_type, track_path,
+                 frames_iter, values_iter):
+        self.type = track_type
+        self.path = track_path
+        # default to linear
+        self.interp = LINEAR_INTERPOLATION
+        self.frames = list()
+        self.values = list()
+
+        for frame in frames_iter:
+            self.frames.append(frame)
+        for value in values_iter:
+            self.values.append(value)
+
+        assert len(self.frames) == len(self.values)
+
+    def add_frame_data(self, frame, value):
+        """Add add frame to track"""
+        self.frames.append(frame)
+        self.values.append(value)
+
+    def frame_end(self):
+        """The frame number of last frame"""
+        if not self.frames:
+            return 0
+        return self.frames[-1]
+
+    def frame_begin(self):
+        """The frame number of first frame"""
+        if not self.frames:
+            return 0
+        return self.frames[0]
+
+    def convert_to_keys_object(self):
+        """Convert to a godot animation keys object"""
+        # need to be overrided
+        assert False
+
+    def blend_frames(self, frame_val1, frame_val2):
+        """Blend two frame values into one"""
+        # need to be overrided
+        assert False
+
+    def to_string(self):
+        """Serialize a track object"""
+        return self.convert_to_keys_object().to_string()
+
+    def blend(self, track):
+        """Blend current track with another one, used in nla editor"""
+        assert self.interp == track.interp
+        assert self.type == track.type
+
+        if self.frame_begin() > track.frame_end():
+            self.frames = track.frames + self.frames
+            self.values = track.value + self.values
+        elif self.frame_end() < track.frame_begin():
+            self.frames = self.frames + track.frames
+            self.values = self.values + track.values
+        else:
+            new_frames = list()
+            new_values = list()
+
+            blend_begin = max(self.frame_begin(), track.frame_begin())
+            blend_end = min(self.frame_end(), track.frame_end())
+
+            self_frame_idx = 0
+            track_frame_idx = 0
+            while self.frames[self_frame_idx] != blend_begin:
+                new_frames.append(self.frames[self_frame_idx])
+                new_values.append(self.values[self_frame_idx])
+                self_frame_idx += 1
+
+            while track.frames[track_frame_idx] != blend_begin:
+                new_frames.append(track.frames[track_frame_idx])
+                new_values.append(track.values[track_frame_idx])
+                track_frame_idx += 1
+
+            while (self_frame_idx < len(self.frames) and
+                   track_frame_idx < len(track.frames) and
+                   self.frames[self_frame_idx] <= blend_end and
+                   track.frames[track_frame_idx] <= blend_end):
+                if (self.frames[self_frame_idx] ==
+                        track.frames[track_frame_idx]):
+                    new_frames.append(self.frames[self_frame_idx])
+
+                    new_values.append(
+                        self.blend_frames(
+                            self.values[self_frame_idx],
+                            track.values[track_frame_idx]
+                        )
+                    )
+
+                    self_frame_idx += 1
+                    track_frame_idx += 1
+                elif (self.frames[self_frame_idx] <
+                      track.frames[track_frame_idx]):
+                    new_frames.append(self.frames[self_frame_idx])
+                    new_values.append(self.values[self_frame_idx])
+                    self_frame_idx += 1
+                else:
+                    new_frames.append(track.frames[track_frame_idx])
+                    new_values.append(track.values[track_frame_idx])
+                    track_frame_idx += 1
+
+            while self_frame_idx < len(self.frames):
+                new_frames.append(self.frames[self_frame_idx])
+                new_values.append(self.values[self_frame_idx])
+                self_frame_idx += 1
+
+            while track_frame_idx < len(track.frames):
+                new_frames.append(track.frames[track_frame_idx])
+                new_values.append(track.values[track_frame_idx])
+                track_frame_idx += 1
+
+            self.frames = new_frames
+            self.values = new_values
+
+
+class TransformTrack(Track):
+    """Animation track whose frame value is TranslationFrame object"""
+    def __init__(self, track_path, frames_iter=(), values_iter=()):
+        super().__init__("transform", track_path, frames_iter, values_iter)
+        self.parent_trans_inverse = mathutils.Matrix.Identity(4)
+
+        # Fix of object's rotation, directional object like
+        # camera, spotLight has different initial orientation
+        self.is_directional = False
+
+        self.interp = LINEAR_INTERPOLATION
+
+    def set_parent_inverse(self, parent_inverse):
+        """Blender interpolate is matrix_basis, it needs to left multiply
+        its parent's object.matrix_parent_inverse to get
+        matrix_local(parent space transform)"""
+        self.parent_trans_inverse = mathutils.Matrix(parent_inverse)
+
+    def blend_frames(self, frame_val1, frame_val2):
+        """Blend two transform frames into one"""
+        # fixme: currently only blend with ADD
+        new_frame = TransformFrame()
+        for frame in (frame_val1, frame_val2):
+            if frame.rotation_mode != 'QUATERNION':
+                frame.rotation_quaternion = (
+                    frame.rotation_euler.to_quaternion()
+                )
+
+        new_frame.rotation_quaternion = (
+            frame_val1.rotation_quaternion * frame_val2.rotation_quaternion
+        )
+
+        new_frame.location = frame_val1.location + frame_val2.location
+        new_frame.scale = frame_val1.scale
+
+        return new_frame
+
+    def convert_to_keys_object(self):
+        """Convert a transform track to godot structure"""
+        array = Array(prefix='[', suffix=']')
+        last_mat = None
+        for index, frame in enumerate(self.frames):
+            mat = self.parent_trans_inverse * self.values[index].to_matrix()
+            if self.is_directional:
+                mat = fix_directional_transform(mat)
+            # convert from z-up to y-up
+            mat = fix_matrix(mat)
+
+            if last_mat and last_mat == mat:
+                # avoid export duplicate keyframe
+                continue
+
+            location = mat.to_translation()
+            quaternion = mat.to_quaternion()
+            scale = mat.to_scale()
+
+            last_mat = mat
+
+            array.append(frame / bpy.context.scene.render.fps)
+            # transition default 1.0
+            array.append(1.0)
+            array.append(location.x)
+            array.append(location.y)
+            array.append(location.z)
+            array.append(quaternion.x)
+            array.append(quaternion.y)
+            array.append(quaternion.z)
+            array.append(quaternion.w)
+            array.append(scale.x)
+            array.append(scale.y)
+            array.append(scale.z)
+
+        return array
+
+
+class ValueTrack(Track):
+    """Animation track which has the type 'value' in godot"""
+    def __init__(self, track_path, interp=LINEAR_INTERPOLATION,
+                 frames_iter=(), values_iter=()):
+        super().__init__("value", track_path, frames_iter, values_iter)
+        self.interp = interp
+
+    def blend_frames(self, frame_val1, frame_val2):
+        # xxx: default use REPLACE
+        return max(frame_val1, frame_val2)
+
+    def convert_to_keys_object(self):
+        """Convert a value track to a godot keys object"""
+        time_array = Array(prefix='PoolRealArray(', suffix=')')
+        transition_array = Array(prefix='PoolRealArray(', suffix=')')
+        value_array = Array(prefix='[', suffix=']')
+        for index, frame in enumerate(self.frames):
+            if (self.interp == LINEAR_INTERPOLATION and index > 0 and
+                    self.values[index] == self.values[index - 1]):
+                continue
+
+            time = frame / bpy.context.scene.render.fps
+            time_array.append(time)
+            transition_array.append(1)
+            value_array.append(self.values[index])
+
+        keys_map = Map()
+        keys_map["times"] = time_array.to_string()
+        keys_map["transitions"] = transition_array.to_string()
+        keys_map["update"] = 0
+        keys_map["values"] = value_array.to_string()
+
+        return keys_map
+
+
+class FloatTrack(ValueTrack):
+    """Value track whose frame value is float"""
+    def blend_frames(self, frame_val1, frame_val2):
+        return max(frame_val1, frame_val2)
+
+
+class ColorTrack(ValueTrack):
+    """Value track whose frame value is mathutils.Color"""
+    def blend_frames(self, frame_val1, frame_val2):
+        return mathutils.Color(
+            tuple(map(max, frame_val1, frame_val2))
+        )
+
+
+def get_fcurve_frame_range(fcurve):
+    """Return the a tuple denoting the frame range of fcurve"""
+    return int(fcurve.range()[0]), int(fcurve.range()[1]) + 1
+
+
+def build_const_interp_value_track(track_path, action_strip, converter,
+                                   fcurve):
+    """Build a godot value track from a Blender const interpolation fcurve"""
+    track = FloatTrack(track_path)
+    track.interp = NEAREST_INTERPOLATION
+
+    if converter is None:
+        for keyframe in fcurve.keyframe_points:
+            point_x, point_y = action_strip.evalute_keyframe(keyframe)
+            track.add_frame_data(point_x, point_y)
+    else:
+        for keyframe in fcurve.keyframe_points:
+            point_x, point_y = action_strip.evalute_keyframe(keyframe)
+            track.add_frame_data(point_x, converter(point_y))
+
+    return track
+
+
+def build_linear_interp_value_track(track_path, action_strip, converter,
+                                    fcurve):
+    """Build a godot value track by evaluate every frame of Blender fcurve"""
+    track = FloatTrack(track_path)
+
+    frame_range = get_fcurve_frame_range(fcurve)
+    if converter is None:
+        for frame in range(frame_range[0], frame_range[1]):
+            track.add_frame_data(
+                frame, action_strip.evaluate_fcurve(fcurve, frame)
+            )
+    else:
+        for frame in range(frame_range[0], frame_range[1]):
+            track.add_frame_data(
+                frame, converter(action_strip.evaluate_fcurve(fcurve, frame))
+            )
+
+    return track
+
+
+class AnimationResource(InternalResource):
+    """Internal resource with type Animation"""
+    fps = bpy.context.scene.render.fps
+
+    def __init__(self, name):
+        super().__init__('Animation', name)
+        self['step'] = 0.1
+        self['length'] = 0
+        self.tracks = collections.OrderedDict()
+
+    def add_track(self, track):
+        """add a track to animation resource"""
+        node_path_str = track.path.to_string()
+        if node_path_str in self.tracks:
+            updated_track = self.tracks[node_path_str]
+            updated_track.blend(track)
+            track_length = updated_track.frame_end() / self.fps
+            if track_length > self['length']:
+                self['length'] = track_length
+        else:
+            track_id_str = 'tracks/{}'.format(len(self.tracks))
+            self.tracks[node_path_str] = track
+            updated_track = track
+
+            track_length = track.frame_end() / self.fps
+            if track_length > self['length']:
+                self['length'] = track_length
+
+            self[track_id_str + '/type'] = '"{}"'.format(track.type)
+            self[track_id_str + '/path'] = node_path_str
+            self[track_id_str + '/interp'] = track.interp
+            self[track_id_str + '/keys'] = track
+
+    def add_attribute_track(self, action_strip, fcurve,
+                            converter, node_path):
+        """Add a track into AnimationResource, the track is a
+        one-one mapping to one fcurve."""
+        if fcurve is not None and fcurve.keyframe_points:
+            interpolation = fcurve.keyframe_points[0].interpolation
+            if interpolation == 'CONSTANT':
+                new_track = build_const_interp_value_track(
+                    node_path, action_strip, converter, fcurve
+                )
+            else:
+                new_track = build_linear_interp_value_track(
+                    node_path, action_strip, converter, fcurve
+                )
+            self.add_track(new_track)
+
+
+class AnimationPlayer(NodeTemplate):
+    """Godot scene node with type AnimationPlayer"""
+    def __init__(self, name, parent):
+        super().__init__(name, "AnimationPlayer", parent)
+        # use parent node as the animation root node
+        self['root_node'] = NodePath(self.get_path(), parent.get_path())
+        # blender actions not in nla_tracks are treated as default
+        self.active_animation = None
+
+    def add_active_animation_resource(self, escn_file, resource_name):
+        """Active animation resource corresponding to blender active action,
+        however, int some animation mode it may hold active action from
+        children objects"""
+        self.active_animation = self.create_animation_resource(
+            escn_file, resource_name
+        )
+
+    def create_animation_resource(self, escn_file, resource_name):
+        """Create a new animation resource and add it into escn file"""
+        resource_name_filtered = re.sub(r'[\[\]\{\}]+', '', resource_name)
+
+        new_anim_resource = AnimationResource(resource_name_filtered)
+        # add animation resource without checking hash,
+        # blender action is in world space, while godot animation
+        # is in local space (parent space),  so identical actions
+        # are not necessarily generates identical godot animations
+        resource_id = escn_file.force_add_internal_resource(new_anim_resource)
+
+        # this filter may not catch all illegal char
+        self['anims/{}'.format(resource_name_filtered)] = (
+            "SubResource({})".format(resource_id))
+
+        return new_anim_resource
+
+
+def find_child_animation_player(node):
+    """Find AnimationPlayer in node's children, None is
+    returned if not find one"""
+    for child in node.children:
+        if child.get_type() == 'AnimationPlayer':
+            return child
+    return None
+
+
+def get_animation_player(escn_file, export_settings, godot_node):
+    """Get a AnimationPlayer node, its return value depends
+    on animation exporting settings"""
+    animation_player = None
+    # the parent of AnimationPlayer
+    animation_base = None
+
+    if export_settings['animation_modes'] == 'ACTIONS':
+        animation_player = None
+        animation_base = godot_node
+    elif export_settings['animation_modes'] == 'SCENE_ANIMATION':
+        scene_root = escn_file.nodes[0]
+        animation_player = find_child_animation_player(scene_root)
+        animation_base = scene_root
+    else:  # export_settings['animation_modes'] == 'SQUASHED_ACTIONS':
+        animation_base = godot_node
+        node_ptr = godot_node
+        while node_ptr is not None:
+            animation_player = find_child_animation_player(node_ptr)
+            if animation_player is not None:
+                break
+            node_ptr = node_ptr.parent
+
+    if animation_player is None:
+        animation_player = AnimationPlayer(
+            name='AnimationPlayer',
+            parent=animation_base,
+        )
+
+        escn_file.add_node(animation_player)
+
+    return animation_player

+ 17 - 28
io_scene_godot/converters/simple_nodes.py

@@ -7,9 +7,7 @@ import math
 import logging
 import logging
 import mathutils
 import mathutils
 from ..structures import NodeTemplate, fix_directional_transform
 from ..structures import NodeTemplate, fix_directional_transform
-from .animation import (export_animation_data, AttributeConvertInfo,
-                        CONVERT_AS_BOOL, CONVERT_AS_FLOAT,
-                        CONVERT_AS_MULTI_VALUE)
+from .animation import (export_animation_data, AttributeConvertInfo)
 
 
 
 
 def export_empty_node(escn_file, export_settings, node, parent_gd_node):
 def export_empty_node(escn_file, export_settings, node, parent_gd_node):
@@ -27,12 +25,9 @@ class CameraNode(NodeTemplate):
     """Camera node in godot scene"""
     """Camera node in godot scene"""
     _cam_attr_conv = [
     _cam_attr_conv = [
         # blender attr, godot attr, converter lambda, type
         # blender attr, godot attr, converter lambda, type
-        AttributeConvertInfo(
-            'clip_end', 'far', lambda x: x, CONVERT_AS_FLOAT),
-        AttributeConvertInfo(
-            'clip_start', 'near', lambda x: x, CONVERT_AS_FLOAT),
-        AttributeConvertInfo(
-            'ortho_scale', 'size', lambda x: x, CONVERT_AS_FLOAT),
+        AttributeConvertInfo('clip_end', 'far', lambda x: x),
+        AttributeConvertInfo('clip_start', 'near', lambda x: x),
+        AttributeConvertInfo('ortho_scale', 'size', lambda x: x),
     ]
     ]
 
 
     def __init__(self, name, parent):
     def __init__(self, name, parent):
@@ -55,7 +50,7 @@ def export_camera_node(escn_file, export_settings, node, parent_gd_node):
     camera = node.data
     camera = node.data
 
 
     for item in cam_node.attribute_conversion:
     for item in cam_node.attribute_conversion:
-        blender_attr, gd_attr, converter, _ = item
+        blender_attr, gd_attr, converter = item
         cam_node[gd_attr] = converter(getattr(camera, blender_attr))
         cam_node[gd_attr] = converter(getattr(camera, blender_attr))
 
 
     if camera.type == "PERSP":
     if camera.type == "PERSP":
@@ -80,29 +75,23 @@ class LightNode(NodeTemplate):
     """Base class for godot light node"""
     """Base class for godot light node"""
     _light_attr_conv = [
     _light_attr_conv = [
         AttributeConvertInfo(
         AttributeConvertInfo(
-            'use_specular', 'light_specular',
-            lambda x: 1.0 if x else 0.0, CONVERT_AS_BOOL),
-        AttributeConvertInfo(
-            'energy', 'light_energy', lambda x: x, CONVERT_AS_FLOAT),
-        AttributeConvertInfo(
-            'color', 'light_color', mathutils.Color, CONVERT_AS_MULTI_VALUE),
-        AttributeConvertInfo(
-            'shadow_color', 'shadow_color',
-            mathutils.Color, CONVERT_AS_MULTI_VALUE),
+            'use_specular', 'light_specular', lambda x: 1.0 if x else 0.0
+        ),
+        AttributeConvertInfo('energy', 'light_energy', lambda x: x),
+        AttributeConvertInfo('color', 'light_color', mathutils.Color),
+        AttributeConvertInfo('shadow_color', 'shadow_color', mathutils.Color),
     ]
     ]
     _omni_attr_conv = [
     _omni_attr_conv = [
-        AttributeConvertInfo(
-            'distance', 'omni_range', lambda x: x, CONVERT_AS_FLOAT)
+        AttributeConvertInfo('distance', 'omni_range', lambda x: x),
     ]
     ]
     _spot_attr_conv = [
     _spot_attr_conv = [
         AttributeConvertInfo(
         AttributeConvertInfo(
-            'spot_size', 'spot_angle',
-            lambda x: math.degrees(x/2), CONVERT_AS_FLOAT),
+            'spot_size', 'spot_angle', lambda x: math.degrees(x/2)
+        ),
         AttributeConvertInfo(
         AttributeConvertInfo(
-            'spot_blend', 'spot_angle_attenuation',
-            lambda x: 0.2/(x + 0.01), CONVERT_AS_FLOAT),
-        AttributeConvertInfo('distance', 'spot_range',
-                             lambda x: x, CONVERT_AS_FLOAT),
+            'spot_blend', 'spot_angle_attenuation', lambda x: 0.2/(x + 0.01)
+        ),
+        AttributeConvertInfo('distance', 'spot_range', lambda x: x),
     ]
     ]
 
 
     @property
     @property
@@ -151,7 +140,7 @@ def export_lamp_node(escn_file, export_settings, node, parent_gd_node):
 
 
     if light_node is not None:
     if light_node is not None:
         for item in light_node.attribute_conversion:
         for item in light_node.attribute_conversion:
-            bl_attr, gd_attr, converter, _ = item
+            bl_attr, gd_attr, converter = item
             light_node[gd_attr] = converter(getattr(light, bl_attr))
             light_node[gd_attr] = converter(getattr(light, bl_attr))
 
 
         # Properties common to all lights
         # Properties common to all lights

+ 11 - 2
io_scene_godot/structures.py

@@ -364,12 +364,21 @@ def fix_bone_attachment_transform(attachment_obj, blender_transform):
     along bone direction axis"""
     along bone direction axis"""
     armature_obj = attachment_obj.parent
     armature_obj = attachment_obj.parent
     bone_length = armature_obj.data.bones[attachment_obj.parent_bone].length
     bone_length = armature_obj.data.bones[attachment_obj.parent_bone].length
-    # sometimes this transform could be read-only, so copy is required
-    mtx = copy.copy(blender_transform)
+    mtx = mathutils.Matrix(blender_transform)
     mtx[1][3] += bone_length
     mtx[1][3] += bone_length
     return mtx
     return mtx
 
 
 
 
+def fix_bone_attachment_location(attachment_obj, location_vec):
+    """Fix the bone length difference in location vec3 of
+    BoneAttachment object"""
+    armature_obj = attachment_obj.parent
+    bone_length = armature_obj.data.bones[attachment_obj.parent_bone].length
+    vec = mathutils.Vector(location_vec)
+    vec[1] += bone_length
+    return vec
+
+
 # ------------------ Implicit Conversions of Blender Types --------------------
 # ------------------ Implicit Conversions of Blender Types --------------------
 def mat4_to_string(mtx):
 def mat4_to_string(mtx):
     """Converts a matrix to a "Transform" string that can be parsed by Godot"""
     """Converts a matrix to a "Transform" string that can be parsed by Godot"""

+ 0 - 0
tests/reference_exports/action_animation/constraint_external_IK.escn → tests/reference_exports/action_with_constraint/constraint_external_IK.escn


+ 0 - 0
tests/reference_exports/action_animation/constraint_internal_IK.escn → tests/reference_exports/action_with_constraint/constraint_internal_IK.escn


File diff suppressed because it is too large
+ 2 - 2
tests/reference_exports/action_with_constraint/stashed_constraint.escn


+ 11 - 2
tests/reference_exports/light/animation_point_light_shadow.escn

@@ -55,9 +55,18 @@ resource_name = "LampAction"
 step = 0.1
 step = 0.1
 length = 1.25
 length = 1.25
 tracks/0/type = "value"
 tracks/0/type = "value"
-tracks/0/path = NodePath(".:shadow_color")
-tracks/0/interp = 1
+tracks/0/path = NodePath(".:shadow_enabled")
+tracks/0/interp = 0
 tracks/0/keys = {
 tracks/0/keys = {
+	"times":PoolRealArray(0.0416667, 0.833333, 1.25),
+	"transitions":PoolRealArray(1, 1, 1),
+	"update":0,
+	"values":[true, false, false]
+}
+tracks/1/type = "value"
+tracks/1/path = NodePath(".:shadow_color")
+tracks/1/interp = 1
+tracks/1/keys = {
 	"times":PoolRealArray(0.0416667, 0.0833333, 0.125, 0.166667, 0.208333, 0.25, 0.291667, 0.333333, 0.375, 0.416667),
 	"times":PoolRealArray(0.0416667, 0.0833333, 0.125, 0.166667, 0.208333, 0.25, 0.291667, 0.333333, 0.375, 0.416667),
 	"transitions":PoolRealArray(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
 	"transitions":PoolRealArray(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
 	"update":0,
 	"update":0,

File diff suppressed because it is too large
+ 29 - 0
tests/reference_exports/nla_animation/animation_multi_strip.escn


File diff suppressed because it is too large
+ 8 - 0
tests/reference_exports/nla_animation/nla_with_active_action.escn


File diff suppressed because it is too large
+ 10 - 0
tests/reference_exports/nla_animation/nla_with_no_active_action.escn


File diff suppressed because it is too large
+ 8 - 0
tests/reference_exports/nla_animation/nla_with_stashed_action.escn


File diff suppressed because it is too large
+ 10 - 0
tests/reference_exports/shape_key/animation_shapekey_with_transform.escn


+ 0 - 0
tests/test_scenes/action_animation/constraint_external_IK.blend → tests/test_scenes/action_with_constraint/constraint_external_IK.blend


+ 0 - 0
tests/test_scenes/action_animation/constraint_internal_IK.blend → tests/test_scenes/action_with_constraint/constraint_internal_IK.blend


+ 0 - 0
tests/test_scenes/action_animation/animation_with_nla_tracks.blend → tests/test_scenes/action_with_constraint/stashed_constraint.blend


二進制
tests/test_scenes/nla_animation/animation_multi_strip.blend


二進制
tests/test_scenes/nla_animation/nla_with_active_action.blend


二進制
tests/test_scenes/nla_animation/nla_with_no_active_action.blend


二進制
tests/test_scenes/nla_animation/nla_with_stashed_action.blend


二進制
tests/test_scenes/shape_key/animation_shapekey_with_transform.blend


Some files were not shown because too many files changed in this diff