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 #####
20 from mathutils
import Vector
, Matrix
21 from math
import radians
22 from bpy_extras
.anim_utils
import bake_action
25 def hasIKConstraint(pose_bone
):
26 #utility function / predicate, returns True if given bone has IK constraint
27 ik
= [constraint
for constraint
in pose_bone
.constraints
if constraint
.type == "IK"]
34 def createDictionary(perf_arm
, end_arm
):
36 for end_bone
in end_arm
.bones
:
37 for mapping
in end_bone
.reverseMap
:
38 end_bone
.reverseMap
.remove(0)
40 for perf_bone
in perf_arm
.bones
:
41 #find its match and add perf_bone to the match's mapping
43 end_bone
= end_arm
.bones
[perf_bone
.map]
44 newMap
= end_bone
.reverseMap
.add()
45 newMap
.name
= perf_bone
.name
46 end_bone
.foot
= perf_bone
.foot
48 #root is the root of the enduser
49 root
= end_arm
.bones
[0].name
50 feetBones
= [bone
.name
for bone
in perf_arm
.bones
if bone
.foot
]
51 return feetBones
, root
54 def loadMapping(perf_arm
, end_arm
):
55 for end_bone
in end_arm
.bones
:
56 #find its match and add perf_bone to the match's mapping
57 if end_bone
.reverseMap
:
58 for perf_bone
in end_bone
.reverseMap
:
59 perf_arm
.bones
[perf_bone
.name
].map = end_bone
.name
61 #creation of intermediate armature
62 # the intermediate armature has the hierarchy of the end user,
63 # does not have rotation inheritance
64 # and bone roll is identical to the performer
65 # its purpose is to copy over the rotations
66 # easily while concentrating on the hierarchy changes
69 def createIntermediate(performer_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
):
70 #creates and keyframes an empty with its location
71 #the original position of the tail bone
72 #useful for storing the important data in the original motion
73 #i.e. using this empty to IK the chain to that pos / DEBUG
75 #Simple 1to1 retarget of a bone
76 def singleBoneRetarget(inter_bone
, perf_bone
):
77 perf_world_rotation
= perf_bone
.matrix
78 inter_world_base_rotation
= inter_bone
.bone
.matrix_local
79 inter_world_base_inv
= inter_world_base_rotation
.inverted()
80 bake_matrix
= inter_world_base_inv
.to_3x3() @ perf_world_rotation
.to_3x3()
81 return bake_matrix
.to_4x4()
83 #uses 1to1 and interpolation/averaging to match many to 1 retarget
84 def manyPerfToSingleInterRetarget(inter_bone
, performer_bones_s
):
85 retarget_matrices
= [singleBoneRetarget(inter_bone
, perf_bone
) for perf_bone
in performer_bones_s
]
86 lerp_matrix
= Matrix()
87 for i
in range(len(retarget_matrices
) - 1):
88 first_mat
= retarget_matrices
[i
]
89 next_mat
= retarget_matrices
[i
+ 1]
90 lerp_matrix
= first_mat
.lerp(next_mat
, 0.5)
93 #determines the type of hierarchy change needed and calls the
95 def retargetPerfToInter(inter_bone
):
96 if inter_bone
.bone
.reverseMap
:
97 perf_bone_name
= inter_bone
.bone
.reverseMap
98 # 1 to many not supported yet
99 # then its either a many to 1 or 1 to 1
100 if len(perf_bone_name
) > 1:
101 performer_bones_s
= [performer_bones
[map.name
] for map in perf_bone_name
]
102 #we need to map several performance bone to a single
103 inter_bone
.matrix_basis
= manyPerfToSingleInterRetarget(inter_bone
, performer_bones_s
)
105 perf_bone
= performer_bones
[perf_bone_name
[0].name
]
106 inter_bone
.matrix_basis
= singleBoneRetarget(inter_bone
, perf_bone
)
107 #Some bones have incorrect roll on the source armature, and need to be marked for fixing
108 if inter_bone
.bone
.twistFix
:
109 inter_bone
.matrix_basis
@= Matrix
.Rotation(radians(180), 4, "Y")
110 rot_mode
= inter_bone
.rotation_mode
111 if rot_mode
== "QUATERNION":
112 inter_bone
.keyframe_insert("rotation_quaternion")
113 elif rot_mode
== "AXIS_ANGLE":
114 inter_bone
.keyframe_insert("rotation_axis_angle")
116 inter_bone
.keyframe_insert("rotation_euler")
118 #creates the intermediate armature object
119 inter_obj
= enduser_obj
.copy()
120 inter_obj
.data
= inter_obj
.data
.copy() # duplicate data
121 bpy
.context
.collection
.objects
.link(inter_obj
)
122 inter_obj
.name
= "intermediate"
123 bpy
.context
.view_layer
.objects
.active
= inter_obj
124 bpy
.ops
.object.mode_set(mode
='EDIT')
125 #add some temporary connecting bones in case end user bones are not connected to their parents
127 print("creating temp bones")
128 for bone
in inter_obj
.data
.edit_bones
:
129 if not bone
.use_connect
and bone
.parent
:
130 if inter_obj
.data
.bones
[bone
.parent
.name
].reverseMap
or inter_obj
.data
.bones
[bone
.name
].reverseMap
:
131 newBone
= inter_obj
.data
.edit_bones
.new("Temp")
132 newBone
.head
= bone
.parent
.tail
133 newBone
.tail
= bone
.head
134 newBone
.parent
= bone
.parent
135 bone
.parent
= newBone
136 bone
.use_connect
= True
137 newBone
.use_connect
= True
138 rollDict
[bone
.name
] = bone
.roll
141 print("retargeting to intermediate")
142 bpy
.ops
.object.mode_set(mode
="OBJECT")
143 inter_obj
.data
.name
= "inter_arm"
144 inter_arm
= inter_obj
.data
145 performer_bones
= performer_obj
.pose
.bones
146 inter_bones
= inter_obj
.pose
.bones
148 for inter_bone
in inter_bones
:
149 if inter_bone
.bone
.reverseMap
:
150 inter_bone
.bone
.use_inherit_rotation
= False
152 inter_bone
.bone
.use_inherit_rotation
= True
154 for t
in range(s_frame
, e_frame
, step
):
155 if (t
- s_frame
) % 10 == 0:
156 print("First pass: retargeting frame {0}/{1}".format(t
, e_frame
- s_frame
))
158 for bone
in inter_bones
:
159 retargetPerfToInter(bone
)
163 # this procedure copies the rotations over from the intermediate
164 # armature to the end user one.
165 # As the hierarchies are 1 to 1, this is a simple matter of
166 # copying the rotation, while keeping in mind bone roll, parenting, etc.
167 # TODO: Control Bones: If a certain bone is constrained in a way
168 # that its rotation is determined by another (a control bone)
169 # We should determine the right pos of the control bone.
170 # Scale: ? Should work but needs testing.
173 def retargetEnduser(inter_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
):
174 inter_bones
= inter_obj
.pose
.bones
175 end_bones
= enduser_obj
.pose
.bones
177 #Basic "visual baking" function, for transferring rotations from intermediate to end user
178 def bakeTransform(end_bone
):
179 src_bone
= inter_bones
[end_bone
.name
]
181 bake_matrix
= src_bone
.matrix
182 rest_matrix
= trg_bone
.bone
.matrix_local
184 if trg_bone
.parent
and trg_bone
.bone
.use_inherit_rotation
:
185 srcParent
= src_bone
.parent
186 if "Temp" in srcParent
.name
:
187 srcParent
= srcParent
.parent
188 parent_mat
= srcParent
.matrix
189 parent_rest
= trg_bone
.parent
.bone
.matrix_local
190 parent_rest_inv
= parent_rest
.inverted()
191 parent_mat_inv
= parent_mat
.inverted()
192 bake_matrix
= parent_mat_inv
@ bake_matrix
193 rest_matrix
= parent_rest_inv
@ rest_matrix
195 rest_matrix_inv
= rest_matrix
.inverted()
196 bake_matrix
= rest_matrix_inv
@ bake_matrix
197 end_bone
.matrix_basis
= bake_matrix
198 rot_mode
= end_bone
.rotation_mode
199 if rot_mode
== "QUATERNION":
200 end_bone
.keyframe_insert("rotation_quaternion")
201 elif rot_mode
== "AXIS_ANGLE":
202 end_bone
.keyframe_insert("rotation_axis_angle")
204 end_bone
.keyframe_insert("rotation_euler")
205 if not end_bone
.bone
.use_connect
:
206 end_bone
.keyframe_insert("location")
208 for bone
in end_bone
.children
:
211 for t
in range(s_frame
, e_frame
, step
):
212 if (t
- s_frame
) % 10 == 0:
213 print("Second pass: retargeting frame {0}/{1}".format(t
, e_frame
- s_frame
))
215 end_bone
= end_bones
[root
]
216 end_bone
.location
= Vector((0, 0, 0))
217 end_bone
.keyframe_insert("location")
218 bakeTransform(end_bone
)
220 #receives the performer feet bones as a variable
221 # by "feet" I mean those bones that have plants
222 # (they don't move, despite root moving) somewhere in the animation.
225 def copyTranslation(performer_obj
, enduser_obj
, perfFeet
, root
, s_frame
, e_frame
, scene
, enduser_obj_mat
):
227 perf_bones
= performer_obj
.pose
.bones
228 end_bones
= enduser_obj
.pose
.bones
230 perfRoot
= perf_bones
[0].name
231 endFeet
= [perf_bones
[perfBone
].bone
.map for perfBone
in perfFeet
]
232 locDictKeys
= perfFeet
+ endFeet
+ [perfRoot
]
235 return bone
.center
+ (bone
.vector
/ 2)
237 #Step 1 - we create a dict that contains these keys:
238 #(Performer) Hips, Feet
240 # where the values are their world position on each frame in range (s,e)
243 for key
in locDictKeys
:
246 for t
in range(scene
.frame_start
, scene
.frame_end
):
248 for bone
in perfFeet
:
249 locDict
[bone
].append(tailLoc(perf_bones
[bone
]))
250 locDict
[perfRoot
].append(tailLoc(perf_bones
[perfRoot
]))
252 locDict
[bone
].append(tailLoc(end_bones
[bone
]))
254 # now we take our locDict and analyze it.
255 # we need to derive all chains
257 def locDeriv(key
, t
):
259 return graph
[t
+ 1] - graph
[t
]
261 # now find the plant frames, where perfFeet don't move much
265 for i
in range(len(locDict
[key
]) - 1):
268 hipV
= locDeriv(perfRoot
, i
)
269 endV
= locDeriv(perf_bones
[key
].bone
.map, i
)
270 #this is a plant frame.
271 #lets see what the original hip delta is, and the corresponding
274 linearAvg
.append(hipV
.length
/ endV
.length
)
276 action_name
= performer_obj
.animation_data
.action
.name
277 #is there a stride_bone?
278 if "stride_bone" in bpy
.data
.objects
:
279 stride_action
= bpy
.data
.actions
.new("Stride Bone " + action_name
)
280 stride_action
.use_fake_user
= True
281 stride_bone
= enduser_obj
.parent
282 stride_bone
.animation_data
.action
= stride_action
284 bpy
.ops
.object.mode_set(mode
='OBJECT')
286 stride_bone
= bpy
.context
.active_object
287 stride_bone
.name
= "stride_bone"
288 stride_bone
.location
= enduser_obj_mat
.to_translation()
290 #determine the average change in scale needed
291 avg
= sum(linearAvg
) / len(linearAvg
)
294 scene
.frame_set(s_frame
)
295 initialPos
= (tailLoc(perf_bones
[perfRoot
]) / avg
)
296 for t
in range(s_frame
, e_frame
):
298 #calculate the new position, by dividing by the found ratio between performer and enduser
299 newTranslation
= (tailLoc(perf_bones
[perfRoot
]) / avg
)
300 stride_bone
.location
= enduser_obj_mat
@ (newTranslation
- initialPos
)
301 stride_bone
.keyframe_insert("location")
302 stride_bone
.animation_data
.action
.name
= ("Stride Bone " + action_name
)
307 def IKRetarget(performer_obj
, enduser_obj
, s_frame
, e_frame
, scene
, step
):
308 end_bones
= enduser_obj
.pose
.bones
309 for pose_bone
in end_bones
:
310 ik_constraint
= hasIKConstraint(pose_bone
)
312 target_is_bone
= False
313 # set constraint target to corresponding empty if targetless,
314 # if not, keyframe current target to corresponding empty
315 perf_bone
= pose_bone
.bone
.reverseMap
[-1].name
316 bpy
.ops
.object.mode_set(mode
='EDIT')
317 orgLocTrg
= originalLocationTarget(pose_bone
, enduser_obj
)
318 bpy
.ops
.object.mode_set(mode
='OBJECT')
319 if not ik_constraint
.target
:
320 ik_constraint
.target
= enduser_obj
321 ik_constraint
.subtarget
= pose_bone
.name
+ "IK"
324 # There is a target now
325 if ik_constraint
.subtarget
:
326 target
= ik_constraint
.target
.pose
.bones
[ik_constraint
.subtarget
]
327 target
.bone
.use_local_location
= False
328 target_is_bone
= True
330 target
= ik_constraint
.target
332 # bake the correct locations for the ik target bones
333 for t
in range(s_frame
, e_frame
, step
):
336 final_loc
= pose_bone
.tail
- target
.bone
.matrix_local
.to_translation()
338 final_loc
= pose_bone
.tail
339 target
.location
= final_loc
340 target
.keyframe_insert("location")
341 ik_constraint
.mute
= False
342 scene
.frame_set(s_frame
)
343 bpy
.ops
.object.mode_set(mode
='OBJECT')
346 def turnOffIK(enduser_obj
):
347 end_bones
= enduser_obj
.pose
.bones
348 for pose_bone
in end_bones
:
349 ik_constraint
= hasIKConstraint(pose_bone
)
351 ik_constraint
.mute
= True
354 #copy the object matrixes and clear them (to be reinserted later)
355 def cleanAndStoreObjMat(performer_obj
, enduser_obj
):
356 perf_obj_mat
= performer_obj
.matrix_world
.copy()
357 enduser_obj_mat
= enduser_obj
.matrix_world
.copy()
359 performer_obj
.matrix_world
= zero_mat
360 enduser_obj
.matrix_world
= zero_mat
361 return perf_obj_mat
, enduser_obj_mat
364 #restore the object matrixes after parenting the auto generated IK empties
365 def restoreObjMat(performer_obj
, enduser_obj
, perf_obj_mat
, enduser_obj_mat
, stride_bone
, scene
, s_frame
):
366 pose_bones
= enduser_obj
.pose
.bones
367 for pose_bone
in pose_bones
:
368 if pose_bone
.name
+ "Org" in bpy
.data
.objects
:
369 empty
= bpy
.data
.objects
[pose_bone
.name
+ "Org"]
370 empty
.parent
= stride_bone
371 performer_obj
.matrix_world
= perf_obj_mat
372 enduser_obj
.parent
= stride_bone
373 scene
.frame_set(s_frame
)
374 enduser_obj_mat
= enduser_obj_mat
.to_3x3().to_4x4() @ Matrix
.Translation(stride_bone
.matrix_world
.to_translation())
375 enduser_obj
.matrix_world
= enduser_obj_mat
378 #create (or return if exists) the related IK empty to the bone
379 def originalLocationTarget(end_bone
, enduser_obj
):
380 ik_bone
= hasIKConstraint(end_bone
).subtarget
382 print("Adding IK bones for: " + end_bone
.name
)
383 newBone
= enduser_obj
.data
.edit_bones
.new(end_bone
.name
+ "IK")
384 newBone
.head
= end_bone
.tail
385 newBone
.tail
= end_bone
.tail
+ Vector((0, 0.1, 0))
387 newBone
= enduser_obj
.pose
.bones
[ik_bone
]
391 #create the specified NLA setup for base animation, constraints and tweak layer.
392 def NLASystemInitialize(enduser_arm
, context
):
393 enduser_obj
= context
.active_object
394 NLATracks
= enduser_arm
.mocapNLATracks
[enduser_obj
.data
.active_mocap
]
395 name
= NLATracks
.name
396 anim_data
= enduser_obj
.animation_data
398 if ("Base " + name
) in bpy
.data
.actions
:
399 mocapAction
= bpy
.data
.actions
[("Base " + name
)]
401 print("That retargeted anim has no base action")
402 anim_data
.use_nla
= True
403 for track
in anim_data
.nla_tracks
:
404 anim_data
.nla_tracks
.remove(track
)
405 mocapTrack
= anim_data
.nla_tracks
.new()
406 mocapTrack
.name
= "Base " + name
407 NLATracks
.base_track
= mocapTrack
.name
408 mocapStrip
= mocapTrack
.strips
.new("Base " + name
, s_frame
, mocapAction
)
409 constraintTrack
= anim_data
.nla_tracks
.new()
410 constraintTrack
.name
= "Auto fixes " + name
411 NLATracks
.auto_fix_track
= constraintTrack
.name
412 if ("Auto fixes " + name
) in bpy
.data
.actions
:
413 constraintAction
= bpy
.data
.actions
[("Auto fixes " + name
)]
415 constraintAction
= bpy
.data
.actions
.new("Auto fixes " + name
)
416 constraintAction
.use_fake_user
= True
417 constraintStrip
= constraintTrack
.strips
.new("Auto fixes " + name
, s_frame
, constraintAction
)
418 constraintStrip
.extrapolation
= "NOTHING"
419 userTrack
= anim_data
.nla_tracks
.new()
420 userTrack
.name
= "Manual fixes " + name
421 NLATracks
.manual_fix_track
= userTrack
.name
422 if ("Manual fixes " + name
) in bpy
.data
.actions
:
423 userAction
= bpy
.data
.actions
[("Manual fixes " + name
)]
425 userAction
= bpy
.data
.actions
.new("Manual fixes " + name
)
426 userAction
.use_fake_user
= True
427 userStrip
= userTrack
.strips
.new("Manual fixes " + name
, s_frame
, userAction
)
428 userStrip
.extrapolation
= "HOLD"
429 userStrip
.blend_type
= "ADD"
430 anim_data
.nla_tracks
.active
= constraintTrack
431 anim_data
.action_extrapolation
= "NOTHING"
432 #set the stride_bone's action
433 if "stride_bone" in bpy
.data
.objects
:
434 stride_bone
= bpy
.data
.objects
["stride_bone"]
435 if NLATracks
.stride_action
:
436 stride_bone
.animation_data
.action
= bpy
.data
.actions
[NLATracks
.stride_action
]
438 NLATracks
.stride_action
= stride_bone
.animation_data
.action
.name
439 stride_bone
.animation_data
.action
.use_fake_user
= True
440 anim_data
.action
= None
443 def preAdvancedRetargeting(performer_obj
, enduser_obj
):
444 createDictionary(performer_obj
.data
, enduser_obj
.data
)
445 bones
= enduser_obj
.pose
.bones
446 map_bones
= [bone
for bone
in bones
if bone
.bone
.reverseMap
]
447 perf_root
= performer_obj
.pose
.bones
[0].name
448 for bone
in map_bones
:
449 perf_bone
= bone
.bone
.reverseMap
[0].name
451 cons
= bone
.constraints
.new('COPY_ROTATION')
452 cons
.name
= "retargetTemp"
453 locks
= bone
.lock_rotation
454 cons
.use_x
= not locks
[0]
455 cons
.use_y
= not locks
[1]
456 cons
.use_z
= not locks
[2]
457 cons
.target
= performer_obj
458 cons
.subtarget
= perf_bone
459 cons
.target_space
= 'WORLD'
460 cons
.owner_space
= 'WORLD'
462 if (not bone
.bone
.use_connect
) and (perf_bone
!= perf_root
):
463 cons
= bone
.constraints
.new('COPY_LOCATION')
464 cons
.name
= "retargetTemp"
465 cons
.target
= performer_obj
466 cons
.subtarget
= perf_bone
470 if bone
.bone
.poseSpace
:
471 cons
.target_space
= 'POSE'
472 cons
.owner_space
= 'POSE'
474 cons
.target_space
= 'LOCAL'
475 cons
.owner_space
= 'LOCAL'
477 def prepareForBake(enduser_obj
):
478 bones
= enduser_obj
.pose
.bones
480 bone
.bone
.select
= False
481 map_bones
= [bone
for bone
in bones
if bone
.bone
.reverseMap
]
482 for bone
in map_bones
:
483 for cons
in bone
.constraints
:
484 if "retargetTemp" in cons
.name
:
485 bone
.bone
.select
= True
488 def cleanTempConstraints(enduser_obj
):
489 bones
= enduser_obj
.pose
.bones
490 map_bones
= [bone
for bone
in bones
if bone
.bone
.reverseMap
]
491 for bone
in map_bones
:
492 for cons
in bone
.constraints
:
493 if "retargetTemp" in cons
.name
:
494 bone
.constraints
.remove(cons
)
497 #Main function that runs the retargeting sequence.
498 #If advanced == True, we assume constraint's were already created
499 def totalRetarget(performer_obj
, enduser_obj
, scene
, s_frame
, e_frame
):
500 perf_arm
= performer_obj
.data
501 end_arm
= enduser_obj
.data
502 advanced
= end_arm
.advancedRetarget
503 step
= end_arm
.frameStep
504 enduser_obj
.animation_data_create()
507 enduser_obj
.animation_data
.action
= bpy
.data
.actions
.new("temp")
508 enduser_obj
.animation_data
.action
.use_fake_user
= True
510 print("no need to create new action")
512 print("creating Dictionary")
513 feetBones
, root
= createDictionary(perf_arm
, end_arm
)
514 print("cleaning stuff up")
515 perf_obj_mat
, enduser_obj_mat
= cleanAndStoreObjMat(performer_obj
, enduser_obj
)
517 turnOffIK(enduser_obj
)
518 print("Creating intermediate armature (for first pass)")
519 inter_obj
= createIntermediate(performer_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
)
520 print("First pass: retargeting from intermediate to end user")
521 retargetEnduser(inter_obj
, enduser_obj
, root
, s_frame
, e_frame
, scene
, step
)
523 prepareForBake(enduser_obj
)
524 print("Retargeting pose (Advanced Retarget)")
528 action
=enduser_obj
.animation_data
.action
,
534 name
= performer_obj
.animation_data
.action
.name
[:10]
535 #We trim the name down to 10 chars because of Action Name length maximum
536 enduser_obj
.animation_data
.action
.name
= "Base " + name
537 print("Second pass: retargeting root translation and clean up")
538 stride_bone
= copyTranslation(performer_obj
, enduser_obj
, feetBones
, root
, s_frame
, e_frame
, scene
, enduser_obj_mat
)
541 bpy
.ops
.object.select_all(action
='DESELECT')
542 bpy
.context
.view_layer
.objects
.active
= enduser_obj
543 bpy
.ops
.object.select_pattern(pattern
=enduser_obj
.name
, extend
=False)
544 IKRetarget(performer_obj
, enduser_obj
, s_frame
, e_frame
, scene
, step
)
545 bpy
.ops
.object.select_pattern(pattern
=stride_bone
.name
, extend
=False)
546 restoreObjMat(performer_obj
, enduser_obj
, perf_obj_mat
, enduser_obj_mat
, stride_bone
, scene
, s_frame
)
547 bpy
.ops
.object.mode_set(mode
='OBJECT')
549 bpy
.ops
.object.select_pattern(pattern
=inter_obj
.name
, extend
=False)
550 bpy
.ops
.object.delete()
552 cleanTempConstraints(enduser_obj
)
553 bpy
.ops
.object.select_pattern(pattern
=enduser_obj
.name
, extend
=False)
555 if not name
in [tracks
.name
for tracks
in end_arm
.mocapNLATracks
]:
556 NLATracks
= end_arm
.mocapNLATracks
.add()
557 NLATracks
.name
= name
559 NLATracks
= end_arm
.mocapNLATracks
[name
]
560 end_arm
.active_mocap
= name
561 print("retargeting done!")
564 def isRigAdvanced(enduser_obj
):
565 bones
= enduser_obj
.pose
.bones
567 for constraint
in bone
.constraints
:
568 if constraint
.type != "IK":
570 if enduser_obj
.data
.animation_data
:
571 if enduser_obj
.data
.animation_data
.drivers
: