Created
May 21, 2015 17:52
-
-
Save wesen/687d6fdf455cbc5da286 to your computer and use it in GitHub Desktop.
euler filter addon for blender
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import bpy | |
| from math import pi | |
| from mathutils import Euler | |
| import itertools | |
| bl_info = { | |
| "name": "Euler Filter", | |
| "author": "Manuel Odendahl", | |
| "version": (0, 1), | |
| "blender": (2, 74, 0), | |
| "location": "Search > Euler Filter", | |
| "description": "Euler Filter", | |
| "warning": "", | |
| "wiki_url": "http://", | |
| "tracker_url": "https://", | |
| "category": "Animation" | |
| } | |
| def get_fcu_keyframe_numbers(fcu): | |
| return sorted([p.co[0] for p in fcu.keyframe_points]) | |
| def get_selected_fcu_keyframe_numbers(fcu): | |
| return sorted([p.co[0] for p in fcu.keyframe_points if p.select_control_point]) | |
| def update_euler_keyframes(bone, keyframes): | |
| for kf in keyframes: | |
| bone.rotation_euler = kf["rotation_euler"] | |
| bone.keyframe_insert(data_path='rotation_euler', frame=kf["key"]) | |
| ################################################# | |
| # actual euler filter | |
| def euler_to_string(e): | |
| return "%.2f, %.2f, %.2f" % (r(e[0]), r(e[1]), r(e[2])) | |
| def degrees(a): | |
| return a / 360.0 * 2 * pi | |
| def d(a): | |
| return degrees(a) | |
| def r(a): | |
| return a / (2 * pi) * 360.0 | |
| def wrap_angle(a): | |
| return (a + pi) % (2 * pi) - pi | |
| def euler_distance(e1, e2): | |
| return abs(e1[0] - e2[0]) + abs(e1[1] - e2[1]) + abs(e1[2] - e2[2]) | |
| def euler_axis_index(axis): | |
| if axis == 'X': | |
| return 0 | |
| if axis == 'Y': | |
| return 1 | |
| if axis == 'Z': | |
| return 2 | |
| return None | |
| def flip_euler(euler, rotation_mode): | |
| ret = euler.copy() | |
| inner_axis = rotation_mode[0] | |
| outer_axis = rotation_mode[2] | |
| middle_axis = rotation_mode[1] | |
| ret[euler_axis_index(inner_axis)] += pi | |
| ret[euler_axis_index(outer_axis)] += pi | |
| ret[euler_axis_index(middle_axis)] *= -1 | |
| ret[euler_axis_index(middle_axis)] += pi | |
| return ret | |
| def naive_flip_diff(a1, a2): | |
| while abs(a1 - a2) > pi: | |
| if a1 < a2: | |
| a2 -= 2 * pi | |
| else: | |
| a2 += 2 * pi | |
| return a2 | |
| def euler_filter(kfs, rotation_mode): | |
| if len(kfs) <= 1: | |
| return kfs | |
| prev = kfs[0]["rotation_euler"] | |
| ret = [{"key": kfs[0]["key"], | |
| "rotation_euler": prev.copy()}] | |
| for i in range(1, len(kfs)): | |
| e = kfs[i]["rotation_euler"].copy() | |
| e[0] = naive_flip_diff(prev[0], e[0]) | |
| e[1] = naive_flip_diff(prev[1], e[1]) | |
| e[2] = naive_flip_diff(prev[2], e[2]) | |
| fe = flip_euler(e, rotation_mode) | |
| fe[0] = naive_flip_diff(prev[0], fe[0]) | |
| fe[1] = naive_flip_diff(prev[1], fe[1]) | |
| fe[2] = naive_flip_diff(prev[2], fe[2]) | |
| de = euler_distance(prev, e) | |
| dfe = euler_distance(prev, fe) | |
| # print("distance: %s, flipped distance: %s" % (de, dfe)) | |
| if dfe < de: | |
| e = fe | |
| prev = e | |
| ret += [{"key": kfs[i]["key"], | |
| "rotation_euler": e}] | |
| return ret | |
| ################################################# | |
| # keyframes / fcurves helpers | |
| def split_data_path(data_path): | |
| """ | |
| :param data_path: an FCurve data path | |
| :return: object path, property | |
| """ | |
| return data_path.rsplit(".", 1) | |
| # XXX when needed for rotation_mode | |
| def get_bone_from_fcurve(obj, fcurve): | |
| """ | |
| Resolve the path of a bone from the data_path of an fcurve | |
| :param obj: object the action belongs to (to resolve the fcurve data_path) | |
| :param fcurve: the fcurve | |
| :return: the resolved bone | |
| """ | |
| bone, prop = split_data_path(fcurve.data_path) | |
| return obj.path_resolve(bone) | |
| def get_selected_rotation_fcurves(context): | |
| """ | |
| Returns the selected rotation euler curves. | |
| This checks that 3 curves for the same bone with property "rotation_euler" are selected, | |
| and that their array_index cover 0, 1, 2. | |
| :param context: | |
| :return: fcurves, error_string | |
| """ | |
| obj = context.active_object | |
| if not obj: | |
| return None, "No object selected" | |
| if not obj.animation_data: | |
| return None, "Object have no animation data" | |
| if not obj.animation_data.action: | |
| return None, "Object has no action" | |
| fcurves = obj.animation_data.action.fcurves | |
| selected_fcurves = [] | |
| selected_bone = None | |
| for fc in fcurves: | |
| if not fc.select: | |
| continue | |
| bone, prop = split_data_path(fc.data_path) | |
| if prop != "rotation_euler": | |
| continue | |
| if not selected_bone: | |
| selected_bone = bone | |
| if bone != selected_bone: | |
| return None, "Only select the rotation of a single object" | |
| selected_fcurves.append(fc) | |
| if len(selected_fcurves) != 3: | |
| return None, "Select only XYZ rotation curves" | |
| selected_fcurves = sorted(selected_fcurves, key=lambda fcu: fcu.array_index) | |
| for i in range(3): | |
| if selected_fcurves[i].array_index != i: | |
| return None, "Wrong index for rotation curves, selected all 3 angles" | |
| return selected_fcurves, None | |
| def get_selected_rotation_keyframes(context): | |
| """ | |
| Returns the selected rotation keyframes. | |
| The keyframes are in the format {"key": frame, "rotation_euler": Euler}. | |
| If there is an error, keyframes, fcurves will be None, and error_string will be set to a descriptive string. | |
| :param context: | |
| :return: keyframes, fcurves, error_string | |
| """ | |
| fcurves, error = get_selected_rotation_fcurves(context) | |
| if not fcurves or len(fcurves) != 3: | |
| return None, None, error | |
| fcu_keyframes = [get_selected_fcu_keyframe_numbers(fcu) for fcu in fcurves] | |
| if fcu_keyframes[0] != fcu_keyframes[1] or fcu_keyframes[1] != fcu_keyframes[2]: | |
| # XXX warn | |
| print("All 3 rotation angles need to be keyframed together") | |
| return None, None, "All 3 rotation angles need to be keyframed together on every keyframe" | |
| keyframes = sorted(set(itertools.chain.from_iterable([get_selected_fcu_keyframe_numbers(fcu) for fcu in fcurves]))) | |
| res = [] | |
| for keyframe in keyframes: | |
| euler = Euler([fcurve.evaluate(keyframe) for fcurve in fcurves], 'XYZ') | |
| res += [{ | |
| "key": keyframe, | |
| "rotation_euler": euler, | |
| }] | |
| return res, fcurves, None | |
| def refresh_fcurve_editor(context): | |
| """ | |
| Execute a meaningless command on F-Curve Editor which has the effect of | |
| refreshing the graph. | |
| From: http://blender.stackexchange.com/questions/7261/refresh-an-f-curve-with-python-after-changing-extrapolation-mode | |
| XXX Sadly selects all the values. | |
| """ | |
| old_area_type = context.area.type | |
| context.area.type = 'GRAPH_EDITOR' | |
| bpy.ops.graph.clean(threshold=0) | |
| context.area.type = old_area_type | |
| ################################################# | |
| # noinspection PyPep8Naming | |
| class GRAPH_OT_EulerFilter(bpy.types.Operator): | |
| """Filter euler rotations to remove danger of gimbal lock. | |
| """ | |
| bl_idname = "graph.euler_filter" | |
| bl_label = "Euler Filter" | |
| bl_description = "Filter euler rotations to remove danger of gimbal lock" | |
| bl_options = {'REGISTER', 'UNDO'} | |
| bl_space_type = 'GRAPH_EDITOR' | |
| bl_region_type = 'UI' | |
| @classmethod | |
| def poll(cls, context): | |
| fcus = get_selected_rotation_fcurves(context) | |
| return fcus | |
| def execute(self, context): | |
| kfs, fcus, error = get_selected_rotation_keyframes(context) | |
| if not kfs: | |
| self.report({'ERROR'}, error) | |
| return {'CANCELLED'} | |
| bone = get_bone_from_fcurve(context.active_object, fcus[0]) | |
| efs = euler_filter(kfs, bone.rotation_mode) | |
| for i in range(len(efs)): | |
| e = efs[i]["rotation_euler"] | |
| frame = efs[i]["key"] | |
| # p = kfs[i]["rotation_euler"] | |
| # print("%s -> %s" % (euler_to_string(p), euler_to_string(e))) | |
| for i in range(3): | |
| fcus[i].keyframe_points.insert(frame=frame, value=e[i]) | |
| refresh_fcurve_editor(context) | |
| return {'FINISHED'} | |
| ################################################# | |
| def register(): | |
| bpy.utils.register_class(GRAPH_OT_EulerFilter) | |
| def unregister(): | |
| bpy.utils.unregister_class(GRAPH_OT_EulerFilter) | |
| if __name__ == "__main__": | |
| register() | |
| ################################################# | |
| def test(): | |
| def get_euler_keyframes(action, bone=None): | |
| if bone: | |
| data_path = bone.path_from_id() + ".rotation_euler" | |
| rotation_mode = bone.rotation_mode | |
| else: | |
| rotation_mode = "XYZ" | |
| data_path = "rotation_euler" | |
| fcurves = [get_fcurve_for_data_path(action, data_path, i) for i in range(0, 3)] | |
| keyframes = sorted(set(itertools.chain.from_iterable([get_fcu_keyframe_numbers(fcu) for fcu in fcurves]))) | |
| res = [] | |
| for keyframe in keyframes: | |
| euler = Euler([fcurve.evaluate(keyframe) for fcurve in fcurves], rotation_mode) | |
| res += [{ | |
| "key": keyframe, | |
| "rotation_euler": euler, | |
| }] | |
| return res | |
| def get_fcurve_for_data_path(action, data_path, index): | |
| for fcu in action.fcurves: | |
| if fcu.data_path == data_path and fcu.array_index == index: | |
| return fcu | |
| return action.fcurves.new(data_path, index=index) | |
| scene = bpy.context.scene | |
| scene.frame_current = 1 | |
| (scene.frame_start, scene.frame_end) | |
| action = bpy.data.actions["Action"] | |
| hector = bpy.data.objects['Hector_RIG_proxy'] | |
| head = hector.pose.bones['Head_CTRL'] | |
| kfs = get_euler_keyframes(action, head) | |
| efs = euler_filter(kfs) | |
| for i in range(len(efs)): | |
| e = efs[i]["rotation_euler"] | |
| p = kfs[i]["rotation_euler"] | |
| print("%s -> %s" % (euler_to_string(p), euler_to_string(e))) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment