1 # SPDX-License-Identifier: GPL-2.0-or-later
4 Quat/Euler Rotation Mode Converter v0.1
7 - Changes (pose) bone rotation mode
8 - Converts keyframes from one rotation mode to another
9 - Creates fcurves/keyframes in target rotation mode
10 - Deletes previous fcurves/keyframes.
11 - Converts multiple bones
12 - Converts multiple Actions
15 - To convert object's rotation mode (already done in Mutant Bob script,
16 but not done in this one.
17 - To understand "EnumProperty" and write it well.
21 GitHub: https://github.com/MarioMey/rotation_mode_addon/
22 BlenderArtist thread: http://blenderartists.org/forum/showthread.php?388197-Quat-Euler-Rotation-Mode-Converter
24 Mutant Bob did the "hard code" of this script. Thanks him!
25 blender.stackexchange.com/questions/40711/how-to-convert-quaternions-keyframes-to-euler-ones-in-several-actions
31 # "name": "Rotation Mode Converter",
32 # "author": "Mario Mey / Mutant Bob",
34 # "blender": (2, 91, 0),
35 # 'location': 'Pose Mode -> Header -> Pose -> Convert Rotation Modes',
36 # "description": "Converts Animation between different rotation orders",
39 # "tracker_url": "https://github.com/MarioMey/rotation_mode_addon/",
40 # "category": "Animation",
44 from bpy
.props
import (
50 def get_or_create_fcurve(action
, data_path
, array_index
=-1, group
=None):
51 for fc
in action
.fcurves
:
52 if fc
.data_path
== data_path
and (array_index
< 0 or fc
.array_index
== array_index
):
55 fc
= action
.fcurves
.new(data_path
, index
=array_index
)
59 def add_keyframe_quat(action
, quat
, frame
, bone_prefix
, group
):
60 for i
in range(len(quat
)):
61 fc
= get_or_create_fcurve(action
, bone_prefix
+ "rotation_quaternion", i
, group
)
62 pos
= len(fc
.keyframe_points
)
63 fc
.keyframe_points
.add(1)
64 fc
.keyframe_points
[pos
].co
= [frame
, quat
[i
]]
67 def add_keyframe_euler(action
, euler
, frame
, bone_prefix
, group
):
68 for i
in range(len(euler
)):
69 fc
= get_or_create_fcurve(action
, bone_prefix
+ "rotation_euler", i
, group
)
70 pos
= len(fc
.keyframe_points
)
71 fc
.keyframe_points
.add(1)
72 fc
.keyframe_points
[pos
].co
= [frame
, euler
[i
]]
75 def frames_matching(action
, data_path
):
77 for fc
in action
.fcurves
:
78 if fc
.data_path
== data_path
:
79 fri
= [kp
.co
[0] for kp
in fc
.keyframe_points
]
83 def group_qe(obj
, action
, bone
, bone_prefix
, order
):
84 """Converts only one group/bone in one action - Quat to euler."""
86 data_path
= bone_prefix
+ "rotation_quaternion"
87 frames
= frames_matching(action
, data_path
)
88 group
= action
.groups
[bone
.name
]
91 quat
= bone
.rotation_quaternion
.copy()
92 for fc
in action
.fcurves
:
93 if fc
.data_path
== data_path
:
94 quat
[fc
.array_index
] = fc
.evaluate(fr
)
95 euler
= quat
.to_euler(order
)
97 add_keyframe_euler(action
, euler
, fr
, bone_prefix
, group
)
98 bone
.rotation_mode
= order
100 def group_eq(obj
, action
, bone
, bone_prefix
, order
):
101 """Converts only one group/bone in one action - Euler to Quat."""
103 data_path
= bone_prefix
+ "rotation_euler"
104 frames
= frames_matching(action
, data_path
)
105 group
= action
.groups
[bone
.name
]
108 euler
= bone
.rotation_euler
.copy()
109 for fc
in action
.fcurves
:
110 if fc
.data_path
== data_path
:
111 euler
[fc
.array_index
] = fc
.evaluate(fr
)
112 quat
= euler
.to_quaternion()
114 add_keyframe_quat(action
, quat
, fr
, bone_prefix
, group
)
115 bone
.rotation_mode
= order
117 def convert_curves_of_bone_in_action(obj
, action
, bone
, order
):
118 """Convert given bone's curves in given action to given rotation order."""
122 for fcurve
in action
.fcurves
:
123 if fcurve
.group
.name
== bone
.name
:
125 # If To-Euler conversion
126 if order
!= 'QUATERNION':
127 if fcurve
.data_path
.endswith('rotation_quaternion'):
129 bone_prefix
= fcurve
.data_path
[:-len('rotation_quaternion')]
132 # If To-Quat conversion
134 if fcurve
.data_path
.endswith('rotation_euler'):
136 bone_prefix
= fcurve
.data_path
[:-len('rotation_euler')]
139 # If To-Euler conversion
140 if to_euler
and order
!= 'QUATERNION':
141 # Converts the group/bone from Quat to Euler
142 group_qe(obj
, action
, bone
, bone_prefix
, order
)
144 # Removes quaternion fcurves
145 for key
in action
.fcurves
:
146 if key
.data_path
== 'pose.bones["' + bone
.name
+ '"].rotation_quaternion':
147 action
.fcurves
.remove(key
)
149 # If To-Quat conversion
151 # Converts the group/bone from Euler to Quat
152 group_eq(obj
, action
, bone
, bone_prefix
, order
)
154 # Removes euler fcurves
155 for key
in action
.fcurves
:
156 if key
.data_path
== 'pose.bones["' + bone
.name
+ '"].rotation_euler':
157 action
.fcurves
.remove(key
)
159 # Changes rotation mode to new one
160 bone
.rotation_mode
= order
162 class POSE_OT_convert_rotation(bpy
.types
.Operator
):
163 bl_label
= 'Convert Rotation Modes'
164 bl_idname
= 'pose.convert_rotation'
165 bl_description
= 'Convert animation from any rotation mode to any other'
166 bl_options
= {'REGISTER', 'UNDO'}
169 target_rotation_mode
: EnumProperty(
171 ('QUATERNION', 'Quaternion', 'Quaternion'),
172 ('XYZ', 'XYZ', 'XYZ Euler'),
173 ('XZY', 'XZY', 'XZY Euler'),
174 ('YXZ', 'YXZ', 'YXZ Euler'),
175 ('YZX', 'YZX', 'YZX Euler'),
176 ('ZXY', 'ZXY', 'ZXY Euler'),
177 ('ZYX', 'ZYX', 'ZYX Euler')
180 description
="The target rotation mode",
181 default
='QUATERNION',
183 affected_bones
: EnumProperty(
184 name
="Affected Bones",
186 ('SELECT', 'Selected', 'Selected'),
187 ('ALL', 'All', 'All'),
189 description
="Which bones to affect",
192 affected_actions
: EnumProperty(
193 name
="Affected Action",
195 ('SINGLE', 'Single', 'Single'),
196 ('ALL', 'All', 'All'),
198 description
="Which Actions to affect",
201 selected_action
: StringProperty(name
="Action")
203 def invoke(self
, context
, event
):
205 if ob
and ob
.type=='ARMATURE' and ob
.animation_data
and ob
.animation_data
.action
:
206 self
.selected_action
= context
.object.animation_data
.action
.name
208 self
.affected_actions
= 'ALL'
210 wm
= context
.window_manager
211 return wm
.invoke_props_dialog(self
)
213 def draw(self
, context
):
215 layout
.use_property_split
= True
216 layout
.use_property_decorate
= False
218 layout
.row().prop(self
, 'affected_bones', expand
=True)
219 layout
.row().prop(self
, 'affected_actions', expand
=True)
220 if self
.affected_actions
=='SINGLE':
221 layout
.prop_search(self
, 'selected_action', bpy
.data
, 'actions')
222 layout
.prop(self
, 'target_rotation_mode')
224 def execute(self
, context
):
225 obj
= context
.active_object
227 actions
= [bpy
.data
.actions
.get(self
.selected_action
)]
228 pose_bones
= context
.selected_pose_bones
229 if self
.affected_bones
=='ALL':
230 pose_bones
= obj
.pose
.bones
231 if self
.affected_actions
=='ALL':
232 actions
= bpy
.data
.actions
234 for action
in actions
:
235 for pb
in pose_bones
:
236 convert_curves_of_bone_in_action(obj
, action
, pb
, self
.target_rotation_mode
)
240 def draw_convert_rotation(self
, context
):
241 self
.layout
.separator()
242 self
.layout
.operator(POSE_OT_convert_rotation
.bl_idname
)
245 POSE_OT_convert_rotation
249 from bpy
.utils
import register_class
255 bpy
.types
.VIEW3D_MT_pose
.append(draw_convert_rotation
)
258 from bpy
.utils
import unregister_class
262 unregister_class(cls
)
264 bpy
.types
.VIEW3D_MT_pose
.remove(draw_convert_rotation
)