1452 lines
64 KiB
Python
1452 lines
64 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
"""
|
|
Author: Jeremy Ernst
|
|
|
|
This module is a collection of utility functions for generic rigging and skinning tasks.
|
|
There are also classes in the module that deal with saving and loading skin weight files.
|
|
"""
|
|
|
|
from __future__ import print_function
|
|
import json
|
|
import os
|
|
import time
|
|
|
|
import maya.cmds as cmds
|
|
from maya import OpenMaya, OpenMayaAnim
|
|
|
|
import System.mathUtils as mathUtils
|
|
import utils as utils
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# GLOBALS
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
ATTRIBUTES = ['skinningMethod', 'normalizeWeights', 'dropoffRate',
|
|
'maintainMaxInfluences', 'maxInfluences','bindMethod',
|
|
'useComponents', 'normalizeWeights', 'weightDistribution',
|
|
'heatmapFalloff']
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# FUNCTIONS (STAND-ALONE)
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def getScaleFactor():
|
|
# ===========================================================================
|
|
# #find meshes
|
|
# ===========================================================================
|
|
weightedMeshes = []
|
|
skinClusters = cmds.ls(type='skinCluster')
|
|
|
|
for cluster in skinClusters:
|
|
geometry = cmds.skinCluster(cluster, q=True, g=True)[0]
|
|
geoTransform = cmds.listRelatives(geometry, parent=True)[0]
|
|
weightedMeshes.append(geoTransform)
|
|
|
|
# scale factor of 1 = 180cm
|
|
if len(weightedMeshes) > 0:
|
|
scale = cmds.exactWorldBoundingBox(weightedMeshes, ii=True)[5]
|
|
scaleFactor = scale / 180
|
|
else:
|
|
scaleFactor = 1
|
|
|
|
return scaleFactor
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createDriverSkeleton():
|
|
# Original Author: Jeremy Ernst
|
|
|
|
# ===========================================================================
|
|
# #duplicate the entire skeleton
|
|
# ===========================================================================
|
|
duplicateSkel = cmds.duplicate("root", rc=True)[0]
|
|
cmds.select("root", hi=True)
|
|
joints = cmds.ls(sl=True)
|
|
|
|
cmds.select(duplicateSkel, hi=True)
|
|
dupeJoints = cmds.ls(sl=True)
|
|
|
|
# ===========================================================================
|
|
# #rename the duplicate joints
|
|
# ===========================================================================
|
|
driverJoints = []
|
|
for i in range(int(len(dupeJoints))):
|
|
|
|
if cmds.objExists(dupeJoints[i]):
|
|
driverJoint = cmds.rename(dupeJoints[i], "driver_" + joints[i])
|
|
driverJoints.append(driverJoint)
|
|
|
|
# ===========================================================================
|
|
# #create a direct connection between the driver and the export joints
|
|
# ===========================================================================
|
|
for joint in driverJoints:
|
|
exportJoint = joint.partition("driver_")[2]
|
|
cmds.connectAttr(joint + ".translate", exportJoint + ".translate")
|
|
cmds.connectAttr(joint + ".rotate", exportJoint + ".rotate")
|
|
cmds.connectAttr(joint + ".scale", exportJoint + ".scale")
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def buildSkeleton():
|
|
# get all of the rig modules in the scene
|
|
modules = utils.returnRigModules()
|
|
|
|
# go through each one, and find the created bones for that modules
|
|
createdJoints = []
|
|
for module in modules:
|
|
if module != "ART_Root_Module":
|
|
joints = cmds.getAttr(module + ".Created_Bones")
|
|
splitJoints = joints.split("::")
|
|
|
|
for bone in splitJoints:
|
|
if bone != "":
|
|
# create the joint
|
|
if cmds.objExists(bone):
|
|
cmds.warning(
|
|
"Object with name: " + bone + " already exists. Renaming object with _renamed suffix.")
|
|
cmds.rename(bone, bone + "_renamed")
|
|
cmds.select(clear=True)
|
|
newJoint = cmds.joint(name=str(bone))
|
|
cmds.select(clear=True)
|
|
|
|
# find the LRA control
|
|
moduleName = cmds.getAttr(module + ".moduleName")
|
|
baseName = cmds.getAttr(module + ".baseName")
|
|
|
|
# find prefix/suffix if available
|
|
prefix = moduleName.partition(baseName)[0]
|
|
suffix = moduleName.partition(baseName)[2]
|
|
if prefix == "":
|
|
prefixSeparator = " "
|
|
else:
|
|
prefixSeparator = prefix
|
|
|
|
if suffix == "":
|
|
suffixSeparator = " "
|
|
else:
|
|
suffixSeparator = suffix
|
|
|
|
# get the bone name (for example, thigh)
|
|
if prefix == "":
|
|
boneName = bone.partition(suffixSeparator)[0]
|
|
else:
|
|
boneName = bone.partition(prefixSeparator)[2].partition(suffixSeparator)[0]
|
|
|
|
# get the lra node
|
|
lra = prefix + baseName + suffix + "_" + boneName + "_lra"
|
|
lra = lra.replace(" ", "")
|
|
|
|
if not cmds.objExists(lra):
|
|
lra = prefix + baseName + suffix + "_lra"
|
|
lra = lra.replace(" ", "")
|
|
|
|
# position bone at lra
|
|
constraint = cmds.parentConstraint(lra, newJoint)[0]
|
|
cmds.delete(constraint)
|
|
|
|
# find parent joint and append data to list
|
|
if bone != splitJoints[0]:
|
|
relatives = str(cmds.listRelatives(lra, fullPath=True))
|
|
relatives = relatives.split("|")
|
|
relatives = relatives[::-1]
|
|
for relative in relatives:
|
|
searchKey = lra.partition("_lra")[0]
|
|
|
|
if relative.find(searchKey) == -1:
|
|
parentBoneName = relative.partition(moduleName + "_")[2].partition("_mover")[0]
|
|
parent = prefix + parentBoneName + suffix
|
|
createdJoints.append([bone, parent])
|
|
break
|
|
else:
|
|
parent = cmds.getAttr(module + ".parentModuleBone")
|
|
createdJoints.append([bone, parent])
|
|
|
|
# if this is the root module, it's a bit simpler
|
|
else:
|
|
jointName = cmds.getAttr(module + ".Created_Bones")
|
|
# create the joint
|
|
if cmds.objExists(jointName):
|
|
cmds.warning(
|
|
"Object with name: " + jointName + " already exists. Renaming object with _renamed suffix.")
|
|
cmds.rename(jointName, jointName + "_renamed")
|
|
|
|
cmds.select(clear=True)
|
|
cmds.joint(name=str(jointName))
|
|
cmds.select(clear=True)
|
|
|
|
# go through the data and setup the hierarchy now that all bones have been created
|
|
for joint in createdJoints:
|
|
cmds.parent(joint[0], joint[1])
|
|
|
|
# freeze rotation transformations
|
|
cmds.makeIdentity("root", t=False, r=True, s=False, apply=True)
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createBoneConnection(mover, childMover, userSpecName):
|
|
if cmds.objExists(userSpecName + "_parentGrp"):
|
|
cmds.delete(userSpecName + "_parentGrp")
|
|
|
|
grp = cmds.group(empty=True, name=userSpecName + "_parentGrp")
|
|
cmds.parent(grp, userSpecName + "_bone_representations")
|
|
|
|
crv = cmds.curve(d=1, p=[(0, 0, 0), (0, 20, 0)], k=[0, 1], name=mover + "_to_" + childMover + "_connector_geo")
|
|
cmds.parent(crv, grp)
|
|
|
|
cmds.select(crv + ".cv[0]")
|
|
topCluster = cmds.cluster(name=mover + "_to_" + childMover + "_top_cluster")[1]
|
|
cmds.select(crv + ".cv[1]")
|
|
botCluster = cmds.cluster(name=mover + "_to_" + childMover + "_end_cluster")[1]
|
|
cmds.pointConstraint(mover, topCluster)
|
|
cmds.pointConstraint(childMover, botCluster)
|
|
|
|
cmds.setAttr(topCluster + ".v", False, lock=True)
|
|
cmds.setAttr(botCluster + ".v", False, lock=True)
|
|
cmds.setAttr(crv + ".overrideEnabled", 1)
|
|
cmds.setAttr(crv + ".overrideColor", 1)
|
|
cmds.setAttr(crv + ".overrideDisplayType", 2)
|
|
|
|
cmds.parent(topCluster, grp)
|
|
cmds.parent(botCluster, grp)
|
|
cmds.setAttr(crv + ".inheritsTransform", 0)
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createFkRig(joints, networkNode, numRigs, slot):
|
|
# Original Author: Jeremy Ernst
|
|
|
|
# lists
|
|
fkControls = []
|
|
|
|
# ===========================================================================
|
|
# #take the list of incoming joints and find each joint's mover
|
|
# ===========================================================================
|
|
for joint in joints:
|
|
globalMover = utils.findAssociatedMover(joint, networkNode)
|
|
|
|
# =======================================================================
|
|
# #if a mover is found, duplicate it and unparent the duplicate
|
|
# =======================================================================
|
|
if globalMover != None:
|
|
dupe = cmds.duplicate(globalMover, rr=True)[0]
|
|
utils.deleteChildren(dupe)
|
|
parent = cmds.listRelatives(dupe, parent=True)
|
|
if parent != None:
|
|
cmds.parent(dupe, world=True)
|
|
|
|
# turn on visiblity of the control
|
|
cmds.setAttr(dupe + ".v", 1)
|
|
|
|
# ensure pivot is correct
|
|
piv = cmds.xform(joint, q=True, ws=True, rp=True)
|
|
cmds.xform(dupe, ws=True, rp=piv)
|
|
|
|
# rename the control
|
|
fkControl = cmds.rename(dupe, "fk_" + joint + "_anim")
|
|
fkControls.append([joint, fkControl])
|
|
|
|
# create a group for the control
|
|
controlGrp = cmds.group(empty=True, name="fk_" + joint + "_anim_grp")
|
|
constraint = cmds.parentConstraint(joint, controlGrp)[0]
|
|
cmds.delete(constraint)
|
|
|
|
# parent the control under the controlGrp
|
|
cmds.parent(fkControl, controlGrp)
|
|
|
|
# freeze transformations on the control
|
|
cmds.makeIdentity(fkControl, t=1, r=1, s=1, apply=True)
|
|
|
|
# color the control
|
|
colorControl(fkControl)
|
|
|
|
returnData = None
|
|
createdControls = []
|
|
|
|
# ===========================================================================
|
|
# #go through the fk controls data and get the joint parent to create the hierarchy
|
|
# ===========================================================================
|
|
for data in fkControls:
|
|
joint = data[0]
|
|
control = data[1]
|
|
createdControls.append(control)
|
|
|
|
# =======================================================================
|
|
# #parent the anim group to the parent control
|
|
# =======================================================================
|
|
parent = cmds.listRelatives(joint, parent=True)
|
|
if parent != None:
|
|
group = control + "_grp"
|
|
parentControl = "fk_" + parent[0] + "_anim"
|
|
if parent[0] in joints:
|
|
if cmds.objExists(parentControl):
|
|
cmds.parent(group, parentControl)
|
|
|
|
else:
|
|
returnData = group
|
|
|
|
# =======================================================================
|
|
# #lastly, connect controls up to blender nodes to drive driver joints
|
|
# =======================================================================
|
|
cmds.pointConstraint(control, "driver_" + joint, mo=True)
|
|
cmds.orientConstraint(control, "driver_" + joint)
|
|
|
|
# plug master control scale into a new mult node that takes joint.scale into input 1, and master.scale into
|
|
# input 2, and plugs that into driver joint
|
|
if cmds.objExists("master_anim"):
|
|
globalScaleMult = cmds.shadingNode("multiplyDivide", asUtility=True, name=joint + "_globalScale")
|
|
cmds.connectAttr("master_anim.scale", globalScaleMult + ".input1")
|
|
cmds.connectAttr(control + ".scale", globalScaleMult + ".input2")
|
|
createConstraint(globalScaleMult, "driver_" + joint, "scale", False, numRigs, slot, "output")
|
|
else:
|
|
createConstraint(control, "driver_" + joint, "scale", False, numRigs, slot)
|
|
|
|
return [returnData, createdControls]
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createCounterTwistRig(joints, name, networkNode, startJoint, endJoint, parentGrp, offset=[0, 0, 0]):
|
|
# Original Author: Jeremy Ernst
|
|
|
|
# Usage: create a counter twist rig using the incoming joints
|
|
if len(joints) == 0:
|
|
return
|
|
|
|
# data lists
|
|
createdGroups = []
|
|
createdControls = []
|
|
|
|
# ===========================================================================
|
|
# #create the manual controls for the twist joints
|
|
# ===========================================================================
|
|
for i in range(len(joints)):
|
|
|
|
# driven group
|
|
group = cmds.group(empty=True, name=joints[i] + "_driven_grp")
|
|
|
|
constraint = cmds.parentConstraint(joints[i], group)[0]
|
|
cmds.delete(constraint)
|
|
|
|
# anim group
|
|
animGrp = cmds.duplicate(group, po=True, name=joints[i] + "_anim_grp")[0]
|
|
cmds.parent(group, animGrp)
|
|
createdGroups.append(animGrp)
|
|
|
|
# create the twist control
|
|
twistCtrl = createControl("arrow", 1.5, joints[i] + "_anim")
|
|
createdControls.append(twistCtrl)
|
|
constraint = cmds.pointConstraint(joints[i], twistCtrl)[0]
|
|
cmds.delete(constraint)
|
|
cmds.parent(twistCtrl, group)
|
|
colorControl(twistCtrl)
|
|
|
|
cmds.setAttr(twistCtrl + ".rx", offset[0])
|
|
cmds.setAttr(twistCtrl + ".ry", offset[1])
|
|
cmds.setAttr(twistCtrl + ".rz", offset[2])
|
|
cmds.setAttr(twistCtrl + ".v", lock=True, keyable=False)
|
|
|
|
cmds.makeIdentity(twistCtrl, t=1, r=1, s=1, apply=True)
|
|
|
|
# constrain the driver joint to the twist control
|
|
cmds.parentConstraint(twistCtrl, "driver_" + joints[i])
|
|
|
|
# plug master control scale into a new mult node that takes joint.scale into input 1, and master.scale into
|
|
# input 2, and plugs that into driver joint
|
|
if cmds.objExists("master_anim"):
|
|
globalScaleMult = cmds.shadingNode("multiplyDivide", asUtility=True, name=twistCtrl + "_globalScale")
|
|
cmds.connectAttr("master_anim.scale", globalScaleMult + ".input1")
|
|
cmds.connectAttr(twistCtrl + ".scale", globalScaleMult + ".input2")
|
|
createConstraint(globalScaleMult, "driver_" + joints[i], "scale", False, 2, 1, "output")
|
|
else:
|
|
createConstraint(twistCtrl, "driver_" + joints[i], "scale", False, 2, 1)
|
|
|
|
# ===========================================================================
|
|
# #create the ik twist joint chain
|
|
# ===========================================================================
|
|
ikTwistUpper = cmds.createNode("joint", name=name + "_counter_twist_ik")
|
|
ikTwistLower = cmds.createNode("joint", name=name + "_counter_twist_ik_end")
|
|
|
|
cmds.parent(ikTwistLower, ikTwistUpper)
|
|
|
|
# parent under the driver startJoint
|
|
cmds.parent(ikTwistUpper, "driver_" + startJoint)
|
|
constraint = cmds.parentConstraint("driver_" + startJoint, ikTwistUpper)[0]
|
|
cmds.delete(constraint)
|
|
constraint = cmds.parentConstraint("driver_" + endJoint, ikTwistLower)[0]
|
|
cmds.delete(constraint)
|
|
|
|
# add rp ik to those joints
|
|
twistIK = cmds.ikHandle(sol="ikRPsolver", name=name + "_counter_twist_ikHandle", sj=ikTwistUpper, ee=ikTwistLower)[
|
|
0]
|
|
cmds.parent(twistIK, "driver_" + startJoint)
|
|
|
|
ikWorldPos = cmds.xform(twistIK, q=True, ws=True, t=True)[0]
|
|
if ikWorldPos < 0:
|
|
cmds.setAttr(twistIK + ".twist", 180)
|
|
|
|
# create ik pole vector
|
|
twistVector = cmds.spaceLocator(name=name + "_counter_twist_PoleVector")[0]
|
|
constraint = cmds.parentConstraint("driver_" + startJoint, twistVector)[0]
|
|
cmds.delete(constraint)
|
|
cmds.parent(twistVector, "driver_" + startJoint)
|
|
|
|
# create pole vector constraint
|
|
cmds.poleVectorConstraint(twistVector, twistIK)
|
|
|
|
# ===========================================================================
|
|
# #create roll locator (locator that actually will drive the roll amount)
|
|
# ===========================================================================
|
|
rollLoc = cmds.spaceLocator(name=name + "_counter_twist_tracker")[0]
|
|
cmds.setAttr(rollLoc + ".v", 0, lock=True)
|
|
constraint = cmds.parentConstraint("driver_" + startJoint, rollLoc)[0]
|
|
cmds.delete(constraint)
|
|
|
|
cmds.parent(rollLoc, name + "_group")
|
|
cmds.makeIdentity(rollLoc, t=0, r=1, s=0, apply=True)
|
|
cmds.orientConstraint("driver_" + startJoint, rollLoc, skip=["y", "z"])
|
|
|
|
# Group up and parent into rig
|
|
twistGrp = cmds.group(empty=True, name=name + "_counter_twist_grp")
|
|
constraint = cmds.parentConstraint(startJoint, twistGrp)[0]
|
|
cmds.delete(constraint)
|
|
|
|
for group in createdGroups:
|
|
cmds.parent(group, twistGrp)
|
|
|
|
# ===========================================================================
|
|
# #add twist grp to parent group and add all anim groups to twist group
|
|
# ===========================================================================
|
|
cmds.parent(twistGrp, parentGrp)
|
|
|
|
# constrain the group to the startJoint so it follows with the action of the limb
|
|
cmds.parentConstraint("driver_" + startJoint, twistGrp, mo=True)
|
|
|
|
# ===========================================================================
|
|
# #hook up multiply divide nodes
|
|
# ===========================================================================
|
|
|
|
# first one takes rollLoc rotateX and multiplies it by -1 to get the counter
|
|
counterMultNode = cmds.shadingNode("multiplyDivide", asUtility=True, name=name + "_counter_twist_counterNode")
|
|
cmds.connectAttr(rollLoc + ".rotateX", counterMultNode + ".input1X")
|
|
cmds.setAttr(counterMultNode + ".input2X", -1)
|
|
|
|
# second one takes the output of the counterMultNode and multiplies it with the twist amount
|
|
increment = float(1.0) / float(len(joints))
|
|
defaultValue = 1
|
|
for i in range(len(joints)):
|
|
attrName = joints[i] + "_twistAmt"
|
|
grpName = joints[i] + "_driven_grp"
|
|
cmds.addAttr(name + "_settings", ln=attrName, dv=defaultValue, keyable=True)
|
|
defaultValue -= increment
|
|
|
|
# multNode creation
|
|
rollMultNode = cmds.shadingNode("multiplyDivide", asUtility=True, name=joints[i] + "_rollNode")
|
|
cmds.connectAttr(counterMultNode + ".outputX", rollMultNode + ".input1X")
|
|
cmds.connectAttr(name + "_settings." + attrName, rollMultNode + ".input2X")
|
|
|
|
# connect output of roll node to driven group
|
|
cmds.connectAttr(rollMultNode + ".outputX", grpName + ".rotateX")
|
|
|
|
# add attr on rig settings node for manual twist control visibility
|
|
cmds.addAttr(name + "_settings", longName=(startJoint + "_twistCtrlVis"), at='bool', dv=0, keyable=True)
|
|
cmds.connectAttr(name + "_settings." + startJoint + "_twistCtrlVis", twistGrp + ".v")
|
|
|
|
return createdControls
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createTwistRig(joints, name, networkNode, startJoint, endJoint, parentGrp, offset=[0, -90, 0]):
|
|
# Original Author: Jeremy Ernst
|
|
|
|
# Usage: create a standard twist rig using the incoming joints
|
|
defaultValue = .75
|
|
createdControls = []
|
|
|
|
if len(joints) >= 1:
|
|
# create our roll group
|
|
rollGrp = cmds.group(empty=True, name=name + "_" + startJoint + "_twist_grp")
|
|
cmds.parent(rollGrp, parentGrp)
|
|
cmds.parentConstraint("driver_" + startJoint, rollGrp)
|
|
|
|
for i in range(len(joints)):
|
|
|
|
# =======================================================================
|
|
# #create the twist ctrl group
|
|
# =======================================================================
|
|
twistCtrlGrp = cmds.group(empty=True, name=joints[i] + "_twist_ctrl_grp")
|
|
constraint = cmds.parentConstraint("driver_" + joints[i], twistCtrlGrp)[0]
|
|
cmds.delete(constraint)
|
|
|
|
cmds.parent(twistCtrlGrp, rollGrp)
|
|
|
|
# =======================================================================
|
|
# #create the driven twist group
|
|
# =======================================================================
|
|
twistDrivenGrp = cmds.duplicate(twistCtrlGrp, po=True, name=joints[i] + "_twist_driven_grp")[0]
|
|
cmds.parent(twistDrivenGrp, twistCtrlGrp)
|
|
|
|
# =======================================================================
|
|
# #create the manual twist control
|
|
# =======================================================================
|
|
twistCtrl = createControl("arrow", 1, joints[i] + "_anim")
|
|
createdControls.append(twistCtrl)
|
|
cmds.makeIdentity(twistCtrl, t=0, r=1, s=0, apply=True)
|
|
constraint = cmds.parentConstraint(twistCtrlGrp, twistCtrl)[0]
|
|
cmds.delete(constraint)
|
|
|
|
cmds.setAttr(twistCtrl + ".rx", offset[0])
|
|
cmds.setAttr(twistCtrl + ".ry", offset[1])
|
|
cmds.setAttr(twistCtrl + ".rz", offset[2])
|
|
cmds.setAttr(twistCtrl + ".v", lock=True, keyable=False)
|
|
|
|
cmds.parent(twistCtrl, twistDrivenGrp)
|
|
cmds.makeIdentity(twistCtrl, t=1, r=1, s=1, apply=True)
|
|
colorControl(twistCtrl)
|
|
|
|
# =======================================================================
|
|
# #drive the twist
|
|
# =======================================================================
|
|
cmds.addAttr(name + "_settings", ln=joints[i] + "_twistAmt", dv=defaultValue, keyable=True)
|
|
twistMultNode = cmds.shadingNode("multiplyDivide", asUtility=True, name=joints[i] + "_mult_node")
|
|
cmds.connectAttr("driver_" + endJoint + ".rx", twistMultNode + ".input1X")
|
|
cmds.connectAttr(name + "_settings." + joints[i] + "_twistAmt", twistMultNode + ".input2X")
|
|
cmds.connectAttr(twistMultNode + ".outputX", twistDrivenGrp + ".rx")
|
|
|
|
# constrain the driver joint to the twist control
|
|
cmds.parentConstraint(twistCtrl, "driver_" + joints[i])
|
|
cmds.scaleConstraint(twistCtrl, "driver_" + joints[i])
|
|
|
|
# plug master control scale into a new mult node that takes joint.scale into input 1, and master.scale into
|
|
# input 2, and plugs that into driver joint
|
|
if cmds.objExists("master_anim"):
|
|
globalScaleMult = cmds.shadingNode("multiplyDivide", asUtility=True, name=twistCtrl + "_globalScale")
|
|
cmds.connectAttr("master_anim.scale", globalScaleMult + ".input1")
|
|
cmds.connectAttr(twistCtrl + ".scale", globalScaleMult + ".input2")
|
|
createConstraint(globalScaleMult, "driver_" + joints[i], "scale", False, 2, 1, "output")
|
|
else:
|
|
createConstraint(twistCtrl, "driver_" + joints[i], "scale", False, 2, 1)
|
|
|
|
# increment the default value
|
|
increment = float(0.75) / float(len(joints))
|
|
defaultValue -= increment
|
|
|
|
# add attr on rig settings node for manual twist control visibility
|
|
if len(joints) >= 1:
|
|
cmds.addAttr(name + "_settings", longName=(startJoint + "_twistCtrlVis"), at='bool', dv=0, keyable=True)
|
|
cmds.connectAttr(name + "_settings." + startJoint + "_twistCtrlVis", rollGrp + ".v")
|
|
|
|
return createdControls
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createControlFromMover(joint, networkNode, orientToJoint, buildSpaceSwitch):
|
|
# ===========================================================================
|
|
# #take the list of incoming joints and find each joint's mover
|
|
# ===========================================================================
|
|
globalMover = utils.findAssociatedMover(joint, networkNode)
|
|
|
|
# ===========================================================================
|
|
# #if a mover is found, duplicate it and unparent the duplicate
|
|
# ===========================================================================
|
|
if globalMover is not None:
|
|
control = cmds.duplicate(globalMover, rr=True)[0]
|
|
utils.deleteChildren(control)
|
|
parent = cmds.listRelatives(control, parent=True)
|
|
if parent is not None:
|
|
cmds.parent(control, world=True)
|
|
|
|
# turn on visiblity of the control
|
|
cmds.setAttr(control + ".v", 1)
|
|
|
|
for attr in [".translateX", ".translateY", ".translateZ", ".rotateX", ".rotateY", ".rotateZ", ".scaleX",
|
|
".scaleY", ".scaleZ"]:
|
|
cmds.setAttr(control + attr, lock=False, keyable=True)
|
|
|
|
# ensure pivot is correct
|
|
piv = cmds.xform(joint, q=True, ws=True, rp=True)
|
|
cmds.xform(control, ws=True, rp=piv)
|
|
|
|
# create a group for the control
|
|
controlGrp = cmds.group(empty=True, name=control + "_grp")
|
|
if orientToJoint:
|
|
constraint = cmds.parentConstraint(joint, controlGrp)[0]
|
|
else:
|
|
constraint = cmds.pointConstraint(joint, controlGrp)[0]
|
|
|
|
cmds.delete(constraint)
|
|
|
|
# parent the control under the controlGrp
|
|
cmds.parent(control, controlGrp)
|
|
|
|
# freeze transformations on the control
|
|
cmds.makeIdentity(control, t=1, r=1, s=1, apply=True)
|
|
|
|
# =======================================================================
|
|
# #buildSpaceSwitch nodes if needed
|
|
# =======================================================================
|
|
if buildSpaceSwitch:
|
|
spaceSwitchFollow = cmds.duplicate(controlGrp, po=True, name=control + "_space_switcher_follow")[0]
|
|
spaceSwitch = cmds.duplicate(controlGrp, po=True, name=control + "_space_switcher")[0]
|
|
|
|
utils.deleteChildren(spaceSwitchFollow)
|
|
utils.deleteChildren(spaceSwitch)
|
|
|
|
cmds.parent(spaceSwitch, spaceSwitchFollow)
|
|
cmds.parent(controlGrp, spaceSwitch)
|
|
|
|
return [control, controlGrp, spaceSwitch, spaceSwitchFollow]
|
|
|
|
else:
|
|
return [control, controlGrp]
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createConstraint(target, destination, attr, maintainOffset, numRigs, slot, customAttr=None):
|
|
# Original Author: Jeremy Ernst
|
|
|
|
# ===========================================================================
|
|
# #takes the incoming destination and hooks it up to the source using a ramp node
|
|
# ===========================================================================
|
|
if not cmds.objExists(destination + "_" + attr + "_ramp_node"):
|
|
cmds.shadingNode("ramp", asTexture=True, name=destination + "_" + attr + "_ramp_node")
|
|
cmds.setAttr(destination + "_" + attr + "_ramp_node.type", 1)
|
|
|
|
# ===========================================================================
|
|
# #if maintainOffset is passed in, store current destination attr values in an add node as an offset
|
|
# ===========================================================================
|
|
if maintainOffset:
|
|
if not cmds.objExists(destination + "_" + attr + "_offset_node_" + str(slot)):
|
|
cmds.shadingNode("plusMinusAverage", asUtility=True,
|
|
name=destination + "_" + attr + "_offset_node_" + str(slot))
|
|
|
|
offsetValues = cmds.getAttr(destination + "." + attr)[0]
|
|
cmds.setAttr(destination + "_" + attr + "_offset_node_" + str(slot) + ".input3D[0]", offsetValues[0],
|
|
offsetValues[1], offsetValues[2], type="double3")
|
|
|
|
# ===========================================================================
|
|
# #if maintainOffset is True, passed in the target attribute into the add node to add with the offset values
|
|
# ===========================================================================
|
|
if maintainOffset:
|
|
cmds.connectAttr(target + "." + attr, destination + "_" + attr + "_offset_node_" + str(slot) + ".input3D[1]")
|
|
cmds.connectAttr(destination + "_" + attr + "_offset_node_" + str(slot) + ".output3D",
|
|
destination + "_" + attr + "_ramp_node.colorEntryList[" + str(slot) + "].color")
|
|
|
|
# ===========================================================================
|
|
# #otherwise, connect target + .attr to the ramp
|
|
# ===========================================================================
|
|
else:
|
|
if customAttr is None:
|
|
cmds.connectAttr(target + "." + attr,
|
|
destination + "_" + attr + "_ramp_node.colorEntryList[" + str(slot) + "].color")
|
|
else:
|
|
cmds.connectAttr(target + "." + customAttr,
|
|
destination + "_" + attr + "_ramp_node.colorEntryList[" + str(slot) + "].color")
|
|
|
|
# ===========================================================================
|
|
# #get the point position on the ramp for this slot.
|
|
# ===========================================================================
|
|
try:
|
|
pointPos = 1 / (numRigs - 1) * slot
|
|
except:
|
|
pointPos = 0
|
|
|
|
cmds.setAttr(destination + "_" + attr + "_ramp_node.colorEntryList[" + str(slot) + "].position", pointPos)
|
|
|
|
# ===========================================================================
|
|
# #make the output connection if it doesn't yet exist
|
|
# ===========================================================================
|
|
rampExists = cmds.connectionInfo(destination + "." + attr, isDestination=True)
|
|
if not rampExists:
|
|
cmds.connectAttr(destination + "_" + attr + "_ramp_node.outColor", destination + "." + attr)
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def coPlanarModeSnap(instance, snapTarget, snapMover, ikJoints, orientMovers, orientCtrl, skipAttrs):
|
|
# snap the incoming snapMover to snapTarget using the input attribute
|
|
cmds.undoInfo(openChunk=True)
|
|
cmds.cycleCheck(e=False)
|
|
|
|
# get current aimState
|
|
for attr in [".tx", ".ty", ".tz", ".rx", ".ry", ".rz"]:
|
|
cmds.setAttr(snapMover + attr, lock=False)
|
|
|
|
constraint = cmds.pointConstraint(snapTarget, snapMover, skip=skipAttrs)[0]
|
|
cmds.delete(constraint)
|
|
cmds.cycleCheck(e=True)
|
|
|
|
for attr in [".tx", ".ty", ".tz", ".rx", ".ry", ".rz"]:
|
|
cmds.setAttr(snapMover + attr, lock=True)
|
|
|
|
for i in range(len(ikJoints)):
|
|
connection = cmds.connectionInfo(orientMovers[i] + ".rotateX", sourceFromDestination=True)
|
|
aimConstraint = connection.rpartition(".")[0]
|
|
|
|
for attr in [[".rx", ".offsetX"]]:
|
|
value = cmds.getAttr(orientCtrl + attr[0])
|
|
cmds.setAttr(aimConstraint + attr[1], value)
|
|
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def createControl(controlType, size, name, useScaleFactor=True):
|
|
if useScaleFactor:
|
|
scale = getScaleFactor()
|
|
else:
|
|
scale = 1
|
|
|
|
if controlType == "circle":
|
|
control = cmds.circle(c=(0, 0, 0), sw=360, r=size * scale, d=3, name=name)[0]
|
|
|
|
if controlType == "square":
|
|
control = cmds.circle(c=(0, 0, 0), s=4, sw=360, r=size * scale, d=1, name=name)[0]
|
|
cmds.setAttr(control + ".rz", 45)
|
|
|
|
if controlType == "arrow":
|
|
control = cmds.curve(name=name, d=1,
|
|
p=[(0, -45, 0), (5, -45, 0), (5, -62, 0), (10, -62, 0), (0, -72, 0), (-10, -62, 0),
|
|
(-5, -62, 0), (-5, -45, 0), (0, -45, 0)])
|
|
cmds.xform(control, cp=True)
|
|
bounds = cmds.exactWorldBoundingBox(control)
|
|
length = abs(bounds[1] - bounds[4]) / 2
|
|
cmds.xform(control, r=True, rp=(0, length, 0), sp=(0, length, 0))
|
|
cmds.move(0, 0, 0, rpr=True)
|
|
|
|
cmds.setAttr(control + ".scaleX", size * scale)
|
|
cmds.setAttr(control + ".scaleY", size * scale)
|
|
cmds.setAttr(control + ".scaleZ", size * scale)
|
|
|
|
if controlType == "arrowOnBall":
|
|
control = cmds.curve(name=name, d=1, p=[(0.80718, 0.830576, 8.022739), (0.80718, 4.219206, 7.146586),
|
|
(0.80718, 6.317059, 5.70073), (2.830981, 6.317059, 5.70073),
|
|
(0, 8.422749, 2.94335), (-2.830981, 6.317059, 5.70073),
|
|
(-0.80718, 6.317059, 5.70073), (-0.80718, 4.219352, 7.146486),
|
|
(-0.80718, 0.830576, 8.022739), (-4.187851, 0.830576, 7.158003),
|
|
(-6.310271, 0.830576, 5.705409), (-6.317059, 2.830981, 5.7007),
|
|
(-8.422749, 0, 2.94335), (-6.317059, -2.830981, 5.70073),
|
|
(-6.317059, -0.830576, 5.70073), (-4.225134, -0.830576, 7.142501),
|
|
(-0.827872, -0.830576, 8.017446), (-0.80718, -4.176512, 7.160965),
|
|
(-0.80718, -6.317059, 5.70073), (-2.830981, -6.317059, 5.70073),
|
|
(0, -8.422749, 2.94335), (2.830981, -6.317059, 5.70073),
|
|
(0.80718, -6.317059, 5.70073), (0.80718, -4.21137, 7.151987),
|
|
(0.80718, -0.830576, 8.022739), (4.183345, -0.830576, 7.159155),
|
|
(6.317059, -0.830576, 5.70073), (6.317059, -2.830981, 5.70073),
|
|
(8.422749, 0, 2.94335), (6.317059, 2.830981, 5.70073),
|
|
(6.317059, 0.830576, 5.70073), (4.263245, 0.830576, 7.116234),
|
|
(0.80718, 0.830576, 8.022739)])
|
|
|
|
cmds.setAttr(control + ".scaleX", size * scale)
|
|
cmds.setAttr(control + ".scaleY", size * scale)
|
|
cmds.setAttr(control + ".scaleZ", size * scale)
|
|
|
|
if controlType == "semiCircle":
|
|
control = cmds.curve(name=name, d=3,
|
|
p=[(0, 0, 0), (7, 0, 0), (8, 0, 0), (5, 4, 0), (0, 5, 0), (-5, 4, 0), (-8, 0, 0),
|
|
(-7, 0, 0), (0, 0, 0)])
|
|
cmds.xform(control, ws=True, t=(0, 5, 0))
|
|
cmds.xform(control, ws=True, piv=(0, 0, 0))
|
|
cmds.makeIdentity(control, t=1, apply=True)
|
|
|
|
cmds.setAttr(control + ".scaleX", size * scale)
|
|
cmds.setAttr(control + ".scaleY", size * scale)
|
|
cmds.setAttr(control + ".scaleZ", size * scale)
|
|
|
|
if controlType == "pin":
|
|
control = cmds.curve(name=name, d=1, p=[(12, 0, 0), (0, 0, 0), (-12, -12, 0), (-12, 12, 0), (0, 0, 0)])
|
|
cmds.xform(control, ws=True, piv=[12, 0, 0])
|
|
cmds.setAttr(control + ".scaleY", .5)
|
|
cmds.makeIdentity(control, t=1, apply=True)
|
|
|
|
cmds.setAttr(control + ".scaleX", size * scale)
|
|
cmds.setAttr(control + ".scaleY", size * scale)
|
|
cmds.setAttr(control + ".scaleZ", size * scale)
|
|
|
|
if controlType == "sphere":
|
|
points = [(0, 0, 1), (0, 0.5, 0.866), (0, 0.866025, 0.5), (0, 1, 0), (0, 0.866025, -0.5), (0, 0.5, -0.866025),
|
|
(0, 0, -1), (0, -0.5, -0.866025), (0, -0.866025, -0.5), (0, -1, 0), (0, -0.866025, 0.5),
|
|
(0, -0.5, 0.866025), (0, 0, 1), (0.707107, 0, 0.707107), (1, 0, 0), (0.707107, 0, -0.707107),
|
|
(0, 0, -1), (-0.707107, 0, -0.707107), (-1, 0, 0), (-0.866025, 0.5, 0), (-0.5, 0.866025, 0),
|
|
(0, 1, 0), (0.5, 0.866025, 0), (0.866025, 0.5, 0), (1, 0, 0), (0.866025, -0.5, 0),
|
|
(0.5, -0.866025, 0), (0, -1, 0), (-0.5, -0.866025, 0), (-0.866025, -0.5, 0), (-1, 0, 0),
|
|
(-0.707107, 0, 0.707107), (0, 0, 1)]
|
|
control = cmds.curve(name=name, d=1, p=points)
|
|
|
|
cmds.setAttr(control + ".scaleX", size * scale)
|
|
cmds.setAttr(control + ".scaleY", size * scale)
|
|
cmds.setAttr(control + ".scaleZ", size * scale)
|
|
|
|
return control
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def colorControl(control, value=False):
|
|
# unlock overrides
|
|
cmds.setAttr(control + ".overrideEnabled", 1)
|
|
|
|
if value == False:
|
|
# get the world position of the control
|
|
worldPos = cmds.xform(control, q=True, ws=True, t=True)
|
|
if worldPos[0] > 0:
|
|
cmds.setAttr(control + ".overrideColor", 6)
|
|
else:
|
|
cmds.setAttr(control + ".overrideColor", 13)
|
|
|
|
else:
|
|
cmds.setAttr(control + ".overrideColor", value)
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def fixSkinWeights():
|
|
# get selection
|
|
selection = cmds.ls(sl=True)
|
|
newSelection = []
|
|
|
|
# loop through selection getting the shapes and meshes
|
|
for each in selection:
|
|
shapes = cmds.listRelatives(each, shapes=True, pa=True)
|
|
meshes = cmds.ls(shapes, type="mesh")
|
|
|
|
# duplicate the object
|
|
duplicateObject = cmds.duplicate(each, rr=True)
|
|
cmds.delete(duplicateObject[0], ch=True)
|
|
|
|
newShapes = cmds.listRelatives(duplicateObject[0], shapes=True, pa=True)
|
|
newMeshes = cmds.ls(newShapes, type="mesh")
|
|
|
|
# find skinCluster of original object
|
|
skinCluster = findRelatedSkinCluster(each)
|
|
|
|
# find bones in skinCluster
|
|
bones = cmds.listConnections(skinCluster + ".matrix", d=True)
|
|
maxInfs = cmds.skinCluster(skinCluster, q=True, mi=True)
|
|
newSkin = cmds.skinCluster(bones, duplicateObject[0], mi=maxInfs, dr=4)[0]
|
|
cmds.copySkinWeights(ss=skinCluster, ds=newSkin, nm=True)
|
|
|
|
# rename
|
|
cmds.delete(each)
|
|
newObj = cmds.rename(duplicateObject, each)
|
|
newSelection.append(newObj)
|
|
|
|
cmds.select(newSelection)
|
|
|
|
|
|
def export_skin_weights(file_path=None, geometry=None):
|
|
"""Exports out skin weight from selected geometry"""
|
|
data = list()
|
|
# error handling
|
|
if not file_path:
|
|
return OpenMaya.MGlobal_displayError("No file path given.")
|
|
if not geometry:
|
|
geometry = _geometry_check(geometry)
|
|
if not geometry:
|
|
return OpenMaya.MGlobal_displayError("No valid geometry.")
|
|
|
|
# build up skin data
|
|
skin_clusters = find_skin_clusters(geometry)
|
|
if not skin_clusters:
|
|
skin_message = "No skin clusters found on {0}.".format(geometry)
|
|
OpenMaya.MGlobal_displayWarning(skin_message)
|
|
for skin_cluster in skin_clusters:
|
|
skin_data_init = SkinData(skin_cluster)
|
|
skin_data = skin_data_init.gather_data()
|
|
data.append(skin_data)
|
|
args = [skin_data_init.skin_cluster, file_path]
|
|
export_message = "SkinCluster: {0} has " \
|
|
"been exported to {1}.".format(*args)
|
|
OpenMaya.MGlobal_displayInfo(export_message)
|
|
|
|
# dump data
|
|
file_path = utils.win_path_convert(file_path)
|
|
data = json.dumps(data, sort_keys=True, ensure_ascii=True, indent=2)
|
|
fobj = open(file_path, 'wb')
|
|
fobj.write(data)
|
|
fobj.close()
|
|
|
|
|
|
def find_skin_clusters(nodes):
|
|
"""Uses all incoming to search relatives to
|
|
find associated skinCluster.
|
|
@PARAMS:
|
|
nodes: list
|
|
"""
|
|
skin_clusters = list()
|
|
if not isinstance(nodes, list):
|
|
nodes = [nodes]
|
|
relatives = cmds.listRelatives(nodes, ad=True, path=True)
|
|
all_incoming = utils.find_all_incoming(relatives)
|
|
for node in all_incoming:
|
|
if cmds.nodeType(node) == "skinCluster":
|
|
if node not in skin_clusters:
|
|
skin_clusters.append(node)
|
|
return skin_clusters
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
def findRelatedSkinCluster(object):
|
|
skinClusters = cmds.ls(type='skinCluster')
|
|
|
|
for cluster in skinClusters:
|
|
geometry = cmds.skinCluster(cluster, q=True, g=True)[0]
|
|
geoTransform = cmds.listRelatives(geometry, parent=True)[0]
|
|
|
|
dagPath = cmds.ls(geoTransform, long=True)[0]
|
|
|
|
if geoTransform == object:
|
|
return cluster
|
|
elif dagPath == object:
|
|
return cluster
|
|
|
|
|
|
def import_skin_weights(file_path=None, geometry=None, remove_unused=None):
|
|
# error handling
|
|
if not file_path:
|
|
return OpenMaya.MGlobal_displayError("No file path given.")
|
|
if not geometry:
|
|
return
|
|
|
|
# load data
|
|
if not os.path.exists(file_path):
|
|
path_message = "Could not find {0} file.".format(file_path)
|
|
return OpenMaya.MGlobal_displayWarning(path_message)
|
|
|
|
fobj = open(file_path, "rb")
|
|
data = json.load(fobj)
|
|
|
|
# check verts
|
|
vert_check = _vert_check(data, geometry)
|
|
if not vert_check:
|
|
return
|
|
|
|
# import skin weights
|
|
_import_skin_weights(data, geometry, file_path, remove_unused)
|
|
|
|
|
|
def _import_skin_weights(data, geometry, file_path, remove_unused=None):
|
|
# loop through skin data
|
|
for skin_data in data:
|
|
geometry = skin_data["shape"]
|
|
if not cmds.objExists(geometry):
|
|
continue
|
|
|
|
skin_clusters = find_skin_clusters(geometry)
|
|
if skin_clusters:
|
|
skin_cluster = SkinData(skin_clusters[0])
|
|
skin_cluster.set_data(skin_data)
|
|
else:
|
|
# TODO: make joint remapper, Chris has a setup for this already
|
|
skin_cluster = _create_new_skin_cluster(skin_data, geometry)
|
|
if not skin_cluster:
|
|
continue
|
|
skin_cluster[0].set_data(skin_data)
|
|
if remove_unused:
|
|
if skin_clusters:
|
|
_remove_unused_influences(skin_clusters[0])
|
|
else:
|
|
_remove_unused_influences(skin_cluster[1])
|
|
OpenMaya.MGlobal_displayInfo("Imported {0}".format(file_path))
|
|
|
|
|
|
def _vert_check(data, geometry):
|
|
# check vertex count
|
|
for skin_data in data:
|
|
geometry = skin_data["shape"]
|
|
vert_count = cmds.polyEvaluate(geometry, vertex=True)
|
|
import_vert_count = len(skin_data["blendWeights"])
|
|
if vert_count != import_vert_count:
|
|
vert_message = "The vert count does not match for this geometry: " + str(geometry)
|
|
return OpenMaya.MGlobal_displayError(vert_message)
|
|
return True
|
|
|
|
|
|
def _create_new_skin_cluster(skin_data, geometry):
|
|
# check joints
|
|
joints = skin_data["weights"].keys()
|
|
unused_joints = list()
|
|
scene_joints = set([utils.remove_namespace(joint) for joint \
|
|
in cmds.ls(type="joint")])
|
|
for joint in joints:
|
|
if not joint in scene_joints:
|
|
unused_joints.append(joint)
|
|
# TODO: make joint remapper, Chris has a setup for this already
|
|
if unused_joints and not scene_joints:
|
|
return
|
|
|
|
skin_cluster = cmds.skinCluster(joints, geometry, tsb=True, nw=2,
|
|
n=skin_data["skinCluster"])[0]
|
|
return (SkinData(skin_cluster), skin_cluster)
|
|
|
|
|
|
def _remove_unused_influences(skin_cluster):
|
|
influences_to_remove = list()
|
|
weighted_influences = cmds.skinCluster(skin_cluster, q=True, wi=True)
|
|
final_transforms = cmds.skinCluster(skin_cluster, q=True, inf=True)
|
|
for influence in final_transforms:
|
|
if influence not in weighted_influences:
|
|
influences_to_remove.append(influence)
|
|
for influence in influences_to_remove:
|
|
cmds.skinCluster(skin_cluster, e=True, ri=influence)
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# CLASSES
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
class SkinWeights(object):
|
|
'''
|
|
Takes in a .WEIGHTS file from disk, an existing skinCluster, or a skinned mesh as inputs
|
|
If qtProgbar == 'internal' then it will generate a modal progbar
|
|
'''
|
|
|
|
def __init__(self, skinFile=None, skinCluster=None, mesh=None):
|
|
self.skin = None
|
|
self.vertices = None
|
|
self.skinFile = None
|
|
self.mesh = None
|
|
self.uvSet = None
|
|
self.numVerts = None
|
|
self.skinDict = None
|
|
self.joints = []
|
|
self.applied = False
|
|
|
|
# TODO: check that the file is really on disk
|
|
if skinFile:
|
|
if os.path.exists(skinFile):
|
|
self.skinDict = self.importSkinFile(skinFile)
|
|
self.skinFile = skinFile
|
|
else:
|
|
cmds.warning('riggingUtils.SkinWeights: cannot find file on disk: ' + skinFile)
|
|
if skinCluster:
|
|
if cmds.objExists(skinCluster):
|
|
self.skin = skinCluster
|
|
self.mesh = cmds.listConnections(skinCluster + '.outputGeometry')[0]
|
|
else:
|
|
cmds.warning('riggingUtils.SkinWeights: cannot find skinCluster: ' + skinCluster)
|
|
if mesh:
|
|
if cmds.objExists(mesh):
|
|
self.mesh = mesh
|
|
sk = findRelatedSkinCluster(mesh)
|
|
if sk:
|
|
self.skin = sk
|
|
else:
|
|
cmds.warning('riggingUtils.SkinWeights: mesh has no skinCluster: ' + mesh)
|
|
else:
|
|
cmds.warning('riggingUtils.SkinWeights: Cannot find mesh: ' + mesh)
|
|
|
|
def exportSkinWeights(self, filePath, qtProgbar=None, freq=1):
|
|
time1 = time.time()
|
|
vertPoints = cmds.getAttr(self.mesh + '.vtx[*]')
|
|
|
|
f = {}
|
|
header = {}
|
|
vtxDict = {}
|
|
skin = findRelatedSkinCluster(self.mesh)
|
|
|
|
numVerts = cmds.polyEvaluate(self.mesh, vertex=1)
|
|
|
|
# fill header
|
|
header['mesh'] = self.mesh
|
|
header['skinCluster'] = skin
|
|
header['numVerts'] = numVerts
|
|
header['uvSet'] = cmds.polyUVSet(self.mesh, currentUVSet=1, q=1)[0]
|
|
|
|
# fill vtxDict
|
|
if qtProgbar == 'internal':
|
|
from System.interfaceUtils import progressDialog
|
|
qtProgbar = progressDialog((0, numVerts), label="Exporting skin weights...")
|
|
elif qtProgbar:
|
|
qtProgbar.setRange(0, numVerts)
|
|
qtProgbar.setValue(0)
|
|
for vtx in range(0, numVerts):
|
|
vtxDict[vtx] = {}
|
|
vtxDict[vtx]['world'] = tuple(cmds.pointPosition(self.mesh + '.vtx[' + str(vtx) + ']', w=1))
|
|
vtxDict[vtx]['local'] = tuple(cmds.pointPosition(self.mesh + '.vtx[' + str(vtx) + ']', l=1))
|
|
# we will loop all the skinning influences of this vertex and record their names and values
|
|
vtxDict[vtx]['skinning'] = []
|
|
|
|
joints = cmds.skinPercent(skin, self.mesh + '.vtx[' + str(vtx) + ']', q=1, t=None)
|
|
influence_value = cmds.skinPercent(skin, self.mesh + '.vtx[' + str(vtx) + ']', q=True, v=True)
|
|
|
|
for jnt, val in zip(joints, influence_value):
|
|
if val > 0:
|
|
vtxDict[vtx]['skinning'].append([jnt, val])
|
|
|
|
uv = cmds.polyListComponentConversion(self.mesh + ".vtx[" + str(vtx) + "]", fv=True, tuv=True)
|
|
vtxDict[vtx]['uv'] = cmds.polyEditUV(uv, q=True)
|
|
if qtProgbar:
|
|
qtProgbar.setValue(vtx)
|
|
|
|
f['header'] = header
|
|
f['vtxDict'] = vtxDict
|
|
wFile = file(filePath, mode='w')
|
|
wFile.write(json.dumps(f, wFile, indent=4))
|
|
wFile.close()
|
|
|
|
# TODO: validate this
|
|
time2 = time.time()
|
|
print(f'exportSkinWeights: Weights saved to: {filePath} in {(time2 - time1):.3f} sec')
|
|
return True
|
|
|
|
def importSkinFile(self, filePath):
|
|
import json
|
|
if os.path.exists(filePath):
|
|
f = open(filePath, 'r')
|
|
self.skinDict = json.load(f)
|
|
f.close()
|
|
|
|
self.skin = self.skinDict['header']['skinCluster']
|
|
self.mesh = self.skinDict['header']['mesh']
|
|
self.uvSet = self.skinDict['header']['uvSet']
|
|
self.numVerts = self.skinDict['header']['numVerts']
|
|
self.vertices = self.skinDict['vtxDict']
|
|
|
|
for vtx in self.skinDict['vtxDict']:
|
|
# build joint list
|
|
for inf in self.skinDict['vtxDict'][vtx]['skinning']:
|
|
if inf[0] not in self.joints:
|
|
self.joints.append(inf[0])
|
|
|
|
# This validates that the influence joints exist
|
|
def verifiedInfluences(self, joints):
|
|
jointsInScene = []
|
|
for jnt in joints:
|
|
if cmds.objExists(jnt):
|
|
jointsInScene.append(jnt)
|
|
else:
|
|
cmds.warning('SKIN WEIGHT IMPORT: Cannot find joint that matches', jnt, 'in the current scene.')
|
|
return jointsInScene
|
|
|
|
def square_distance(self, pointA, pointB):
|
|
# squared euclidean distance
|
|
distance = 0
|
|
dimensions = len(pointA) # assumes both points have the same dimensions
|
|
for dimension in range(dimensions):
|
|
distance += (pointA[dimension] - pointB[dimension]) ** 2
|
|
return distance
|
|
|
|
# Toss the UV list to a point array
|
|
def uvListToPointArray(self, uvPoints):
|
|
returnArray = []
|
|
if len(uvPoints) > 2:
|
|
for i in range(0, len(uvPoints), 2):
|
|
newPos = [uvPoints[i], uvPoints[i + 1]]
|
|
if len(newPos) != 2:
|
|
cmds.error(newPos + ' not 2d!')
|
|
returnArray.append(newPos)
|
|
else:
|
|
returnArray.append(uvPoints)
|
|
return returnArray
|
|
|
|
def applySkinWeights(self, mesh, applyBy='Vertex Order', killSkin=0, debug=0, qtProgbar=None):
|
|
self.skin = findRelatedSkinCluster(mesh)
|
|
|
|
# remove existing skin cluster
|
|
if self.skin and killSkin or not self.skin:
|
|
if killSkin:
|
|
bindposes = cmds.listConnections(self.skin + '.bindPose')
|
|
bindposes.append(self.skin)
|
|
for obj in bindposes:
|
|
cmds.delete(obj)
|
|
bind = self.verifiedInfluences(self.joints)
|
|
bind.append(mesh)
|
|
sel = cmds.ls(sl=1)
|
|
cmds.select(cl=1)
|
|
cmds.select(bind)
|
|
self.skin = cmds.skinCluster(skinMethod=0, name=(mesh.split('|')[-1] + "_skinCluster"))[0]
|
|
cmds.select(sel)
|
|
|
|
if self.skin:
|
|
# build kdTree if required
|
|
vtxDict = {}
|
|
vtxList = []
|
|
vtxTree = None
|
|
|
|
# parse weight file
|
|
if applyBy == 'World Position':
|
|
for vtx in self.vertices:
|
|
pos = self.vertices[vtx]['world']
|
|
vtxDict[str(pos)] = vtx
|
|
vtxList.append(pos)
|
|
start = time.time()
|
|
vtxTree = mathUtils.KDTree.construct_from_data(vtxList)
|
|
elapsed = (time.time() - start)
|
|
if applyBy == 'Local Position':
|
|
for vtx in self.vertices:
|
|
pos = self.vertices[vtx]['local']
|
|
vtxDict[str(pos)] = vtx
|
|
vtxList.append(pos)
|
|
start = time.time()
|
|
vtxTree = mathUtils.KDTree.construct_from_data(vtxList)
|
|
elapsed = (time.time() - start)
|
|
if applyBy == 'UV Position':
|
|
for vtx in self.vertices:
|
|
pos = self.vertices[vtx]['uv']
|
|
# When one vtx has multiple UV locations, we add it to the mapping table once for each location
|
|
if len(pos) > 2:
|
|
for i in range(0, len(pos), 2):
|
|
newPos = [pos[i], pos[i + 1]]
|
|
if debug:
|
|
print(f'NEWPOS: {newPos}')
|
|
if len(newPos) > 2:
|
|
cmds.error(newPos + ' not 2d!')
|
|
vtxDict[str(newPos)] = vtx
|
|
vtxList.append(newPos)
|
|
|
|
else:
|
|
vtxDict[str(pos)] = vtx
|
|
vtxList.append(pos)
|
|
if debug:
|
|
print(f'VTXLIST: {vtxList}')
|
|
print(f'VTXDICT: {vtxDict}')
|
|
start = time.time()
|
|
vtxTree = mathUtils.KDTree.construct_from_data(vtxList)
|
|
elapsed = (time.time() - start)
|
|
|
|
verts = cmds.polyEvaluate(mesh, vertex=1)
|
|
# progress bar
|
|
if qtProgbar == 'internal':
|
|
from System.interfaceUtils import progressDialog
|
|
qtProgbar = progressDialog((0, verts), label="Importing skin weights...")
|
|
elif qtProgbar:
|
|
qtProgbar.setRange(0, verts)
|
|
qtProgbar.setValue(0)
|
|
|
|
# set the weights
|
|
try:
|
|
cmds.undoInfo(openChunk=True)
|
|
time1 = time.time()
|
|
for vtx in range(0, verts):
|
|
if applyBy == 'Vertex Order':
|
|
# TODO: check if numVerts match
|
|
cmds.skinPercent(self.skin, mesh + ".vtx[" + str(vtx) + "]",
|
|
transformValue=self.vertices[str(vtx)]['skinning'])
|
|
if applyBy == 'World Position':
|
|
if vtxTree:
|
|
vtxPos = \
|
|
vtxTree.query(tuple(cmds.pointPosition(mesh + '.vtx[' + str(vtx) + ']', w=1)), t=1)[0]
|
|
vtxMaya = mesh + '.vtx[' + str(vtx) + ']'
|
|
if debug:
|
|
print(f'Original vertex {vtxDict[str(vtxPos)]} maps to new vtx {vtxMaya}')
|
|
cmds.skinPercent(self.skin, vtxMaya,
|
|
transformValue=self.vertices[vtxDict[str(vtxPos)]]['skinning'])
|
|
else:
|
|
cmds.warning('IMPORT SKIN WEIGHTS: No existing kdTree built.')
|
|
if applyBy == 'Local Position':
|
|
if vtxTree:
|
|
vtxPos = vtxTree.query(tuple(cmds.pointPosition(mesh + '.vtx[' + str(vtx) + ']')), t=1)[0]
|
|
vtxMaya = mesh + '.vtx[' + str(vtx) + ']'
|
|
if debug:
|
|
print(f'Original vertex {vtxDict[str(vtxPos)]} maps to new vtx {vtxMaya}')
|
|
cmds.skinPercent(self.skin, vtxMaya,
|
|
transformValue=self.vertices[vtxDict[str(vtxPos)]]['skinning'])
|
|
else:
|
|
cmds.warning('IMPORT SKIN WEIGHTS: No existing kdTree built.')
|
|
if applyBy == 'UV Space':
|
|
if vtxTree:
|
|
uvPoint = cmds.polyEditUV(
|
|
cmds.polyListComponentConversion(mesh + ".vtx[" + str(vtx) + "]", fv=True,
|
|
tuv=True),
|
|
q=1)
|
|
pos = self.uvListToPointArray(uvPoint)
|
|
vtxPos = None
|
|
|
|
if debug:
|
|
print(f'uvPoint: {uvPoint}')
|
|
print(f'pos: {pos}')
|
|
# check for multiple uv points assoc with the vert
|
|
if len(pos) > 1:
|
|
distance = None
|
|
vtxPos = None
|
|
for p in pos:
|
|
if debug:
|
|
print(pos)
|
|
print(p)
|
|
closest = vtxTree.query(tuple(p), t=1)[0]
|
|
dist = self.square_distance(p, closest)
|
|
if not distance:
|
|
distance = dist
|
|
vtxPos = closest
|
|
else:
|
|
if dist < distance:
|
|
distance = dist
|
|
vtxPos = closest
|
|
|
|
else:
|
|
vtxPos = vtxTree.query(tuple(pos[0]), t=1)[0]
|
|
vtxMaya = mesh + '.vtx[' + str(vtx) + ']'
|
|
if debug:
|
|
print(f'vtxPos: {str(vtxPos)}')
|
|
print(f'Original UV vertex {vtxDict[str(vtxPos)]} maps to new UV vtx {vtxMaya}')
|
|
print(f'SKINNING: {self.vertices[vtxDict[str(vtxPos)]]["skinning"]} {vtxDict[str(vtxPos)]}')
|
|
cmds.skinPercent(self.skin, vtxMaya,
|
|
transformValue=self.vertices[vtxDict[str(vtxPos)]]['skinning'])
|
|
else:
|
|
cmds.warning('IMPORT SKIN WEIGHTS: No existing kdTree built.')
|
|
|
|
# Update the progressbar, but only every 100th vertex
|
|
if qtProgbar:
|
|
if vtx % 100 == 0 or (vtx + 1) == verts:
|
|
qtProgbar.setValue(vtx + 1)
|
|
time2 = time.time()
|
|
print(f'importSkinWeights: Weights loaded from {self.skinFile} in {(time2 - time1):.3f} sec')
|
|
except Exception as e:
|
|
print(e)
|
|
finally:
|
|
cmds.undoInfo(closeChunk=True)
|
|
|
|
else:
|
|
cmds.warning('IMPORT SKIN WEIGHTS: No skinCluster found on: ' + mesh)
|
|
|
|
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
# # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # #
|
|
class SkinData(object):
|
|
def __init__(self, skin_cluster):
|
|
|
|
# globals/data
|
|
self.skin_cluster = skin_cluster
|
|
deformer = cmds.deformer(skin_cluster, q=True, g=True)[0]
|
|
self.shape = cmds.listRelatives(deformer, parent=True, path=True)[0]
|
|
self.mobject = utils.get_mobject(self.skin_cluster)
|
|
self.skin_set = OpenMayaAnim.MFnSkinCluster(self.mobject)
|
|
self.data = {
|
|
"weights": dict(),
|
|
"blendWeights": list(),
|
|
"skinCluster": self.skin_cluster,
|
|
"shape": self.shape
|
|
}
|
|
|
|
def gather_data(self):
|
|
|
|
# get incluence and blend weight data
|
|
dag_path, mobject = self.get_skin_dag_path_and_mobject()
|
|
self.get_influence_weights(dag_path, mobject)
|
|
self.get_blend_weights(dag_path, mobject)
|
|
|
|
# add in attribute data
|
|
for attribute in ATTRIBUTES:
|
|
self.data[attribute] = cmds.getAttr("{0}.{1}". \
|
|
format(self.skin_cluster,
|
|
attribute))
|
|
return self.data
|
|
|
|
def get_skin_dag_path_and_mobject(self):
|
|
function_set = OpenMaya.MFnSet(self.skin_set.deformerSet())
|
|
selection_list = OpenMaya.MSelectionList()
|
|
function_set.getMembers(selection_list, False)
|
|
dag_path = OpenMaya.MDagPath()
|
|
mobject = OpenMaya.MObject()
|
|
selection_list.getDagPath(0, dag_path, mobject)
|
|
return dag_path, mobject
|
|
|
|
def get_influence_weights(self, dag_path, mobject):
|
|
weights = self._get_weights(dag_path, mobject)
|
|
|
|
influence_paths = OpenMaya.MDagPathArray()
|
|
influence_count = self.skin_set.influenceObjects(influence_paths)
|
|
components_per_influence = weights.length() // influence_count
|
|
for count in range(influence_paths.length()):
|
|
name = influence_paths[count].partialPathName()
|
|
name = utils.remove_namespace(name)
|
|
weight_data = [weights[influence * influence_count + count] \
|
|
for influence in range(components_per_influence)]
|
|
self.data["weights"][name] = weight_data
|
|
|
|
def _get_weights(self, dag_path, mobject):
|
|
"""Where the API magic happens."""
|
|
weights = OpenMaya.MDoubleArray()
|
|
util = OpenMaya.MScriptUtil()
|
|
util.createFromInt(0)
|
|
pointer = util.asUintPtr()
|
|
|
|
# magic call
|
|
self.skin_set.getWeights(dag_path, mobject, weights, pointer);
|
|
return weights
|
|
|
|
def get_blend_weights(self, dag_path, mobject):
|
|
return self._get_blend_weights(dag_path, mobject)
|
|
|
|
def _get_blend_weights(self, dag_path, mobject):
|
|
weights = OpenMaya.MDoubleArray()
|
|
|
|
# magic call
|
|
self.skin_set.getBlendWeights(dag_path, mobject, weights)
|
|
blend_data = [weights[blend_weight] for \
|
|
blend_weight in range(weights.length())]
|
|
self.data["blendWeights"] = blend_data
|
|
|
|
def set_data(self, data):
|
|
"""Final point for importing weights. Sets and applies influences
|
|
and blend weight values.
|
|
@PARAMS:
|
|
data: dict()
|
|
"""
|
|
self.data = data
|
|
dag_path, mobject = self.get_skin_dag_path_and_mobject()
|
|
self.set_influence_weights(dag_path, mobject)
|
|
self.set_blend_weights(dag_path, mobject)
|
|
|
|
# set skinCluster Attributes
|
|
for attribute in ATTRIBUTES:
|
|
cmds.setAttr('{0}.{1}'.format(self.skin_cluster, attribute),
|
|
self.data[attribute])
|
|
|
|
def set_influence_weights(self, dag_path, mobject):
|
|
weights = self._get_weights(dag_path, mobject)
|
|
influence_paths = OpenMaya.MDagPathArray()
|
|
influence_count = self.skin_set.influenceObjects(influence_paths)
|
|
components_per_influence = weights.length() // influence_count
|
|
|
|
# influences
|
|
unused_influences = list()
|
|
influences = [influence_paths[inf_count].partialPathName() for \
|
|
inf_count in range(influence_paths.length())]
|
|
|
|
# build influences/weights
|
|
for imported_influence, imported_weights in self.data['weights'].items():
|
|
for inf_count in range(influence_paths.length()):
|
|
influence_name = influence_paths[inf_count].partialPathName()
|
|
influence_name = utils.remove_namespace(influence_name)
|
|
if influence_name == imported_influence:
|
|
# set the weights
|
|
for count in range(components_per_influence):
|
|
weights.set(imported_weights[count],
|
|
count * influence_count + inf_count)
|
|
influences.remove(influence_name)
|
|
break
|
|
else:
|
|
unused_influences.append(imported_influence)
|
|
|
|
# TODO: make joint remapper
|
|
if unused_influences and influences:
|
|
OpenMaya.MGlobal_displayWarning("Make a joint remapper, Aaron!")
|
|
|
|
# set influences
|
|
influence_array = OpenMaya.MIntArray(influence_count)
|
|
for count in range(influence_count):
|
|
influence_array.set(count, count)
|
|
# set weights
|
|
self.skin_set.setWeights(dag_path, mobject, influence_array, weights, False)
|
|
|
|
def set_blend_weights(self, dag_path, mobject):
|
|
blend_weights = OpenMaya.MDoubleArray(len(self.data['blendWeights']))
|
|
for influence, weight in enumerate(self.data['blendWeights']):
|
|
blend_weights.set(weight, influence)
|
|
self.skin_set.setBlendWeights(dag_path, mobject, blend_weights)
|