Эх сурвалжийг харах

rewrite constraint baking

Jason0214 6 жил өмнө
parent
commit
c5c0bffd41

+ 108 - 33
io_scene_godot/converters/animation/action.py

@@ -9,6 +9,7 @@ import copy
 import bpy
 import mathutils
 from .serializer import FloatTrack, TransformTrack, ColorTrack, TransformFrame
+from .constraint_baking import check_object_constraint
 from ...structures import (NodePath, fix_bone_attachment_location)
 
 
@@ -35,7 +36,7 @@ def get_strip_frame_range(strip):
 class ActionStrip:
     """Abstract of blender action strip, it may override attributes
     of an action object"""
-    def __init__(self, action_or_strip, action_override=None):
+    def __init__(self, action_or_strip):
         self.action = None
         self.frame_range = (0, 0)
 
@@ -47,21 +48,18 @@ class ActionStrip:
 
         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.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
+        elif isinstance(action_or_strip, bpy.types.Action):
             self.action = action_or_strip
             self.frame_range = get_action_frame_range(self.action)
+        else:  # action_or_strip is None
+            self.frame_range = (0, 190)
 
     def evaluate_fcurve(self, fcurve, frame):
         """Evaluate a value of fcurve, DO NOT use fcurve.evalute, as
@@ -93,12 +91,96 @@ def split_fcurve_data_path(data_path):
     return path_list[0], path_list[1]
 
 
+def has_obj_fcurves(action_strip):
+    """Check whether action has object transform information"""
+    if action_strip.action is None:
+        return False
+    for fcurve in action_strip.action.fcurves:
+        obj_path, attribute = split_fcurve_data_path(fcurve.data_path)
+        if obj_path == '':
+            return True
+    return False
+
+
+def export_constrained_xform_action(godot_node, animation_player,
+                                    blender_object, action_strip,
+                                    animation_resource):
+    """Export transform animation of any object has constraints,
+    it use frame_set to traversal each frame, so it's costly"""
+    first_frame, last_frame = action_strip.frame_range
+
+    obj_xform_mats = list()
+    pbone_xform_mats = collections.OrderedDict()
+
+    scene = bpy.context.scene
+    frame_backup = scene.frame_current
+    for frame in range(first_frame, last_frame):
+        scene.frame_set(frame)
+        obj_xform_mats.append(blender_object.matrix_local.copy())
+        if blender_object.pose is not None:
+            for pbone in blender_object.pose.bones:
+                if pbone.name not in pbone_xform_mats:
+                    pbone_xform_mats[pbone.name] = list()
+                pbone_xform_mats[pbone.name].append(
+                    blender_object.convert_space(
+                        pose_bone=pbone, matrix=pbone.matrix,
+                        from_space='POSE', to_space='LOCAL'
+                    )
+                )
+    scene.frame_set(frame_backup)
+
+    if (check_object_constraint(blender_object) or
+            has_obj_fcurves(action_strip)):
+        xform_frames_list = [
+            TransformFrame.factory(mat)
+            for mat in obj_xform_mats
+        ]
+
+        track_path = NodePath(
+            animation_player.parent.get_path(),
+            godot_node.get_path()
+        )
+
+        if godot_node.parent.get_type() == 'BoneAttachment':
+            xform_frames_list = [
+                fix_bone_attachment_location(blender_object, x.location)
+                for x in xform_frames_list
+            ]
+
+        animation_resource.add_obj_xform_track(
+            godot_node.get_type(), track_path,
+            xform_frames_list, action_strip.frame_range,
+            # no need for parent_inverse, as it is directly access matrix_local
+        )
+
+    for pbone_name, pbone_xform_mat_list in pbone_xform_mats.items():
+        if godot_node.find_bone_id(pbone_name) != -1:
+            pbone_xform_frames_list = [
+                TransformFrame.factory(mat)
+                for mat in pbone_xform_mat_list
+            ]
+
+            track_path = NodePath(
+                animation_player.parent.get_path(),
+                godot_node.get_path(),
+                godot_node.find_bone_name(pbone_name),
+            )
+
+            animation_resource.add_track(
+                TransformTrack(
+                    track_path,
+                    frames_iter=range(first_frame, last_frame),
+                    values_iter=pbone_xform_frames_list,
+                )
+            )
+
+
 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"""
+    def init_transform_frames_list(object_path, blender_object, godot_node,
+                                   first_frame, last_frame):
+        """Initialize a list of TransformFrame for an animated object"""
         if object_path.startswith('pose'):
             bone_name = blender_path_to_bone_name(object_path)
 
@@ -136,7 +218,7 @@ def export_transform_action(godot_node, animation_player, blender_object,
         ]
 
     first_frame, last_frame = action_strip.frame_range
-    transform_frame_values_map = collections.OrderedDict()
+    xform_frames_list_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
@@ -145,9 +227,9 @@ def export_transform_action(godot_node, animation_player, blender_object,
         object_path, attribute = split_fcurve_data_path(fcurve.data_path)
 
         if attribute in TransformFrame.ATTRIBUTES:
-            if object_path not in transform_frame_values_map:
+            if object_path not in xform_frames_list_map:
 
-                frame_values = init_transform_frame_values(
+                frame_values = init_transform_frames_list(
                     object_path, blender_object,
                     godot_node, first_frame, last_frame
                 )
@@ -156,20 +238,18 @@ def export_transform_action(godot_node, animation_player, blender_object,
                 if not frame_values:
                     continue
 
-                transform_frame_values_map[object_path] = frame_values
+                xform_frames_list_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)
-                    )
+                xform_frames_list_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():
+    for object_path, frame_value_list in xform_frames_list_map.items():
         if object_path == '':
-            # object_path equals '' represents node itself
-
+            # empty object_path represents transform of object itself
             track_path = NodePath(
                 animation_player.parent.get_path(),
                 godot_node.get_path()
@@ -181,16 +261,11 @@ def export_transform_action(godot_node, animation_player, blender_object,
                     for x in frame_value_list
                 ]
 
-            track = TransformTrack(
-                track_path,
-                frames_iter=range(first_frame, last_frame),
-                values_iter=frame_value_list,
+            animation_resource.add_obj_xform_track(
+                godot_node.get_type(), track_path,
+                frame_value_list, action_strip.frame_range,
+                blender_object.matrix_parent_inverse
             )
-            track.set_parent_inverse(blender_object.matrix_parent_inverse)
-            if godot_node.get_type() in ("SpotLight", "DirectionalLight",
-                                         "Camera", "CollisionShape"):
-                track.is_directional = True
-            animation_resource.add_track(track)
 
         elif object_path.startswith('pose'):
             track_path = NodePath(

+ 25 - 41
io_scene_godot/converters/animation/animation_data.py

@@ -2,19 +2,16 @@
 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_constrained_xform_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
@@ -35,7 +32,6 @@ class ObjectAnimationExporter:
         self.godot_node = godot_node
         self.blender_object = blender_object
 
-        self.action_exporter_func = ACTION_EXPORTER_MAP[action_type]
         self.animation_player = None
 
         self.need_baking = False
@@ -46,6 +42,11 @@ class ObjectAnimationExporter:
         self.check_baking_condition(action_type)
         self.preprocess_nla_tracks(blender_object)
 
+        if not self.need_baking:
+            self.action_exporter_func = ACTION_EXPORTER_MAP[action_type]
+        else:
+            self.action_exporter_func = export_constrained_xform_action
+
     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"""
@@ -68,44 +69,31 @@ class ObjectAnimationExporter:
                 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"""
-        return bake_constraint_to_action(
-            self.blender_object, action_to_bake, False
-        )
-
     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)
+        if active_action is None:
+            # object has constraints on other objects
+            assert self.need_baking
+            anim_rsc_name = self.blender_object.name + 'Action'
         else:
-            action_active_to_export = active_action
+            anim_rsc_name = active_action.name
 
         if self.animation_player.active_animation is None:
             self.animation_player.add_active_animation_resource(
-                escn_file, action_active_to_export.name
+                escn_file, anim_rsc_name
             )
 
         self.action_exporter_func(
             self.godot_node,
             self.animation_player,
             self.blender_object,
-            ActionStrip(action_active_to_export),
+            ActionStrip(active_action),
             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:
+        if not self.need_baking:
             # here export unmuted nla_tracks into animation resource,
             # this is not needed for baking, as baking has applied to
             # active action
@@ -142,7 +130,6 @@ class ObjectAnimationExporter:
     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
@@ -158,28 +145,22 @@ class ObjectAnimationExporter:
             escn_file, anim_name
         )
 
+        if self.need_baking:
+            stashed_track.mute = False
+
         for strip in stashed_track.strips:
             if strip.action:
-                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),
+                    ActionStrip(strip),
                     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 self.need_baking:
+            stashed_track.mute = True
+        else:  # 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:
@@ -228,8 +209,11 @@ def export_animation_data(escn_file, export_settings, godot_node,
     # export actions in nla_tracks, each exported to seperate
     # animation resources
     if export_settings['use_stashed_action']:
+        if blender_object.animation_data:
+            # clear active action, isolate NLA track
+            blender_object.animation_data.action = None
         for stashed_track in anim_exporter.mute_nla_tracks:
             anim_exporter.export_stashed_track(escn_file, stashed_track)
 
-    if active_action is not None:
+    if blender_object.animation_data is not None:
         blender_object.animation_data.action = active_action

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

@@ -1,24 +1,5 @@
 """Collection of helper functions to baking constraints into action"""
-
 import bpy
-import bpy_extras.anim_utils
-
-# 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):
@@ -36,39 +17,3 @@ def check_pose_constraint(blender_object):
             if pose_bone.constraints:
                 return True
     return False
-
-
-def bake_constraint_to_action(blender_object, base_action, 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 = range(
-            int(base_action.frame_range[0]),
-            int(base_action.frame_range[1]) + 1)
-    else:
-        frame_range = range(1, 251)  # 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
-
-    baked_action = bpy_extras.anim_utils.bake_action_objects(
-        object_action_pairs=[(blender_object, action_bake_into)],
-        frames=frame_range,
-        only_selected=False,
-        do_pose=True,
-        do_object=True,
-        do_visual_keying=True,
-    )[0]
-
-    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

+ 30 - 14
io_scene_godot/converters/animation/serializer.py

@@ -52,13 +52,13 @@ class TransformFrame:
     matrix."""
     ATTRIBUTES = {'location', 'scale', 'rotation_quaternion', 'rotation_euler'}
 
-    def __init__(self):
+    def __init__(self, rotation_mode='QUATERNION'):
         self.location = mathutils.Vector((0, 0, 0))
         self.scale = mathutils.Vector((1, 1, 1))
 
-        self.rotation_mode = 'QUATERNION'
+        self.rotation_mode = rotation_mode
         self.rotation_euler = mathutils.Euler((0, 0, 0))
-        self.rotation_quaternion = mathutils.Quaternion()
+        self.rotation_quaternion = mathutils.Quaternion((1.0, 0.0, 0.0, 0.0))
 
     def __eq__(self, other):
         """Overrides the default implementation"""
@@ -69,21 +69,20 @@ class TransformFrame:
         return False
 
     @classmethod
-    def factory(cls, trans_mat, rotation_mode):
-        """Factory function, create cls from a transform matrix"""
-        ret = cls()
-        ret.location = trans_mat.to_translation()
+    def factory(cls, xform_matrix, rotation_mode='QUATERNION'):
+        """Factory method, return an instance created from transform matrix"""
+        xform_frame = cls()
+        xform_frame.rotation_mode = rotation_mode
+        xform_frame.location = xform_matrix.to_translation()
+        xform_frame.rotation_quaternion = xform_matrix.to_quaternion()
         # FIXME: lose negative scale
-        ret.scale = trans_mat.to_scale()
+        xform_frame.scale = xform_matrix.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()
+            xform_frame.rotation_euler = xform_matrix.to_euler()
         else:
-            ret.rotation_euler = trans_mat.to_euler(rotation_mode)
-        return ret
+            xform_frame.rotation_euler = xform_matrix.to_euler(rotation_mode)
+        return xform_frame
 
     def update(self, attribute, array_index, value):
         """Use fcurve data to update the frame"""
@@ -459,6 +458,23 @@ class AnimationResource(InternalResource):
             self[track_id_str + '/interp'] = track.interp
             self[track_id_str + '/keys'] = track
 
+    # pylint: disable-msg=too-many-arguments
+    def add_obj_xform_track(self, node_type, track_path,
+                            xform_frames_list, frame_range,
+                            parent_mat_inverse=mathutils.Matrix.Identity(4)):
+        """Add a object transform track to AnimationResource"""
+        track = TransformTrack(
+            track_path,
+            frames_iter=range(frame_range[0], frame_range[1]),
+            values_iter=xform_frames_list,
+        )
+        track.set_parent_inverse(parent_mat_inverse)
+        if node_type in ("SpotLight", "DirectionalLight",
+                         "Camera", "CollisionShape"):
+            track.is_directional = True
+
+        self.add_track(track)
+
     def add_attribute_track(self, action_strip, fcurve,
                             converter, node_path):
         """Add a track into AnimationResource, the track is a

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
tests/reference_exports/action_with_constraint/bone_attachment_ik.escn


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 1 - 1
tests/reference_exports/action_with_constraint/constraint_external_IK.escn


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
tests/reference_exports/action_with_constraint/constraint_internal_IK.escn


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 2 - 2
tests/reference_exports/action_with_constraint/stashed_constraint.escn


Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно