diff options
Diffstat (limited to 'mocap/retarget.py')
-rw-r--r-- | mocap/retarget.py | 557 |
1 files changed, 557 insertions, 0 deletions
diff --git a/mocap/retarget.py b/mocap/retarget.py new file mode 100644 index 00000000..6e669434 --- /dev/null +++ b/mocap/retarget.py @@ -0,0 +1,557 @@ +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# ##### END GPL LICENSE BLOCK ##### + +# <pep8 compliant> + +import bpy +from mathutils import * +from math import radians, acos, pi +from bl_operators import nla + + +def hasIKConstraint(pose_bone): + #utility function / predicate, returns True if given bone has IK constraint + ik = [constraint for constraint in pose_bone.constraints if constraint.type == "IK"] + if ik: + return ik[0] + else: + return False + + +def createDictionary(perf_arm, end_arm): + # clear any old data + for end_bone in end_arm.bones: + for mapping in end_bone.reverseMap: + end_bone.reverseMap.remove(0) + + for perf_bone in perf_arm.bones: + #find its match and add perf_bone to the match's mapping + if perf_bone.map: + end_bone = end_arm.bones[perf_bone.map] + newMap = end_bone.reverseMap.add() + newMap.name = perf_bone.name + end_bone.foot = perf_bone.foot + + #root is the root of the enduser + root = end_arm.bones[0].name + feetBones = [bone.name for bone in perf_arm.bones if bone.foot] + return feetBones, root + + +def loadMapping(perf_arm, end_arm): + for end_bone in end_arm.bones: + #find its match and add perf_bone to the match's mapping + if end_bone.reverseMap: + for perf_bone in end_bone.reverseMap: + perf_arm.bones[perf_bone.name].map = end_bone.name + +#creation of intermediate armature +# the intermediate armature has the hiearchy of the end user, +# does not have rotation inheritence +# and bone roll is identical to the performer +# its purpose is to copy over the rotations +# easily while concentrating on the hierarchy changes + + +def createIntermediate(performer_obj, enduser_obj, root, s_frame, e_frame, scene, step): + #creates and keyframes an empty with its location + #the original position of the tail bone + #useful for storing the important data in the original motion + #i.e. using this empty to IK the chain to that pos / DEBUG + + #Simple 1to1 retarget of a bone + def singleBoneRetarget(inter_bone, perf_bone): + perf_world_rotation = perf_bone.matrix + inter_world_base_rotation = inter_bone.bone.matrix_local + inter_world_base_inv = inter_world_base_rotation.inverted() + bake_matrix = (inter_world_base_inv.to_3x3() * perf_world_rotation.to_3x3()) + return bake_matrix.to_4x4() + + #uses 1to1 and interpolation/averaging to match many to 1 retarget + def manyPerfToSingleInterRetarget(inter_bone, performer_bones_s): + retarget_matrices = [singleBoneRetarget(inter_bone, perf_bone) for perf_bone in performer_bones_s] + lerp_matrix = Matrix() + for i in range(len(retarget_matrices) - 1): + first_mat = retarget_matrices[i] + next_mat = retarget_matrices[i + 1] + lerp_matrix = first_mat.lerp(next_mat, 0.5) + return lerp_matrix + + #determines the type of hierachy change needed and calls the + #right function + def retargetPerfToInter(inter_bone): + if inter_bone.bone.reverseMap: + perf_bone_name = inter_bone.bone.reverseMap + # 1 to many not supported yet + # then its either a many to 1 or 1 to 1 + if len(perf_bone_name) > 1: + performer_bones_s = [performer_bones[map.name] for map in perf_bone_name] + #we need to map several performance bone to a single + inter_bone.matrix_basis = manyPerfToSingleInterRetarget(inter_bone, performer_bones_s) + else: + perf_bone = performer_bones[perf_bone_name[0].name] + inter_bone.matrix_basis = singleBoneRetarget(inter_bone, perf_bone) + #Some bones have incorrect roll on the source armature, and need to be marked for fixing + if inter_bone.bone.twistFix: + inter_bone.matrix_basis *= Matrix.Rotation(radians(180), 4, "Y") + rot_mode = inter_bone.rotation_mode + if rot_mode == "QUATERNION": + inter_bone.keyframe_insert("rotation_quaternion") + elif rot_mode == "AXIS_ANGLE": + inter_bone.keyframe_insert("rotation_axis_angle") + else: + inter_bone.keyframe_insert("rotation_euler") + + #creates the intermediate armature object + inter_obj = enduser_obj.copy() + inter_obj.data = inter_obj.data.copy() # duplicate data + bpy.context.scene.objects.link(inter_obj) + inter_obj.name = "intermediate" + bpy.context.scene.objects.active = inter_obj + bpy.ops.object.mode_set(mode='EDIT') + #add some temporary connecting bones in case end user bones are not connected to their parents + rollDict = {} + print("creating temp bones") + for bone in inter_obj.data.edit_bones: + if not bone.use_connect and bone.parent: + if inter_obj.data.bones[bone.parent.name].reverseMap or inter_obj.data.bones[bone.name].reverseMap: + newBone = inter_obj.data.edit_bones.new("Temp") + newBone.head = bone.parent.tail + newBone.tail = bone.head + newBone.parent = bone.parent + bone.parent = newBone + bone.use_connect = True + newBone.use_connect = True + rollDict[bone.name] = bone.roll + bone.roll = 0 + #resets roll + print("retargeting to intermediate") + bpy.ops.object.mode_set(mode="OBJECT") + inter_obj.data.name = "inter_arm" + inter_arm = inter_obj.data + performer_bones = performer_obj.pose.bones + inter_bones = inter_obj.pose.bones + #clears inheritance + for inter_bone in inter_bones: + if inter_bone.bone.reverseMap: + inter_bone.bone.use_inherit_rotation = False + else: + inter_bone.bone.use_inherit_rotation = True + + for t in range(s_frame, e_frame, step): + if (t - s_frame) % 10 == 0: + print("First pass: retargeting frame {0}/{1}".format(t, e_frame - s_frame)) + scene.frame_set(t) + for bone in inter_bones: + retargetPerfToInter(bone) + + return inter_obj + +# this procedure copies the rotations over from the intermediate +# armature to the end user one. +# As the hierarchies are 1 to 1, this is a simple matter of +# copying the rotation, while keeping in mind bone roll, parenting, etc. +# TODO: Control Bones: If a certain bone is constrained in a way +# that its rotation is determined by another (a control bone) +# We should determine the right pos of the control bone. +# Scale: ? Should work but needs testing. + + +def retargetEnduser(inter_obj, enduser_obj, root, s_frame, e_frame, scene, step): + inter_bones = inter_obj.pose.bones + end_bones = enduser_obj.pose.bones + + #Basic "visual baking" function, for transfering rotations from intermediate to end user + def bakeTransform(end_bone): + src_bone = inter_bones[end_bone.name] + trg_bone = end_bone + bake_matrix = src_bone.matrix + rest_matrix = trg_bone.bone.matrix_local + + if trg_bone.parent and trg_bone.bone.use_inherit_rotation: + srcParent = src_bone.parent + if "Temp" in srcParent.name: + srcParent = srcParent.parent + parent_mat = srcParent.matrix + parent_rest = trg_bone.parent.bone.matrix_local + parent_rest_inv = parent_rest.inverted() + parent_mat_inv = parent_mat.inverted() + bake_matrix = parent_mat_inv * bake_matrix + rest_matrix = parent_rest_inv * rest_matrix + + rest_matrix_inv = rest_matrix.inverted() + bake_matrix = rest_matrix_inv * bake_matrix + end_bone.matrix_basis = bake_matrix + rot_mode = end_bone.rotation_mode + if rot_mode == "QUATERNION": + end_bone.keyframe_insert("rotation_quaternion") + elif rot_mode == "AXIS_ANGLE": + end_bone.keyframe_insert("rotation_axis_angle") + else: + end_bone.keyframe_insert("rotation_euler") + if not end_bone.bone.use_connect: + end_bone.keyframe_insert("location") + + for bone in end_bone.children: + bakeTransform(bone) + + for t in range(s_frame, e_frame, step): + if (t - s_frame) % 10 == 0: + print("Second pass: retargeting frame {0}/{1}".format(t, e_frame - s_frame)) + scene.frame_set(t) + end_bone = end_bones[root] + end_bone.location = Vector((0, 0, 0)) + end_bone.keyframe_insert("location") + bakeTransform(end_bone) + +#recieves the performer feet bones as a variable +# by "feet" I mean those bones that have plants +# (they don't move, despite root moving) somewhere in the animation. + + +def copyTranslation(performer_obj, enduser_obj, perfFeet, root, s_frame, e_frame, scene, enduser_obj_mat): + + perf_bones = performer_obj.pose.bones + end_bones = enduser_obj.pose.bones + + perfRoot = perf_bones[0].name + endFeet = [perf_bones[perfBone].bone.map for perfBone in perfFeet] + locDictKeys = perfFeet + endFeet + [perfRoot] + + def tailLoc(bone): + return bone.center + (bone.vector / 2) + + #Step 1 - we create a dict that contains these keys: + #(Performer) Hips, Feet + #(End user) Feet + # where the values are their world position on each frame in range (s,e) + + locDict = {} + for key in locDictKeys: + locDict[key] = [] + + for t in range(scene.frame_start, scene.frame_end): + scene.frame_set(t) + for bone in perfFeet: + locDict[bone].append(tailLoc(perf_bones[bone])) + locDict[perfRoot].append(tailLoc(perf_bones[perfRoot])) + for bone in endFeet: + locDict[bone].append(tailLoc(end_bones[bone])) + + # now we take our locDict and analyze it. + # we need to derive all chains + + def locDeriv(key, t): + graph = locDict[key] + return graph[t + 1] - graph[t] + + # now find the plant frames, where perfFeet don't move much + + linearAvg = [] + + for key in perfFeet: + for i in range(len(locDict[key]) - 1): + v = locDeriv(key, i) + if (v.length < 0.1): + hipV = locDeriv(perfRoot, i) + endV = locDeriv(perf_bones[key].bone.map, i) + #this is a plant frame. + #lets see what the original hip delta is, and the corresponding + #end bone's delta + if endV.length != 0: + linearAvg.append(hipV.length / endV.length) + + action_name = performer_obj.animation_data.action.name + #is there a stride_bone? + if "stride_bone" in bpy.data.objects: + stride_action = bpy.data.actions.new("Stride Bone " + action_name) + stride_action.use_fake_user = True + stride_bone = enduser_obj.parent + stride_bone.animation_data.action = stride_action + else: + bpy.ops.object.mode_set(mode='OBJECT') + bpy.ops.object.add() + stride_bone = bpy.context.active_object + stride_bone.name = "stride_bone" + stride_bone.location = enduser_obj_mat.to_translation() + if linearAvg: + #determine the average change in scale needed + avg = sum(linearAvg) / len(linearAvg) + scene.frame_set(s_frame) + initialPos = (tailLoc(perf_bones[perfRoot]) / avg) + for t in range(s_frame, e_frame): + scene.frame_set(t) + #calculate the new position, by dividing by the found ratio between performer and enduser + newTranslation = (tailLoc(perf_bones[perfRoot]) / avg) + stride_bone.location = enduser_obj_mat * (newTranslation - initialPos) + stride_bone.keyframe_insert("location") + else: + stride_bone.keyframe_insert("location") + stride_bone.animation_data.action.name = ("Stride Bone " + action_name) + + return stride_bone + + +def IKRetarget(performer_obj, enduser_obj, s_frame, e_frame, scene, step): + bpy.ops.object.select_name(name=enduser_obj.name, extend=False) + end_bones = enduser_obj.pose.bones + for pose_bone in end_bones: + ik_constraint = hasIKConstraint(pose_bone) + if ik_constraint: + target_is_bone = False + # set constraint target to corresponding empty if targetless, + # if not, keyframe current target to corresponding empty + perf_bone = pose_bone.bone.reverseMap[-1].name + bpy.ops.object.mode_set(mode='EDIT') + orgLocTrg = originalLocationTarget(pose_bone, enduser_obj) + bpy.ops.object.mode_set(mode='OBJECT') + if not ik_constraint.target: + ik_constraint.target = enduser_obj + ik_constraint.subtarget = pose_bone.name + "IK" + target = orgLocTrg + + # There is a target now + if ik_constraint.subtarget: + target = ik_constraint.target.pose.bones[ik_constraint.subtarget] + target.bone.use_local_location = False + target_is_bone = True + else: + target = ik_constraint.target + + # bake the correct locations for the ik target bones + for t in range(s_frame, e_frame, step): + scene.frame_set(t) + if target_is_bone: + final_loc = pose_bone.tail - target.bone.matrix_local.to_translation() + else: + final_loc = pose_bone.tail + target.location = final_loc + target.keyframe_insert("location") + ik_constraint.mute = False + scene.frame_set(s_frame) + bpy.ops.object.mode_set(mode='OBJECT') + + +def turnOffIK(enduser_obj): + end_bones = enduser_obj.pose.bones + for pose_bone in end_bones: + ik_constraint = hasIKConstraint(pose_bone) + if ik_constraint: + ik_constraint.mute = True + + +#copy the object matrixes and clear them (to be reinserted later) +def cleanAndStoreObjMat(performer_obj, enduser_obj): + perf_obj_mat = performer_obj.matrix_world.copy() + enduser_obj_mat = enduser_obj.matrix_world.copy() + zero_mat = Matrix() + performer_obj.matrix_world = zero_mat + enduser_obj.matrix_world = zero_mat + return perf_obj_mat, enduser_obj_mat + + +#restore the object matrixes after parenting the auto generated IK empties +def restoreObjMat(performer_obj, enduser_obj, perf_obj_mat, enduser_obj_mat, stride_bone, scene, s_frame): + pose_bones = enduser_obj.pose.bones + for pose_bone in pose_bones: + if pose_bone.name + "Org" in bpy.data.objects: + empty = bpy.data.objects[pose_bone.name + "Org"] + empty.parent = stride_bone + performer_obj.matrix_world = perf_obj_mat + enduser_obj.parent = stride_bone + scene.frame_set(s_frame) + enduser_obj_mat = enduser_obj_mat.to_3x3().to_4x4() * Matrix.Translation(stride_bone.matrix_world.to_translation()) + enduser_obj.matrix_world = enduser_obj_mat + + +#create (or return if exists) the related IK empty to the bone +def originalLocationTarget(end_bone, enduser_obj): + if not end_bone.name + "IK" in enduser_obj.data.bones: + newBone = enduser_obj.data.edit_bones.new(end_bone.name + "IK") + newBone.head = end_bone.tail + newBone.tail = end_bone.tail + Vector((0, 0.1, 0)) + else: + newBone = enduser_obj.pose.bones[end_bone.name + "IK"] + return newBone + + +#create the specified NLA setup for base animation, constraints and tweak layer. +def NLASystemInitialize(enduser_arm, context): + enduser_obj = context.active_object + NLATracks = enduser_arm.mocapNLATracks[enduser_obj.data.active_mocap] + name = NLATracks.name + anim_data = enduser_obj.animation_data + s_frame = 0 + if ("Base " + name) in bpy.data.actions: + mocapAction = bpy.data.actions[("Base " + name)] + else: + print("That retargeted anim has no base action") + anim_data.use_nla = True + for track in anim_data.nla_tracks: + anim_data.nla_tracks.remove(track) + mocapTrack = anim_data.nla_tracks.new() + mocapTrack.name = "Base " + name + NLATracks.base_track = mocapTrack.name + mocapStrip = mocapTrack.strips.new("Base " + name, s_frame, mocapAction) + constraintTrack = anim_data.nla_tracks.new() + constraintTrack.name = "Auto fixes " + name + NLATracks.auto_fix_track = constraintTrack.name + if ("Auto fixes " + name) in bpy.data.actions: + constraintAction = bpy.data.actions[("Auto fixes " + name)] + else: + constraintAction = bpy.data.actions.new("Auto fixes " + name) + constraintAction.use_fake_user = True + constraintStrip = constraintTrack.strips.new("Auto fixes " + name, s_frame, constraintAction) + constraintStrip.extrapolation = "NOTHING" + userTrack = anim_data.nla_tracks.new() + userTrack.name = "Manual fixes " + name + NLATracks.manual_fix_track = userTrack.name + if ("Manual fixes " + name) in bpy.data.actions: + userAction = bpy.data.actions[("Manual fixes " + name)] + else: + userAction = bpy.data.actions.new("Manual fixes " + name) + userAction.use_fake_user = True + userStrip = userTrack.strips.new("Manual fixes " + name, s_frame, userAction) + userStrip.extrapolation = "HOLD" + userStrip.blend_type = "ADD" + anim_data.nla_tracks.active = constraintTrack + anim_data.action_extrapolation = "NOTHING" + #set the stride_bone's action + if "stride_bone" in bpy.data.objects: + stride_bone = bpy.data.objects["stride_bone"] + if NLATracks.stride_action: + stride_bone.animation_data.action = bpy.data.actions[NLATracks.stride_action] + else: + NLATracks.stride_action = stride_bone.animation_data.action.name + stride_bone.animation_data.action.use_fake_user = True + anim_data.action = None + + +def preAdvancedRetargeting(performer_obj, enduser_obj): + createDictionary(performer_obj.data, enduser_obj.data) + bones = enduser_obj.pose.bones + map_bones = [bone for bone in bones if bone.bone.reverseMap] + perf_root = performer_obj.pose.bones[0].name + for bone in map_bones: + perf_bone = bone.bone.reverseMap[0].name + + cons = bone.constraints.new('COPY_ROTATION') + cons.name = "retargetTemp" + locks = bone.lock_rotation + cons.use_x = not locks[0] + cons.use_y = not locks[1] + cons.use_z = not locks[2] + cons.target = performer_obj + cons.subtarget = perf_bone + cons.target_space = 'WORLD' + cons.owner_space = 'WORLD' + + if (not bone.bone.use_connect) and (perf_bone != perf_root): + cons = bone.constraints.new('COPY_LOCATION') + cons.name = "retargetTemp" + cons.target = performer_obj + cons.subtarget = perf_bone + cons.use_x = True + cons.use_y = True + cons.use_z = True + cons.target_space = 'LOCAL' + cons.owner_space = 'LOCAL' + + +def prepareForBake(enduser_obj): + bones = enduser_obj.pose.bones + for bone in bones: + bone.bone.select = False + map_bones = [bone for bone in bones if bone.bone.reverseMap] + for bone in map_bones: + for cons in bone.constraints: + if "retargetTemp" in cons.name: + bone.bone.select = True + + +def cleanTempConstraints(enduser_obj): + bones = enduser_obj.pose.bones + map_bones = [bone for bone in bones if bone.bone.reverseMap] + for bone in map_bones: + for cons in bone.constraints: + if "retargetTemp" in cons.name: + bone.constraints.remove(cons) + + +#Main function that runs the retargeting sequence. +#If advanced == True, we assume constraint's were already created +def totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame): + perf_arm = performer_obj.data + end_arm = enduser_obj.data + advanced = end_arm.advancedRetarget + step = end_arm.frameStep + + try: + enduser_obj.animation_data.action = bpy.data.actions.new("temp") + enduser_obj.animation_data.action.use_fake_user = True + except: + print("no need to create new action") + + print("creating Dictionary") + feetBones, root = createDictionary(perf_arm, end_arm) + print("cleaning stuff up") + perf_obj_mat, enduser_obj_mat = cleanAndStoreObjMat(performer_obj, enduser_obj) + if not advanced: + turnOffIK(enduser_obj) + print("Creating intermediate armature (for first pass)") + inter_obj = createIntermediate(performer_obj, enduser_obj, root, s_frame, e_frame, scene, step) + print("First pass: retargeting from intermediate to end user") + retargetEnduser(inter_obj, enduser_obj, root, s_frame, e_frame, scene, step) + else: + prepareForBake(enduser_obj) + print("Retargeting pose (Advanced Retarget)") + nla.bake(s_frame, e_frame, action=enduser_obj.animation_data.action, only_selected=True, do_pose=True, do_object=False, step=step) + name = performer_obj.animation_data.action.name + enduser_obj.animation_data.action.name = "Base " + name + print("Second pass: retargeting root translation and clean up") + stride_bone = copyTranslation(performer_obj, enduser_obj, feetBones, root, s_frame, e_frame, scene, enduser_obj_mat) + if not advanced: + IKRetarget(performer_obj, enduser_obj, s_frame, e_frame, scene, step) + bpy.ops.object.select_name(name=stride_bone.name, extend=False) + restoreObjMat(performer_obj, enduser_obj, perf_obj_mat, enduser_obj_mat, stride_bone, scene, s_frame) + bpy.ops.object.mode_set(mode='OBJECT') + if not advanced: + bpy.ops.object.select_name(name=inter_obj.name, extend=False) + bpy.ops.object.delete() + else: + cleanTempConstraints(enduser_obj) + bpy.ops.object.select_name(name=enduser_obj.name, extend=False) + + if not name in [tracks.name for tracks in end_arm.mocapNLATracks]: + NLATracks = end_arm.mocapNLATracks.add() + NLATracks.name = name + else: + NLATracks = end_arm.mocapNLATracks[name] + end_arm.active_mocap = name + print("retargeting done!") + + +def isRigAdvanced(enduser_obj): + bones = enduser_obj.pose.bones + for bone in bones: + for constraint in bone.constraints: + if constraint.type != "IK": + return True + if enduser_obj.data.animation_data: + if enduser_obj.data.animation_data.drivers: + return True |