962 lines
35 KiB
Python
962 lines
35 KiB
Python
|
# - * - 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)
|
||
|
|