diff options
Diffstat (limited to 'release/scripts')
25 files changed, 3103 insertions, 48 deletions
diff --git a/release/scripts/modules/bpy_types.py b/release/scripts/modules/bpy_types.py index 8766c873dd8..75b199dcc26 100644 --- a/release/scripts/modules/bpy_types.py +++ b/release/scripts/modules/bpy_types.py @@ -57,7 +57,7 @@ class Library(bpy_types.ID): "curves", "grease_pencil", "groups", "images", \ "lamps", "lattices", "materials", "metaballs", \ "meshes", "node_groups", "objects", "scenes", \ - "sounds", "textures", "texts", "fonts", "worlds" + "sounds", "speakers", "textures", "texts", "fonts", "worlds" return tuple(id_block for attr in attr_links for id_block in getattr(bpy.data, attr) if id_block.library == self) @@ -411,6 +411,16 @@ class Text(bpy_types.ID): TypeMap = {} +class Sound(bpy_types.ID): + __slots__ = () + + @property + def factory(self): + """The aud.Factory object of the sound.""" + import aud + return aud._sound_from_pointer(self.as_pointer()) + + class RNAMeta(type): def __new__(cls, name, bases, classdict, **args): result = type.__new__(cls, name, bases, classdict) diff --git a/release/scripts/modules/mocap_constraints.py b/release/scripts/modules/mocap_constraints.py new file mode 100644 index 00000000000..540e8fa06db --- /dev/null +++ b/release/scripts/modules/mocap_constraints.py @@ -0,0 +1,434 @@ +# ##### 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 bl_operators import nla +from retarget import hasIKConstraint + +### Utility Functions + + +def getConsObj(bone): + #utility function - returns related IK target if bone has IK + ik = [constraint for constraint in bone.constraints if constraint.type == "IK"] + if ik: + ik = ik[0] + cons_obj = ik.target + if ik.subtarget: + cons_obj = ik.target.pose.bones[ik.subtarget] + else: + cons_obj = bone + return cons_obj + + +def consObjToBone(cons_obj): + #Utility function - returns related bone from ik object + if cons_obj.name[-3:] == "Org": + return cons_obj.name[:-3] + else: + return cons_obj.name + +### And and Remove Constraints (called from operators) + + +def addNewConstraint(m_constraint, cons_obj): + #Decide the correct Blender constraint according to the Mocap constraint type + if m_constraint.type == "point" or m_constraint.type == "freeze": + c_type = "LIMIT_LOCATION" + if m_constraint.type == "distance": + c_type = "LIMIT_DISTANCE" + if m_constraint.type == "floor": + c_type = "LIMIT_LOCATION" + #create and store the new constraint within m_constraint + real_constraint = cons_obj.constraints.new(c_type) + real_constraint.name = "Auto fixes " + str(len(cons_obj.constraints)) + m_constraint.real_constraint_bone = consObjToBone(cons_obj) + m_constraint.real_constraint = real_constraint.name + #set the rest of the constraint properties + setConstraint(m_constraint, bpy.context) + + +def removeConstraint(m_constraint, cons_obj): + #remove the influence fcurve and Blender constraint + oldConstraint = cons_obj.constraints[m_constraint.real_constraint] + removeFcurves(cons_obj, bpy.context.active_object, oldConstraint, m_constraint) + cons_obj.constraints.remove(oldConstraint) + +### Update functions. There are 3: UpdateType/Bone +### update framing (deals with changes in the desired frame range) +### And setConstraint which deals with the rest + + +def updateConstraintBoneType(m_constraint, context): + #If the constraint exists, we need to remove it + #from the old bone + obj = context.active_object + bones = obj.pose.bones + if m_constraint.real_constraint: + bone = bones[m_constraint.real_constraint_bone] + cons_obj = getConsObj(bone) + removeConstraint(m_constraint, cons_obj) + #Regardless, after that we create a new constraint + if m_constraint.constrained_bone: + bone = bones[m_constraint.constrained_bone] + cons_obj = getConsObj(bone) + addNewConstraint(m_constraint, cons_obj) + + +def setConstraintFraming(m_constraint, context): + obj = context.active_object + bones = obj.pose.bones + bone = bones[m_constraint.constrained_bone] + cons_obj = getConsObj(bone) + real_constraint = cons_obj.constraints[m_constraint.real_constraint] + #remove the old keyframes + removeFcurves(cons_obj, obj, real_constraint, m_constraint) + #set the new ones according to the m_constraint properties + s, e = m_constraint.s_frame, m_constraint.e_frame + s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out + real_constraint.influence = 1 + real_constraint.keyframe_insert(data_path="influence", frame=s) + real_constraint.keyframe_insert(data_path="influence", frame=e) + real_constraint.influence = 0 + real_constraint.keyframe_insert(data_path="influence", frame=s - s_in) + real_constraint.keyframe_insert(data_path="influence", frame=e + s_out) + + +def removeFcurves(cons_obj, obj, real_constraint, m_constraint): + #Determine if the constrained object is a bone or an empty + if isinstance(cons_obj, bpy.types.PoseBone): + fcurves = obj.animation_data.action.fcurves + else: + fcurves = cons_obj.animation_data.action.fcurves + #Find the RNA data path of the constraint's influence + RNA_paths = [] + RNA_paths.append(real_constraint.path_from_id("influence")) + if m_constraint.type == "floor" or m_constraint.type == "point": + RNA_paths += [real_constraint.path_from_id("max_x"), real_constraint.path_from_id("min_x")] + RNA_paths += [real_constraint.path_from_id("max_y"), real_constraint.path_from_id("min_y")] + RNA_paths += [real_constraint.path_from_id("max_z"), real_constraint.path_from_id("min_z")] + #Retrieve the correct fcurve via the RNA data path and remove it + fcurves_del = [fcurve for fcurve in fcurves if fcurve.data_path in RNA_paths] + #clear the fcurve and set the frames. + if fcurves_del: + for fcurve in fcurves_del: + fcurves.remove(fcurve) + #remove armature fcurves (if user keyframed m_constraint properties) + if obj.data.animation_data and m_constraint.type == "point": + if obj.data.animation_data.action: + path = m_constraint.path_from_id("targetPoint") + m_fcurves = [fcurve for fcurve in obj.data.animation_data.action.fcurves if fcurve.data_path == path] + for curve in m_fcurves: + obj.data.animation_data.action.fcurves.remove(curve) + +#Utility function for copying property fcurves over + + +def copyFCurve(newCurve, oldCurve): + for point in oldCurve.keyframe_points: + newCurve.keyframe_points.insert(frame=point.co.x, value=point.co.y) + +#Creates new fcurves for the constraint properties (for floor and point) + + +def createConstraintFCurves(cons_obj, obj, real_constraint): + if isinstance(cons_obj, bpy.types.PoseBone): + c_fcurves = obj.animation_data.action.fcurves + else: + c_fcurves = cons_obj.animation_data.action.fcurves + c_x_path = [real_constraint.path_from_id("max_x"), real_constraint.path_from_id("min_x")] + c_y_path = [real_constraint.path_from_id("max_y"), real_constraint.path_from_id("min_y")] + c_z_path = [real_constraint.path_from_id("max_z"), real_constraint.path_from_id("min_z")] + c_constraints_path = c_x_path + c_y_path + c_z_path + existing_curves = [fcurve for fcurve in c_fcurves if fcurve.data_path in c_constraints_path] + if existing_curves: + for curve in existing_curves: + c_fcurves.remove(curve) + xCurves, yCurves, zCurves = [], [], [] + for path in c_constraints_path: + newCurve = c_fcurves.new(path) + if path in c_x_path: + xCurves.append(newCurve) + elif path in c_y_path: + yCurves.append(newCurve) + else: + zCurves.append(newCurve) + return xCurves, yCurves, zCurves + + +# Function that copies all settings from m_constraint to the real Blender constraints +# Is only called when blender constraint already exists + + +def setConstraint(m_constraint, context): + if not m_constraint.constrained_bone: + return + obj = context.active_object + bones = obj.pose.bones + bone = bones[m_constraint.constrained_bone] + cons_obj = getConsObj(bone) + real_constraint = cons_obj.constraints[m_constraint.real_constraint] + NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap] + obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track] + + #frame changing section + setConstraintFraming(m_constraint, context) + s, e = m_constraint.s_frame, m_constraint.e_frame + s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out + s -= s_in + e += s_out + #Set the blender constraint parameters + if m_constraint.type == "point": + constraint_settings = False # are fix settings keyframed? + if not m_constraint.targetSpace == "constrained_boneB": + real_constraint.owner_space = m_constraint.targetSpace + else: + real_constraint.owner_space = "LOCAL" + if obj.data.animation_data: + if obj.data.animation_data.action: + path = m_constraint.path_from_id("targetPoint") + m_fcurves = [fcurve for fcurve in obj.data.animation_data.action.fcurves if fcurve.data_path == path] + if m_fcurves: + constraint_settings = True + xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint) + for curve in xCurves: + copyFCurve(curve, m_fcurves[0]) + for curve in yCurves: + copyFCurve(curve, m_fcurves[1]) + for curve in zCurves: + copyFCurve(curve, m_fcurves[2]) + if m_constraint.targetSpace == "constrained_boneB" and m_constraint.constrained_boneB: + c_frame = context.scene.frame_current + bakedPos = {} + src_bone = bones[m_constraint.constrained_boneB] + if not constraint_settings: + xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint) + print("please wait a moment, calculating fix") + for t in range(s, e): + context.scene.frame_set(t) + src_bone_pos = src_bone.matrix.to_translation() + bakedPos[t] = src_bone_pos + m_constraint.targetPoint # final position for constrained bone in object space + context.scene.frame_set(c_frame) + for frame in bakedPos.keys(): + pos = bakedPos[frame] + for xCurve in xCurves: + xCurve.keyframe_points.insert(frame=frame, value=pos.x) + for yCurve in yCurves: + yCurve.keyframe_points.insert(frame=frame, value=pos.y) + for zCurve in zCurves: + zCurve.keyframe_points.insert(frame=frame, value=pos.z) + + if not constraint_settings: + x, y, z = m_constraint.targetPoint + real_constraint.max_x = x + real_constraint.max_y = y + real_constraint.max_z = z + real_constraint.min_x = x + real_constraint.min_y = y + real_constraint.min_z = z + real_constraint.use_max_x = True + real_constraint.use_max_y = True + real_constraint.use_max_z = True + real_constraint.use_min_x = True + real_constraint.use_min_y = True + real_constraint.use_min_z = True + + if m_constraint.type == "freeze": + real_constraint.owner_space = m_constraint.targetSpace + bpy.context.scene.frame_set(m_constraint.s_frame) + if isinstance(cons_obj, bpy.types.PoseBone): + x, y, z = cons_obj.bone.center + (cons_obj.bone.vector / 2) + obj.matrix_world.to_translation() + else: + x, y, z = cons_obj.matrix_world.to_translation() + + real_constraint.max_x = x + real_constraint.max_y = y + real_constraint.max_z = z + real_constraint.min_x = x + real_constraint.min_y = y + real_constraint.min_z = z + real_constraint.use_max_x = True + real_constraint.use_max_y = True + real_constraint.use_max_z = True + real_constraint.use_min_x = True + real_constraint.use_min_y = True + real_constraint.use_min_z = True + + if m_constraint.type == "distance" and m_constraint.constrained_boneB: + real_constraint.owner_space = "WORLD" + real_constraint.target = getConsObj(bones[m_constraint.constrained_boneB]) + real_constraint.limit_mode = "LIMITDIST_ONSURFACE" + real_constraint.distance = m_constraint.targetDist + + if m_constraint.type == "floor" and m_constraint.targetMesh: + real_constraint.mute = True + real_constraint.owner_space = "WORLD" + #calculate the positions thoughout the range + s, e = m_constraint.s_frame, m_constraint.e_frame + s_in, s_out = m_constraint.smooth_in, m_constraint.smooth_out + s -= s_in + e += s_out + bakedPos = {} + floor = bpy.data.objects[m_constraint.targetMesh] + c_frame = context.scene.frame_current + print("please wait a moment, calculating fix") + for t in range(s, e): + context.scene.frame_set(t) + axis = Vector((0, 0, 100)) * obj.matrix_world.to_3x3() + offset = Vector((0, 0, m_constraint.targetDist)) * obj.matrix_world.to_3x3() + ray_origin = cons_obj.matrix_world.to_translation() - offset # world position of constrained bone + ray_target = ray_origin + axis + #convert ray points to floor's object space + ray_origin *= floor.matrix_world.inverted() + ray_target *= floor.matrix_world.inverted() + hit, nor, ind = floor.ray_cast(ray_origin, ray_target) + if hit != Vector((0, 0, 0)): + bakedPos[t] = (hit * floor.matrix_world) + bakedPos[t] += Vector((0, 0, m_constraint.targetDist)) + else: + bakedPos[t] = cons_obj.matrix_world.to_translation() + context.scene.frame_set(c_frame) + #create keyframes for real constraint + xCurves, yCurves, zCurves = createConstraintFCurves(cons_obj, obj, real_constraint) + for frame in bakedPos.keys(): + pos = bakedPos[frame] + for xCurve in xCurves: + xCurve.keyframe_points.insert(frame=frame, value=pos.x) + for yCurve in yCurves: + yCurve.keyframe_points.insert(frame=frame, value=pos.y) + for zCurve in zCurves: + zCurve.keyframe_points.insert(frame=frame, value=pos.z) + real_constraint.use_max_x = True + real_constraint.use_max_y = True + real_constraint.use_max_z = True + real_constraint.use_min_x = True + real_constraint.use_min_y = True + real_constraint.use_min_z = True + + # active/baked check + real_constraint.mute = (not m_constraint.active) + + +def locBake(s_frame, e_frame, bones): + scene = bpy.context.scene + bakeDict = {} + for bone in bones: + bakeDict[bone.name] = {} + for t in range(s_frame, e_frame): + scene.frame_set(t) + for bone in bones: + bakeDict[bone.name][t] = bone.matrix.copy() + for t in range(s_frame, e_frame): + for bone in bones: + print(bone.bone.matrix_local.to_translation()) + bone.matrix = bakeDict[bone.name][t] + bone.keyframe_insert("location", frame=t) + + +# Baking function which bakes all bones effected by the constraint +def bakeAllConstraints(obj, s_frame, e_frame, bones): + for bone in bones: + bone.bone.select = False + selectedBones = [] # Marks bones that need a full bake + simpleBake = [] # Marks bones that need only a location bake + for end_bone in bones: + if end_bone.name in [m_constraint.real_constraint_bone for m_constraint in obj.data.mocap_constraints]: + #For all bones that have a constraint: + ik = hasIKConstraint(end_bone) + cons_obj = getConsObj(end_bone) + if ik: + #If it's an auto generated IK: + if ik.chain_count == 0: + selectedBones += bones # Chain len 0, bake everything + else: + selectedBones += [end_bone] + end_bone.parent_recursive[:ik.chain_count - 1] # Bake the chain + else: + #It's either an FK bone which we should just bake + #OR a user created IK target bone + simpleBake += [end_bone] + for bone in selectedBones: + bone.bone.select = True + NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap] + obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track] + constraintTrack = obj.animation_data.nla_tracks[NLATracks.auto_fix_track] + constraintStrip = constraintTrack.strips[0] + constraintStrip.action_frame_start = s_frame + constraintStrip.action_frame_end = e_frame + constraintStrip.frame_start = s_frame + constraintStrip.frame_end = e_frame + if selectedBones: + #Use bake function from NLA Bake Action operator + nla.bake(s_frame, e_frame, action=constraintStrip.action, only_selected=True, do_pose=True, do_object=False) + if simpleBake: + #Do a "simple" bake, location only, world space only. + locBake(s_frame, e_frame, simpleBake) + + +#Calls the baking function and decativates releveant constraints +def bakeConstraints(context): + obj = context.active_object + bones = obj.pose.bones + s_frame, e_frame = context.scene.frame_start, context.scene.frame_end + #Bake relevant bones + bakeAllConstraints(obj, s_frame, e_frame, bones) + for m_constraint in obj.data.mocap_constraints: + end_bone = bones[m_constraint.real_constraint_bone] + cons_obj = getConsObj(end_bone) + # It's a control empty: turn the ik off + if not isinstance(cons_obj, bpy.types.PoseBone): + ik_con = hasIKConstraint(end_bone) + if ik_con: + ik_con.mute = True + # Deactivate related Blender Constraint + m_constraint.active = False + + +#Deletes the baked fcurves and reactivates relevant constraints +def unbakeConstraints(context): + # to unbake constraints we delete the whole strip + obj = context.active_object + bones = obj.pose.bones + scene = bpy.context.scene + NLATracks = obj.data.mocapNLATracks[obj.data.active_mocap] + obj.animation_data.action = bpy.data.actions[NLATracks.auto_fix_track] + constraintTrack = obj.animation_data.nla_tracks[NLATracks.auto_fix_track] + constraintStrip = constraintTrack.strips[0] + action = constraintStrip.action + # delete the fcurves on the strip + for fcurve in action.fcurves: + action.fcurves.remove(fcurve) + # reactivate relevant constraints + for m_constraint in obj.data.mocap_constraints: + end_bone = bones[m_constraint.real_constraint_bone] + cons_obj = getConsObj(end_bone) + # It's a control empty: turn the ik back on + if not isinstance(cons_obj, bpy.types.PoseBone): + ik_con = hasIKConstraint(end_bone) + if ik_con: + ik_con.mute = False + m_constraint.active = True + + +def updateConstraints(obj, context): + fixes = obj.data.mocap_constraints + for fix in fixes: + fix.active = False + fix.active = True diff --git a/release/scripts/modules/mocap_tools.py b/release/scripts/modules/mocap_tools.py new file mode 100644 index 00000000000..e5d4dcb6554 --- /dev/null +++ b/release/scripts/modules/mocap_tools.py @@ -0,0 +1,856 @@ +# ##### 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> + +from math import hypot, sqrt, isfinite, radians, pi +import bpy +import time +from mathutils import Vector, Matrix + + +#Vector utility functions +class NdVector: + vec = [] + + def __init__(self, vec): + self.vec = vec[:] + + def __len__(self): + return len(self.vec) + + def __mul__(self, otherMember): + if (isinstance(otherMember, int) or + isinstance(otherMember, float)): + return NdVector([otherMember * x for x in self.vec]) + else: + a = self.vec + b = otherMember.vec + n = len(self) + return sum([a[i] * b[i] for i in range(n)]) + + def __sub__(self, otherVec): + a = self.vec + b = otherVec.vec + n = len(self) + return NdVector([a[i] - b[i] for i in range(n)]) + + def __add__(self, otherVec): + a = self.vec + b = otherVec.vec + n = len(self) + return NdVector([a[i] + b[i] for i in range(n)]) + + def __div__(self, scalar): + return NdVector([x / scalar for x in self.vec]) + + def vecLength(self): + return sqrt(self * self) + + def vecLengthSq(self): + return (self * self) + + def normalize(self): + len = self.length + self.vec = [x / len for x in self.vec] + + def copy(self): + return NdVector(self.vec) + + def __getitem__(self, i): + return self.vec[i] + + def x(self): + return self.vec[0] + + def y(self): + return self.vec[1] + + def resize_2d(self): + return Vector((self.x, self.y)) + + length = property(vecLength) + lengthSq = property(vecLengthSq) + x = property(x) + y = property(y) + + +class dataPoint: + index = 0 + # x,y1,y2,y3 coordinate of original point + co = NdVector((0, 0, 0, 0, 0)) + #position according to parametric view of original data, [0,1] range + u = 0 + #use this for anything + temp = 0 + + def __init__(self, index, co, u=0): + self.index = index + self.co = co + self.u = u + + +def crossCorrelationMatch(curvesA, curvesB, margin): + dataA = [] + dataB = [] + end = len(curvesA[0].keyframe_points) + + for i in range(1, end): + vec = [] + for fcurve in curvesA: + vec.append(fcurve.evaluate(i)) + dataA.append(NdVector(vec)) + vec = [] + for fcurve in curvesB: + vec.append(fcurve.evaluate(i)) + dataB.append(NdVector(vec)) + + def comp(a, b): + return a * b + + N = len(dataA) + Rxy = [0.0] * N + for i in range(N): + for j in range(i, min(i + N, N)): + Rxy[i] += comp(dataA[j], dataB[j - i]) + for j in range(i): + Rxy[i] += comp(dataA[j], dataB[j - i + N]) + Rxy[i] /= float(N) + def bestLocalMaximum(Rxy): + Rxyd = [Rxy[i] - Rxy[i - 1] for i in range(1, len(Rxy))] + maxs = [] + for i in range(1, len(Rxyd) - 1): + a = Rxyd[i - 1] + b = Rxyd[i] + #sign change (zerocrossing) at point i, denoting max point (only) + if (a >= 0 and b < 0) or (a < 0 and b >= 0): + 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))]) + ss = [] + for flm in flms: + diff = [] + + for i in range(len(dataA) - flm): + diff.append((dataA[i] - dataB[i + flm]).lengthSq) + + def lowerErrorSlice(diff, e): + #index, error at index + bestSlice = (0, 100000) + for i in range(e, len(diff) - e): + errorSlice = sum(diff[i - e:i + e + 1]) + if errorSlice < bestSlice[1]: + bestSlice = (i, errorSlice, flm) + return bestSlice + + s = lowerErrorSlice(diff, margin) + ss.append(s) + + ss.sort(key = lambda x: x[1]) + return ss[0][2], ss[0][0], dataA + +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 + + 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)) + + for curve in fcurves: + pts = curve.keyframe_points + for i in range(len(pts) - 1, -1, -1): + pts.remove(pts[i]) + + for c, curve in enumerate(fcurves): + pts = curve.keyframe_points + for i in range(len(loop)): + pts.insert(i + 2, loop[i][c]) + + context.scene.frame_end = flm + + +def simplifyCurves(curveGroup, error, reparaError, maxIterations, group_mode): + + def unitTangent(v, data_pts): + tang = NdVector((0, 0, 0, 0, 0)) + if v != 0: + #If it's not the first point, we can calculate a leftside tangent + tang += data_pts[v].co - data_pts[v - 1].co + if v != len(data_pts) - 1: + #If it's not the last point, we can calculate a rightside tangent + tang += data_pts[v + 1].co - data_pts[v].co + tang.normalize() + return tang + + #assign parametric u value for each point in original data + def chordLength(data_pts, s, e): + totalLength = 0 + for pt in data_pts[s:e + 1]: + i = pt.index + if i == s: + chordLength = 0 + else: + chordLength = (data_pts[i].co - data_pts[i - 1].co).length + totalLength += chordLength + pt.temp = totalLength + for pt in data_pts[s:e + 1]: + if totalLength == 0: + print(s, e) + pt.u = (pt.temp / totalLength) + + # get binomial coefficient, 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, + (3, 2): 3, + (3, 3): 1, + (2, 0): 1, + (2, 1): 2, + (2, 2): 1} + #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) + + # fit a single cubic to data points in range [s(tart),e(nd)]. + def fitSingleCubic(data_pts, s, e): + + # A - matrix used for calculating C matrices for fitting + def A(i, j, s, e, t1, t2): + if j == 1: + t = t1 + if j == 2: + t = t2 + u = data_pts[i].u + return t * bernsteinPoly(3, j, u) + + # X component, used for calculating X matrices for fitting + def xComponent(i, s, e): + di = data_pts[i].co + u = data_pts[i].u + v0 = data_pts[s].co + v3 = data_pts[e].co + a = v0 * bernsteinPoly(3, 0, u) + b = v0 * bernsteinPoly(3, 1, u) + c = v3 * bernsteinPoly(3, 2, u) + d = v3 * bernsteinPoly(3, 3, u) + return (di - (a + b + c + d)) + + t1 = unitTangent(s, data_pts) + t2 = unitTangent(e, data_pts) + c11 = sum([A(i, 1, s, e, t1, t2) * A(i, 1, s, e, t1, t2) for i in range(s, e + 1)]) + c12 = sum([A(i, 1, s, e, t1, t2) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)]) + c21 = c12 + c22 = sum([A(i, 2, s, e, t1, t2) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)]) + + x1 = sum([xComponent(i, s, e) * A(i, 1, s, e, t1, t2) for i in range(s, e + 1)]) + x2 = sum([xComponent(i, s, e) * A(i, 2, s, e, t1, t2) for i in range(s, e + 1)]) + + # calculate Determinate of the 3 matrices + det_cc = c11 * c22 - c21 * c12 + det_cx = c11 * x2 - c12 * x1 + det_xc = x1 * c22 - x2 * c12 + + # if matrix is not homogenous, fudge the data a bit + if det_cc == 0: + det_cc = 0.01 + + # alpha's are the correct offset for bezier handles + alpha0 = det_xc / det_cc # offset from right (first) point + alpha1 = det_cx / det_cc # offset from left (last) point + + sRightHandle = data_pts[s].co.copy() + sTangent = t1 * abs(alpha0) + sRightHandle += sTangent # position of first pt's handle + eLeftHandle = data_pts[e].co.copy() + eTangent = t2 * abs(alpha1) + eLeftHandle += eTangent # position of last pt's handle. + + # return a 4 member tuple representing the bezier + return (data_pts[s].co, + sRightHandle, + eLeftHandle, + data_pts[e].co) + + # convert 2 given data points into a cubic bezier. + # handles are offset along the tangent at + # a 3rd of the length between the points. + def fitSingleCubic2Pts(data_pts, s, e): + alpha0 = alpha1 = (data_pts[s].co - data_pts[e].co).length / 3 + + sRightHandle = data_pts[s].co.copy() + sTangent = unitTangent(s, data_pts) * abs(alpha0) + sRightHandle += sTangent # position of first pt's handle + eLeftHandle = data_pts[e].co.copy() + eTangent = unitTangent(e, data_pts) * abs(alpha1) + eLeftHandle += eTangent # position of last pt's handle. + + #return a 4 member tuple representing the bezier + return (data_pts[s].co, + sRightHandle, + eLeftHandle, + data_pts[e].co) + + #evaluate bezier, represented by a 4 member tuple (pts) at point t. + def bezierEval(pts, t): + sumVec = NdVector((0, 0, 0, 0, 0)) + for i in range(4): + sumVec += pts[i] * bernsteinPoly(3, i, t) + return sumVec + + #calculate the highest error between bezier and original data + #returns the distance and the index of the point where max error occurs. + def maxErrorAmount(data_pts, bez, s, e): + maxError = 0 + maxErrorPt = s + if e - s < 3: + return 0, None + for pt in data_pts[s:e + 1]: + bezVal = bezierEval(bez, pt.u) + normalize_error = pt.co.length + if normalize_error == 0: + normalize_error = 1 + tmpError = (pt.co - bezVal).length / normalize_error + if tmpError >= maxError: + maxError = tmpError + maxErrorPt = pt.index + return maxError, maxErrorPt + + #calculated bezier derivative at point t. + #That is, tangent of point t. + def getBezDerivative(bez, t): + n = len(bez) - 1 + sumVec = NdVector((0, 0, 0, 0, 0)) + for i in range(n - 1): + sumVec += (bez[i + 1] - bez[i]) * bernsteinPoly(n - 1, i, t) + return sumVec + + #use Newton-Raphson to find a better paramterization of datapoints, + #one that minimizes the distance (or error) + # between bezier and original data. + def newtonRaphson(data_pts, s, e, bez): + for pt in data_pts[s:e + 1]: + if pt.index == s: + pt.u = 0 + elif pt.index == e: + pt.u = 1 + else: + u = pt.u + qu = bezierEval(bez, pt.u) + qud = getBezDerivative(bez, u) + #we wish to minimize f(u), + #the squared distance between curve and data + fu = (qu - pt.co).length ** 2 + fud = (2 * (qu.x - pt.co.x) * (qud.x)) - (2 * (qu.y - pt.co.y) * (qud.y)) + if fud == 0: + fu = 0 + fud = 1 + pt.u = pt.u - (fu / fud) + + def createDataPts(curveGroup, group_mode): + data_pts = [] + if group_mode: + print([x.data_path for x in curveGroup]) + for i in range(len(curveGroup[0].keyframe_points)): + x = curveGroup[0].keyframe_points[i].co.x + y1 = curveGroup[0].keyframe_points[i].co.y + y2 = curveGroup[1].keyframe_points[i].co.y + y3 = curveGroup[2].keyframe_points[i].co.y + y4 = 0 + if len(curveGroup) == 4: + y4 = curveGroup[3].keyframe_points[i].co.y + data_pts.append(dataPoint(i, NdVector((x, y1, y2, y3, y4)))) + else: + for i in range(len(curveGroup.keyframe_points)): + x = curveGroup.keyframe_points[i].co.x + y1 = curveGroup.keyframe_points[i].co.y + y2 = 0 + y3 = 0 + y4 = 0 + data_pts.append(dataPoint(i, NdVector((x, y1, y2, y3, y4)))) + return data_pts + + def fitCubic(data_pts, s, e): + # if there are less than 3 points, fit a single basic bezier + if e - s < 3: + bez = fitSingleCubic2Pts(data_pts, s, e) + else: + #if there are more, parameterize the points + # and fit a single cubic bezier + chordLength(data_pts, s, e) + bez = fitSingleCubic(data_pts, s, e) + + #calculate max error and point where it occurs + maxError, maxErrorPt = maxErrorAmount(data_pts, bez, s, e) + #if error is small enough, reparameterization might be enough + if maxError < reparaError and maxError > error: + for i in range(maxIterations): + newtonRaphson(data_pts, s, e, bez) + if e - s < 3: + bez = fitSingleCubic2Pts(data_pts, s, e) + else: + bez = fitSingleCubic(data_pts, s, e) + + #recalculate max error and point where it occurs + maxError, maxErrorPt = maxErrorAmount(data_pts, bez, s, e) + + #repara wasn't enough, we need 2 beziers for this range. + #Split the bezier at point of maximum error + if maxError > error: + fitCubic(data_pts, s, maxErrorPt) + fitCubic(data_pts, maxErrorPt, e) + else: + #error is small enough, return the beziers. + beziers.append(bez) + return + + def createNewCurves(curveGroup, beziers, group_mode): + #remove all existing data points + if group_mode: + for fcurve in curveGroup: + for i in range(len(fcurve.keyframe_points) - 1, 0, -1): + fcurve.keyframe_points.remove(fcurve.keyframe_points[i]) + else: + fcurve = curveGroup + for i in range(len(fcurve.keyframe_points) - 1, 0, -1): + fcurve.keyframe_points.remove(fcurve.keyframe_points[i]) + + #insert the calculated beziers to blender data.\ + if group_mode: + for fullbez in beziers: + for i, fcurve in enumerate(curveGroup): + bez = [Vector((vec[0], vec[i + 1])) for vec in fullbez] + newKey = fcurve.keyframe_points.insert(frame=bez[0].x, value=bez[0].y) + newKey.handle_right = (bez[1].x, bez[1].y) + + newKey = fcurve.keyframe_points.insert(frame=bez[3].x, value=bez[3].y) + newKey.handle_left = (bez[2].x, bez[2].y) + else: + for bez in beziers: + for vec in bez: + vec.resize_2d() + newKey = fcurve.keyframe_points.insert(frame=bez[0].x, value=bez[0].y) + newKey.handle_right = (bez[1].x, bez[1].y) + + newKey = fcurve.keyframe_points.insert(frame=bez[3].x, value=bez[3].y) + newKey.handle_left = (bez[2].x, bez[2].y) + + # indices are detached from data point's frame (x) value and + # stored in the dataPoint object, represent a range + + data_pts = createDataPts(curveGroup, group_mode) + + s = 0 # start + e = len(data_pts) - 1 # end + + beziers = [] + + #begin the recursive fitting algorithm. + fitCubic(data_pts, s, e) + #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) + + +def fcurves_simplify(context, obj, sel_opt="all", error=0.002, group_mode=True): + # main vars + fcurves = obj.animation_data.action.fcurves + + if sel_opt == "sel": + sel_fcurves = [fcurve for fcurve in fcurves if fcurve.select] + else: + sel_fcurves = fcurves[:] + + #Error threshold for Newton Raphson reparamatizing + reparaError = error * 32 + maxIterations = 16 + + if group_mode: + fcurveDict = {} + #this loop sorts all the fcurves into groups of 3 or 4, + #based on their RNA Data path, which corresponds to + #which property they effect + for curve in sel_fcurves: + if curve.data_path in fcurveDict: # if this bone has been added, append the curve to its list + fcurveDict[curve.data_path].append(curve) + else: + fcurveDict[curve.data_path] = [curve] # new bone, add a new dict value with this first curve + fcurveGroups = fcurveDict.values() + else: + fcurveGroups = sel_fcurves + + if error > 0.00000: + #simplify every selected curve. + totalt = 0 + for i, fcurveGroup in enumerate(fcurveGroups): + print("Processing curve " + str(i + 1) + "/" + str(len(fcurveGroups))) + t = time.clock() + simplifyCurves(fcurveGroup, error, reparaError, maxIterations, group_mode) + t = time.clock() - t + print(str(t)[:5] + " seconds to process last curve") + totalt += t + print(str(totalt)[:5] + " seconds, total time elapsed") + + return + +# Implementation of non-linear median filter, with variable kernel size +# Double pass - one marks spikes, the other smooths one +# Expects sampled keyframes on everyframe + + +def denoise_median(): + context = bpy.context + obj = context.active_object + fcurves = obj.animation_data.action.fcurves + medKernel = 1 # actually *2+1... since it this is offset + flagKernel = 4 + highThres = (flagKernel * 2) - 1 + lowThres = 0 + for fcurve in fcurves: + orgPts = fcurve.keyframe_points[:] + flaggedFrames = [] + # mark frames that are spikes by sorting a large kernel + for i in range(flagKernel, len(fcurve.keyframe_points) - flagKernel): + center = orgPts[i] + neighborhood = orgPts[i - flagKernel: i + flagKernel] + neighborhood.sort(key=lambda pt: pt.co[1]) + weight = neighborhood.index(center) + if weight >= highThres or weight <= lowThres: + flaggedFrames.append((i, center)) + # clean marked frames with a simple median filter + # averages all frames in the kernel equally, except center which has no weight + for i, pt in flaggedFrames: + newValue = 0 + sumWeights = 0 + neighborhood = [neighpt.co[1] for neighpt in orgPts[i - medKernel: i + medKernel + 1] if neighpt != pt] + newValue = sum(neighborhood) / len(neighborhood) + pt.co[1] = newValue + return + + +def rotate_fix_armature(arm_data): + global_matrix = Matrix.Rotation(radians(90), 4, "X") + bpy.ops.object.mode_set(mode='EDIT', toggle=False) + #disconnect all bones for ease of global rotation + connectedBones = [] + for bone in arm_data.edit_bones: + if bone.use_connect: + connectedBones.append(bone.name) + bone.use_connect = False + + #rotate all the bones around their center + for bone in arm_data.edit_bones: + bone.transform(global_matrix) + + #reconnect the bones + for bone in connectedBones: + arm_data.edit_bones[bone].use_connect = True + bpy.ops.object.mode_set(mode='OBJECT', toggle=False) + + +def scale_fix_armature(performer_obj, enduser_obj): + perf_bones = performer_obj.data.bones + end_bones = enduser_obj.data.bones + + def calculateBoundingRadius(bones): + center = Vector() + for bone in bones: + center += bone.head_local + center /= len(bones) + radius = 0 + for bone in bones: + dist = (bone.head_local - center).length + if dist > radius: + radius = dist + return radius + + perf_rad = calculateBoundingRadius(performer_obj.data.bones) + end_rad = calculateBoundingRadius(enduser_obj.data.bones) + #end_avg = enduser_obj.dimensions + factor = end_rad / perf_rad * 1.2 + performer_obj.scale *= factor + + +def guessMapping(performer_obj, enduser_obj): + perf_bones = performer_obj.data.bones + end_bones = enduser_obj.data.bones + + root = perf_bones[0] + + def findBoneSide(bone): + if "Left" in bone: + return "Left", bone.replace("Left", "").lower().replace(".", "") + if "Right" in bone: + return "Right", bone.replace("Right", "").lower().replace(".", "") + if "L" in bone: + return "Left", bone.replace("Left", "").lower().replace(".", "") + if "R" in bone: + return "Right", bone.replace("Right", "").lower().replace(".", "") + return "", bone + + def nameMatch(bone_a, bone_b): + # nameMatch - recieves two strings, returns 2 if they are relatively the same, 1 if they are the same but R and L and 0 if no match at all + side_a, noside_a = findBoneSide(bone_a) + side_b, noside_b = findBoneSide(bone_b) + if side_a == side_b: + if noside_a in noside_b or noside_b in noside_a: + return 2 + else: + if noside_a in noside_b or noside_b in noside_a: + return 1 + return 0 + + 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 + newPossibleBones = [] + for end_bone in possible_bones: + newPossibleBones += list(end_bone.children) + possible_bones = newPossibleBones + + for child in perf_bone.children: + guessSingleMapping(child) + + guessSingleMapping(root) + + +def limit_dof(context, performer_obj, enduser_obj): + limitDict = {} + perf_bones = [bone for bone in performer_obj.pose.bones if bone.bone.map] + c_frame = context.scene.frame_current + for bone in perf_bones: + limitDict[bone.bone.map] = [1000, 1000, 1000, -1000, -1000, -1000] + for t in range(context.scene.frame_start, context.scene.frame_end): + context.scene.frame_set(t) + for bone in perf_bones: + end_bone = enduser_obj.pose.bones[bone.bone.map] + bake_matrix = bone.matrix + rest_matrix = end_bone.bone.matrix_local + + if end_bone.parent and end_bone.bone.use_inherit_rotation: + srcParent = bone.parent + parent_mat = srcParent.matrix + parent_rest = end_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 + + mat = bake_matrix + euler = mat.to_euler() + limitDict[bone.bone.map][0] = min(limitDict[bone.bone.map][0], euler.x) + limitDict[bone.bone.map][1] = min(limitDict[bone.bone.map][1], euler.y) + limitDict[bone.bone.map][2] = min(limitDict[bone.bone.map][2], euler.z) + limitDict[bone.bone.map][3] = max(limitDict[bone.bone.map][3], euler.x) + limitDict[bone.bone.map][4] = max(limitDict[bone.bone.map][4], euler.y) + limitDict[bone.bone.map][5] = max(limitDict[bone.bone.map][5], euler.z) + for bone in enduser_obj.pose.bones: + existingConstraint = [constraint for constraint in bone.constraints if constraint.name == "DOF Limitation"] + if existingConstraint: + bone.constraints.remove(existingConstraint[0]) + end_bones = [bone for bone in enduser_obj.pose.bones if bone.name in limitDict.keys()] + for bone in end_bones: + #~ if not bone.is_in_ik_chain: + newCons = bone.constraints.new("LIMIT_ROTATION") + newCons.name = "DOF Limitation" + newCons.owner_space = "LOCAL" + newCons.min_x, newCons.min_y, newCons.min_z, newCons.max_x, newCons.max_y, newCons.max_z = limitDict[bone.name] + 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) + + +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"] + if existingConstraint: + bone.constraints.remove(existingConstraint[0]) + + +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() + s = int(s) + e = int(e) + y_s = y_fcurve.evaluate(s) + y_e = y_fcurve.evaluate(e) + direction = (y_e - y_s) / abs(y_e - y_s) + existing_cons = [constraint for constraint in stride_obj.constraints if constraint.type == "FOLLOW_PATH"] + for cons in existing_cons: + stride_obj.constraints.remove(cons) + path_cons = stride_obj.constraints.new("FOLLOW_PATH") + if direction < 0: + path_cons.forward_axis = "TRACK_NEGATIVE_Y" + else: + path_cons.forward_axis = "FORWARD_Y" + path_cons.target = path + path_cons.use_curve_follow = True + path.data.path_duration = e - s + try: + path.data.animation_data.action.fcurves + except AttributeError: + path.data.keyframe_insert("eval_time", frame=0) + eval_time_fcurve = [fcurve for fcurve in path.data.animation_data.action.fcurves if fcurve.data_path == "eval_time"] + eval_time_fcurve = eval_time_fcurve[0] + totalLength = 0 + parameterization = {} + print("evaluating curve") + for t in range(s, e - 1): + if s == t: + chordLength = 0 + else: + chordLength = (y_fcurve.evaluate(t) - y_fcurve.evaluate(t + 1)) + totalLength += chordLength + parameterization[t] = totalLength + for t in range(s + 1, e - 1): + if totalLength == 0: + print("no forward motion") + parameterization[t] /= totalLength + parameterization[t] *= e - s + parameterization[e] = e - s + for t in parameterization.keys(): + eval_time_fcurve.keyframe_points.insert(frame=t, value=parameterization[t]) + y_fcurve.mute = True + print("finished path editing") + + +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!="": + selected_bone = enduser_obj.pose.bones[stitch_settings.stick_bone] + else: + selected_bone = enduser_obj.pose.bones[0] + scene = context.scene + TrackNamesA = enduser_obj.data.mocapNLATracks[action_1] + TrackNamesB = enduser_obj.data.mocapNLATracks[action_2] + enduser_obj.data.active_mocap = action_1 + anim_data = enduser_obj.animation_data + # add tracks for action 2 + mocapAction = bpy.data.actions[TrackNamesB.base_track] + mocapTrack = anim_data.nla_tracks.new() + mocapTrack.name = TrackNamesB.base_track + 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 + constraintTrack = anim_data.nla_tracks.new() + constraintTrack.name = TrackNamesB.auto_fix_track + constraintAction = bpy.data.actions[TrackNamesB.auto_fix_track] + constraintStrip = constraintTrack.strips.new(TrackNamesB.auto_fix_track, stitch_settings.blend_frame, constraintAction) + constraintStrip.extrapolation = "HOLD_FORWARD" + constraintStrip.blend_in = stitch_settings.blend_amount + userTrack = anim_data.nla_tracks.new() + userTrack.name = TrackNamesB.manual_fix_track + userAction = bpy.data.actions[TrackNamesB.manual_fix_track] + userStrip = userTrack.strips.new(TrackNamesB.manual_fix_track, stitch_settings.blend_frame, userAction) + userStrip.extrapolation = "HOLD_FORWARD" + userStrip.blend_in = stitch_settings.blend_amount + #stride bone + if enduser_obj.parent: + if enduser_obj.parent.name == "stride_bone": + stride_bone = enduser_obj.parent + stride_anim_data = stride_bone.animation_data + stride_anim_data.use_nla = True + stride_anim_data.action = None + for track in stride_anim_data.nla_tracks: + stride_anim_data.nla_tracks.remove(track) + actionATrack = stride_anim_data.nla_tracks.new() + actionATrack.name = TrackNamesA.stride_action + actionAStrip = actionATrack.strips.new(TrackNamesA.stride_action, 0, bpy.data.actions[TrackNamesA.stride_action]) + actionAStrip.extrapolation = "NOTHING" + 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.blend_in = stitch_settings.blend_amount + actionBStrip.extrapolation = "NOTHING" + #we need to change the stride_bone's action to add the offset + scene.frame_set(stitch_settings.blend_frame - 1) + desired_pos = (selected_bone.matrix.to_translation() * enduser_obj.matrix_world) + 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 pt in fcurve.keyframe_points: + pt.co.y-=offset[i] + pt.handle_left.y-=offset[i] + pt.handle_right.y-=offset[i] + + +def guess_anim_stitch(context, enduser_obj): + stitch_settings = enduser_obj.data.stitch_settings + action_1 = stitch_settings.first_action + action_2 = stitch_settings.second_action + TrackNamesA = enduser_obj.data.mocapNLATracks[action_1] + TrackNamesB = enduser_obj.data.mocapNLATracks[action_2] + mocapA = bpy.data.actions[TrackNamesA.base_track] + mocapB = bpy.data.actions[TrackNamesB.base_track] + curvesA = mocapA.fcurves + curvesB = mocapB.fcurves + flm, s, data = crossCorrelationMatch(curvesA, curvesB, 10) + print(flm,s) + enduser_obj.data.stitch_settings.blend_frame = flm + enduser_obj.data.stitch_settings.second_offset = s
\ No newline at end of file diff --git a/release/scripts/modules/retarget.py b/release/scripts/modules/retarget.py new file mode 100644 index 00000000000..2c4dcbe6bda --- /dev/null +++ b/release/scripts/modules/retarget.py @@ -0,0 +1,621 @@ +# ##### 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 +import cProfile + + +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()) + #~ 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) + 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) + 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.armature.calculate_roll(type='Z') + 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 + + 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] + + #~ 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) + 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.add() + stride_bone = bpy.context.active_object + stride_bone.name = "stride_bone" + print(stride_bone) + stride_bone.location = enduser_obj_mat.to_translation() + print(linearAvg) + 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) #+ stride_bone.location + 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: + 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 + + +#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)) + #~ empty = bpy.context.active_object + #~ empty.name = end_bone.name + "Org" + #~ empty.empty_draw_size = 0.1 + #~ empty.parent = enduser_obj + 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): + 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 + print(name) + 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 = "MULITPLY" - doesn't work due to work, will be activated soon + 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: + 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 + addLocalRot = False; + if (not bone.bone.use_connect) and (perf_bone!=perf_root): + locks = bone.lock_location + #if not (locks[0] or locks[1] or locks[2]): + cons = bone.constraints.new('COPY_LOCATION') + cons.name = "retargetTemp" + 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 = 'LOCAL' + cons.owner_space = 'LOCAL' + addLocalRot = True + + + cons2 = bone.constraints.new('COPY_ROTATION') + cons2.name = "retargetTemp" + locks = bone.lock_rotation + cons2.use_x = not locks[0] + cons2.use_y = not locks[1] + cons2.use_z = not locks[2] + cons2.target = performer_obj + cons2.subtarget = perf_bone + cons2.target_space = 'WORLD' + cons2.owner_space = 'WORLD' + + if perf_bone==perf_root: + addLocalRot = True + + #~ if addLocalRot: + #~ for constraint in bone.constraints: + #~ if constraint.type == 'COPY_ROTATION': + #~ constraint.target_space = 'LOCAL' + #~ constraint.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 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): + 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 + +if __name__ == "__main__": + cProfile.run("profileWrapper()") diff --git a/release/scripts/presets/ffmpeg/DV.py b/release/scripts/presets/ffmpeg/DV.py index 46d2a0a4a2f..926fb241747 100644 --- a/release/scripts/presets/ffmpeg/DV.py +++ b/release/scripts/presets/ffmpeg/DV.py @@ -11,3 +11,4 @@ else: bpy.context.scene.render.ffmpeg_audio_mixrate = 48000 bpy.context.scene.render.ffmpeg_audio_codec = "PCM" +bpy.context.scene.render.ffmpeg_audio_channels = 2 diff --git a/release/scripts/presets/ffmpeg/DVD.py b/release/scripts/presets/ffmpeg/DVD.py index e18ec9f817b..196b5d68406 100644 --- a/release/scripts/presets/ffmpeg/DVD.py +++ b/release/scripts/presets/ffmpeg/DVD.py @@ -21,3 +21,4 @@ bpy.context.scene.render.ffmpeg_muxrate = 10080000 bpy.context.scene.render.ffmpeg_audio_codec = "AC3" bpy.context.scene.render.ffmpeg_audio_bitrate = 448 bpy.context.scene.render.ffmpeg_audio_mixrate = 48000 +bpy.context.scene.render.ffmpeg_audio_channels = 6 diff --git a/release/scripts/presets/ffmpeg/SVCD.py b/release/scripts/presets/ffmpeg/SVCD.py index c71a3851af0..e4459ab5c5c 100644 --- a/release/scripts/presets/ffmpeg/SVCD.py +++ b/release/scripts/presets/ffmpeg/SVCD.py @@ -21,3 +21,4 @@ bpy.context.scene.render.ffmpeg_muxrate = 0 bpy.context.scene.render.ffmpeg_audio_bitrate = 224 bpy.context.scene.render.ffmpeg_audio_mixrate = 44100 bpy.context.scene.render.ffmpeg_audio_codec = "MP2" +bpy.context.scene.render.ffmpeg_audio_channels = 2 diff --git a/release/scripts/presets/ffmpeg/VCD.py b/release/scripts/presets/ffmpeg/VCD.py index faf27efe9e6..c2b73e682a2 100644 --- a/release/scripts/presets/ffmpeg/VCD.py +++ b/release/scripts/presets/ffmpeg/VCD.py @@ -21,3 +21,4 @@ bpy.context.scene.render.ffmpeg_muxrate = 2352 * 75 * 8 bpy.context.scene.render.ffmpeg_audio_bitrate = 224 bpy.context.scene.render.ffmpeg_audio_mixrate = 44100 bpy.context.scene.render.ffmpeg_audio_codec = "MP2" +bpy.context.scene.render.ffmpeg_audio_channels = 2 diff --git a/release/scripts/startup/bl_operators/nla.py b/release/scripts/startup/bl_operators/nla.py index 44ed846e530..4b4630fdd4e 100644 --- a/release/scripts/startup/bl_operators/nla.py +++ b/release/scripts/startup/bl_operators/nla.py @@ -84,7 +84,7 @@ def bake(frame_start, do_pose=True, do_object=True, do_constraint_clear=False, - ): + action=None): scene = bpy.context.scene obj = bpy.context.object @@ -121,7 +121,8 @@ def bake(frame_start, # incase animation data hassnt been created atd = obj.animation_data_create() - action = bpy.data.actions.new("Action") + if action == None: + action = bpy.data.actions.new("Action") atd.action = action if do_pose: @@ -254,3 +255,38 @@ class BakeAction(Operator): def invoke(self, context, event): wm = context.window_manager return wm.invoke_props_dialog(self) + +################################# + + +class ClearUselessActions(bpy.types.Operator): + '''Mark actions with no F-Curves for deletion after save+reload of file preserving "action libraries"''' + bl_idname = "anim.clear_useless_actions" + bl_label = "Clear Useless Actions" + bl_options = {'REGISTER', 'UNDO'} + + only_unused = BoolProperty(name="Only Unused", + description="Only unused (Fake User only) actions get considered", + default=True) + + @classmethod + def poll(cls, context): + return len(bpy.data.actions) != 0 + + def execute(self, context): + removed = 0 + + for action in bpy.data.actions: + # if only user is "fake" user... + if ((self.only_unused is False) or + (action.use_fake_user and action.users == 1)): + + # if it has F-Curves, then it's a "action library" (i.e. walk, wave, jump, etc.) + # and should be left alone as that's what fake users are for! + if not action.fcurves: + # mark action for deletion + action.user_clear() + removed += 1 + + self.report({'INFO'}, "Removed %d empty and/or fake-user only Actions" % (removed)) + return {'FINISHED'} diff --git a/release/scripts/startup/bl_operators/object.py b/release/scripts/startup/bl_operators/object.py index 79f57990f37..413f0e8db9e 100644 --- a/release/scripts/startup/bl_operators/object.py +++ b/release/scripts/startup/bl_operators/object.py @@ -681,3 +681,44 @@ class ClearAllRestrictRender(Operator): for obj in context.scene.objects: obj.hide_render = False return {'FINISHED'} + +class TransformsToDeltasAnim(bpy.types.Operator): + '''Convert object animation for normal transforms to delta transforms''' + bl_idname = "object.anim_transforms_to_deltas" + bl_label = "Animated Transforms to Deltas" + bl_options = {'REGISTER', 'UNDO'} + + @classmethod + def poll(cls, context): + obs = context.selected_editable_objects + return (obs is not None) + + def execute(self, context): + for obj in context.selected_editable_objects: + # get animation data + adt = obj.animation_data + if (adt is None) or (adt.action is None): + self.report({'WARNING'}, "No animation data to convert on object: " + obj.name) + continue + + # if F-Curve uses standard transform path, just append "delta_" to this path + for fcu in adt.action.fcurves: + if fcu.data_path == "location": + fcu.data_path = "delta_location" + obj.location.zero() + elif fcu.data_path == "rotation_euler": + fcu.data_path = "delta_rotation_euler" + obj.rotation_euler.zero() + elif fcu.data_path == "rotation_quaternion": + fcu.data_path = "delta_rotation_quaternion" + obj.rotation_quaternion.identity() + #elif fcu.data_path == "rotation_axis_angle": # XXX: currently not implemented + # fcu.data_path = "delta_rotation_axis_angle" + elif fcu.data_path == "scale": + fcu.data_path = "delta_scale" + obj.scale = (1, 1, 1) + + # hack: force animsys flush by changing frame, so that deltas get run + context.scene.frame_set(context.scene.frame_current) + + return {'FINISHED'} diff --git a/release/scripts/startup/bl_ui/__init__.py b/release/scripts/startup/bl_ui/__init__.py index bf63c6071b9..5fab3b7fd38 100644 --- a/release/scripts/startup/bl_ui/__init__.py +++ b/release/scripts/startup/bl_ui/__init__.py @@ -36,6 +36,7 @@ _modules = ( "properties_data_mesh", "properties_data_metaball", "properties_data_modifier", + "properties_data_speaker", "properties_game", "properties_material", "properties_object_constraint", diff --git a/release/scripts/startup/bl_ui/properties_data_armature.py b/release/scripts/startup/bl_ui/properties_data_armature.py index 94c40d11141..cddf9fef0f2 100644 --- a/release/scripts/startup/bl_ui/properties_data_armature.py +++ b/release/scripts/startup/bl_ui/properties_data_armature.py @@ -71,6 +71,9 @@ class DATA_PT_skeleton(ArmatureButtonsPanel, Panel): flow.prop(arm, "use_deform_envelopes", text="Envelopes") flow.prop(arm, "use_deform_preserve_volume", text="Quaternion") + if context.scene.render.engine == "BLENDER_GAME": + col = layout.column() + col.prop(arm, "vert_deformer") class DATA_PT_display(ArmatureButtonsPanel, Panel): bl_label = "Display" @@ -97,6 +100,15 @@ class DATA_PT_display(ArmatureButtonsPanel, Panel): col.prop(arm, "use_deform_delay", text="Delay Refresh") +class DATA_PT_bone_group_specials(Menu): + bl_label = "Bone Group Specials" + + def draw(self, context): + layout = self.layout + + layout.operator("pose.group_sort", icon='SORTALPHA') + + class DATA_PT_bone_groups(ArmatureButtonsPanel, Panel): bl_label = "Bone Groups" @@ -109,16 +121,25 @@ class DATA_PT_bone_groups(ArmatureButtonsPanel, Panel): ob = context.object pose = ob.pose + group = pose.bone_groups.active row = layout.row() - row.template_list(pose, "bone_groups", pose.bone_groups, "active_index", rows=2) + + rows = 2 + if group: + rows = 5 + row.template_list(pose, "bone_groups", pose.bone_groups, "active_index", rows=rows) col = row.column(align=True) col.active = (ob.proxy is None) col.operator("pose.group_add", icon='ZOOMIN', text="") col.operator("pose.group_remove", icon='ZOOMOUT', text="") + col.menu("DATA_PT_bone_group_specials", icon='DOWNARROW_HLT', text="") + if group: + col.separator() + col.operator("pose.group_move", icon='TRIA_UP', text="").direction = 'UP' + col.operator("pose.group_move", icon='TRIA_DOWN', text="").direction = 'DOWN' - group = pose.bone_groups.active if group: col = layout.column() col.active = (ob.proxy is None) diff --git a/release/scripts/startup/bl_ui/properties_data_speaker.py b/release/scripts/startup/bl_ui/properties_data_speaker.py new file mode 100644 index 00000000000..fe9f798af0c --- /dev/null +++ b/release/scripts/startup/bl_ui/properties_data_speaker.py @@ -0,0 +1,129 @@ +# ##### 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 rna_prop_ui import PropertyPanel + + +class DataButtonsPanel(): + bl_space_type = 'PROPERTIES' + bl_region_type = 'WINDOW' + bl_context = "data" + + @classmethod + def poll(cls, context): + engine = context.scene.render.engine + return context.speaker and (engine in cls.COMPAT_ENGINES) + + +class DATA_PT_context_speaker(DataButtonsPanel, bpy.types.Panel): + bl_label = "" + bl_options = {'HIDE_HEADER'} + COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_GAME'} + + def draw(self, context): + layout = self.layout + + ob = context.object + speaker = context.speaker + space = context.space_data + + split = layout.split(percentage=0.65) + + if ob: + split.template_ID(ob, "data") + elif speaker: + split.template_ID(space, "pin_id") + + +class DATA_PT_speaker(DataButtonsPanel, bpy.types.Panel): + bl_label = "Sound" + COMPAT_ENGINES = {'BLENDER_RENDER'} + + def draw(self, context): + layout = self.layout + + speaker = context.speaker + + split = layout.split(percentage=0.75) + + split.template_ID(speaker, "sound", open="sound.open_mono") + split.prop(speaker, "muted") + + split = layout.split() + + row = split.row() + + row.prop(speaker, "volume") + row.prop(speaker, "pitch") + + +class DATA_PT_distance(DataButtonsPanel, bpy.types.Panel): + bl_label = "Distance" + COMPAT_ENGINES = {'BLENDER_RENDER'} + + def draw(self, context): + layout = self.layout + + speaker = context.speaker + + split = layout.split() + col = split.column() + + col.label("Volume:") + col.prop(speaker, "volume_min", text="Minimum") + col.prop(speaker, "volume_max", text="Maximum") + col.prop(speaker, "attenuation") + + col = split.column() + + col.label("Distance:") + col.prop(speaker, "distance_max", text="Maximum") + col.prop(speaker, "distance_reference", text="Reference") + + +class DATA_PT_cone(DataButtonsPanel, bpy.types.Panel): + bl_label = "Cone" + COMPAT_ENGINES = {'BLENDER_RENDER'} + + def draw(self, context): + layout = self.layout + + speaker = context.speaker + + split = layout.split() + col = split.column() + + col.label("Angle:") + col.prop(speaker, "cone_angle_outer", text="Outer") + col.prop(speaker, "cone_angle_inner", text="Inner") + + col = split.column() + + col.label("Volume:") + col.prop(speaker, "cone_volume_outer", text="Outer") + + +class DATA_PT_custom_props_speaker(DataButtonsPanel, PropertyPanel, bpy.types.Panel): + COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_GAME'} + _context_path = "object.data" + _property_type = bpy.types.Speaker + +if __name__ == "__main__": # only for live edit. + bpy.utils.register_module(__name__) diff --git a/release/scripts/startup/bl_ui/properties_game.py b/release/scripts/startup/bl_ui/properties_game.py index f8be32e6c07..e3c576e7093 100644 --- a/release/scripts/startup/bl_ui/properties_game.py +++ b/release/scripts/startup/bl_ui/properties_game.py @@ -343,6 +343,7 @@ class RENDER_PT_game_performance(RenderButtonsPanel, Panel): row = layout.row() row.prop(gs, "use_frame_rate") row.prop(gs, "use_display_lists") + row.prop(gs, "restrict_animation_updates") class RENDER_PT_game_display(RenderButtonsPanel, Panel): @@ -361,21 +362,6 @@ class RENDER_PT_game_display(RenderButtonsPanel, Panel): flow.prop(gs, "show_mouse", text="Mouse Cursor") -class RENDER_PT_game_sound(RenderButtonsPanel, Panel): - bl_label = "Sound" - COMPAT_ENGINES = {'BLENDER_GAME'} - - def draw(self, context): - layout = self.layout - - scene = context.scene - - layout.prop(scene, "audio_distance_model") - - layout.prop(scene, "audio_doppler_speed", text="Speed") - layout.prop(scene, "audio_doppler_factor") - - class WorldButtonsPanel(): bl_space_type = 'PROPERTIES' bl_region_type = 'WINDOW' diff --git a/release/scripts/startup/bl_ui/properties_object_constraint.py b/release/scripts/startup/bl_ui/properties_object_constraint.py index 867abe4dd5d..c74a0000499 100644 --- a/release/scripts/startup/bl_ui/properties_object_constraint.py +++ b/release/scripts/startup/bl_ui/properties_object_constraint.py @@ -477,6 +477,11 @@ class ConstraintButtonsPanel(): row.label(text="Clamp Region:") row.prop(con, "limit_mode", text="") + row = layout.row() + row.prop(con, "use_transform_limit") + row.label() + + def STRETCH_TO(self, context, layout, con): self.target_template(layout, con) diff --git a/release/scripts/startup/bl_ui/properties_render.py b/release/scripts/startup/bl_ui/properties_render.py index fb14372ebea..c906013e094 100644 --- a/release/scripts/startup/bl_ui/properties_render.py +++ b/release/scripts/startup/bl_ui/properties_render.py @@ -596,9 +596,8 @@ class RENDER_PT_encoding(RenderButtonsPanel, Panel): col = split.column() col.prop(rd, "ffmpeg_audio_bitrate") - col.prop(rd, "ffmpeg_audio_mixrate") - - split.prop(rd, "ffmpeg_audio_volume", slider=True) + col = split.column() + col.prop(rd, "ffmpeg_audio_volume", slider=True) class RENDER_PT_bake(RenderButtonsPanel, Panel): diff --git a/release/scripts/startup/bl_ui/properties_scene.py b/release/scripts/startup/bl_ui/properties_scene.py index 6e96e1228e7..fd7fc8ed462 100644 --- a/release/scripts/startup/bl_ui/properties_scene.py +++ b/release/scripts/startup/bl_ui/properties_scene.py @@ -44,6 +44,36 @@ class SCENE_PT_scene(SceneButtonsPanel, Panel): layout.prop(scene, "background_set", text="Background") +class SCENE_PT_audio(SceneButtonsPanel, Panel): + bl_label = "Audio" + COMPAT_ENGINES = {'BLENDER_RENDER', 'BLENDER_GAME'} + + def draw(self, context): + layout = self.layout + scene = context.scene + rd = context.scene.render + + layout.prop(scene, "audio_volume") + layout.operator("sound.bake_animation") + + split = layout.split() + + col = split.column() + + col.label("Listener:") + col.prop(scene, "audio_distance_model", text="") + col.prop(scene, "audio_doppler_speed", text="Speed") + col.prop(scene, "audio_doppler_factor", text="Doppler") + + col = split.column() + + col.label("Format:") + col.prop(rd, "ffmpeg_audio_channels", text="") + col.prop(rd, "ffmpeg_audio_mixrate", text="Rate") + + layout.operator("sound.mixdown") + + class SCENE_PT_unit(SceneButtonsPanel, Panel): bl_label = "Units" COMPAT_ENGINES = {'BLENDER_RENDER'} diff --git a/release/scripts/startup/bl_ui/space_dopesheet.py b/release/scripts/startup/bl_ui/space_dopesheet.py index dfbd7b3ae14..cab58a3aadb 100644 --- a/release/scripts/startup/bl_ui/space_dopesheet.py +++ b/release/scripts/startup/bl_ui/space_dopesheet.py @@ -34,14 +34,33 @@ def dopesheet_filter(layout, context, genericFiltersOnly=False): row.prop(dopesheet, "show_only_selected", text="") row.prop(dopesheet, "show_hidden", text="") + if is_nla: + row.prop(dopesheet, "show_missing_nla", text="") + if not genericFiltersOnly: + if bpy.data.groups: + row = layout.row(align=True) + row.prop(dopesheet, "show_only_group_objects", text="") + if dopesheet.show_only_group_objects: + row.prop(dopesheet, "filter_group", text="") + + if not is_nla: row = layout.row(align=True) - row.prop(dopesheet, "show_transforms", text="") + row.prop(dopesheet, "show_only_matching_fcurves", text="") + if dopesheet.show_only_matching_fcurves: + row.prop(dopesheet, "filter_fcurve_name", text="") - if is_nla: - row.prop(dopesheet, "show_missing_nla", text="") + row = layout.row() + row.prop(dopesheet, "show_datablock_filters", text="Filters", icon='DISCLOSURE_TRI_RIGHT') - row = layout.row(align=True) + if (not genericFiltersOnly) and (dopesheet.show_datablock_filters): + # TODO: put a box around these? + subrow = row.row() + + row = subrow.row(align=True) + row.prop(dopesheet, "show_transforms", text="") + + row = subrow.row(align=True) row.prop(dopesheet, "show_scenes", text="") row.prop(dopesheet, "show_worlds", text="") row.prop(dopesheet, "show_nodes", text="") @@ -68,18 +87,8 @@ def dopesheet_filter(layout, context, genericFiltersOnly=False): row.prop(dopesheet, "show_armatures", text="") if bpy.data.particles: row.prop(dopesheet, "show_particles", text="") - - if bpy.data.groups: - row = layout.row(align=True) - row.prop(dopesheet, "show_only_group_objects", text="") - if dopesheet.show_only_group_objects: - row.prop(dopesheet, "filter_group", text="") - - if not is_nla: - row = layout.row(align=True) - row.prop(dopesheet, "show_only_matching_fcurves", text="") - if dopesheet.show_only_matching_fcurves: - row.prop(dopesheet, "filter_fcurve_name", text="") + if bpy.data.speakers: + row.prop(dopesheet, "show_speakers", text="") ####################################### @@ -277,7 +286,7 @@ class DOPESHEET_MT_key(Menu): layout.operator("action.keyframe_insert") layout.separator() - layout.operator("action.duplicate") + layout.operator("action.duplicate_move") layout.operator("action.delete") layout.separator() diff --git a/release/scripts/startup/bl_ui/space_graph.py b/release/scripts/startup/bl_ui/space_graph.py index c379ea95ea2..d4b8c415a7f 100644 --- a/release/scripts/startup/bl_ui/space_graph.py +++ b/release/scripts/startup/bl_ui/space_graph.py @@ -78,7 +78,7 @@ class GRAPH_MT_view(Menu): layout.prop(st, "use_auto_merge_keyframes") layout.separator() - layout.prop(st, "use_fancy_drawing") + layout.prop(st, "use_beauty_drawing") layout.separator() if st.show_handles: @@ -206,7 +206,7 @@ class GRAPH_MT_key(Menu): layout.operator("graph.sound_bake") layout.separator() - layout.operator("graph.duplicate") + layout.operator("graph.duplicate_move") layout.operator("graph.delete") layout.separator() diff --git a/release/scripts/startup/bl_ui/space_info.py b/release/scripts/startup/bl_ui/space_info.py index 38c1e24f27e..28990328c46 100644 --- a/release/scripts/startup/bl_ui/space_info.py +++ b/release/scripts/startup/bl_ui/space_info.py @@ -292,6 +292,9 @@ class INFO_MT_add(Menu): layout.operator("object.add", text="Empty", icon='OUTLINER_OB_EMPTY').type = 'EMPTY' layout.separator() + layout.operator("object.speaker_add", text="Speaker", icon='OUTLINER_OB_SPEAKER') + layout.separator() + layout.operator("object.camera_add", text="Camera", icon='OUTLINER_OB_CAMERA') layout.operator_context = 'EXEC_SCREEN' layout.operator_menu_enum("object.lamp_add", "type", text="Lamp", icon='OUTLINER_OB_LAMP') diff --git a/release/scripts/startup/bl_ui/space_nla.py b/release/scripts/startup/bl_ui/space_nla.py index c69af2c9a60..1d4b7c6828f 100644 --- a/release/scripts/startup/bl_ui/space_nla.py +++ b/release/scripts/startup/bl_ui/space_nla.py @@ -69,7 +69,11 @@ class NLA_MT_view(Menu): layout.separator() layout.operator("anim.previewrange_set") layout.operator("anim.previewrange_clear") - + + layout.separator() + layout.operator("nla.view_all") + layout.operator("nla.view_selected") + layout.separator() layout.operator("screen.area_dupli") layout.operator("screen.screen_full_area") @@ -162,6 +166,7 @@ class NLA_MT_add(Menu): layout.operator("nla.actionclip_add") layout.operator("nla.transition_add") + layout.operator("nla.soundclip_add") layout.separator() layout.operator("nla.meta_add") diff --git a/release/scripts/startup/bl_ui/space_sequencer.py b/release/scripts/startup/bl_ui/space_sequencer.py index 84cc365425e..f796ce8da5f 100644 --- a/release/scripts/startup/bl_ui/space_sequencer.py +++ b/release/scripts/startup/bl_ui/space_sequencer.py @@ -112,7 +112,11 @@ class SEQUENCER_MT_view(Menu): layout.operator("sequencer.view_selected") - layout.prop(st, "show_frames") + if st.show_frames: + layout.operator("anim.time_toggle", text="Show Seconds") + else: + layout.operator("anim.time_toggle", text="Show Frames") + layout.prop(st, "show_frame_indicator") if st.display_mode == 'IMAGE': layout.prop(st, "show_safe_margin") @@ -221,6 +225,7 @@ class SEQUENCER_MT_add_effect(Menu): layout.operator("sequencer.effect_strip_add", text="Speed Control").type = 'SPEED' layout.operator("sequencer.effect_strip_add", text="Multicam Selector").type = 'MULTICAM' layout.operator("sequencer.effect_strip_add", text="Adjustment Layer").type = 'ADJUSTMENT' + layout.operator("sequencer.effect_strip_add", text="Title Card").type = 'TITLE_CARD' class SEQUENCER_MT_strip(Menu): @@ -402,7 +407,7 @@ class SEQUENCER_PT_effect(SequencerButtonsPanel, Panel): 'CROSS', 'GAMMA_CROSS', 'MULTIPLY', 'OVER_DROP', 'PLUGIN', 'WIPE', 'GLOW', 'TRANSFORM', 'COLOR', 'SPEED', - 'MULTICAM', 'ADJUSTMENT'} + 'MULTICAM', 'ADJUSTMENT', 'TITLE_CARD'} def draw(self, context): layout = self.layout @@ -470,6 +475,11 @@ class SEQUENCER_PT_effect(SequencerButtonsPanel, Panel): row.label("Cut To") for i in range(1, strip.channel): row.operator("sequencer.cut_multicam", text=str(i)).camera = i + elif strip.type == "TITLE_CARD": + layout.prop(strip, "title") + layout.prop(strip, "subtitle") + layout.prop(strip, "color_foreground") + layout.prop(strip, "color_background") col = layout.column(align=True) if strip.type == 'SPEED': @@ -541,7 +551,8 @@ class SEQUENCER_PT_input(SequencerButtonsPanel, Panel): 'CROSS', 'GAMMA_CROSS', 'MULTIPLY', 'OVER_DROP', 'PLUGIN', 'WIPE', 'GLOW', 'TRANSFORM', 'COLOR', - 'MULTICAM', 'SPEED', 'ADJUSTMENT'} + 'MULTICAM', 'SPEED', 'ADJUSTMENT', + 'TITLE_CARD'} def draw(self, context): layout = self.layout @@ -640,8 +651,11 @@ class SEQUENCER_PT_sound(SequencerButtonsPanel, Panel): row.prop(strip.sound, "use_memory_cache") + layout.prop(strip, "waveform") layout.prop(strip, "volume") layout.prop(strip, "attenuation") + layout.prop(strip, "pitch") + layout.prop(strip, "pan") col = layout.column(align=True) col.label(text="Trim Duration:") @@ -700,7 +714,8 @@ class SEQUENCER_PT_filter(SequencerButtonsPanel, Panel): 'CROSS', 'GAMMA_CROSS', 'MULTIPLY', 'OVER_DROP', 'PLUGIN', 'WIPE', 'GLOW', 'TRANSFORM', 'COLOR', - 'MULTICAM', 'SPEED', 'ADJUSTMENT'} + 'MULTICAM', 'SPEED', 'ADJUSTMENT', + 'TITLE_CARD'} def draw(self, context): layout = self.layout diff --git a/release/scripts/startup/bl_ui/space_userpref.py b/release/scripts/startup/bl_ui/space_userpref.py index 148338368fe..732a38308b0 100644 --- a/release/scripts/startup/bl_ui/space_userpref.py +++ b/release/scripts/startup/bl_ui/space_userpref.py @@ -746,6 +746,7 @@ class USERPREF_PT_file(Panel): col.prop(paths, "save_version") col.prop(paths, "recent_files") + col.prop(paths, "use_update_recent_files_on_load") col.prop(paths, "use_save_preview_images") col.label(text="Auto Save:") col.prop(paths, "use_auto_save_temporary_files") diff --git a/release/scripts/startup/bl_ui/space_view3d.py b/release/scripts/startup/bl_ui/space_view3d.py index fa22e216ec9..c1add444bf1 100644 --- a/release/scripts/startup/bl_ui/space_view3d.py +++ b/release/scripts/startup/bl_ui/space_view3d.py @@ -179,6 +179,10 @@ class VIEW3D_MT_transform(Menu): layout.operator("object.randomize_transform") layout.operator("object.align") + + layout.separator() + + layout.operator("object.anim_transforms_to_deltas") class VIEW3D_MT_mirror(Menu): @@ -1259,12 +1263,15 @@ class VIEW3D_MT_pose_transform(Menu): layout.operator("pose.transforms_clear", text="All") + layout.separator() + layout.operator("pose.loc_clear", text="Location") layout.operator("pose.rot_clear", text="Rotation") layout.operator("pose.scale_clear", text="Scale") - layout.label(text="Origin") + layout.separator() + layout.operator("pose.user_transforms_clear", text="Reset unkeyed") class VIEW3D_MT_pose_slide(Menu): bl_label = "In-Betweens" diff --git a/release/scripts/startup/ui_mocap.py b/release/scripts/startup/ui_mocap.py new file mode 100644 index 00000000000..23354f9d722 --- /dev/null +++ b/release/scripts/startup/ui_mocap.py @@ -0,0 +1,842 @@ +# ##### 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 bpy.props import * +from bpy import * +import mocap_constraints +import retarget +import mocap_tools + +### reloads modules (for testing purposes only) +from imp import reload +reload(mocap_constraints) +reload(retarget) +reload(mocap_tools) + +from mocap_constraints import * + +# MocapConstraint class +# Defines MocapConstraint datatype, used to add and configute mocap constraints +# Attached to Armature data + + +class MocapConstraint(bpy.types.PropertyGroup): + name = bpy.props.StringProperty(name="Name", + default="Mocap Fix", + description="Name of Mocap Fix", + update=setConstraint) + constrained_bone = bpy.props.StringProperty(name="Bone", + default="", + description="Constrained Bone", + update=updateConstraintBoneType) + constrained_boneB = bpy.props.StringProperty(name="Bone (2)", + default="", + description="Other Constrained Bone (optional, depends on type)", + update=setConstraint) + s_frame = bpy.props.IntProperty(name="S", + default=0, + description="Start frame of Fix", + update=setConstraint) + e_frame = bpy.props.IntProperty(name="E", + default=100, + description="End frame of Fix", + update=setConstraint) + smooth_in = bpy.props.IntProperty(name="In", + default=10, + description="Amount of frames to smooth in", + update=setConstraint, + min=0) + smooth_out = bpy.props.IntProperty(name="Out", + default=10, + description="Amount of frames to smooth out", + update=setConstraint, + min=0) + targetMesh = bpy.props.StringProperty(name="Mesh", + default="", + description="Target of Fix - Mesh (optional, depends on type)", + update=setConstraint) + active = bpy.props.BoolProperty(name="Active", + default=True, + description="Fix is active", + update=setConstraint) + show_expanded = bpy.props.BoolProperty(name="Show Expanded", + default=True, + description="Fix is fully shown") + targetPoint = bpy.props.FloatVectorProperty(name="Point", size=3, + subtype="XYZ", default=(0.0, 0.0, 0.0), + description="Target of Fix - Point", + update=setConstraint) + targetDist = bpy.props.FloatProperty(name="Offset", + default=0.0, + description="Distance and Floor Fixes - Desired offset", + update=setConstraint) + targetSpace = bpy.props.EnumProperty( + items=[("WORLD", "World Space", "Evaluate target in global space"), + ("LOCAL", "Object space", "Evaluate target in object space"), + ("constrained_boneB", "Other Bone Space", "Evaluate target in specified other bone space")], + name="Space", + description="In which space should Point type target be evaluated", + update=setConstraint) + type = bpy.props.EnumProperty(name="Type of constraint", + items=[("point", "Maintain Position", "Bone is at a specific point"), + ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"), + ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"), + ("distance", "Maintain distance", "Target bones maintained specified distance")], + description="Type of Fix", + update=updateConstraintBoneType) + real_constraint = bpy.props.StringProperty() + real_constraint_bone = bpy.props.StringProperty() + + +bpy.utils.register_class(MocapConstraint) + +bpy.types.Armature.mocap_constraints = bpy.props.CollectionProperty(type=MocapConstraint) + + +class AnimationStitchSettings(bpy.types.PropertyGroup): + first_action = bpy.props.StringProperty(name="Action 1", + description="First action in stitch") + second_action = bpy.props.StringProperty(name="Action 2", + description="Second action in stitch") + blend_frame = bpy.props.IntProperty(name="Stitch frame", + description="Frame to locate stitch on") + blend_amount = bpy.props.IntProperty(name="Blend amount", + description="Size of blending transitiion, on both sides of the stitch", + default=10) + second_offset = bpy.props.IntProperty(name="Second offset", + description="Frame offset for 2nd animation, where it should start", + default=10) + stick_bone = bpy.props.StringProperty(name="Stick Bone", + description="Bone to freeze during transition", + default="") + +bpy.utils.register_class(AnimationStitchSettings) + + +class MocapNLATracks(bpy.types.PropertyGroup): + name = bpy.props.StringProperty() + base_track = bpy.props.StringProperty() + auto_fix_track = bpy.props.StringProperty() + manual_fix_track = bpy.props.StringProperty() + stride_action = bpy.props.StringProperty() + +bpy.utils.register_class(MocapNLATracks) + + +def advancedRetargetToggle(self, context): + 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") + return + else: + performer_obj = performer_obj[0] + if self.advancedRetarget: + retarget.preAdvancedRetargeting(performer_obj, enduser_obj) + else: + retarget.cleanTempConstraints(enduser_obj) + + + +bpy.types.Armature.stitch_settings = bpy.props.PointerProperty(type=AnimationStitchSettings) +bpy.types.Armature.active_mocap = bpy.props.StringProperty(update=retarget.NLASystemInitialize) +bpy.types.Armature.mocapNLATracks = bpy.props.CollectionProperty(type=MocapNLATracks) +bpy.types.Armature.advancedRetarget = bpy.props.BoolProperty(default=False, update=advancedRetargetToggle) +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): + if self.IKRetarget: + if not self.is_in_ik_chain: + print(self.name + " IK toggled ON!") + ik = self.constraints.new('IK') + #ik the whole chain up to the root, excluding + chainLen = 0 + for parent_bone in self.parent_recursive: + chainLen += 1 + if hasIKConstraint(parent_bone): + break + deformer_children = [child for child in parent_bone.children if child.bone.use_deform] + #~ if len(deformer_children) > 1: + #~ break + ik.chain_count = chainLen + for bone in self.parent_recursive: + if bone.is_in_ik_chain: + bone.IKRetarget = True + else: + print(self.name + " IK toggled OFF!") + cnstrn_bones = [] + newChainLength = [] + if hasIKConstraint(self): + cnstrn_bones = [self] + elif self.is_in_ik_chain: + cnstrn_bones = [child for child in self.children_recursive if hasIKConstraint(child)] + for cnstrn_bone in cnstrn_bones: + newChainLength.append(cnstrn_bone.parent_recursive.index(self) + 1) + if cnstrn_bones: + # remove constraint, and update IK retarget for all parents of cnstrn_bone up to chain_len + for i, cnstrn_bone in enumerate(cnstrn_bones): + print(cnstrn_bone.name) + if newChainLength: + ik = hasIKConstraint(cnstrn_bone) + ik.chain_count = newChainLength[i] + else: + ik = hasIKConstraint(cnstrn_bone) + cnstrn_bone.constraints.remove(ik) + cnstrn_bone.IKRetarget = False + for bone in cnstrn_bone.parent_recursive: + if not bone.is_in_ik_chain: + bone.IKRetarget = False + + +class MocapMapping(bpy.types.PropertyGroup): + name = bpy.props.StringProperty() + +bpy.utils.register_class(MocapMapping) + +bpy.types.Bone.map = bpy.props.StringProperty() +bpy.types.Bone.reverseMap = bpy.props.CollectionProperty(type=MocapMapping) +bpy.types.Bone.foot = bpy.props.BoolProperty(name="Foot", + description="Marks this bone as a 'foot', which determines retargeted animation's translation", + default=False) +bpy.types.Bone.twistFix = bpy.props.BoolProperty(name="Twist Fix", + description="Fix Twist on this bone", + default=False) +bpy.types.PoseBone.IKRetarget = bpy.props.BoolProperty(name="IK", + description="Toggles IK Retargeting method for given bone", + update=toggleIKBone, default=False) + + +def updateIKRetarget(): + # ensures that Blender constraints and IK properties are in sync + # currently runs when module is loaded, should run when scene is loaded + # or user adds a constraint to armature. Will be corrected in the future, + # once python callbacks are implemented + for obj in bpy.data.objects: + if obj.pose: + bones = obj.pose.bones + for pose_bone in bones: + if pose_bone.is_in_ik_chain or hasIKConstraint(pose_bone): + pose_bone.IKRetarget = True + else: + pose_bone.IKRetarget = False + +updateIKRetarget() + + +class MocapPanel(bpy.types.Panel): + # Motion capture retargeting panel + bl_label = "Mocap tools" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + self.layout.label("Preprocessing") + row = self.layout.row(align=True) + row.alignment = 'EXPAND' + row.operator("mocap.samples", text='Samples to Beziers') + row.operator("mocap.denoise", text='Clean noise') + row.operator("mocap.rotate_fix", text='Fix BVH Axis Orientation') + row.operator("mocap.scale_fix", text='Auto scale Performer') + row2 = self.layout.row(align=True) + row2.operator("mocap.looper", text='Loop animation') + row2.operator("mocap.limitdof", text='Constrain Rig') + row2.operator("mocap.removelimitdof", text='Unconstrain Rig') + self.layout.label("Retargeting") + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj] + if enduser_obj is None or len(performer_obj) != 1: + self.layout.label("Select performer rig and target rig (as active)") + else: + self.layout.operator("mocap.guessmapping", text="Guess Hiearchy Mapping") + row3 = self.layout.row(align=True) + column1 = row3.column(align=True) + column1.label("Performer Rig") + column2 = row3.column(align=True) + column2.label("Enduser Rig") + performer_obj = performer_obj[0] + if performer_obj.data and enduser_obj.data: + if performer_obj.data.name in bpy.data.armatures and enduser_obj.data.name in bpy.data.armatures: + perf = performer_obj.data + enduser_arm = enduser_obj.data + perf_pose_bones = enduser_obj.pose.bones + for bone in perf.bones: + row = self.layout.row() + row.prop(data=bone, property='foot', text='', icon='POSE_DATA') + row.label(bone.name) + row.prop_search(bone, "map", enduser_arm, "bones") + row.operator("mocap.selectmap", text='', icon='CURSOR').perf_bone = bone.name + label_mod = "FK" + if bone.map: + pose_bone = perf_pose_bones[bone.map] + if pose_bone.is_in_ik_chain: + label_mod = "ik chain" + if hasIKConstraint(pose_bone): + label_mod = "ik end" + row.prop(data=bone, property='twistFix', text='', icon='RNA') + row.prop(pose_bone, 'IKRetarget') + row.label(label_mod) + else: + row.label(" ") + row.label(" ") + mapRow = self.layout.row() + mapRow.operator("mocap.savemapping", text='Save mapping') + mapRow.operator("mocap.loadmapping", text='Load mapping') + self.layout.prop(data=performer_obj.animation_data.action, property='name', text='Action Name') + self.layout.prop(enduser_arm, "advancedRetarget", text='Advanced Retarget') + self.layout.prop(enduser_arm, "frameStep") + self.layout.operator("mocap.retarget", text='RETARGET!') + + +class MocapConstraintsPanel(bpy.types.Panel): + #Motion capture constraints panel + bl_label = "Mocap Fixes" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + layout = self.layout + if context.active_object: + if context.active_object.data: + if context.active_object.data.name in bpy.data.armatures: + enduser_obj = context.active_object + enduser_arm = enduser_obj.data + layout.operator_menu_enum("mocap.addmocapfix", "type") + layout.operator("mocap.updateconstraints", text='Update Fixes') + bakeRow = layout.row() + bakeRow.operator("mocap.bakeconstraints", text='Bake Fixes') + bakeRow.operator("mocap.unbakeconstraints", text='Unbake Fixes') + layout.separator() + for i, m_constraint in enumerate(enduser_arm.mocap_constraints): + box = layout.box() + headerRow = box.row() + headerRow.prop(m_constraint, 'show_expanded', text='', icon='TRIA_DOWN' if m_constraint.show_expanded else 'TRIA_RIGHT', emboss=False) + headerRow.prop(m_constraint, 'type', text='') + headerRow.prop(m_constraint, 'name', text='') + headerRow.prop(m_constraint, 'active', icon='MUTE_IPO_ON' if not m_constraint.active else'MUTE_IPO_OFF', text='', emboss=False) + headerRow.operator("mocap.removeconstraint", text="", icon='X', emboss=False).constraint = i + if m_constraint.show_expanded: + box.separator() + box.prop_search(m_constraint, 'constrained_bone', enduser_obj.pose, "bones", icon='BONE_DATA') + if m_constraint.type == "distance" or m_constraint.type == "point": + box.prop_search(m_constraint, 'constrained_boneB', enduser_obj.pose, "bones", icon='CONSTRAINT_BONE') + frameRow = box.row() + frameRow.label("Frame Range:") + frameRow.prop(m_constraint, 's_frame') + frameRow.prop(m_constraint, 'e_frame') + smoothRow = box.row() + smoothRow.label("Smoothing:") + smoothRow.prop(m_constraint, 'smooth_in') + smoothRow.prop(m_constraint, 'smooth_out') + targetRow = box.row() + targetLabelCol = targetRow.column() + targetLabelCol.label("Target settings:") + targetPropCol = targetRow.column() + if m_constraint.type == "floor": + targetPropCol.prop_search(m_constraint, 'targetMesh', bpy.data, "objects") + if m_constraint.type == "point" or m_constraint.type == "freeze": + box.prop(m_constraint, 'targetSpace') + if m_constraint.type == "point": + targetPropCol.prop(m_constraint, 'targetPoint') + if m_constraint.type == "distance" or m_constraint.type == "floor": + targetPropCol.prop(m_constraint, 'targetDist') + layout.separator() + + +class ExtraToolsPanel(bpy.types.Panel): + # Motion capture retargeting panel + bl_label = "Extra Mocap Tools" + bl_space_type = "PROPERTIES" + bl_region_type = "WINDOW" + bl_context = "object" + + def draw(self, context): + layout = self.layout + layout.operator('mocap.pathediting', text="Follow Path") + layout.label("Animation Stitching") + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + if activeIsArmature: + enduser_arm = context.active_object.data + layout.label("Retargeted Animations:") + 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") + layout.prop(settings, "blend_frame") + layout.prop(settings, "blend_amount") + layout.prop(settings, "second_offset") + layout.prop_search(settings, "stick_bone", context.active_object.pose, "bones") + layout.operator('mocap.animstitchguess', text="Guess Settings") + layout.operator('mocap.animstitch', text="Stitch Animations") + + +class OBJECT_OT_RetargetButton(bpy.types.Operator): + '''Retarget animation from selected armature to active armature ''' + bl_idname = "mocap.retarget" + bl_label = "Retargets active action from Performer to Enduser" + bl_options = {'REGISTER', 'UNDO'} + + 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] + s_frame, e_frame = performer_obj.animation_data.action.frame_range + s_frame = int(s_frame) + e_frame = int(e_frame) + if retarget.isRigAdvanced(enduser_obj) and not enduser_obj.data.advancedRetarget: + print("Recommended to use Advanced Retargeting method") + enduser_obj.data.advancedRetarget = True + else: + retarget.totalRetarget(performer_obj, enduser_obj, scene, s_frame, e_frame) + 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_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): + '''Save mapping to active armature (for future retargets) ''' + bl_idname = "mocap.savemapping" + bl_label = "Saves user generated mapping from Performer to Enduser" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + retarget.createDictionary(performer_obj.data, enduser_obj.data) + 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_LoadMappingButton(bpy.types.Operator): + '''Load saved mapping from active armature''' + bl_idname = "mocap.loadmapping" + bl_label = "Loads user generated mapping from Performer to Enduser" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + retarget.loadMapping(performer_obj.data, enduser_obj.data) + 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_SelectMapBoneButton(bpy.types.Operator): + '''Select a bone for faster mapping''' + bl_idname = "mocap.selectmap" + bl_label = "Select a bone for faster mapping" + perf_bone = bpy.props.StringProperty() + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + selectedBone = "" + for bone in enduser_obj.data.bones: + boneVis = bone.layers + for i in range(32): + if boneVis[i] and enduser_obj.data.layers[i]: + if bone.select: + selectedBone = bone.name + break + performer_obj.data.bones[self.perf_bone].map = selectedBone + 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_ConvertSamplesButton(bpy.types.Operator): + '''Convert active armature's sampled keyframed to beziers''' + bl_idname = "mocap.samples" + bl_label = "Converts samples / simplifies keyframes to beziers" + + def execute(self, context): + mocap_tools.fcurves_simplify(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + return context.active_object.animation_data + + +class OBJECT_OT_LooperButton(bpy.types.Operator): + '''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" + + def execute(self, context): + mocap_tools.autoloop_anim() + return {"FINISHED"} + + @classmethod + def poll(cls, context): + return context.active_object.animation_data + + +class OBJECT_OT_DenoiseButton(bpy.types.Operator): + '''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 " + + def execute(self, context): + mocap_tools.denoise_median() + return {"FINISHED"} + + @classmethod + def poll(cls, context): + return context.active_object + + @classmethod + def poll(cls, context): + return context.active_object.animation_data + + +class OBJECT_OT_LimitDOFButton(bpy.types.Operator): + '''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" + + def execute(self, context): + performer_obj = [obj for obj in context.selected_objects if obj != context.active_object][0] + mocap_tools.limit_dof(context, performer_obj, context.active_object) + 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_RemoveLimitDOFButton(bpy.types.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" + + def execute(self, context): + mocap_tools.limit_dof_toggle_off(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + activeIsArmature = False + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + return activeIsArmature + + +class OBJECT_OT_RotateFixArmature(bpy.types.Operator): + '''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)" + + def execute(self, context): + mocap_tools.rotate_fix_armature(context.active_object.data) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_ScaleFixArmature(bpy.types.Operator): + '''Rescale selected armature to match the active animation, for convienence''' + bl_idname = "mocap.scale_fix" + bl_label = "Scales performer armature to match target armature" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + mocap_tools.scale_fix_armature(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 MOCAP_OT_AddMocapFix(bpy.types.Operator): + '''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" + type = bpy.props.EnumProperty(name="Type of Fix", + items=[("point", "Maintain Position", "Bone is at a specific point"), + ("freeze", "Maintain Position at frame", "Bone does not move from location specified in target frame"), + ("floor", "Stay above", "Bone does not cross specified mesh object eg floor"), + ("distance", "Maintain distance", "Target bones maintained specified distance")], + description="Type of fix") + + def execute(self, context): + enduser_obj = bpy.context.active_object + enduser_arm = enduser_obj.data + new_mcon = enduser_arm.mocap_constraints.add() + new_mcon.type = self.type + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_RemoveMocapConstraint(bpy.types.Operator): + '''Remove this post-retarget fix''' + bl_idname = "mocap.removeconstraint" + bl_label = "Removes fixes from target armature" + constraint = bpy.props.IntProperty() + + def execute(self, context): + enduser_obj = bpy.context.active_object + enduser_arm = enduser_obj.data + m_constraints = enduser_arm.mocap_constraints + m_constraint = m_constraints[self.constraint] + if m_constraint.real_constraint: + bone = enduser_obj.pose.bones[m_constraint.real_constraint_bone] + cons_obj = getConsObj(bone) + removeConstraint(m_constraint, cons_obj) + m_constraints.remove(self.constraint) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_BakeMocapConstraints(bpy.types.Operator): + '''Bake all post-retarget fixes to the Retarget Fixes NLA Track''' + bl_idname = "mocap.bakeconstraints" + bl_label = "Bake all fixes to target armature" + + def execute(self, context): + bakeConstraints(context) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_UnbakeMocapConstraints(bpy.types.Operator): + '''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" + + def execute(self, context): + unbakeConstraints(context) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_UpdateMocapConstraints(bpy.types.Operator): + '''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" + + def execute(self, context): + updateConstraints(context.active_object, context) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + return isinstance(context.active_object.data, bpy.types.Armature) + + +class OBJECT_OT_GuessHierachyMapping(bpy.types.Operator): + '''Attemps to auto figure out hierarchy mapping''' + bl_idname = "mocap.guessmapping" + bl_label = "Attemps to auto figure out hierarchy mapping" + + def execute(self, context): + enduser_obj = bpy.context.active_object + performer_obj = [obj for obj in bpy.context.selected_objects if obj != enduser_obj][0] + mocap_tools.guessMapping(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_PathEditing(bpy.types.Operator): + '''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" + + def execute(self, context): + path = [obj for obj in context.selected_objects if obj != context.active_object][0] + mocap_tools.path_editing(context, context.active_object, path) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + if context.active_object: + selected_objs = [obj for obj in context.selected_objects if obj != context.active_object and isinstance(obj.data, bpy.types.Curve)] + return selected_objs + else: + return False + + +class OBJECT_OT_AnimationStitchingButton(bpy.types.Operator): + '''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" + + def execute(self, context): + mocap_tools.anim_stitch(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + activeIsArmature = False + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + if activeIsArmature: + 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): + '''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" + + def execute(self, context): + mocap_tools.guess_anim_stitch(context, context.active_object) + return {"FINISHED"} + + @classmethod + def poll(cls, context): + activeIsArmature = False + if context.active_object: + activeIsArmature = isinstance(context.active_object.data, bpy.types.Armature) + if activeIsArmature: + stitch_settings = context.active_object.data.stitch_settings + return (stitch_settings.first_action and stitch_settings.second_action) + return False + +def register(): + bpy.utils.register_module(__name__) + + +def unregister(): + bpy.utils.unregister_module(__name__) + +if __name__ == "__main__": + register() |