# SPDX-License-Identifier: GPL-2.0-or-later ''' Quat/Euler Rotation Mode Converter v0.1 This script/addon: - Changes (pose) bone rotation mode - Converts keyframes from one rotation mode to another - Creates fcurves/keyframes in target rotation mode - Deletes previous fcurves/keyframes. - Converts multiple bones - Converts multiple Actions TO-DO: - To convert object's rotation mode (already done in Mutant Bob script, but not done in this one. - To understand "EnumProperty" and write it well. - Code clean - ... GitHub: https://github.com/MarioMey/rotation_mode_addon/ BlenderArtist thread: http://blenderartists.org/forum/showthread.php?388197-Quat-Euler-Rotation-Mode-Converter Mutant Bob did the "hard code" of this script. Thanks him! blender.stackexchange.com/questions/40711/how-to-convert-quaternions-keyframes-to-euler-ones-in-several-actions ''' # bl_info = { # "name": "Rotation Mode Converter", # "author": "Mario Mey / Mutant Bob", # "version": (0, 2), # "blender": (2, 91, 0), # 'location': 'Pose Mode -> Header -> Pose -> Convert Rotation Modes', # "description": "Converts Animation between different rotation orders", # "warning": "", # "doc_url": "", # "tracker_url": "https://github.com/MarioMey/rotation_mode_addon/", # "category": "Animation", # } import bpy from bpy.props import ( EnumProperty, StringProperty, ) def get_or_create_fcurve(action, data_path, array_index=-1, group=None): for fc in action.fcurves: if fc.data_path == data_path and (array_index < 0 or fc.array_index == array_index): return fc fc = action.fcurves.new(data_path, index=array_index) fc.group = group return fc def add_keyframe_quat(action, quat, frame, bone_prefix, group): for i in range(len(quat)): fc = get_or_create_fcurve(action, bone_prefix + "rotation_quaternion", i, group) pos = len(fc.keyframe_points) fc.keyframe_points.add(1) fc.keyframe_points[pos].co = [frame, quat[i]] fc.update() def add_keyframe_euler(action, euler, frame, bone_prefix, group): for i in range(len(euler)): fc = get_or_create_fcurve(action, bone_prefix + "rotation_euler", i, group) pos = len(fc.keyframe_points) fc.keyframe_points.add(1) fc.keyframe_points[pos].co = [frame, euler[i]] fc.update() def frames_matching(action, data_path): frames = set() for fc in action.fcurves: if fc.data_path == data_path: fri = [kp.co[0] for kp in fc.keyframe_points] frames.update(fri) return frames def group_qe(obj, action, bone, bone_prefix, order): """Converts only one group/bone in one action - Quat to euler.""" pose_bone = bone data_path = bone_prefix + "rotation_quaternion" frames = frames_matching(action, data_path) group = action.groups[bone.name] for fr in frames: quat = bone.rotation_quaternion.copy() for fc in action.fcurves: if fc.data_path == data_path: quat[fc.array_index] = fc.evaluate(fr) euler = quat.to_euler(order) add_keyframe_euler(action, euler, fr, bone_prefix, group) bone.rotation_mode = order def group_eq(obj, action, bone, bone_prefix, order): """Converts only one group/bone in one action - Euler to Quat.""" pose_bone = bone data_path = bone_prefix + "rotation_euler" frames = frames_matching(action, data_path) group = action.groups[bone.name] for fr in frames: euler = bone.rotation_euler.copy() for fc in action.fcurves: if fc.data_path == data_path: euler[fc.array_index] = fc.evaluate(fr) quat = euler.to_quaternion() add_keyframe_quat(action, quat, fr, bone_prefix, group) bone.rotation_mode = order def convert_curves_of_bone_in_action(obj, action, bone, order): """Convert given bone's curves in given action to given rotation order.""" to_euler = False bone_prefix = '' for fcurve in action.fcurves: if fcurve.group.name == bone.name: # If To-Euler conversion if order != 'QUATERNION': if fcurve.data_path.endswith('rotation_quaternion'): to_euler = True bone_prefix = fcurve.data_path[:-len('rotation_quaternion')] break # If To-Quat conversion else: if fcurve.data_path.endswith('rotation_euler'): to_euler = True bone_prefix = fcurve.data_path[:-len('rotation_euler')] break # If To-Euler conversion if to_euler and order != 'QUATERNION': # Converts the group/bone from Quat to Euler group_qe(obj, action, bone, bone_prefix, order) # Removes quaternion fcurves for key in action.fcurves: if key.data_path == 'pose.bones["' + bone.name + '"].rotation_quaternion': action.fcurves.remove(key) # If To-Quat conversion elif to_euler: # Converts the group/bone from Euler to Quat group_eq(obj, action, bone, bone_prefix, order) # Removes euler fcurves for key in action.fcurves: if key.data_path == 'pose.bones["' + bone.name + '"].rotation_euler': action.fcurves.remove(key) # Changes rotation mode to new one bone.rotation_mode = order class POSE_OT_convert_rotation(bpy.types.Operator): bl_label = 'Convert Rotation Modes' bl_idname = 'pose.convert_rotation' bl_description = 'Convert animation from any rotation mode to any other' bl_options = {'REGISTER', 'UNDO'} # Properties. target_rotation_mode: EnumProperty( items=[ ('QUATERNION', 'Quaternion', 'Quaternion'), ('XYZ', 'XYZ', 'XYZ Euler'), ('XZY', 'XZY', 'XZY Euler'), ('YXZ', 'YXZ', 'YXZ Euler'), ('YZX', 'YZX', 'YZX Euler'), ('ZXY', 'ZXY', 'ZXY Euler'), ('ZYX', 'ZYX', 'ZYX Euler') ], name='Convert To', description="The target rotation mode", default='QUATERNION', ) affected_bones: EnumProperty( name="Affected Bones", items=[ ('SELECT', 'Selected', 'Selected'), ('ALL', 'All', 'All'), ], description="Which bones to affect", default='SELECT', ) affected_actions: EnumProperty( name="Affected Action", items=[ ('SINGLE', 'Single', 'Single'), ('ALL', 'All', 'All'), ], description="Which Actions to affect", default='SINGLE', ) selected_action: StringProperty(name="Action") def invoke(self, context, event): ob = context.object if ob and ob.type=='ARMATURE' and ob.animation_data and ob.animation_data.action: self.selected_action = context.object.animation_data.action.name else: self.affected_actions = 'ALL' wm = context.window_manager return wm.invoke_props_dialog(self) def draw(self, context): layout = self.layout layout.use_property_split = True layout.use_property_decorate = False layout.row().prop(self, 'affected_bones', expand=True) layout.row().prop(self, 'affected_actions', expand=True) if self.affected_actions=='SINGLE': layout.prop_search(self, 'selected_action', bpy.data, 'actions') layout.prop(self, 'target_rotation_mode') def execute(self, context): obj = context.active_object actions = [bpy.data.actions.get(self.selected_action)] pose_bones = context.selected_pose_bones if self.affected_bones=='ALL': pose_bones = obj.pose.bones if self.affected_actions=='ALL': actions = bpy.data.actions for action in actions: for pb in pose_bones: convert_curves_of_bone_in_action(obj, action, pb, self.target_rotation_mode) return {'FINISHED'} def draw_convert_rotation(self, context): self.layout.separator() self.layout.operator(POSE_OT_convert_rotation.bl_idname) classes = [ POSE_OT_convert_rotation ] def register(): from bpy.utils import register_class # Classes. for cls in classes: register_class(cls) bpy.types.VIEW3D_MT_pose.append(draw_convert_rotation) def unregister(): from bpy.utils import unregister_class # Classes. for cls in classes: unregister_class(cls) bpy.types.VIEW3D_MT_pose.remove(draw_convert_rotation)