# - * - coding: utf - 8 - * - # PEP8 formatting ##################################################################################### # # Spring Magic for Maya # # Calculate bone chain animation by settings, support collisions and wind force # Can work with rigging controller as well # # Need pringMagic.ui file to work with # This script need also icon file support, which should be put in same folder # # feel free to mail me redtank@outlook.com for any bug or issue # # Yanbin Bai # 2021.02 # ##################################################################################### import math import logging # import copy import pymel.core as pm import pymel.core.datatypes as dt import maya.cmds as cmds import Animation.springmagic.decorators as decorators import Animation.springmagic.springMath as springMath from Animation.springmagic.utility import * from itertools import cycle from itertools import chain from weakref import WeakValueDictionary from collections import OrderedDict ################################### # spring magic #################################### kWindObjectName = 'spring_wind' kSpringProxySuffix = '_SpringProxy' kCollisionPlaneSuffix = '_SpringColPlane' kCapsuleNameSuffix = '_collision_capsule' kNullSuffix = '_SpringNull' # kTwistNullSuffix = '_SpringTwistNull' class Spring: def __init__(self, ratio=0.5, twistRatio=0.0, tension=0.0, extend=0.0, inertia=0.0): self.ratio = ratio self.twist_ratio = twistRatio self.tension = tension self.extend = extend self.inertia = inertia class SpringMagic: def __init__(self, startFrame, endFrame, subDiv=1.0, isLoop=False, isPoseMatch=False, isCollision=False, isFastMove=False, wipeSubframe=True): self.start_frame = startFrame self.end_frame = endFrame self.sub_div = subDiv self.is_loop = isLoop self.is_pose_match = isPoseMatch self.is_fast_move = isFastMove self.wipe_subframe = wipeSubframe self.is_collision = isCollision self.collision_planes_list = None self.wind = None class SpringData: cur_position_locator = None prev_position_locator = None prev_grand_child_position_locator = None _instances = WeakValueDictionary() @property def Count(self): return len(self._instances) def __init__(self, springMagic, spring, transform, child, grand_child, grand_parent): # self.current_child_position self._instances[id(self)] = self self.springMagic = springMagic self.spring = spring self.parent = transform self.child = child self.grand_child = grand_child self.grand_parent = grand_parent self.child_position = get_translation(child) self.grand_child_position = get_translation(grand_child) if grand_child else None self.previous_child_position = self.child_position self.rotation = get_rotation(transform) self.up_vector = get_matrix(transform)[4:7] transform_pos = get_translation(transform) self.bone_length = springMath.distance(transform_pos, self.child_position) self.has_child_collide = False self.has_plane_collide = False # create temporary locators use for aim constraint if not SpringData.cur_position_locator: SpringData.cur_position_locator = pm.spaceLocator(name='cur_position_locator') if not SpringData.prev_position_locator: SpringData.prev_position_locator = pm.spaceLocator(name='prev_position_locator') if not SpringData.prev_grand_child_position_locator: SpringData.prev_grand_child_position_locator = pm.spaceLocator(name='prev_grand_child_position_locator') # Weight attribute to de/activate the aim constraint in pose match mode self.pairblend_weight_attribute = None self.aim_constraint = None self.__create_child_proxy() self.__prepare_animation_key() self.__create_aim_constraint() self.__init_pairblend_weight() def __del__(self): if self.Count == 0: # print('Last Counter object deleted') # delete temporary locators (useful, it's delete constraints at the same time) pm.delete(SpringData.cur_position_locator, SpringData.prev_position_locator, SpringData.prev_grand_child_position_locator) SpringData.cur_position_locator = None SpringData.prev_position_locator = None SpringData.prev_grand_child_position_locator = None # remove all spring nulls, add recursive incase name spaces # pm.delete(pm.ls('*' + kNullSuffix + '*', recursive=1)) else: # print(self.Count, 'Counter objects remaining') pass if self.child_proxy: # remove spring nulls, add recursive incase name spaces pm.delete(pm.ls('*' + self.child_proxy + '*', recursive=1)) def update(self, has_collision, has_hit_plane, child_pos_corrected): # Update current transform with the new values self.child_position = get_translation(self.child) self.grand_child_position = get_translation(self.grand_child) if self.grand_child else None self.previous_child_position = child_pos_corrected self.rotation = get_rotation(self.parent) self.up_vector = get_matrix(self.parent)[4:7] self.has_child_collide = has_collision self.has_plane_collide = has_hit_plane def __create_child_proxy(self): # create a null at child pos, then parent to obj parent for calculation child_proxy_locator_name = self.parent.name() + kNullSuffix child_proxy_list = pm.ls(child_proxy_locator_name) if not child_proxy_list: self.child_proxy = pm.spaceLocator(name=child_proxy_locator_name) else: self.child_proxy = child_proxy_list[0] self.child_proxy.getShape().setAttr('visibility', False) pm.parent(self.child_proxy, self.parent.getParent()) # pm.parent(child_proxy, self.grand_parent) if not self.springMagic.is_pose_match: self.child_proxy.setTranslation(self.child.getTranslation(space='world'), space='world') self.child_proxy.setRotation(self.child.getRotation(space='world'), space='world') def __prepare_animation_key(self): if not self.springMagic.is_pose_match: # remove exists keys pm.cutKey(self.parent, time=(self.springMagic.start_frame, self.springMagic.end_frame + 0.99999)) pm.cutKey(self.child, time=(self.springMagic.start_frame, self.springMagic.end_frame + 0.99999)) # set key pm.setKeyframe(self.parent, attribute='rotate') if self.spring.extend != 0.0: pm.setKeyframe(self.child, attribute='tx') def __create_aim_constraint(self): # Create a constraint per transform to speed up computation, not active yet (weight=0) self.aim_constraint = pm.aimConstraint(SpringData.cur_position_locator, SpringData.prev_position_locator, SpringData.prev_grand_child_position_locator, self.parent, aimVector=[1, 0, 0], upVector=[0, 1, 0], maintainOffset=False, weight=0) def __init_pairblend_weight(self): # if transform rotation has no animation, set a key at start frame to force the creation of a pairblend when the aim constraint is created for rotation_input in ['rx', 'ry', 'rz']: rotation_connection = pm.listConnections(self.parent + '.' + rotation_input, d=False, s=True) if not rotation_connection: pm.setKeyframe(self.parent, attribute=rotation_input) pairblends = pm.listConnections(self.parent, type="pairBlend", destination=True, skipConversionNodes=True) # Find the pairblend connected to the aim constraint for pairblend in pairblends: connected_constraint_list = cmds.listConnections(pairblend.name(), type='constraint', destination=False) if self.aim_constraint.name() in connected_constraint_list: # Get pairblend weight connected attribute # return [u'joint2.blendAim1')] weight_attribute_list = cmds.listConnections(pairblend + '.weight', d=False, s=True, p=True) if weight_attribute_list: self.pairblend_weight_attribute = weight_attribute_list[0] def set_pairblend_weight(self, blend_value): if self.pairblend_weight_attribute: pm.setAttr(self.pairblend_weight_attribute, blend_value) def keyframe_child_proxy(self): if self.child_proxy: # Deactivate pairblend weight # Aim constraint weight set to 0 is not enough, it paratizes the process self.set_pairblend_weight(0.0) self.child_proxy.setTranslation(self.child.getTranslation(space='world'), space='world') pm.setKeyframe(self.child_proxy, attribute='translate') self.child_proxy.setRotation(self.child.getRotation(space='world'), space='world') pm.setKeyframe(self.child_proxy, attribute='rotate') self.set_pairblend_weight(1.0) def apply_inertia(self, currentChildPosition): ratio = self.spring.ratio / self.springMagic.sub_div inertia_offset = [0.0, 0.0, 0.0] if self.spring.inertia > 0.0: bone_ref_loc_offset_dir = currentChildPosition - self.child_position bone_ref_loc_offset_distance = ((bone_ref_loc_offset_dir) * (1 - ratio) * (1 - self.spring.inertia)).length() inertia_offset = bone_ref_loc_offset_dir.normal() * (bone_ref_loc_offset_distance / self.springMagic.sub_div) # apply mass force_direction = self.child_position - self.previous_child_position force_distance = force_direction.length() * self.spring.inertia # offset position inertia_offset += force_direction.normal() * (force_distance / self.springMagic.sub_div) return inertia_offset def apply_wind(self, frame): wind_offset = [0.0, 0.0, 0.0] if self.springMagic.wind: wind_max_force = self.springMagic.wind.getAttr('MaxForce') wind_min_force = self.springMagic.wind.getAttr('MinForce') wind_frequency = self.springMagic.wind.getAttr('Frequency') mid_force = (wind_max_force + wind_min_force) / 2 # get source x - axis direction in world space wind_direction = get_matrix(self.springMagic.wind)[:3] # sDirection = sObj.getMatrix()[0][:3] wind_direction = dt.Vector(wind_direction[0], wind_direction[1], wind_direction[2]).normal() wind_distance = math.sin(frame * wind_frequency) * (wind_max_force - wind_min_force) + mid_force # offset position wind_offset = wind_direction.normal() * wind_distance return wind_offset def detect_collision(self, new_obj_pos, new_child_pos, capsule_list): col_pre = col_cur = None child_pos_corrected = self.child_position if self.springMagic.is_collision and capsule_list: if preCheckCollision(new_obj_pos, self.bone_length, capsule_list): # check collision from previous pos to cur pos col_pre, col_body_pre, hitCylinder_pre = springMath.checkCollision(new_child_pos, self.child_position, capsule_list, True) # check collision from cur pos to previous pos col_cur, col_body_cur, hitCylinder_cur = springMath.checkCollision(new_child_pos, self.child_position, capsule_list, False) if col_pre and (col_cur is None): new_child_pos = col_pre elif col_cur and (col_pre is None): child_pos_corrected = col_cur elif col_pre and col_cur: # move cur child pose to closest out point if both pre and cur pos are already inside of col body # if distance(col_pre, new_child_pos) < distance(col_cur, new_child_pos): mid_point = (self.child_position + new_child_pos) / 2 if springMath.distance(col_pre, mid_point) < springMath.distance(col_cur, mid_point): new_child_pos = col_pre else: new_child_pos = col_cur if self.springMagic.is_fast_move: child_pos_corrected = new_child_pos # # draw debug locator # if col_pre and col_cur: # locator1 = pm.spaceLocator(name=obj.name() + '_col_pre_locator_' + str(i)) # locator1.setTranslation(col_pre) # locator1 = pm.spaceLocator(name=obj.name() + '_col_cur_locator_' + str(i)) # locator1.setTranslation(col_cur) return True if col_pre or col_cur else False, new_child_pos, child_pos_corrected def detect_plane_hit(self, new_obj_pos, new_child_pos, grand_parent_has_plane_collision): has_hit_plane = False if self.springMagic.is_collision and self.springMagic.collision_planes_list[0]: collision_plane = self.springMagic.collision_planes_list[0] has_plane_collision = springMath.checkPlaneCollision(new_obj_pos, new_child_pos, collision_plane) if has_plane_collision or grand_parent_has_plane_collision: new_child_pos = repeatMoveToPlane(self.parent, new_child_pos, self.child, collision_plane, 3) has_hit_plane = True return has_hit_plane, new_child_pos # calculate upvector by interpolation y axis for twist def compute_up_vector(self): twist_ratio = self.spring.twist_ratio / self.springMagic.sub_div cur_obj_yAxis = get_matrix(self.child_proxy)[4:7] prev_up_vector = dt.Vector(self.up_vector[0], self.up_vector[1], self.up_vector[2]).normal() cur_up_vector = dt.Vector(cur_obj_yAxis[0], cur_obj_yAxis[1], cur_obj_yAxis[2]).normal() up_vector = (prev_up_vector * (1 - twist_ratio)) + (cur_up_vector * twist_ratio) return up_vector def aim_by_ratio(self, upVector, newChildPos, childPosCorrected): ratio = self.spring.ratio / self.springMagic.sub_div tension = self.spring.tension / (1.0 / (springMath.sigmoid(1 - self.springMagic.sub_div) + 0.5)) # print("obj: " + str(self.parent.name())) # print("newChildPos: " + str(newChildPos)) # print("childPosCorrected: " + str(childPosCorrected)) # print("grand_child_position: " + str(self.grand_child_position)) # print("upVector: " + str(upVector)) # print("ratio: " + str(ratio)) # print("tension: " + str(tension)) SpringData.cur_position_locator.setTranslation(newChildPos) SpringData.prev_position_locator.setTranslation(childPosCorrected) pm.aimConstraint(self.parent, e=True, worldUpVector=upVector) pm.aimConstraint(SpringData.cur_position_locator, self.parent, e=True, w=ratio) pm.aimConstraint(SpringData.prev_position_locator, self.parent, e=True, w=1 - ratio) if self.has_child_collide and self.grand_child_position and tension != 0: SpringData.prev_grand_child_position_locator.setTranslation(self.grand_child_position) pm.aimConstraint(SpringData.prev_grand_child_position_locator, self.parent, e=True, w=(1 - ratio) * tension) pm.setKeyframe(self.parent, attribute='rotate') pm.aimConstraint(SpringData.cur_position_locator, SpringData.prev_position_locator, SpringData.prev_grand_child_position_locator, self.parent, e=True, w=0.0) def extend_bone(self, childPosCorrected): if self.spring.extend != 0.0: child_translation = self.child.getTranslation() # get length between bone pos and child pos x2 = (childPosCorrected - get_translation(self.parent)).length() x3 = (self.bone_length * (1 - self.spring.extend)) + (x2 * self.spring.extend) self.child.setTranslation([x3, child_translation[1], child_translation[2]]) pm.setKeyframe(self.child, attribute='tx') # else: # self.child.setTranslation([self.bone_length, child_translation[1], child_translation[2]]) def createCollisionPlane(): # remove exist plane collision_plane = get_node('*' + kCollisionPlaneSuffix + '*') if collision_plane: pm.delete(collision_plane) collision_plane = pm.polyPlane(name="the" + kCollisionPlaneSuffix, sx=1, sy=1, w=10, h=10, ch=1)[0] # one side display pm.setAttr(collision_plane.doubleSided, False) # lock scale pm.setAttr(collision_plane.sx, lock=True) pm.setAttr(collision_plane.sy, lock=True) pm.setAttr(collision_plane.sz, lock=True) pm.select(collision_plane) def removeBody(clear=False): cylinder_list = getCapsule(clear) pm.delete(cylinder_list) collision_plane = get_node('*' + kCollisionPlaneSuffix + '*') if collision_plane: pm.delete(collision_plane) def addWindObj(): windCone = pm.cone(name=kWindObjectName)[0] windCone.setScale([5, 5, 5]) pm.delete(windCone, constructionHistory=1) # add wind attr pm.addAttr(windCone, longName='MaxForce', attributeType='float') pm.setAttr(windCone.name() + '.MaxForce', 1, e=1, keyable=1) pm.addAttr(windCone, longName='MinForce', attributeType='float') pm.setAttr(windCone.name() + '.MinForce', 0.5, e=1, keyable=1) pm.addAttr(windCone, longName='Frequency', attributeType='float') pm.setAttr(windCone.name() + '.Frequency', 1, e=1, keyable=1) # pm.addAttr(windCone, longName='Wave', attributeType='float') # pm.setAttr(windCone.name() + '.Wave', 0.5, e=1, keyable=1) setWireShading(windCone, False) pm.makeIdentity(apply=True) windCone.setRotation([0, 0, 90]) def bindControls(linked_chains=False): selected_ctrls = pm.ls(sl=True) pm.select(clear=True) # The chains are linked, we can sort them if linked_chains: # Create list for every ctrls chains # ie [[ctrl1, ctrl1.1, ctrl1.2], [ctrl2, ctrl2.1, ctrl2.2, ctrl2.3]] all_ctrls_descendants_list = pm.listRelatives(selected_ctrls, allDescendents=True) top_hierarchy_ctrls_list = [x for x in selected_ctrls if x not in all_ctrls_descendants_list] ctrls_chains_list = map(lambda x: [x] + [y for y in pm.listRelatives(x, allDescendents=True) if y in selected_ctrls][::-1], top_hierarchy_ctrls_list) # No sorting possible because the controlers have no lineage else: ctrls_chains_list = [selected_ctrls] proxy_joint_chain_list = [] for ctrls_list in ctrls_chains_list: proxy_joint_list = [] for ctrl in ctrls_list: # create proxy joint in ctrl world position ctrl_position = pm.xform(ctrl, worldSpace=1, rp=1, q=1) proxyJoint = pm.joint(name=ctrl.name() + kSpringProxySuffix, position=ctrl_position, radius=0.2, roo='xyz') proxy_joint_list.append(proxyJoint) for joint in proxy_joint_list: # set joint orientation pm.joint(joint, edit=1, orientJoint='xyz', zeroScaleOrient=True) # Straight bones alignment joint.setRotation([0, 0, 0]) joint.setAttr('rotateAxis', [0, 0, 0]) joint.setAttr('jointOrient', [0, 0, 0]) # Free rotation (move rotation values to joint orient values) # pm.makeIdentity(proxy_joint_list[idx], apply=True, t=False, r=True, s=False, pn=True) if proxy_joint_list: # parent root proxy joint to control parent pm.parent(proxy_joint_list[0], ctrls_list[0].getParent()) # Necessary to start a new joint chain pm.select(clear=True) proxy_joint_chain_list += [proxy_joint_list] for idx, joint in enumerate(proxy_joint_list[:-1]): # orient joint chain cns = pm.aimConstraint(ctrls_list[idx + 1], proxy_joint_list[idx], aimVector=[1, 0, 0], upVector=[0, 0, 0], worldUpVector=[0, 1, 0], skip='x') pm.delete(cns) for idx, joint in enumerate(proxy_joint_list): pm.parentConstraint(proxy_joint_list[idx], ctrls_list[idx], maintainOffset=True) pm.select(proxy_joint_chain_list) def clearBind(startFrame, endFrame): proxyJointLst = pm.ls(sl=True) pm.select(d=True) ctrlList = [] for bone in proxyJointLst: ctrl = pm.ls(bone.name().split(kSpringProxySuffix)[0])[0] ctrlList.append(ctrl) if ctrlList: pm.bakeResults(*ctrlList, t=(startFrame, endFrame)) pm.delete(proxyJointLst) def bindPose(): pm.runtime.GoToBindPose() # Prepare all information to call SpringMagicMaya function def startCompute(spring, springMagic, progression_callback=None): autokeyframe_state = cmds.autoKeyframe(query=True, state=True) cmds.autoKeyframe(state=False) # get selection obj objs = pm.ls(sl=True) # check objects validity for obj in objs: # has duplicate name obj nameCntErr = (len(pm.ls(obj.name())) > 1) # is a duplicate obj nameValidErr = (obj.name().find('|') > 0) if nameCntErr or nameValidErr: raise ValueError(obj.name() + ' has duplicate name object! Stopped!') obj_translation = obj.getTranslation() if (obj_translation[0] < 0 or abs(obj_translation[1]) > 0.001 or abs(obj_translation[2]) > 0.001) and obj.getParent() and (obj.getParent() in objs): pm.warning(obj.getParent().name() + "'s X axis not point to child! May get broken result!") # Search for collision objects if springMagic.is_collision: springMagic.collision_planes_list = [get_node('*' + kCollisionPlaneSuffix + '*')] # Search for a wind object if pm.ls(kWindObjectName): springMagic.wind = pm.ls(kWindObjectName)[0] SpringMagicMaya(objs, spring, springMagic, progression_callback) cmds.autoKeyframe(state=autokeyframe_state) # @decorators.viewportOff @decorators.gShowProgress(status="SpringMagic does his magic") def SpringMagicMaya(objs, spring, springMagic, progression_callback=None): # on each frame go through all objs and do: # 1. make a vectorA from current obj position to previous child position # 2. make a vectorB from current obj position to current child position # 3. calculate the angle between two vectors # 4. rotate the obj towards vectorA base on spring value start_frame = springMagic.start_frame end_frame = springMagic.end_frame sub_div = springMagic.sub_div # remove all spring nulls, add recursive incase name spaces pm.delete(pm.ls('*' + kNullSuffix + '*', recursive=True)) # get all capsules in scene capsule_list = getCapsule(True) if springMagic.is_collision else None if progression_callback: progression_callback(0) # Save object previous frame information in a ordered dict spring_data_dict = OrderedDict() # Initialize data on the first frame pm.currentTime(start_frame, edit=True) # Create a list of objects chains # ie [[nt.Joint(u'joint1'), nt.Joint(u'joint2'), nt.Joint(u'joint4')], [nt.Joint(u'joint7'), nt.Joint(u'joint8'), nt.Joint(u'joint10')]] all_joints_descendants_list = pm.listRelatives(objs, allDescendents=True, type='transform') top_hierarchy_joints_list = [x for x in objs if x not in all_joints_descendants_list] # transforms_chains_list = map(lambda x: [x] + [y for y in pm.listRelatives(x, allDescendents=True) if y in objs][::-1], top_hierarchy_joints_list) # Deal with the specific case of root bone with no parent. # The root bone is considered the driver, so we remove it from the calculation. transforms_chains_list = map(lambda x: ([x] if x.getParent() else []) + [y for y in pm.listRelatives(x, allDescendents=True) if y in objs][::-1], top_hierarchy_joints_list) # Remove empty lists transforms_chains_list = [x for x in transforms_chains_list if x != []] # Create progression bar generator values number_of_progession_step = 0 if springMagic.is_pose_match: number_of_progession_step += end_frame - start_frame + 1 if springMagic.is_loop: # Doesn't process the first frame on the first loop number_of_progession_step += ((end_frame - start_frame) * 2 + 1) * sub_div else: # Doesn't process the first frame number_of_progession_step += (end_frame - start_frame) * sub_div progression_increment = 100.0 / number_of_progession_step progression_generator = frange(progression_increment, 100.0 + progression_increment, progression_increment) # Create spring data for each transforms at start frame for transforms_chain in transforms_chains_list: if SpringMagicMaya.isInterrupted(): break transforms_cycle = cycle(transforms_chain) # Prime the pump parent = first_transform = next(transforms_cycle) grand_parent = parent.getParent() child = next(transforms_cycle) grand_child = next(transforms_cycle) # skip end bone for transform in transforms_chain[:-1]: if SpringMagicMaya.isInterrupted(): break # End of cycle iteration if grand_child == first_transform: grand_child = None spring_data_dict[parent.name()] = SpringData(springMagic, spring, parent, child, grand_child, grand_parent) grand_parent, parent, child, grand_child = parent, child, grand_child, next(transforms_cycle) # Save joints position over timeline # Parse timeline just one time if springMagic.is_pose_match: for frame in range(0, end_frame - start_frame + 1): if SpringMagicMaya.isInterrupted(): break pm.currentTime(start_frame + frame, edit=True) for spring_data in spring_data_dict.values(): if not SpringMagicMaya.isInterrupted(): spring_data.keyframe_child_proxy() progression = progression_generator.next() progression = clamp(progression, 0, 100) if progression_callback: progression_callback(progression) SpringMagicMaya.progress(progression) # Generate frame index # Skip first frame on first calculation pass frame_increment = 1.0 / sub_div frame_generator = frange(frame_increment, end_frame - start_frame + frame_increment, frame_increment) # On second calculation pass compute first frame if springMagic.is_loop: frame_generator = chain(frame_generator, frange(0, end_frame - start_frame + frame_increment, frame_increment)) for frame in frame_generator: # print('Frame: ' + str(frame)) if SpringMagicMaya.isInterrupted(): break pm.currentTime(start_frame + frame, edit=True) for previous_frame_spring_data in spring_data_dict.values(): if SpringMagicMaya.isInterrupted(): break grand_parent_spring_data = None if previous_frame_spring_data.grand_parent and previous_frame_spring_data.grand_parent.name() in spring_data_dict.keys(): grand_parent_spring_data = spring_data_dict[previous_frame_spring_data.grand_parent.name()] # get current position of parent and child parent_pos = get_translation(previous_frame_spring_data.parent) # print("obj: " + str(previous_frame_spring_data.parent.name())) new_child_pos = get_translation(previous_frame_spring_data.child_proxy) # Apply inertia new_child_pos += previous_frame_spring_data.apply_inertia(new_child_pos) # apply wind new_child_pos += previous_frame_spring_data.apply_wind(start_frame + frame) # detect collision has_collision, new_child_pos, child_pos_corrected = previous_frame_spring_data.detect_collision(parent_pos, new_child_pos, capsule_list) # detect plane collision grand_parent_has_plane_collision = False if grand_parent_spring_data: grand_parent_has_plane_collision = grand_parent_spring_data.has_plane_collide has_hit_plane, new_child_pos = previous_frame_spring_data.detect_plane_hit(parent_pos, new_child_pos, grand_parent_has_plane_collision) # calculate upvector by interpolation y axis for twist up_vector = previous_frame_spring_data.compute_up_vector() # apply aim constraint to do actual rotation previous_frame_spring_data.aim_by_ratio(up_vector, new_child_pos, child_pos_corrected) # Extend bone if needed (update child translation) previous_frame_spring_data.extend_bone(child_pos_corrected) # Update current transform with the new values previous_frame_spring_data.update(has_collision, has_hit_plane, child_pos_corrected) # Update the grand parent has_child_collide value if grand_parent_spring_data: grand_parent_spring_data.has_child_collide = has_collision progression = progression_generator.next() progression = clamp(progression, 0, 100) if progression_callback: progression_callback(progression) SpringMagicMaya.progress(progression) # bake result on frame if springMagic.wipe_subframe and not SpringMagicMaya.isInterrupted(): transform_to_bake_list = [spring_data.parent for spring_data in spring_data_dict.values()] # Deactivate all pairblend otherwise bake doesn't work with animation layers for spring_data in spring_data_dict.values(): spring_data.set_pairblend_weight(0.0) bakeAnim(transform_to_bake_list, start_frame, end_frame) def bakeAnim(objList, startFrame, endFrame): pm.bakeResults( objList, t=(startFrame, endFrame), sampleBy=1, disableImplicitControl=False, preserveOutsideKeys=True, sparseAnimCurveBake=False, removeBakedAttributeFromLayer=False, bakeOnOverrideLayer=False, minimizeRotation=True, shape=False, simulation=False) SM_boneTransformDict = {} def copyBonePose(): global SM_boneTransformDict for obj in pm.ls(sl=True): SM_boneTransformDict[obj] = [obj.getTranslation(), obj.getRotation()] def pasteBonePose(): global SM_boneTransformDict for obj in pm.ls(sl=True): if obj in SM_boneTransformDict.keys(): logging.debug(SM_boneTransformDict[obj][0]) obj.setTranslation(SM_boneTransformDict[obj][0]) obj.setRotation(SM_boneTransformDict[obj][1]) def preCheckCollision(objPos, objLength, capsuleList): # print('objPos:' + str(objPos)) # print('objLength:' + str(objLength)) # pre check bone length compare with collision body radius # will improve performance if bone is far from capsule for capsule in capsuleList: capsule_children_list = pm.listRelatives(capsule, children=1, type='transform') p = capsule_children_list[0].getTranslation(space='world') q = capsule_children_list[1].getTranslation(space='world') r = capsule.getAttr('scaleZ') bone_to_capsule_distance = springMath.dist_to_line(p, q, objPos) # print('p:' + str(p)) # print('q:' + str(q)) # print('r:' + str(r)) # print('boneToCapsuleDistance:' + str(bone_to_capsule_distance)) # means close enough to have a hit change if bone_to_capsule_distance < objLength + r: return True return False def repeatMoveToPlane(obj, objPos, objTarget, colPlane, times): # Y axis direction of plane n = dt.Vector(get_matrix(colPlane)[4:7]) q = get_translation(colPlane) d = n.dot(q) # for i in range(times): # pt = objPos # obj.setTranslation(proj_pt_to_plane(pt, n, d), space='world') # if (i + 1) != times: # obj.setTranslation(get_translation(objTarget), space='world') pt = objPos outPos = springMath.proj_pt_to_plane(pt, n, d) return outPos def setWireShading(obj, tmp): obj.getShape().overrideEnabled.set(True) obj.getShape().overrideShading.set(False) if tmp: obj.getShape().overrideDisplayType.set(1) def addCapsuleSphereConstraint(sphereObj): # create a locator and make sphere follow it locator = pm.spaceLocator(name=sphereObj.name() + '_locator' + kCapsuleNameSuffix) locator.setTranslation(sphereObj.getTranslation()) locator.setRotation(sphereObj.getRotation()) locator.getShape().setAttr('visibility', False) pm.parentConstraint(locator, sphereObj) return locator def createCapsuleGeometry(size): # create geometry cylinder, cylinder_history = pm.cylinder(radius=size, sections=8, heightRatio=3) pm.rename(cylinder.name(), cylinder.name() + kCapsuleNameSuffix) sphereA, sphereA_history = pm.sphere(radius=size, endSweep=180, sections=4) pm.rename(sphereA.name(), sphereA.name() + kCapsuleNameSuffix) sphereB, sphereB_history = pm.sphere(radius=size, endSweep=180, sections=4) pm.rename(sphereB.name(), sphereB.name() + kCapsuleNameSuffix) # set to wireframe shader setWireShading(cylinder, False) setWireShading(sphereA, True) setWireShading(sphereB, True) # build a capsule with geometry cylinder.setAttr('rotateZ', 90) sphereA.setAttr('translateY', -1.5 * size) sphereB.setAttr('rotateZ', 180) sphereB.setAttr('translateY', 1.5 * size) # add constrain locatorA = addCapsuleSphereConstraint(sphereA) locatorB = addCapsuleSphereConstraint(sphereB) pm.parent(locatorA, cylinder) pm.parent(locatorB, cylinder) pm.parent(sphereA, cylinder) pm.parent(sphereB, cylinder) sphereA.setAttr('inheritsTransform', False) sphereB.setAttr('inheritsTransform', False) pm.connectAttr(cylinder.scaleY, (sphereA_history.name() + '.radius')) pm.connectAttr(cylinder.scaleY, (sphereB_history.name() + '.radius')) pm.connectAttr(cylinder.scaleY, cylinder.scaleZ) return cylinder def getCapsule(getAll): if getAll: nurbsTransLst = pm.ls(type='transform') else: nurbsTransLst = pm.ls(sl=True) nurbsSurfaceLst = [] for obj in nurbsTransLst: if obj.getShape() and (pm.nodeType(obj.getShape()) == 'nurbsSurface'): nurbsSurfaceLst.append(obj) cylinderLst = [] for obj in nurbsTransLst: if 'ylinder' in obj.name() and kCapsuleNameSuffix in obj.name(): cylinderLst.append(obj) return cylinderLst def addCapsuleBody(): # create capsule body for collision # place capsule at ori point of nothing selected in scene # place capsule match with object position and rotation if select scene object collisionBoneList = [] objs = pm.ls(sl=True) for obj in objs: children = pm.listRelatives(obj, children=1) # only add capsule to the obj which has child if children: collisionBoneList.append([obj, children[0]]) if collisionBoneList: for couple in collisionBoneList: baseBone = couple[0] endBone = couple[1] capsule = createCapsuleGeometry(1) pm.parent(capsule, baseBone) # match capsule to bone endBoneTrans = endBone.getTranslation() capsule.setTranslation(endBoneTrans * 0.5) capsule.setAttr('scaleX', endBoneTrans[0] / 3) capsule.setAttr('scaleY', endBoneTrans[0] / 3) cns = pm.aimConstraint(endBone, capsule, aimVector=[1, 0, 0]) pm.delete(cns) else: capsule = createCapsuleGeometry(1) capsule.setAttr('scaleX', 10) capsule.setAttr('scaleY', 10) pm.select(clear=1)