From 78b147fbc20cc5cbd30057474686a5fb91124fab Mon Sep 17 00:00:00 2001 From: Benjy Cook Date: Wed, 17 Aug 2011 10:13:24 +0000 Subject: Commenting and pep8 compliance --- release/scripts/modules/mocap_tools.py | 152 ++++++++++++++++++++++----------- release/scripts/modules/retarget.py | 87 +++++-------------- release/scripts/startup/ui_mocap.py | 86 ++++++++++--------- 3 files changed, 166 insertions(+), 159 deletions(-) (limited to 'release/scripts') diff --git a/release/scripts/modules/mocap_tools.py b/release/scripts/modules/mocap_tools.py index e5d4dcb6554..6c22f718296 100644 --- a/release/scripts/modules/mocap_tools.py +++ b/release/scripts/modules/mocap_tools.py @@ -24,7 +24,9 @@ import time from mathutils import Vector, Matrix -#Vector utility functions +# A Python implementation of n sized Vectors. +# Mathutils has a max size of 4, and we need at least 5 for Simplify Curves and even more for Cross Correlation. +# Vector utility functions class NdVector: vec = [] @@ -90,6 +92,7 @@ class NdVector: y = property(y) +#Sampled Data Point class for Simplify Curves class dataPoint: index = 0 # x,y1,y2,y3 coordinate of original point @@ -105,11 +108,19 @@ class dataPoint: self.u = u +#Cross Correlation Function +#http://en.wikipedia.org/wiki/Cross_correlation +#IN: curvesA, curvesB - bpy_collection/list of fcurves to analyze. Auto-Correlation is when they are the same. +# margin - When searching for the best "start" frame, how large a neighborhood of frames should we inspect (similar to epsilon in Calculus) +#OUT: startFrame, length of new anim, and curvesA def crossCorrelationMatch(curvesA, curvesB, margin): dataA = [] dataB = [] - end = len(curvesA[0].keyframe_points) + start, end = curvesA[0].range() + start = int(start) + end = int(end) + #transfer all fcurves data on each frame to a single NdVector. for i in range(1, end): vec = [] for fcurve in curvesA: @@ -120,9 +131,11 @@ def crossCorrelationMatch(curvesA, curvesB, margin): vec.append(fcurve.evaluate(i)) dataB.append(NdVector(vec)) + #Comparator for Cross Correlation. "Classic" implementation uses dot product, as do we. def comp(a, b): return a * b + #Create Rxy, which holds the Cross Correlation data. N = len(dataA) Rxy = [0.0] * N for i in range(N): @@ -131,7 +144,9 @@ def crossCorrelationMatch(curvesA, curvesB, margin): for j in range(i): Rxy[i] += comp(dataA[j], dataB[j - i + N]) Rxy[i] /= float(N) - def bestLocalMaximum(Rxy): + + #Find the Local maximums in the Cross Correlation data via numerical derivative. + def LocalMaximums(Rxy): Rxyd = [Rxy[i] - Rxy[i - 1] for i in range(1, len(Rxy))] maxs = [] for i in range(1, len(Rxyd) - 1): @@ -142,9 +157,12 @@ def crossCorrelationMatch(curvesA, curvesB, margin): maxs.append((i, max(Rxy[i], Rxy[i - 1]))) return [x[0] for x in maxs] #~ return max(maxs, key=lambda x: x[1])[0] - - flms = bestLocalMaximum(Rxy[0:int(len(Rxy))]) + + #flms - the possible offsets of the first part of the animation. In Auto-Corr, this is the length of the loop. + flms = LocalMaximums(Rxy[0:int(len(Rxy))]) ss = [] + + #for every local maximum, find the best one - i.e. also has the best start frame. for flm in flms: diff = [] @@ -159,20 +177,28 @@ def crossCorrelationMatch(curvesA, curvesB, margin): if errorSlice < bestSlice[1]: bestSlice = (i, errorSlice, flm) return bestSlice - + s = lowerErrorSlice(diff, margin) ss.append(s) - ss.sort(key = lambda x: x[1]) + #Find the best result and return it. + ss.sort(key=lambda x: x[1]) return ss[0][2], ss[0][0], dataA + +#Uses auto correlation (cross correlation of the same set of curves) and trims the active_object's fcurves +#Except for location curves (which in mocap tend to be not cyclic, e.g. a walk cycle forward) +#Transfers the fcurve data to a list of NdVector (length of list is number of fcurves), and calls the cross correlation function. +#Then trims the fcurve accordingly. +#IN: Nothing, set the object you want as active and call. Assumes object has animation_data.action! +#OUT: Trims the object's fcurves (except location curves). def autoloop_anim(): context = bpy.context obj = context.active_object - + def locCurve(x): x.data_path == "location" - + fcurves = [x for x in obj.animation_data.action.fcurves if not locCurve(x)] margin = 10 @@ -180,13 +206,10 @@ def autoloop_anim(): flm, s, data = crossCorrelationMatch(fcurves, fcurves, margin) loop = data[s:s + flm] - #find *all* loops, s:s+flm, s+flm:s+2flm, etc... - #and interpolate between all - # to find "the perfect loop". - #Maybe before finding s? interp(i,i+flm,i+2flm).... - #~ for i in range(1, margin + 1): - #~ w1 = sqrt(float(i) / margin) - #~ loop[-i] = (loop[-i] * w1) + (loop[0] * (1 - w1)) + #performs blending with a root falloff on the seam's neighborhood to ensure good tiling. + for i in range(1, margin + 1): + w1 = sqrt(float(i) / margin) + loop[-i] = (loop[-i] * w1) + (loop[0] * (1 - w1)) for curve in fcurves: pts = curve.keyframe_points @@ -201,8 +224,16 @@ def autoloop_anim(): context.scene.frame_end = flm +#simplifyCurves: performes the bulk of the samples to bezier conversion. +#IN: curveGroup - which can be a collection of singleFcurves, or grouped (via nested lists) . +# error - threshold of permittable error (max distance) of the new beziers to the original data +# reparaError - threshold of error where we should try to fix the parameterization rather than split the existing curve. > error, usually by a small constant factor for best performance. +# maxIterations - maximum number of iterations of reparameterizations we should attempt. (Newton-Rahpson is not guarenteed to converge, so this is needed). +# group_mode - boolean, indicating wether we should place bezier keyframes on the same x (frame), or optimize each individual curve. +#OUT: None. Deletes the existing curves and creates the new beziers. def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): + #Calculates the unit tangent of point v def unitTangent(v, data_pts): tang = NdVector((0, 0, 0, 0, 0)) if v != 0: @@ -214,7 +245,8 @@ def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): tang.normalize() return tang - #assign parametric u value for each point in original data + #assign parametric u value for each point in original data, via relative arc length + #http://en.wikipedia.org/wiki/Arc_length def chordLength(data_pts, s, e): totalLength = 0 for pt in data_pts[s:e + 1]: @@ -230,7 +262,7 @@ def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): print(s, e) pt.u = (pt.temp / totalLength) - # get binomial coefficient, this function/table is only called with args + # get binomial coefficient lookup table, this function/table is only called with args # (3,0),(3,1),(3,2),(3,3),(2,0),(2,1),(2,2)! binomDict = {(3, 0): 1, (3, 1): 3, @@ -239,8 +271,8 @@ def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): (2, 0): 1, (2, 1): 2, (2, 2): 1} - #value at pt t of a single bernstein Polynomial + #value at pt t of a single bernstein Polynomial def bernsteinPoly(n, i, t): binomCoeff = binomDict[(n, i)] return binomCoeff * pow(t, i) * pow(1 - t, n - i) @@ -380,6 +412,7 @@ def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): fud = 1 pt.u = pt.u - (fu / fud) + #Create data_pts, a list of dataPoint type, each is assigned index i, and an NdVector def createDataPts(curveGroup, group_mode): data_pts = [] if group_mode: @@ -403,6 +436,7 @@ def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): data_pts.append(dataPoint(i, NdVector((x, y1, y2, y3, y4)))) return data_pts + #Recursively fit cubic beziers to the data_pts between s and e def fitCubic(data_pts, s, e): # if there are less than 3 points, fit a single basic bezier if e - s < 3: @@ -437,6 +471,7 @@ def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): beziers.append(bez) return + # deletes the sampled points and creates beziers. def createNewCurves(curveGroup, beziers, group_mode): #remove all existing data points if group_mode: @@ -483,15 +518,14 @@ def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): #remove old Fcurves and insert the new ones createNewCurves(curveGroup, beziers, group_mode) -#Main function of simplification -#sel_opt: either "sel" or "all" for which curves to effect -#error: maximum error allowed, in fraction (20% = 0.0020), -#i.e. divide by 10000 from percentage wanted. -#group_mode: boolean, to analyze each curve seperately or in groups, -#where group is all curves that effect the same property -#(e.g. a bone's x,y,z rotation) - +#Main function of simplification, which called by Operator +#IN: +# sel_opt- either "sel" (selected) or "all" for which curves to effect +# error- maximum error allowed, in fraction (20% = 0.0020, which is the default), +# i.e. divide by 10000 from percentage wanted. +# group_mode- boolean, to analyze each curve seperately or in groups, +# where a group is all curves that effect the same property/RNA path def fcurves_simplify(context, obj, sel_opt="all", error=0.002, group_mode=True): # main vars fcurves = obj.animation_data.action.fcurves @@ -533,11 +567,12 @@ def fcurves_simplify(context, obj, sel_opt="all", error=0.002, group_mode=True): return + # Implementation of non-linear median filter, with variable kernel size -# Double pass - one marks spikes, the other smooths one +# Double pass - one marks spikes, the other smooths them # Expects sampled keyframes on everyframe - - +# IN: None. Performs the operations on the active_object's fcurves. Expects animation_data.action to exist! +# OUT: None. Fixes the fcurves "in-place". def denoise_median(): context = bpy.context obj = context.active_object @@ -568,6 +603,9 @@ def denoise_median(): return +# Recieves armature, and rotations all bones by 90 degrees along the X axis +# This fixes the common axis issue BVH files have when importing. +# IN: Armature (bpy.types.Armature) def rotate_fix_armature(arm_data): global_matrix = Matrix.Rotation(radians(90), 4, "X") bpy.ops.object.mode_set(mode='EDIT', toggle=False) @@ -588,6 +626,8 @@ def rotate_fix_armature(arm_data): bpy.ops.object.mode_set(mode='OBJECT', toggle=False) +#Roughly scales the performer armature to match the enduser armature +#IN: perfromer_obj, enduser_obj, Blender objects whose .data is an armature. def scale_fix_armature(performer_obj, enduser_obj): perf_bones = performer_obj.data.bones end_bones = enduser_obj.data.bones @@ -611,6 +651,8 @@ def scale_fix_armature(performer_obj, enduser_obj): performer_obj.scale *= factor +#Guess Mapping +#Given a performer and enduser armature, attempts to guess the hiearchy mapping def guessMapping(performer_obj, enduser_obj): perf_bones = performer_obj.data.bones end_bones = enduser_obj.data.bones @@ -642,11 +684,16 @@ def guessMapping(performer_obj, enduser_obj): def guessSingleMapping(perf_bone): possible_bones = [end_bones[0]] + while possible_bones: for end_bone in possible_bones: match = nameMatch(perf_bone.name, end_bone.name) if match == 2 and not perf_bone.map: perf_bone.map = end_bone.name + #~ elif match == 1 and not perf_bone.map: + #~ oppo = perf_bones[oppositeBone(perf_bone)].map + # if oppo: + # perf_bone = oppo newPossibleBones = [] for end_bone in possible_bones: newPossibleBones += list(end_bone.children) @@ -658,6 +705,9 @@ def guessMapping(performer_obj, enduser_obj): guessSingleMapping(root) +# Creates limit rotation constraints on the enduser armature based on range of motion (max min of fcurves) of the performer. +# IN: context (bpy.context, etc.), and 2 blender objects which are armatures +# OUT: creates the limit constraints. def limit_dof(context, performer_obj, enduser_obj): limitDict = {} perf_bones = [bone for bone in performer_obj.pose.bones if bone.bone.map] @@ -705,18 +755,10 @@ def limit_dof(context, performer_obj, enduser_obj): newCons.use_limit_x = True newCons.use_limit_y = True newCons.use_limit_z = True - #~ else: - #~ bone.ik_min_x, bone.ik_min_y, bone.ik_min_z, bone.ik_max_x, bone.ik_max_y, bone.ik_max_z = limitDict[bone.name] - #~ bone.use_ik_limit_x = True - #~ bone.use_ik_limit_y = True - #~ bone.use_ik_limit_z= True - #~ bone.ik_stiffness_x = 1/((limitDict[bone.name][3] - limitDict[bone.name][0])/(2*pi))) - #~ bone.ik_stiffness_y = 1/((limitDict[bone.name][4] - limitDict[bone.name][1])/(2*pi))) - #~ bone.ik_stiffness_z = 1/((limitDict[bone.name][5] - limitDict[bone.name][2])/(2*pi))) - context.scene.frame_set(c_frame) +# Removes the constraints that were added by limit_dof on the enduser_obj def limit_dof_toggle_off(context, enduser_obj): for bone in enduser_obj.pose.bones: existingConstraint = [constraint for constraint in bone.constraints if constraint.name == "DOF Limitation"] @@ -724,6 +766,8 @@ def limit_dof_toggle_off(context, enduser_obj): bone.constraints.remove(existingConstraint[0]) +# Reparameterizes a blender path via keyframing it's eval_time to match a stride_object's forward velocity. +# IN: Context, stride object (blender object with location keyframes), path object. def path_editing(context, stride_obj, path): y_fcurve = [fcurve for fcurve in stride_obj.animation_data.action.fcurves if fcurve.data_path == "location"][1] s, e = context.scene.frame_start, context.scene.frame_end # y_fcurve.range() @@ -771,11 +815,14 @@ def path_editing(context, stride_obj, path): print("finished path editing") +#Animation Stitching +#Stitches two retargeted animations together via NLA settings. +#IN: enduser_obj, a blender armature that has had two retargets applied. def anim_stitch(context, enduser_obj): stitch_settings = enduser_obj.data.stitch_settings action_1 = stitch_settings.first_action action_2 = stitch_settings.second_action - if stitch_settings.stick_bone!="": + if stitch_settings.stick_bone != "": selected_bone = enduser_obj.pose.bones[stitch_settings.stick_bone] else: selected_bone = enduser_obj.pose.bones[0] @@ -791,8 +838,8 @@ def anim_stitch(context, enduser_obj): mocapStrip = mocapTrack.strips.new(TrackNamesB.base_track, stitch_settings.blend_frame, mocapAction) mocapStrip.extrapolation = "HOLD_FORWARD" mocapStrip.blend_in = stitch_settings.blend_amount - mocapStrip.action_frame_start+=stitch_settings.second_offset - mocapStrip.action_frame_end+=stitch_settings.second_offset + mocapStrip.action_frame_start += stitch_settings.second_offset + mocapStrip.action_frame_end += stitch_settings.second_offset constraintTrack = anim_data.nla_tracks.new() constraintTrack.name = TrackNamesB.auto_fix_track constraintAction = bpy.data.actions[TrackNamesB.auto_fix_track] @@ -821,8 +868,8 @@ def anim_stitch(context, enduser_obj): actionBTrack = stride_anim_data.nla_tracks.new() actionBTrack.name = TrackNamesB.stride_action actionBStrip = actionBTrack.strips.new(TrackNamesB.stride_action, stitch_settings.blend_frame, bpy.data.actions[TrackNamesB.stride_action]) - actionBStrip.action_frame_start+=stitch_settings.second_offset - actionBStrip.action_frame_end+=stitch_settings.second_offset + actionBStrip.action_frame_start += stitch_settings.second_offset + actionBStrip.action_frame_end += stitch_settings.second_offset actionBStrip.blend_in = stitch_settings.blend_amount actionBStrip.extrapolation = "NOTHING" #we need to change the stride_bone's action to add the offset @@ -831,15 +878,16 @@ def anim_stitch(context, enduser_obj): scene.frame_set(stitch_settings.blend_frame) actual_pos = (selected_bone.matrix.to_translation() * enduser_obj.matrix_world) offset = actual_pos - desired_pos - - for i,fcurve in enumerate([fcurve for fcurve in bpy.data.actions[TrackNamesB.stride_action].fcurves if fcurve.data_path=="location"]): - print(offset[i],i,fcurve.array_index) + + for i, fcurve in enumerate([fcurve for fcurve in bpy.data.actions[TrackNamesB.stride_action].fcurves if fcurve.data_path == "location"]): + print(offset[i], i, fcurve.array_index) for pt in fcurve.keyframe_points: - pt.co.y-=offset[i] - pt.handle_left.y-=offset[i] - pt.handle_right.y-=offset[i] + pt.co.y -= offset[i] + pt.handle_left.y -= offset[i] + pt.handle_right.y -= offset[i] +#Guesses setting for animation stitching via Cross Correlation def guess_anim_stitch(context, enduser_obj): stitch_settings = enduser_obj.data.stitch_settings action_1 = stitch_settings.first_action @@ -851,6 +899,6 @@ def guess_anim_stitch(context, enduser_obj): curvesA = mocapA.fcurves curvesB = mocapB.fcurves flm, s, data = crossCorrelationMatch(curvesA, curvesB, 10) - print(flm,s) + print("Guessed the following for start and offset: ", s, flm) enduser_obj.data.stitch_settings.blend_frame = flm - enduser_obj.data.stitch_settings.second_offset = s \ No newline at end of file + enduser_obj.data.stitch_settings.second_offset = s diff --git a/release/scripts/modules/retarget.py b/release/scripts/modules/retarget.py index 662dfc218ab..67e8c7da55d 100644 --- a/release/scripts/modules/retarget.py +++ b/release/scripts/modules/retarget.py @@ -22,7 +22,6 @@ import bpy from mathutils import * from math import radians, acos, pi from bl_operators import nla -import cProfile def hasIKConstraint(pose_bone): @@ -53,8 +52,8 @@ def createDictionary(perf_arm, end_arm): feetBones = [bone.name for bone in perf_arm.bones if bone.foot] return feetBones, root -def loadMapping(perf_arm, end_arm): +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: @@ -80,17 +79,7 @@ def createIntermediate(performer_obj, enduser_obj, root, s_frame, e_frame, scene 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()) - #~ orgEul = inter_bone.bone.matrix_local.to_euler("XYZ") - #~ eul = bake_matrix.to_euler("XYZ", orgEul) - #~ diff = -bake_matrix.to_euler().y + inter_bone.bone.matrix.to_euler().y - #~ eul.rotate_axis("Y", diff) - #~ eul.make_compatible(orgEul) - #~ bake_matrix = eul.to_matrix() - #~ #diff = abs(diff) - #bake_matrix = bake_matrix* Matrix.Rotation(pi/2, 3, "Y") - #~ scene = bpy.context.scene - #~ print(scene.frame_current, inter_bone.name, bake_matrix.to_euler().y) + 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 @@ -117,6 +106,7 @@ def createIntermediate(performer_obj, enduser_obj, root, s_frame, e_frame, scene 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 @@ -151,7 +141,6 @@ def createIntermediate(performer_obj, enduser_obj, root, s_frame, e_frame, scene bone.roll = 0 #resets roll print("retargeting to intermediate") - #bpy.ops.armature.calculate_roll(type='Z') bpy.ops.object.mode_set(mode="OBJECT") inter_obj.data.name = "inter_arm" inter_arm = inter_obj.data @@ -187,6 +176,7 @@ 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 @@ -265,29 +255,21 @@ def copyTranslation(performer_obj, enduser_obj, perfFeet, root, s_frame, e_frame # 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] - #~ locDeriv = {} - #~ for key in locDictKeys: - #~ locDeriv[key] = [] - - #~ for key in locDict.keys(): - #~ graph = locDict[key] - #~ locDeriv[key] = [graph[t + 1] - graph[t] for t in range(len(graph) - 1)] - # 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) + v = locDeriv(key, i) if (v.length < 0.1): - hipV = locDeriv(perfRoot,i) - endV = locDeriv(perf_bones[key].bone.map,i) + 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 @@ -312,7 +294,7 @@ def copyTranslation(performer_obj, enduser_obj, perfFeet, root, s_frame, e_frame #determine the average change in scale needed avg = sum(linearAvg) / len(linearAvg) scene.frame_set(s_frame) - initialPos = (tailLoc(perf_bones[perfRoot]) / avg) #+ stride_bone.location + 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 @@ -320,7 +302,6 @@ def copyTranslation(performer_obj, enduser_obj, perfFeet, root, s_frame, e_frame 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) @@ -342,7 +323,7 @@ def IKRetarget(performer_obj, enduser_obj, s_frame, e_frame, scene, step): bpy.ops.object.mode_set(mode='OBJECT') if not ik_constraint.target: ik_constraint.target = enduser_obj - ik_constraint.subtarget = pose_bone.name+"IK" + ik_constraint.subtarget = pose_bone.name + "IK" target = orgLocTrg # There is a target now @@ -370,14 +351,6 @@ def IKRetarget(performer_obj, enduser_obj, s_frame, e_frame, scene, step): def turnOffIK(enduser_obj): end_bones = enduser_obj.pose.bones for pose_bone in end_bones: - if pose_bone.is_in_ik_chain: - pass - # TODO: - # set stiffness according to place on chain - # and values from analysis that is stored in the bone - #pose_bone.ik_stiffness_x = 0.5 - #pose_bone.ik_stiffness_y = 0.5 - #pose_bone.ik_stiffness_z = 0.5 ik_constraint = hasIKConstraint(pose_bone) if ik_constraint: ik_constraint.mute = True @@ -412,18 +385,14 @@ 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)) - #~ empty = bpy.context.active_object - #~ empty.name = end_bone.name + "Org" - #~ empty.empty_draw_size = 0.1 - #~ empty.parent = enduser_obj + 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, name): +def NLASystemInitialize(enduser_arm, context): enduser_obj = context.active_object NLATracks = enduser_arm.mocapNLATracks[enduser_obj.data.active_mocap] name = NLATracks.name @@ -461,9 +430,8 @@ def NLASystemInitialize(enduser_arm, context):#enduser_obj, name): userAction.use_fake_user = True userStrip = userTrack.strips.new("Manual fixes " + name, s_frame, userAction) userStrip.extrapolation = "HOLD" - #userStrip.blend_type = "MULITPLY" - doesn't work due to work, will be activated soon + userStrip.blend_type = "ADD" anim_data.nla_tracks.active = constraintTrack - #anim_data.action = constraintAction anim_data.action_extrapolation = "NOTHING" #set the stride_bone's action if "stride_bone" in bpy.data.objects: @@ -494,8 +462,8 @@ def preAdvancedRetargeting(performer_obj, enduser_obj): cons.subtarget = perf_bone cons.target_space = 'WORLD' cons.owner_space = 'WORLD' - - if (not bone.bone.use_connect) and (perf_bone!=perf_root): + + if (not bone.bone.use_connect) and (perf_bone != perf_root): cons = bone.constraints.new('COPY_LOCATION') cons.name = "retargetTemp" cons.target = performer_obj @@ -517,6 +485,7 @@ def prepareForBake(enduser_obj): 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] @@ -525,6 +494,7 @@ def cleanTempConstraints(enduser_obj): 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): @@ -532,13 +502,13 @@ def totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame): 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") @@ -576,22 +546,6 @@ def totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame): NLATracks = end_arm.mocapNLATracks[name] end_arm.active_mocap = name print("retargeting done!") - -def profileWrapper(): - context = bpy.context - scene = context.scene - s_frame = scene.frame_start - e_frame = scene.frame_end - enduser_obj = context.active_object - performer_obj = [obj for obj in context.selected_objects if obj != enduser_obj] - if enduser_obj is None or len(performer_obj) != 1: - print("Need active and selected armatures") - else: - performer_obj = performer_obj[0] - s_frame, e_frame = performer_obj.animation_data.action.frame_range - s_frame = int(s_frame) - e_frame = int(e_frame) - totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame) def isRigAdvanced(enduser_obj): @@ -603,6 +557,3 @@ def isRigAdvanced(enduser_obj): if enduser_obj.data.animation_data: if enduser_obj.data.animation_data.drivers: return True - -if __name__ == "__main__": - cProfile.run("profileWrapper()") diff --git a/release/scripts/startup/ui_mocap.py b/release/scripts/startup/ui_mocap.py index 23354f9d722..3cb33776b0b 100644 --- a/release/scripts/startup/ui_mocap.py +++ b/release/scripts/startup/ui_mocap.py @@ -112,6 +112,7 @@ bpy.utils.register_class(MocapConstraint) bpy.types.Armature.mocap_constraints = bpy.props.CollectionProperty(type=MocapConstraint) +# Animation Stitch Settings, used for animation stitching of 2 retargeted animations. class AnimationStitchSettings(bpy.types.PropertyGroup): first_action = bpy.props.StringProperty(name="Action 1", description="First action in stitch") @@ -132,6 +133,7 @@ class AnimationStitchSettings(bpy.types.PropertyGroup): bpy.utils.register_class(AnimationStitchSettings) +# MocapNLA Tracks. Stores which tracks/actions are associated with each retargeted animation. class MocapNLATracks(bpy.types.PropertyGroup): name = bpy.props.StringProperty() base_track = bpy.props.StringProperty() @@ -141,7 +143,8 @@ class MocapNLATracks(bpy.types.PropertyGroup): bpy.utils.register_class(MocapNLATracks) - + +#Update function for Advanced Retarget boolean variable. def advancedRetargetToggle(self, context): enduser_obj = context.active_object performer_obj = [obj for obj in context.selected_objects if obj != enduser_obj] @@ -156,20 +159,23 @@ def advancedRetargetToggle(self, context): retarget.cleanTempConstraints(enduser_obj) - +#Animation Stitch Settings Property bpy.types.Armature.stitch_settings = bpy.props.PointerProperty(type=AnimationStitchSettings) -bpy.types.Armature.active_mocap = bpy.props.StringProperty(update=retarget.NLASystemInitialize) +#Current/Active retargeted animation on the armature +bpy.types.Armature.active_mocap = bpy.props.StringProperty(update=retarget.NLASystemInitialize) +#Collection of retargeted animations and their NLA Tracks on the armature bpy.types.Armature.mocapNLATracks = bpy.props.CollectionProperty(type=MocapNLATracks) +#Advanced retargeting boolean property bpy.types.Armature.advancedRetarget = bpy.props.BoolProperty(default=False, update=advancedRetargetToggle) +#frame step - frequency of frames to retarget. Skipping is useful for previewing, faster work etc. bpy.types.Armature.frameStep = smooth_out = bpy.props.IntProperty(name="Frame Skip", default=1, description="Amount of frames to skip - for previewing retargets quickly. 1 is fully sampled", min=1) -#Update function for IK functionality. Is called when IK prop checkboxes are toggled. - def toggleIKBone(self, context): + #Update function for IK functionality. Is called when IK prop checkboxes are toggled. if self.IKRetarget: if not self.is_in_ik_chain: print(self.name + " IK toggled ON!") @@ -211,21 +217,29 @@ def toggleIKBone(self, context): for bone in cnstrn_bone.parent_recursive: if not bone.is_in_ik_chain: bone.IKRetarget = False - + +#MocapMap class for storing mapping on enduser performer, +# where a bone may be linked to more than one on the performer class MocapMapping(bpy.types.PropertyGroup): name = bpy.props.StringProperty() bpy.utils.register_class(MocapMapping) +#string property for storing performer->enduser mapping bpy.types.Bone.map = bpy.props.StringProperty() +#Collection Property for storing enduser->performer mapping bpy.types.Bone.reverseMap = bpy.props.CollectionProperty(type=MocapMapping) +#Boolean property for storing foot bone toggle bpy.types.Bone.foot = bpy.props.BoolProperty(name="Foot", description="Marks this bone as a 'foot', which determines retargeted animation's translation", default=False) +#Boolean property for storing if this bone is twisted along the y axis, +# which can happen due to various sources of performers bpy.types.Bone.twistFix = bpy.props.BoolProperty(name="Twist Fix", description="Fix Twist on this bone", default=False) +#Boolean property for toggling ik retargeting for this bone bpy.types.PoseBone.IKRetarget = bpy.props.BoolProperty(name="IK", description="Toggles IK Retargeting method for given bone", update=toggleIKBone, default=False) @@ -384,7 +398,7 @@ class ExtraToolsPanel(bpy.types.Panel): if activeIsArmature: enduser_arm = context.active_object.data layout.label("Retargeted Animations:") - layout.prop_search(enduser_arm, "active_mocap",enduser_arm, "mocapNLATracks") + layout.prop_search(enduser_arm, "active_mocap", enduser_arm, "mocapNLATracks") settings = enduser_arm.stitch_settings layout.prop_search(settings, "first_action", enduser_arm, "mocapNLATracks") layout.prop_search(settings, "second_action", enduser_arm, "mocapNLATracks") @@ -397,6 +411,8 @@ class ExtraToolsPanel(bpy.types.Panel): class OBJECT_OT_RetargetButton(bpy.types.Operator): + #Retargeting operator. Assumes selected and active armatures, where the performer (the selected one) + # has an action for retargeting '''Retarget animation from selected armature to active armature ''' bl_idname = "mocap.retarget" bl_label = "Retargets active action from Performer to Enduser" @@ -428,41 +444,13 @@ class OBJECT_OT_RetargetButton(bpy.types.Operator): activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] if performer_obj: - return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) + return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) and performer_obj[0].animation_data else: return False - - - #~ class OBJECT_OT_AdvancedRetargetButton(bpy.types.Operator): - #~ '''Prepare for advanced retargeting ''' - #~ bl_idname = "mocap.preretarget" - #~ bl_label = "Prepares retarget of active action from Performer to Enduser" - - #~ def execute(self, context): - #~ scene = context.scene - #~ s_frame = scene.frame_start - #~ e_frame = scene.frame_end - #~ enduser_obj = context.active_object - #~ performer_obj = [obj for obj in context.selected_objects if obj != enduser_obj] - #~ if enduser_obj is None or len(performer_obj) != 1: - #~ print("Need active and selected armatures") - #~ else: - #~ performer_obj = performer_obj[0] - #~ retarget.preAdvancedRetargeting(performer_obj, enduser_obj) - #~ return {"FINISHED"} - - #~ @classmethod - #~ def poll(cls, context): - #~ if context.active_object: - #~ activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) - #~ performer_obj = [obj for obj in context.selected_objects if obj != context.active_object] - #~ if performer_obj: - #~ return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) - #~ else: - #~ return False class OBJECT_OT_SaveMappingButton(bpy.types.Operator): + #Operator for saving mapping to enduser armature '''Save mapping to active armature (for future retargets) ''' bl_idname = "mocap.savemapping" bl_label = "Saves user generated mapping from Performer to Enduser" @@ -486,6 +474,7 @@ class OBJECT_OT_SaveMappingButton(bpy.types.Operator): class OBJECT_OT_LoadMappingButton(bpy.types.Operator): '''Load saved mapping from active armature''' + #Operator for loading mapping to enduser armature bl_idname = "mocap.loadmapping" bl_label = "Loads user generated mapping from Performer to Enduser" @@ -504,9 +493,10 @@ class OBJECT_OT_LoadMappingButton(bpy.types.Operator): return activeIsArmature and isinstance(performer_obj[0].data, bpy.types.Armature) else: return False - + class OBJECT_OT_SelectMapBoneButton(bpy.types.Operator): + #Operator for setting selected bone in enduser armature to the performer mapping '''Select a bone for faster mapping''' bl_idname = "mocap.selectmap" bl_label = "Select a bone for faster mapping" @@ -538,6 +528,7 @@ class OBJECT_OT_SelectMapBoneButton(bpy.types.Operator): class OBJECT_OT_ConvertSamplesButton(bpy.types.Operator): + #Operator to convert samples to beziers on the selected object '''Convert active armature's sampled keyframed to beziers''' bl_idname = "mocap.samples" bl_label = "Converts samples / simplifies keyframes to beziers" @@ -552,6 +543,7 @@ class OBJECT_OT_ConvertSamplesButton(bpy.types.Operator): class OBJECT_OT_LooperButton(bpy.types.Operator): + #Operator to trim fcurves which contain a few loops to a single one on the selected object '''Trim active armature's animation to a single cycle, given a cyclic animation (such as a walk cycle)''' bl_idname = "mocap.looper" bl_label = "loops animation / sampled mocap data" @@ -566,6 +558,7 @@ class OBJECT_OT_LooperButton(bpy.types.Operator): class OBJECT_OT_DenoiseButton(bpy.types.Operator): + #Operator to denoise impluse noise on the active object's fcurves '''Denoise active armature's animation. Good for dealing with 'bad' frames inherent in mocap animation''' bl_idname = "mocap.denoise" bl_label = "Denoises sampled mocap data " @@ -584,6 +577,7 @@ class OBJECT_OT_DenoiseButton(bpy.types.Operator): class OBJECT_OT_LimitDOFButton(bpy.types.Operator): + #Operator to analyze performer armature and apply rotation constraints on the enduser armature '''Create limit constraints on the active armature from the selected armature's animation's range of motion''' bl_idname = "mocap.limitdof" bl_label = "Analyzes animations Max/Min DOF and adds hard/soft constraints" @@ -605,6 +599,7 @@ class OBJECT_OT_LimitDOFButton(bpy.types.Operator): class OBJECT_OT_RemoveLimitDOFButton(bpy.types.Operator): + #Removes constraints created by above operator '''Removes previously created limit constraints on the active armature''' bl_idname = "mocap.removelimitdof" bl_label = "Removes previously created limit constraints on the active armature" @@ -622,6 +617,7 @@ class OBJECT_OT_RemoveLimitDOFButton(bpy.types.Operator): class OBJECT_OT_RotateFixArmature(bpy.types.Operator): + #Operator to fix common imported Mocap data issue of wrong axis system on active object '''Realign the active armature's axis system to match Blender (Commonly needed after bvh import)''' bl_idname = "mocap.rotate_fix" bl_label = "Rotates selected armature 90 degrees (fix for bvh import)" @@ -637,6 +633,7 @@ class OBJECT_OT_RotateFixArmature(bpy.types.Operator): class OBJECT_OT_ScaleFixArmature(bpy.types.Operator): + #Operator to scale down the selected armature to match the active one '''Rescale selected armature to match the active animation, for convienence''' bl_idname = "mocap.scale_fix" bl_label = "Scales performer armature to match target armature" @@ -659,6 +656,7 @@ class OBJECT_OT_ScaleFixArmature(bpy.types.Operator): class MOCAP_OT_AddMocapFix(bpy.types.Operator): + #Operator to add a post-retarget fix '''Add a post-retarget fix - useful for fixing certain artifacts following the retarget''' bl_idname = "mocap.addmocapfix" bl_label = "Add Mocap Fix to target armature" @@ -683,6 +681,7 @@ class MOCAP_OT_AddMocapFix(bpy.types.Operator): class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator): + #Operator to remove a post-retarget fix '''Remove this post-retarget fix''' bl_idname = "mocap.removeconstraint" bl_label = "Removes fixes from target armature" @@ -707,6 +706,7 @@ class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator): class OBJECT_OT_BakeMocapConstraints(bpy.types.Operator): + #Operator to bake all post-retarget fixes '''Bake all post-retarget fixes to the Retarget Fixes NLA Track''' bl_idname = "mocap.bakeconstraints" bl_label = "Bake all fixes to target armature" @@ -722,6 +722,7 @@ class OBJECT_OT_BakeMocapConstraints(bpy.types.Operator): class OBJECT_OT_UnbakeMocapConstraints(bpy.types.Operator): + #Operator to unbake all post-retarget fixes '''Unbake all post-retarget fixes - removes the baked data from the Retarget Fixes NLA Track''' bl_idname = "mocap.unbakeconstraints" bl_label = "Unbake all fixes to target armature" @@ -737,6 +738,8 @@ class OBJECT_OT_UnbakeMocapConstraints(bpy.types.Operator): class OBJECT_OT_UpdateMocapConstraints(bpy.types.Operator): + #Operator to update all post-retarget fixes, similar to update dependencies on drivers + #Needed because python properties lack certain callbacks and some fixes take a while to recalculate. '''Updates all post-retarget fixes - needed after changes to armature object or pose''' bl_idname = "mocap.updateconstraints" bl_label = "Updates all fixes to target armature - neccesary to take under consideration changes to armature object or pose" @@ -752,6 +755,7 @@ class OBJECT_OT_UpdateMocapConstraints(bpy.types.Operator): class OBJECT_OT_GuessHierachyMapping(bpy.types.Operator): + #Operator which calls heurisitic function to guess mapping between 2 armatures '''Attemps to auto figure out hierarchy mapping''' bl_idname = "mocap.guessmapping" bl_label = "Attemps to auto figure out hierarchy mapping" @@ -774,6 +778,7 @@ class OBJECT_OT_GuessHierachyMapping(bpy.types.Operator): class OBJECT_OT_PathEditing(bpy.types.Operator): + #Operator which calls path editing function, making active object follow the selected curve. '''Sets active object (stride object) to follow the selected curve''' bl_idname = "mocap.pathediting" bl_label = "Sets active object (stride object) to follow the selected curve" @@ -793,6 +798,7 @@ class OBJECT_OT_PathEditing(bpy.types.Operator): class OBJECT_OT_AnimationStitchingButton(bpy.types.Operator): + #Operator which calls stitching function, combining 2 animations onto the NLA. '''Stitches two defined animations into a single one via alignment of NLA Tracks''' bl_idname = "mocap.animstitch" bl_label = "Stitches two defined animations into a single one via alignment of NLA Tracks" @@ -810,9 +816,10 @@ class OBJECT_OT_AnimationStitchingButton(bpy.types.Operator): stitch_settings = context.active_object.data.stitch_settings return (stitch_settings.first_action and stitch_settings.second_action) return False - + class OBJECT_OT_GuessAnimationStitchingButton(bpy.types.Operator): + #Operator which calls stitching function heuristic, setting good values for above operator. '''Guesses the stitch frame and second offset for animation stitch''' bl_idname = "mocap.animstitchguess" bl_label = "Guesses the stitch frame and second offset for animation stitch" @@ -831,6 +838,7 @@ class OBJECT_OT_GuessAnimationStitchingButton(bpy.types.Operator): return (stitch_settings.first_action and stitch_settings.second_action) return False + def register(): bpy.utils.register_module(__name__) -- cgit v1.2.3