diff --git a/release/scripts/modules/bpy_extras/__init__.py b/release/scripts/modules/bpy_extras/__init__.py index d853d5fda10..fd653a4129c 100644 --- a/release/scripts/modules/bpy_extras/__init__.py +++ b/release/scripts/modules/bpy_extras/__init__.py @@ -23,6 +23,7 @@ Utility modules assosiated with the bpy module. """ __all__ = ( + "anim_utils", "object_utils", "io_utils", "image_utils", diff --git a/release/scripts/modules/bpy_extras/anim_utils.py b/release/scripts/modules/bpy_extras/anim_utils.py new file mode 100644 index 00000000000..9482dc3e1c9 --- /dev/null +++ b/release/scripts/modules/bpy_extras/anim_utils.py @@ -0,0 +1,247 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +__all__ = ( + "bake_action", + ) + +import bpy + + +def bake_action(frame_start, + frame_end, + frame_step=1, + only_selected=False, + do_pose=True, + do_object=True, + do_constraint_clear=False, + do_clean=False, + action=None, + ): + + """ + Return an image from the file path with options to search multiple paths + and return a placeholder if its not found. + + :arg frame_start: First frame to bake. + :type frame_start: int + :arg frame_end: Last frame to bake. + :type frame_end: int + :arg frame_step: Frame step. + :type frame_step: int + :arg only_selected: Only bake selected data. + :type only_selected: bool + :arg do_pose: Bake pose channels. + :type do_pose: bool + :arg do_object: Bake objects. + :type do_object: bool + :arg do_constraint_clear: Remove constraints. + :type do_constraint_clear: bool + :arg do_clean: Remove redundant keyframes after baking. + :type do_clean: bool + :arg action: An action to bake the data into, or None for a new action + to be created. + :type action: :class:`bpy.types.Action` or None + + :return: an action or None + :rtype: :class:`bpy.types.Action` + """ + + # ------------------------------------------------------------------------- + # Helper Functions + + def pose_frame_info(obj): + from mathutils import Matrix + + info = {} + + pose = obj.pose + + pose_items = pose.bones.items() + + for name, pbone in pose_items: + binfo = {} + bone = pbone.bone + + binfo["parent"] = getattr(bone.parent, "name", None) + binfo["bone"] = bone + binfo["pbone"] = pbone + binfo["matrix_local"] = bone.matrix_local.copy() + try: + binfo["matrix_local_inv"] = binfo["matrix_local"].inverted() + except: + binfo["matrix_local_inv"] = Matrix() + + binfo["matrix"] = bone.matrix.copy() + binfo["matrix_pose"] = pbone.matrix.copy() + try: + binfo["matrix_pose_inv"] = binfo["matrix_pose"].inverted() + except: + binfo["matrix_pose_inv"] = Matrix() + + info[name] = binfo + + for name, pbone in pose_items: + binfo = info[name] + binfo_parent = binfo.get("parent", None) + if binfo_parent: + binfo_parent = info[binfo_parent] + + matrix = binfo["matrix_pose"] + rest_matrix = binfo["matrix_local"] + + if binfo_parent: + matrix = binfo_parent["matrix_pose_inv"] * matrix + rest_matrix = binfo_parent["matrix_local_inv"] * rest_matrix + + binfo["matrix_key"] = rest_matrix.inverted() * matrix + + return info + + + def obj_frame_info(obj): + info = {} + # parent = obj.parent + info["matrix_key"] = obj.matrix_local.copy() + return info + + # ------------------------------------------------------------------------- + # Setup the Context + + # TODO, pass data rather then grabbing from the context! + scene = bpy.context.scene + obj = bpy.context.object + pose = obj.pose + frame_back = scene.frame_current + + if pose is None: + do_pose = False + + if do_pose is None and do_object is None: + return None + + pose_info = [] + obj_info = [] + + frame_range = range(frame_start, frame_end + 1, frame_step) + + # ------------------------------------------------------------------------- + # Collect transformations + + # could speed this up by applying steps here too... + for f in frame_range: + scene.frame_set(f) + + if do_pose: + pose_info.append(pose_frame_info(obj)) + if do_object: + obj_info.append(obj_frame_info(obj)) + + f += 1 + + # ------------------------------------------------------------------------- + # Create action + + # incase animation data hassnt been created + atd = obj.animation_data_create() + if action is None: + action = bpy.data.actions.new("Action") + atd.action = action + + if do_pose: + pose_items = pose.bones.items() + else: + pose_items = [] # skip + + # ------------------------------------------------------------------------- + # Apply transformations to action + + # pose + for name, pbone in (pose_items if do_pose else ()): + if only_selected and not pbone.bone.select: + continue + + if do_constraint_clear: + while pbone.constraints: + pbone.constraints.remove(pbone.constraints[0]) + + for f in frame_range: + matrix = pose_info[(f - frame_start) // frame_step][name]["matrix_key"] + + # pbone.location = matrix.to_translation() + # pbone.rotation_quaternion = matrix.to_quaternion() + pbone.matrix_basis = matrix + + pbone.keyframe_insert("location", -1, f, name) + + rotation_mode = pbone.rotation_mode + + if rotation_mode == 'QUATERNION': + pbone.keyframe_insert("rotation_quaternion", -1, f, name) + elif rotation_mode == 'AXIS_ANGLE': + pbone.keyframe_insert("rotation_axis_angle", -1, f, name) + else: # euler, XYZ, ZXY etc + pbone.keyframe_insert("rotation_euler", -1, f, name) + + pbone.keyframe_insert("scale", -1, f, name) + + # object. TODO. multiple objects + if do_object: + if do_constraint_clear: + while obj.constraints: + obj.constraints.remove(obj.constraints[0]) + + for f in frame_range: + matrix = obj_info[(f - frame_start) // frame_step]["matrix_key"] + obj.matrix_local = matrix + + obj.keyframe_insert("location", -1, f) + + rotation_mode = obj.rotation_mode + + if rotation_mode == 'QUATERNION': + obj.keyframe_insert("rotation_quaternion", -1, f) + elif rotation_mode == 'AXIS_ANGLE': + obj.keyframe_insert("rotation_axis_angle", -1, f) + else: # euler, XYZ, ZXY etc + obj.keyframe_insert("rotation_euler", -1, f) + + obj.keyframe_insert("scale", -1, f) + + scene.frame_set(frame_back) + + # ------------------------------------------------------------------------- + # Clean + + if do_clean: + for fcu in action.fcurves: + keyframe_points = fcu.keyframe_points + i = 1 + while i < len(fcu.keyframe_points) - 1: + val_prev = keyframe_points[i - 1].co[1] + val_next = keyframe_points[i + 1].co[1] + val = keyframe_points[i].co[1] + + if abs(val - val_prev) + abs(val - val_next) < 0.0001: + keyframe_points.remove(keyframe_points[i]) + else: + i += 1 + + return action diff --git a/release/scripts/startup/bl_operators/__init__.py b/release/scripts/startup/bl_operators/__init__.py index 918e9153b73..f5f8b992356 100644 --- a/release/scripts/startup/bl_operators/__init__.py +++ b/release/scripts/startup/bl_operators/__init__.py @@ -29,7 +29,6 @@ _modules = ( "console", "image", "mesh", - "nla", "object_align", "object", "object_randomize_transform", diff --git a/release/scripts/startup/bl_operators/anim.py b/release/scripts/startup/bl_operators/anim.py index f33d5614629..660194abf8c 100644 --- a/release/scripts/startup/bl_operators/anim.py +++ b/release/scripts/startup/bl_operators/anim.py @@ -18,8 +18,14 @@ # +if "bpy" in locals(): + import imp + if "anim_utils" in locals(): + imp.reload(anim_utils) + import bpy from bpy.types import Operator +from bpy.props import IntProperty, BoolProperty, EnumProperty class ANIM_OT_keying_set_export(Operator): @@ -125,3 +131,105 @@ class ANIM_OT_keying_set_export(Operator): wm = context.window_manager wm.fileselect_add(self) return {'RUNNING_MODAL'} + + +class BakeAction(Operator): + '''Bake animation to an Action''' + bl_idname = "nla.bake" + bl_label = "Bake Action" + bl_options = {'REGISTER', 'UNDO'} + + frame_start = IntProperty( + name="Start Frame", + description="Start frame for baking", + min=0, max=300000, + default=1, + ) + frame_end = IntProperty( + name="End Frame", + description="End frame for baking", + min=1, max=300000, + default=250, + ) + step = IntProperty( + name="Frame Step", + description="Frame Step", + min=1, max=120, + default=1, + ) + only_selected = BoolProperty( + name="Only Selected", + default=True, + ) + clear_consraints = BoolProperty( + name="Clear Constraints", + default=False, + ) + bake_types = EnumProperty( + name="Bake Data", + options={'ENUM_FLAG'}, + items=(('POSE', "Pose", ""), + ('OBJECT', "Object", ""), + ), + default={'POSE'}, + ) + + def execute(self, context): + + from bpy_extras import anim_utils + + action = anim_utils.bake_action(self.frame_start, + self.frame_end, + self.step, + self.only_selected, + 'POSE' in self.bake_types, + 'OBJECT' in self.bake_types, + self.clear_consraints, + True, + ) + + if action is None: + self.report({'INFO'}, "Nothing to bake") + return {'CANCELLED'} + + return {'FINISHED'} + + def invoke(self, context, event): + wm = context.window_manager + return wm.invoke_props_dialog(self) + + +class ClearUselessActions(Operator): + '''Mark actions with no F-Curves for deletion after save+reload of ''' \ + '''file preserving "action libraries"''' + bl_idname = "anim.clear_useless_actions" + bl_label = "Clear Useless Actions" + bl_options = {'REGISTER', 'UNDO'} + + only_unused = BoolProperty(name="Only Unused", + description="Only unused (Fake User only) actions get considered", + default=True) + + @classmethod + def poll(cls, context): + return len(bpy.data.actions) != 0 + + def execute(self, context): + removed = 0 + + for action in bpy.data.actions: + # if only user is "fake" user... + if ((self.only_unused is False) or + (action.use_fake_user and action.users == 1)): + + # if it has F-Curves, then it's a "action library" + # (i.e. walk, wave, jump, etc.) + # and should be left alone as that's what fake users are for! + if not action.fcurves: + # mark action for deletion + action.user_clear() + removed += 1 + + self.report({'INFO'}, "Removed %d empty and/or fake-user only Actions" + % removed) + return {'FINISHED'} diff --git a/release/scripts/startup/bl_operators/nla.py b/release/scripts/startup/bl_operators/nla.py deleted file mode 100644 index feb0016b1c7..00000000000 --- a/release/scripts/startup/bl_operators/nla.py +++ /dev/null @@ -1,306 +0,0 @@ -# ##### BEGIN GPL LICENSE BLOCK ##### -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software Foundation, -# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# ##### END GPL LICENSE BLOCK ##### - -# - -import bpy -from bpy.types import Operator - - -def pose_frame_info(obj): - from mathutils import Matrix - - info = {} - - pose = obj.pose - - pose_items = pose.bones.items() - - for name, pbone in pose_items: - binfo = {} - bone = pbone.bone - - binfo["parent"] = getattr(bone.parent, "name", None) - binfo["bone"] = bone - binfo["pbone"] = pbone - binfo["matrix_local"] = bone.matrix_local.copy() - try: - binfo["matrix_local_inv"] = binfo["matrix_local"].inverted() - except: - binfo["matrix_local_inv"] = Matrix() - - binfo["matrix"] = bone.matrix.copy() - binfo["matrix_pose"] = pbone.matrix.copy() - try: - binfo["matrix_pose_inv"] = binfo["matrix_pose"].inverted() - except: - binfo["matrix_pose_inv"] = Matrix() - - info[name] = binfo - - for name, pbone in pose_items: - binfo = info[name] - binfo_parent = binfo.get("parent", None) - if binfo_parent: - binfo_parent = info[binfo_parent] - - matrix = binfo["matrix_pose"] - rest_matrix = binfo["matrix_local"] - - if binfo_parent: - matrix = binfo_parent["matrix_pose_inv"] * matrix - rest_matrix = binfo_parent["matrix_local_inv"] * rest_matrix - - binfo["matrix_key"] = rest_matrix.inverted() * matrix - - return info - - -def obj_frame_info(obj): - info = {} - # parent = obj.parent - info["matrix_key"] = obj.matrix_local.copy() - return info - - -def bake(frame_start, - frame_end, step=1, - only_selected=False, - do_pose=True, - do_object=True, - do_constraint_clear=False, - action=None): - - scene = bpy.context.scene - obj = bpy.context.object - pose = obj.pose - frame_back = scene.frame_current - - if pose is None: - do_pose = False - - if do_pose is None and do_object is None: - return None - - pose_info = [] - obj_info = [] - - frame_range = range(frame_start, frame_end + 1, step) - - # ------------------------------------------------------------------------- - # Collect transformations - - # could speed this up by applying steps here too... - for f in frame_range: - scene.frame_set(f) - - if do_pose: - pose_info.append(pose_frame_info(obj)) - if do_object: - obj_info.append(obj_frame_info(obj)) - - f += 1 - - # ------------------------------------------------------------------------- - # Create action - - # incase animation data hassnt been created - atd = obj.animation_data_create() - if action is None: - action = bpy.data.actions.new("Action") - atd.action = action - - if do_pose: - pose_items = pose.bones.items() - else: - pose_items = [] # skip - - # ------------------------------------------------------------------------- - # Apply transformations to action - - # pose - for name, pbone in (pose_items if do_pose else ()): - if only_selected and not pbone.bone.select: - continue - - if do_constraint_clear: - while pbone.constraints: - pbone.constraints.remove(pbone.constraints[0]) - - for f in frame_range: - matrix = pose_info[(f - frame_start) // step][name]["matrix_key"] - - # pbone.location = matrix.to_translation() - # pbone.rotation_quaternion = matrix.to_quaternion() - pbone.matrix_basis = matrix - - pbone.keyframe_insert("location", -1, f, name) - - rotation_mode = pbone.rotation_mode - - if rotation_mode == 'QUATERNION': - pbone.keyframe_insert("rotation_quaternion", -1, f, name) - elif rotation_mode == 'AXIS_ANGLE': - pbone.keyframe_insert("rotation_axis_angle", -1, f, name) - else: # euler, XYZ, ZXY etc - pbone.keyframe_insert("rotation_euler", -1, f, name) - - pbone.keyframe_insert("scale", -1, f, name) - - # object. TODO. multiple objects - if do_object: - if do_constraint_clear: - while obj.constraints: - obj.constraints.remove(obj.constraints[0]) - - for f in frame_range: - matrix = obj_info[(f - frame_start) // step]["matrix_key"] - obj.matrix_local = matrix - - obj.keyframe_insert("location", -1, f) - - rotation_mode = obj.rotation_mode - - if rotation_mode == 'QUATERNION': - obj.keyframe_insert("rotation_quaternion", -1, f) - elif rotation_mode == 'AXIS_ANGLE': - obj.keyframe_insert("rotation_axis_angle", -1, f) - else: # euler, XYZ, ZXY etc - obj.keyframe_insert("rotation_euler", -1, f) - - obj.keyframe_insert("scale", -1, f) - - scene.frame_set(frame_back) - - return action - - -from bpy.props import IntProperty, BoolProperty, EnumProperty - - -class BakeAction(Operator): - '''Bake animation to an Action''' - bl_idname = "nla.bake" - bl_label = "Bake Action" - bl_options = {'REGISTER', 'UNDO'} - - frame_start = IntProperty( - name="Start Frame", - description="Start frame for baking", - min=0, max=300000, - default=1, - ) - frame_end = IntProperty( - name="End Frame", - description="End frame for baking", - min=1, max=300000, - default=250, - ) - step = IntProperty( - name="Frame Step", - description="Frame Step", - min=1, max=120, - default=1, - ) - only_selected = BoolProperty( - name="Only Selected", - default=True, - ) - clear_consraints = BoolProperty( - name="Clear Constraints", - default=False, - ) - bake_types = EnumProperty( - name="Bake Data", - options={'ENUM_FLAG'}, - items=(('POSE', "Pose", ""), - ('OBJECT', "Object", ""), - ), - default={'POSE'}, - ) - - def execute(self, context): - - action = bake(self.frame_start, - self.frame_end, - self.step, - self.only_selected, - 'POSE' in self.bake_types, - 'OBJECT' in self.bake_types, - self.clear_consraints, - ) - - if action is None: - self.report({'INFO'}, "Nothing to bake") - return {'CANCELLED'} - - # basic cleanup, could move elsewhere - for fcu in action.fcurves: - keyframe_points = fcu.keyframe_points - i = 1 - while i < len(fcu.keyframe_points) - 1: - val_prev = keyframe_points[i - 1].co[1] - val_next = keyframe_points[i + 1].co[1] - val = keyframe_points[i].co[1] - - if abs(val - val_prev) + abs(val - val_next) < 0.0001: - keyframe_points.remove(keyframe_points[i]) - else: - i += 1 - - return {'FINISHED'} - - def invoke(self, context, event): - wm = context.window_manager - return wm.invoke_props_dialog(self) - - -class ClearUselessActions(Operator): - '''Mark actions with no F-Curves for deletion after save+reload of ''' \ - '''file preserving "action libraries"''' - bl_idname = "anim.clear_useless_actions" - bl_label = "Clear Useless Actions" - bl_options = {'REGISTER', 'UNDO'} - - only_unused = BoolProperty(name="Only Unused", - description="Only unused (Fake User only) actions get considered", - default=True) - - @classmethod - def poll(cls, context): - return len(bpy.data.actions) != 0 - - def execute(self, context): - removed = 0 - - for action in bpy.data.actions: - # if only user is "fake" user... - if ((self.only_unused is False) or - (action.use_fake_user and action.users == 1)): - - # if it has F-Curves, then it's a "action library" - # (i.e. walk, wave, jump, etc.) - # and should be left alone as that's what fake users are for! - if not action.fcurves: - # mark action for deletion - action.user_clear() - removed += 1 - - self.report({'INFO'}, "Removed %d empty and/or fake-user only Actions" - % removed) - return {'FINISHED'}