1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 Quaternion/Euler Rotation Mode Converter v0.1
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
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.
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
33 # "name": "Rotation Mode Converter",
34 # "author": "Mario Mey / Mutant Bob",
36 # "blender": (2, 91, 0),
37 # 'location': 'Pose Mode -> Header -> Pose -> Convert Rotation Modes',
38 # "description": "Converts Animation between different rotation orders",
41 # "tracker_url": "https://github.com/MarioMey/rotation_mode_addon/",
42 # "category": "Animation",
46 from bpy
.props
import (
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
):
57 fc
= action
.fcurves
.new(data_path
, index
=array_index
)
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
]]
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
]]
80 def frames_matching(action
, data_path
):
82 for fc
in action
.fcurves
:
83 if fc
.data_path
== data_path
:
84 fri
= [kp
.co
[0] for kp
in fc
.keyframe_points
]
89 def group_qe(_obj
, action
, bone
, bone_prefix
, order
):
90 """Converts only one group/bone in one action - Quaternion to euler."""
92 data_path
= bone_prefix
+ "rotation_quaternion"
93 frames
= frames_matching(action
, data_path
)
94 group
= action
.groups
[bone
.name
]
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."""
110 data_path
= bone_prefix
+ "rotation_euler"
111 frames
= frames_matching(action
, data_path
)
112 group
= action
.groups
[bone
.name
]
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."""
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'):
137 bone_prefix
= fcurve
.data_path
[:-len('rotation_quaternion')]
140 # If To-Quaternion conversion
142 if fcurve
.data_path
.endswith('rotation_euler'):
144 bone_prefix
= fcurve
.data_path
[:-len('rotation_euler')]
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
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'}
179 target_rotation_mode
: EnumProperty(
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')
190 description
="The target rotation mode",
191 default
='QUATERNION',
193 affected_bones
: EnumProperty(
194 name
="Affected Bones",
196 ('SELECT', 'Selected', 'Selected'),
197 ('ALL', 'All', 'All'),
199 description
="Which bones to affect",
202 affected_actions
: EnumProperty(
203 name
="Affected Action",
205 ('SINGLE', 'Single', 'Single'),
206 ('ALL', 'All', 'All'),
208 description
="Which Actions to affect",
211 selected_action
: StringProperty(name
="Action")
213 def invoke(self
, context
, event
):
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
218 self
.affected_actions
= 'ALL'
220 wm
= context
.window_manager
221 return wm
.invoke_props_dialog(self
)
223 def draw(self
, context
):
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
)
251 def draw_convert_rotation(self
, _context
):
252 self
.layout
.separator()
253 self
.layout
.operator(POSE_OT_convert_rotation
.bl_idname
)
257 POSE_OT_convert_rotation
262 from bpy
.utils
import register_class
268 bpy
.types
.VIEW3D_MT_pose
.append(draw_convert_rotation
)
272 from bpy
.utils
import unregister_class
276 unregister_class(cls
)
278 bpy
.types
.VIEW3D_MT_pose
.remove(draw_convert_rotation
)