1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
22 from mathutils
import Vector
, Matrix
23 from math
import radians
24 from bpy_extras
.anim_utils
import bake_action
27 def hasIKConstraint(pose_bone
):
28 #utility function / predicate, returns True if given bone has IK constraint
29 ik
= [constraint
for constraint
in pose_bone
.constraints
if constraint
.type == "IK"]
36 def createDictionary(perf_arm
, end_arm
):
38 for end_bone
in end_arm
.bones
:
39 for mapping
in end_bone
.reverseMap
:
40 end_bone
.reverseMap
.remove(0)
42 for perf_bone
in perf_arm
.bones
:
43 #find its match and add perf_bone to the match's mapping
45 end_bone
= end_arm
.bones
[perf_bone
.map]
46 newMap
= end_bone
.reverseMap
.add()
47 newMap
.name
= perf_bone
.name
48 end_bone
.foot
= perf_bone
.foot
50 #root is the root of the enduser
51 root
= end_arm
.bones
[0].name
52 feetBones
= [bone
.name
for bone
in perf_arm
.bones
if bone
.foot
]
53 return feetBones
, root
56 def loadMapping(perf_arm
, end_arm
):
57 for end_bone
in end_arm
.bones
:
58 #find its match and add perf_bone to the match's mapping
59 if end_bone
.reverseMap
:
60 for perf_bone
in end_bone
.reverseMap
:
61 perf_arm
.bones
[perf_bone
.name
].map = end_bone
.name
63 #creation of intermediate armature
64 # the intermediate armature has the hiearchy of the end user,
65 # does not have rotation inheritence
66 # and bone roll is identical to the performer
67 # its purpose is to copy over the rotations
68 # easily while concentrating on the hierarchy changes
71 def createIntermediate(performer_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
):
72 #creates and keyframes an empty with its location
73 #the original position of the tail bone
74 #useful for storing the important data in the original motion
75 #i.e. using this empty to IK the chain to that pos / DEBUG
77 #Simple 1to1 retarget of a bone
78 def singleBoneRetarget(inter_bone
, perf_bone
):
79 perf_world_rotation
= perf_bone
.matrix
80 inter_world_base_rotation
= inter_bone
.bone
.matrix_local
81 inter_world_base_inv
= inter_world_base_rotation
.inverted()
82 bake_matrix
= (inter_world_base_inv
.to_3x3() * perf_world_rotation
.to_3x3())
83 return bake_matrix
.to_4x4()
85 #uses 1to1 and interpolation/averaging to match many to 1 retarget
86 def manyPerfToSingleInterRetarget(inter_bone
, performer_bones_s
):
87 retarget_matrices
= [singleBoneRetarget(inter_bone
, perf_bone
) for perf_bone
in performer_bones_s
]
88 lerp_matrix
= Matrix()
89 for i
in range(len(retarget_matrices
) - 1):
90 first_mat
= retarget_matrices
[i
]
91 next_mat
= retarget_matrices
[i
+ 1]
92 lerp_matrix
= first_mat
.lerp(next_mat
, 0.5)
95 #determines the type of hierachy change needed and calls the
97 def retargetPerfToInter(inter_bone
):
98 if inter_bone
.bone
.reverseMap
:
99 perf_bone_name
= inter_bone
.bone
.reverseMap
100 # 1 to many not supported yet
101 # then its either a many to 1 or 1 to 1
102 if len(perf_bone_name
) > 1:
103 performer_bones_s
= [performer_bones
[map.name
] for map in perf_bone_name
]
104 #we need to map several performance bone to a single
105 inter_bone
.matrix_basis
= manyPerfToSingleInterRetarget(inter_bone
, performer_bones_s
)
107 perf_bone
= performer_bones
[perf_bone_name
[0].name
]
108 inter_bone
.matrix_basis
= singleBoneRetarget(inter_bone
, perf_bone
)
109 #Some bones have incorrect roll on the source armature, and need to be marked for fixing
110 if inter_bone
.bone
.twistFix
:
111 inter_bone
.matrix_basis
*= Matrix
.Rotation(radians(180), 4, "Y")
112 rot_mode
= inter_bone
.rotation_mode
113 if rot_mode
== "QUATERNION":
114 inter_bone
.keyframe_insert("rotation_quaternion")
115 elif rot_mode
== "AXIS_ANGLE":
116 inter_bone
.keyframe_insert("rotation_axis_angle")
118 inter_bone
.keyframe_insert("rotation_euler")
120 #creates the intermediate armature object
121 inter_obj
= enduser_obj
.copy()
122 inter_obj
.data
= inter_obj
.data
.copy() # duplicate data
123 bpy
.context
.scene
.objects
.link(inter_obj
)
124 inter_obj
.name
= "intermediate"
125 bpy
.context
.scene
.objects
.active
= inter_obj
126 bpy
.ops
.object.mode_set(mode
='EDIT')
127 #add some temporary connecting bones in case end user bones are not connected to their parents
129 print("creating temp bones")
130 for bone
in inter_obj
.data
.edit_bones
:
131 if not bone
.use_connect
and bone
.parent
:
132 if inter_obj
.data
.bones
[bone
.parent
.name
].reverseMap
or inter_obj
.data
.bones
[bone
.name
].reverseMap
:
133 newBone
= inter_obj
.data
.edit_bones
.new("Temp")
134 newBone
.head
= bone
.parent
.tail
135 newBone
.tail
= bone
.head
136 newBone
.parent
= bone
.parent
137 bone
.parent
= newBone
138 bone
.use_connect
= True
139 newBone
.use_connect
= True
140 rollDict
[bone
.name
] = bone
.roll
143 print("retargeting to intermediate")
144 bpy
.ops
.object.mode_set(mode
="OBJECT")
145 inter_obj
.data
.name
= "inter_arm"
146 inter_arm
= inter_obj
.data
147 performer_bones
= performer_obj
.pose
.bones
148 inter_bones
= inter_obj
.pose
.bones
150 for inter_bone
in inter_bones
:
151 if inter_bone
.bone
.reverseMap
:
152 inter_bone
.bone
.use_inherit_rotation
= False
154 inter_bone
.bone
.use_inherit_rotation
= True
156 for t
in range(s_frame
, e_frame
, step
):
157 if (t
- s_frame
) % 10 == 0:
158 print("First pass: retargeting frame {0}/{1}".format(t
, e_frame
- s_frame
))
160 for bone
in inter_bones
:
161 retargetPerfToInter(bone
)
165 # this procedure copies the rotations over from the intermediate
166 # armature to the end user one.
167 # As the hierarchies are 1 to 1, this is a simple matter of
168 # copying the rotation, while keeping in mind bone roll, parenting, etc.
169 # TODO: Control Bones: If a certain bone is constrained in a way
170 # that its rotation is determined by another (a control bone)
171 # We should determine the right pos of the control bone.
172 # Scale: ? Should work but needs testing.
175 def retargetEnduser(inter_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
):
176 inter_bones
= inter_obj
.pose
.bones
177 end_bones
= enduser_obj
.pose
.bones
179 #Basic "visual baking" function, for transfering rotations from intermediate to end user
180 def bakeTransform(end_bone
):
181 src_bone
= inter_bones
[end_bone
.name
]
183 bake_matrix
= src_bone
.matrix
184 rest_matrix
= trg_bone
.bone
.matrix_local
186 if trg_bone
.parent
and trg_bone
.bone
.use_inherit_rotation
:
187 srcParent
= src_bone
.parent
188 if "Temp" in srcParent
.name
:
189 srcParent
= srcParent
.parent
190 parent_mat
= srcParent
.matrix
191 parent_rest
= trg_bone
.parent
.bone
.matrix_local
192 parent_rest_inv
= parent_rest
.inverted()
193 parent_mat_inv
= parent_mat
.inverted()
194 bake_matrix
= parent_mat_inv
* bake_matrix
195 rest_matrix
= parent_rest_inv
* rest_matrix
197 rest_matrix_inv
= rest_matrix
.inverted()
198 bake_matrix
= rest_matrix_inv
* bake_matrix
199 end_bone
.matrix_basis
= bake_matrix
200 rot_mode
= end_bone
.rotation_mode
201 if rot_mode
== "QUATERNION":
202 end_bone
.keyframe_insert("rotation_quaternion")
203 elif rot_mode
== "AXIS_ANGLE":
204 end_bone
.keyframe_insert("rotation_axis_angle")
206 end_bone
.keyframe_insert("rotation_euler")
207 if not end_bone
.bone
.use_connect
:
208 end_bone
.keyframe_insert("location")
210 for bone
in end_bone
.children
:
213 for t
in range(s_frame
, e_frame
, step
):
214 if (t
- s_frame
) % 10 == 0:
215 print("Second pass: retargeting frame {0}/{1}".format(t
, e_frame
- s_frame
))
217 end_bone
= end_bones
[root
]
218 end_bone
.location
= Vector((0, 0, 0))
219 end_bone
.keyframe_insert("location")
220 bakeTransform(end_bone
)
222 #recieves the performer feet bones as a variable
223 # by "feet" I mean those bones that have plants
224 # (they don't move, despite root moving) somewhere in the animation.
227 def copyTranslation(performer_obj
, enduser_obj
, perfFeet
, root
, s_frame
, e_frame
, scene
, enduser_obj_mat
):
229 perf_bones
= performer_obj
.pose
.bones
230 end_bones
= enduser_obj
.pose
.bones
232 perfRoot
= perf_bones
[0].name
233 endFeet
= [perf_bones
[perfBone
].bone
.map for perfBone
in perfFeet
]
234 locDictKeys
= perfFeet
+ endFeet
+ [perfRoot
]
237 return bone
.center
+ (bone
.vector
/ 2)
239 #Step 1 - we create a dict that contains these keys:
240 #(Performer) Hips, Feet
242 # where the values are their world position on each frame in range (s,e)
245 for key
in locDictKeys
:
248 for t
in range(scene
.frame_start
, scene
.frame_end
):
250 for bone
in perfFeet
:
251 locDict
[bone
].append(tailLoc(perf_bones
[bone
]))
252 locDict
[perfRoot
].append(tailLoc(perf_bones
[perfRoot
]))
254 locDict
[bone
].append(tailLoc(end_bones
[bone
]))
256 # now we take our locDict and analyze it.
257 # we need to derive all chains
259 def locDeriv(key
, t
):
261 return graph
[t
+ 1] - graph
[t
]
263 # now find the plant frames, where perfFeet don't move much
267 for i
in range(len(locDict
[key
]) - 1):
270 hipV
= locDeriv(perfRoot
, i
)
271 endV
= locDeriv(perf_bones
[key
].bone
.map, i
)
272 #this is a plant frame.
273 #lets see what the original hip delta is, and the corresponding
276 linearAvg
.append(hipV
.length
/ endV
.length
)
278 action_name
= performer_obj
.animation_data
.action
.name
279 #is there a stride_bone?
280 if "stride_bone" in bpy
.data
.objects
:
281 stride_action
= bpy
.data
.actions
.new("Stride Bone " + action_name
)
282 stride_action
.use_fake_user
= True
283 stride_bone
= enduser_obj
.parent
284 stride_bone
.animation_data
.action
= stride_action
286 bpy
.ops
.object.mode_set(mode
='OBJECT')
288 stride_bone
= bpy
.context
.active_object
289 stride_bone
.name
= "stride_bone"
290 stride_bone
.location
= enduser_obj_mat
.to_translation()
292 #determine the average change in scale needed
293 avg
= sum(linearAvg
) / len(linearAvg
)
296 scene
.frame_set(s_frame
)
297 initialPos
= (tailLoc(perf_bones
[perfRoot
]) / avg
)
298 for t
in range(s_frame
, e_frame
):
300 #calculate the new position, by dividing by the found ratio between performer and enduser
301 newTranslation
= (tailLoc(perf_bones
[perfRoot
]) / avg
)
302 stride_bone
.location
= enduser_obj_mat
* (newTranslation
- initialPos
)
303 stride_bone
.keyframe_insert("location")
304 stride_bone
.animation_data
.action
.name
= ("Stride Bone " + action_name
)
309 def IKRetarget(performer_obj
, enduser_obj
, s_frame
, e_frame
, scene
, step
):
310 end_bones
= enduser_obj
.pose
.bones
311 for pose_bone
in end_bones
:
312 ik_constraint
= hasIKConstraint(pose_bone
)
314 target_is_bone
= False
315 # set constraint target to corresponding empty if targetless,
316 # if not, keyframe current target to corresponding empty
317 perf_bone
= pose_bone
.bone
.reverseMap
[-1].name
318 bpy
.ops
.object.mode_set(mode
='EDIT')
319 orgLocTrg
= originalLocationTarget(pose_bone
, enduser_obj
)
320 bpy
.ops
.object.mode_set(mode
='OBJECT')
321 if not ik_constraint
.target
:
322 ik_constraint
.target
= enduser_obj
323 ik_constraint
.subtarget
= pose_bone
.name
+ "IK"
326 # There is a target now
327 if ik_constraint
.subtarget
:
328 target
= ik_constraint
.target
.pose
.bones
[ik_constraint
.subtarget
]
329 target
.bone
.use_local_location
= False
330 target_is_bone
= True
332 target
= ik_constraint
.target
334 # bake the correct locations for the ik target bones
335 for t
in range(s_frame
, e_frame
, step
):
338 final_loc
= pose_bone
.tail
- target
.bone
.matrix_local
.to_translation()
340 final_loc
= pose_bone
.tail
341 target
.location
= final_loc
342 target
.keyframe_insert("location")
343 ik_constraint
.mute
= False
344 scene
.frame_set(s_frame
)
345 bpy
.ops
.object.mode_set(mode
='OBJECT')
348 def turnOffIK(enduser_obj
):
349 end_bones
= enduser_obj
.pose
.bones
350 for pose_bone
in end_bones
:
351 ik_constraint
= hasIKConstraint(pose_bone
)
353 ik_constraint
.mute
= True
356 #copy the object matrixes and clear them (to be reinserted later)
357 def cleanAndStoreObjMat(performer_obj
, enduser_obj
):
358 perf_obj_mat
= performer_obj
.matrix_world
.copy()
359 enduser_obj_mat
= enduser_obj
.matrix_world
.copy()
361 performer_obj
.matrix_world
= zero_mat
362 enduser_obj
.matrix_world
= zero_mat
363 return perf_obj_mat
, enduser_obj_mat
366 #restore the object matrixes after parenting the auto generated IK empties
367 def restoreObjMat(performer_obj
, enduser_obj
, perf_obj_mat
, enduser_obj_mat
, stride_bone
, scene
, s_frame
):
368 pose_bones
= enduser_obj
.pose
.bones
369 for pose_bone
in pose_bones
:
370 if pose_bone
.name
+ "Org" in bpy
.data
.objects
:
371 empty
= bpy
.data
.objects
[pose_bone
.name
+ "Org"]
372 empty
.parent
= stride_bone
373 performer_obj
.matrix_world
= perf_obj_mat
374 enduser_obj
.parent
= stride_bone
375 scene
.frame_set(s_frame
)
376 enduser_obj_mat
= enduser_obj_mat
.to_3x3().to_4x4() * Matrix
.Translation(stride_bone
.matrix_world
.to_translation())
377 enduser_obj
.matrix_world
= enduser_obj_mat
380 #create (or return if exists) the related IK empty to the bone
381 def originalLocationTarget(end_bone
, enduser_obj
):
382 ik_bone
= hasIKConstraint(end_bone
).subtarget
384 print("Adding IK bones for: " + end_bone
.name
)
385 newBone
= enduser_obj
.data
.edit_bones
.new(end_bone
.name
+ "IK")
386 newBone
.head
= end_bone
.tail
387 newBone
.tail
= end_bone
.tail
+ Vector((0, 0.1, 0))
389 newBone
= enduser_obj
.pose
.bones
[ik_bone
]
393 #create the specified NLA setup for base animation, constraints and tweak layer.
394 def NLASystemInitialize(enduser_arm
, context
):
395 enduser_obj
= context
.active_object
396 NLATracks
= enduser_arm
.mocapNLATracks
[enduser_obj
.data
.active_mocap
]
397 name
= NLATracks
.name
398 anim_data
= enduser_obj
.animation_data
400 if ("Base " + name
) in bpy
.data
.actions
:
401 mocapAction
= bpy
.data
.actions
[("Base " + name
)]
403 print("That retargeted anim has no base action")
404 anim_data
.use_nla
= True
405 for track
in anim_data
.nla_tracks
:
406 anim_data
.nla_tracks
.remove(track
)
407 mocapTrack
= anim_data
.nla_tracks
.new()
408 mocapTrack
.name
= "Base " + name
409 NLATracks
.base_track
= mocapTrack
.name
410 mocapStrip
= mocapTrack
.strips
.new("Base " + name
, s_frame
, mocapAction
)
411 constraintTrack
= anim_data
.nla_tracks
.new()
412 constraintTrack
.name
= "Auto fixes " + name
413 NLATracks
.auto_fix_track
= constraintTrack
.name
414 if ("Auto fixes " + name
) in bpy
.data
.actions
:
415 constraintAction
= bpy
.data
.actions
[("Auto fixes " + name
)]
417 constraintAction
= bpy
.data
.actions
.new("Auto fixes " + name
)
418 constraintAction
.use_fake_user
= True
419 constraintStrip
= constraintTrack
.strips
.new("Auto fixes " + name
, s_frame
, constraintAction
)
420 constraintStrip
.extrapolation
= "NOTHING"
421 userTrack
= anim_data
.nla_tracks
.new()
422 userTrack
.name
= "Manual fixes " + name
423 NLATracks
.manual_fix_track
= userTrack
.name
424 if ("Manual fixes " + name
) in bpy
.data
.actions
:
425 userAction
= bpy
.data
.actions
[("Manual fixes " + name
)]
427 userAction
= bpy
.data
.actions
.new("Manual fixes " + name
)
428 userAction
.use_fake_user
= True
429 userStrip
= userTrack
.strips
.new("Manual fixes " + name
, s_frame
, userAction
)
430 userStrip
.extrapolation
= "HOLD"
431 userStrip
.blend_type
= "ADD"
432 anim_data
.nla_tracks
.active
= constraintTrack
433 anim_data
.action_extrapolation
= "NOTHING"
434 #set the stride_bone's action
435 if "stride_bone" in bpy
.data
.objects
:
436 stride_bone
= bpy
.data
.objects
["stride_bone"]
437 if NLATracks
.stride_action
:
438 stride_bone
.animation_data
.action
= bpy
.data
.actions
[NLATracks
.stride_action
]
440 NLATracks
.stride_action
= stride_bone
.animation_data
.action
.name
441 stride_bone
.animation_data
.action
.use_fake_user
= True
442 anim_data
.action
= None
445 def preAdvancedRetargeting(performer_obj
, enduser_obj
):
446 createDictionary(performer_obj
.data
, enduser_obj
.data
)
447 bones
= enduser_obj
.pose
.bones
448 map_bones
= [bone
for bone
in bones
if bone
.bone
.reverseMap
]
449 perf_root
= performer_obj
.pose
.bones
[0].name
450 for bone
in map_bones
:
451 perf_bone
= bone
.bone
.reverseMap
[0].name
453 cons
= bone
.constraints
.new('COPY_ROTATION')
454 cons
.name
= "retargetTemp"
455 locks
= bone
.lock_rotation
456 cons
.use_x
= not locks
[0]
457 cons
.use_y
= not locks
[1]
458 cons
.use_z
= not locks
[2]
459 cons
.target
= performer_obj
460 cons
.subtarget
= perf_bone
461 cons
.target_space
= 'WORLD'
462 cons
.owner_space
= 'WORLD'
464 if (not bone
.bone
.use_connect
) and (perf_bone
!= perf_root
):
465 cons
= bone
.constraints
.new('COPY_LOCATION')
466 cons
.name
= "retargetTemp"
467 cons
.target
= performer_obj
468 cons
.subtarget
= perf_bone
472 cons
.target_space
= 'LOCAL'
473 cons
.owner_space
= 'LOCAL'
476 def prepareForBake(enduser_obj
):
477 bones
= enduser_obj
.pose
.bones
479 bone
.bone
.select
= False
480 map_bones
= [bone
for bone
in bones
if bone
.bone
.reverseMap
]
481 for bone
in map_bones
:
482 for cons
in bone
.constraints
:
483 if "retargetTemp" in cons
.name
:
484 bone
.bone
.select
= True
487 def cleanTempConstraints(enduser_obj
):
488 bones
= enduser_obj
.pose
.bones
489 map_bones
= [bone
for bone
in bones
if bone
.bone
.reverseMap
]
490 for bone
in map_bones
:
491 for cons
in bone
.constraints
:
492 if "retargetTemp" in cons
.name
:
493 bone
.constraints
.remove(cons
)
496 #Main function that runs the retargeting sequence.
497 #If advanced == True, we assume constraint's were already created
498 def totalRetarget(performer_obj
, enduser_obj
, scene
, s_frame
, e_frame
):
499 perf_arm
= performer_obj
.data
500 end_arm
= enduser_obj
.data
501 advanced
= end_arm
.advancedRetarget
502 step
= end_arm
.frameStep
503 enduser_obj
.animation_data_create()
506 enduser_obj
.animation_data
.action
= bpy
.data
.actions
.new("temp")
507 enduser_obj
.animation_data
.action
.use_fake_user
= True
509 print("no need to create new action")
511 print("creating Dictionary")
512 feetBones
, root
= createDictionary(perf_arm
, end_arm
)
513 print("cleaning stuff up")
514 perf_obj_mat
, enduser_obj_mat
= cleanAndStoreObjMat(performer_obj
, enduser_obj
)
516 turnOffIK(enduser_obj
)
517 print("Creating intermediate armature (for first pass)")
518 inter_obj
= createIntermediate(performer_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
)
519 print("First pass: retargeting from intermediate to end user")
520 retargetEnduser(inter_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
)
522 prepareForBake(enduser_obj
)
523 print("Retargeting pose (Advanced Retarget)")
524 bake_action(s_frame
, e_frame
, action
=enduser_obj
.animation_data
.action
, only_selected
=True, do_pose
=True, do_object
=False, frame_step
=step
)
525 name
= performer_obj
.animation_data
.action
.name
[:10]
526 #We trim the name down to 10 chars because of Action Name length maximum
527 enduser_obj
.animation_data
.action
.name
= "Base " + name
528 print("Second pass: retargeting root translation and clean up")
529 stride_bone
= copyTranslation(performer_obj
, enduser_obj
, feetBones
, root
, s_frame
, e_frame
, scene
, enduser_obj_mat
)
532 bpy
.ops
.object.select_all(action
='DESELECT')
533 bpy
.context
.scene
.objects
.active
= enduser_obj
534 bpy
.ops
.object.select_pattern(pattern
=enduser_obj
.name
, extend
=False)
535 IKRetarget(performer_obj
, enduser_obj
, s_frame
, e_frame
, scene
, step
)
536 bpy
.ops
.object.select_pattern(pattern
=stride_bone
.name
, extend
=False)
537 restoreObjMat(performer_obj
, enduser_obj
, perf_obj_mat
, enduser_obj_mat
, stride_bone
, scene
, s_frame
)
538 bpy
.ops
.object.mode_set(mode
='OBJECT')
540 bpy
.ops
.object.select_pattern(pattern
=inter_obj
.name
, extend
=False)
541 bpy
.ops
.object.delete()
543 cleanTempConstraints(enduser_obj
)
544 bpy
.ops
.object.select_pattern(pattern
=enduser_obj
.name
, extend
=False)
546 if not name
in [tracks
.name
for tracks
in end_arm
.mocapNLATracks
]:
547 NLATracks
= end_arm
.mocapNLATracks
.add()
548 NLATracks
.name
= name
550 NLATracks
= end_arm
.mocapNLATracks
[name
]
551 end_arm
.active_mocap
= name
552 print("retargeting done!")
555 def isRigAdvanced(enduser_obj
):
556 bones
= enduser_obj
.pose
.bones
558 for constraint
in bone
.constraints
:
559 if constraint
.type != "IK":
561 if enduser_obj
.data
.animation_data
:
562 if enduser_obj
.data
.animation_data
.drivers
: