Merge branch 'blender-v4.0-release'
[blender-addons.git] / rigify / rot_mode.py
blobe3493eed258ab8b6f8a845f296c5c54853b18240
1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 """
6 Quaternion/Euler Rotation Mode Converter v0.1
8 This script/addon:
9 - Changes (pose) bone rotation mode
10 - Converts keyframes from one rotation mode to another
11 - Creates fcurves/keyframes in target rotation mode
12 - Deletes previous fcurves/keyframes.
13 - Converts multiple bones
14 - Converts multiple Actions
16 TO-DO:
17 - To convert object's rotation mode (already done in Mutant Bob script,
18 but not done in this one.)
19 - To understand "EnumProperty" and write it well.
20 - Code clean
21 - ...
23 GitHub: https://github.com/MarioMey/rotation_mode_addon/
24 BlenderArtist thread: https://blenderartists.org/forum/showthread.php?388197-Quat-Euler-Rotation-Mode-Converter
26 Mutant Bob did the "hard code" of this script. Thanks him!
27 blender.stackexchange.com/questions/40711/how-to-convert-quaternions-keyframes-to-euler-ones-in-several-actions
30 """
32 # bl_info = {
33 # "name": "Rotation Mode Converter",
34 # "author": "Mario Mey / Mutant Bob",
35 # "version": (0, 2),
36 # "blender": (2, 91, 0),
37 # 'location': 'Pose Mode -> Header -> Pose -> Convert Rotation Modes',
38 # "description": "Converts Animation between different rotation orders",
39 # "warning": "",
40 # "doc_url": "",
41 # "tracker_url": "https://github.com/MarioMey/rotation_mode_addon/",
42 # "category": "Animation",
43 # }
45 import bpy
46 from bpy.props import (
47 EnumProperty,
48 StringProperty,
52 def get_or_create_fcurve(action, data_path, array_index=-1, group=None):
53 for fc in action.fcurves:
54 if fc.data_path == data_path and (array_index < 0 or fc.array_index == array_index):
55 return fc
57 fc = action.fcurves.new(data_path, index=array_index)
58 fc.group = group
59 return fc
62 def add_keyframe_quat(action, quat, frame, bone_prefix, group):
63 for i in range(len(quat)):
64 fc = get_or_create_fcurve(action, bone_prefix + "rotation_quaternion", i, group)
65 pos = len(fc.keyframe_points)
66 fc.keyframe_points.add(1)
67 fc.keyframe_points[pos].co = [frame, quat[i]]
68 fc.update()
71 def add_keyframe_euler(action, euler, frame, bone_prefix, group):
72 for i in range(len(euler)):
73 fc = get_or_create_fcurve(action, bone_prefix + "rotation_euler", i, group)
74 pos = len(fc.keyframe_points)
75 fc.keyframe_points.add(1)
76 fc.keyframe_points[pos].co = [frame, euler[i]]
77 fc.update()
80 def frames_matching(action, data_path):
81 frames = set()
82 for fc in action.fcurves:
83 if fc.data_path == data_path:
84 fri = [kp.co[0] for kp in fc.keyframe_points]
85 frames.update(fri)
86 return frames
89 def group_qe(_obj, action, bone, bone_prefix, order):
90 """Converts only one group/bone in one action - Quaternion to euler."""
91 # pose_bone = bone
92 data_path = bone_prefix + "rotation_quaternion"
93 frames = frames_matching(action, data_path)
94 group = action.groups[bone.name]
96 for fr in frames:
97 quat = bone.rotation_quaternion.copy()
98 for fc in action.fcurves:
99 if fc.data_path == data_path:
100 quat[fc.array_index] = fc.evaluate(fr)
101 euler = quat.to_euler(order)
103 add_keyframe_euler(action, euler, fr, bone_prefix, group)
104 bone.rotation_mode = order
107 def group_eq(_obj, action, bone, bone_prefix, order):
108 """Converts only one group/bone in one action - Euler to Quaternion."""
109 # pose_bone = bone
110 data_path = bone_prefix + "rotation_euler"
111 frames = frames_matching(action, data_path)
112 group = action.groups[bone.name]
114 for fr in frames:
115 euler = bone.rotation_euler.copy()
116 for fc in action.fcurves:
117 if fc.data_path == data_path:
118 euler[fc.array_index] = fc.evaluate(fr)
119 quat = euler.to_quaternion()
121 add_keyframe_quat(action, quat, fr, bone_prefix, group)
122 bone.rotation_mode = order
125 def convert_curves_of_bone_in_action(obj, action, bone, order):
126 """Convert given bone's curves in given action to given rotation order."""
127 to_euler = False
128 bone_prefix = ''
130 for fcurve in action.fcurves:
131 if fcurve.group.name == bone.name:
133 # If To-Euler conversion
134 if order != 'QUATERNION':
135 if fcurve.data_path.endswith('rotation_quaternion'):
136 to_euler = True
137 bone_prefix = fcurve.data_path[:-len('rotation_quaternion')]
138 break
140 # If To-Quaternion conversion
141 else:
142 if fcurve.data_path.endswith('rotation_euler'):
143 to_euler = True
144 bone_prefix = fcurve.data_path[:-len('rotation_euler')]
145 break
147 # If To-Euler conversion
148 if to_euler and order != 'QUATERNION':
149 # Converts the group/bone from Quaternion to Euler
150 group_qe(obj, action, bone, bone_prefix, order)
152 # Removes quaternion fcurves
153 for key in action.fcurves:
154 if key.data_path == 'pose.bones["' + bone.name + '"].rotation_quaternion':
155 action.fcurves.remove(key)
157 # If To-Quaternion conversion
158 elif to_euler:
159 # Converts the group/bone from Euler to Quaternion
160 group_eq(obj, action, bone, bone_prefix, order)
162 # Removes euler fcurves
163 for key in action.fcurves:
164 if key.data_path == 'pose.bones["' + bone.name + '"].rotation_euler':
165 action.fcurves.remove(key)
167 # Changes rotation mode to new one
168 bone.rotation_mode = order
171 # noinspection PyPep8Naming
172 class POSE_OT_convert_rotation(bpy.types.Operator):
173 bl_label = 'Convert Rotation Modes'
174 bl_idname = 'pose.convert_rotation'
175 bl_description = 'Convert animation from any rotation mode to any other'
176 bl_options = {'REGISTER', 'UNDO'}
178 # Properties.
179 target_rotation_mode: EnumProperty(
180 items=[
181 ('QUATERNION', 'Quaternion', 'Quaternion'),
182 ('XYZ', 'XYZ', 'XYZ Euler'),
183 ('XZY', 'XZY', 'XZY Euler'),
184 ('YXZ', 'YXZ', 'YXZ Euler'),
185 ('YZX', 'YZX', 'YZX Euler'),
186 ('ZXY', 'ZXY', 'ZXY Euler'),
187 ('ZYX', 'ZYX', 'ZYX Euler')
189 name='Convert To',
190 description="The target rotation mode",
191 default='QUATERNION',
193 affected_bones: EnumProperty(
194 name="Affected Bones",
195 items=[
196 ('SELECT', 'Selected', 'Selected'),
197 ('ALL', 'All', 'All'),
199 description="Which bones to affect",
200 default='SELECT',
202 affected_actions: EnumProperty(
203 name="Affected Action",
204 items=[
205 ('SINGLE', 'Single', 'Single'),
206 ('ALL', 'All', 'All'),
208 description="Which Actions to affect",
209 default='SINGLE',
211 selected_action: StringProperty(name="Action")
213 def invoke(self, context, event):
214 ob = context.object
215 if ob and ob.type == 'ARMATURE' and ob.animation_data and ob.animation_data.action:
216 self.selected_action = context.object.animation_data.action.name
217 else:
218 self.affected_actions = 'ALL'
220 wm = context.window_manager
221 return wm.invoke_props_dialog(self)
223 def draw(self, context):
224 layout = self.layout
225 layout.use_property_split = True
226 layout.use_property_decorate = False
228 layout.row().prop(self, 'affected_bones', expand=True)
229 layout.row().prop(self, 'affected_actions', expand=True)
230 if self.affected_actions == 'SINGLE':
231 layout.prop_search(self, 'selected_action', bpy.data, 'actions')
232 layout.prop(self, 'target_rotation_mode')
234 def execute(self, context):
235 obj = context.active_object
237 actions = [bpy.data.actions.get(self.selected_action)]
238 pose_bones = context.selected_pose_bones
239 if self.affected_bones == 'ALL':
240 pose_bones = obj.pose.bones
241 if self.affected_actions == 'ALL':
242 actions = bpy.data.actions
244 for action in actions:
245 for pb in pose_bones:
246 convert_curves_of_bone_in_action(obj, action, pb, self.target_rotation_mode)
248 return {'FINISHED'}
251 def draw_convert_rotation(self, _context):
252 self.layout.separator()
253 self.layout.operator(POSE_OT_convert_rotation.bl_idname)
256 classes = [
257 POSE_OT_convert_rotation
261 def register():
262 from bpy.utils import register_class
264 # Classes.
265 for cls in classes:
266 register_class(cls)
268 bpy.types.VIEW3D_MT_pose.append(draw_convert_rotation)
271 def unregister():
272 from bpy.utils import unregister_class
274 # Classes.
275 for cls in classes:
276 unregister_class(cls)
278 bpy.types.VIEW3D_MT_pose.remove(draw_convert_rotation)