Compare commits
4 Commits
release-v1
...
release-v1
| Author | SHA1 | Date | |
|---|---|---|---|
| e0d4d0c364 | |||
| d853883d5f | |||
| 3186163e84 | |||
| 618e2abae5 |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
144
2023/scripts/animation_tools/IKFK_SWITCH_README.md
Normal file
144
2023/scripts/animation_tools/IKFK_SWITCH_README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# IKFK Switch Tool
|
||||
|
||||
## 概述
|
||||
IKFK Switch Tool 是一个用于在Maya中实现IK和FK动画之间无缝切换的工具。该工具从原始MEL脚本转换为Python版本,提供更好的跨版本兼容性。
|
||||
|
||||
## 版本信息
|
||||
- **版本**: V1.0
|
||||
- **兼容性**: Maya 2018+
|
||||
- **语言**: Python
|
||||
- **原始版本**: MEL (ikfk_switch.mel)
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 主要功能
|
||||
1. **无缝切换**: 在IK和FK动画之间进行无缝切换,保持动画连续性
|
||||
2. **自动匹配**: 切换时自动匹配控制器位置和旋转
|
||||
3. **关键帧设置**: 自动在切换点设置关键帧
|
||||
4. **预设支持**: 内置ADV绑定预设,支持快速设置
|
||||
|
||||
### UI功能
|
||||
- **FK控制器加载**: 加载FK骨骼和控制器
|
||||
- FK Joint Root/Mid/End
|
||||
- FK Ctrl Root/Mid/End
|
||||
|
||||
- **IK控制器加载**: 加载IK骨骼和控制器
|
||||
- IK Joint Root/Mid/End
|
||||
- IK Ctrl Root (IK手柄)
|
||||
- IK Ctrl Pole (极向量控制器)
|
||||
|
||||
- **切换控制器**: 设置切换属性
|
||||
- Switch Ctrl (切换控制器)
|
||||
- Switch Attr (切换属性名称)
|
||||
|
||||
### 辅助功能
|
||||
- **ADV Build**: 快速为ADV绑定设置IKFK切换
|
||||
- **Empty**: 清空所有输入字段
|
||||
- **BiliBili**: 打开教程链接
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本使用流程
|
||||
|
||||
1. **加载FK控制器**
|
||||
- 选择FK骨骼根节点,点击"< FK Joint Root"
|
||||
- 选择FK骨骼中间节点,点击"< FK Joint Mid"
|
||||
- 选择FK骨骼末端节点,点击"< FK Joint End"
|
||||
- 重复以上步骤加载FK控制器
|
||||
|
||||
2. **加载IK控制器**
|
||||
- 选择IK骨骼节点,点击相应按钮
|
||||
- 选择IK控制器和极向量控制器
|
||||
|
||||
3. **设置切换控制器**
|
||||
- 选择用于切换的控制器,点击"< Switch Ctrl"
|
||||
- 在通道盒中选择切换属性,点击"< Switch Attr"
|
||||
|
||||
4. **构建切换系统**
|
||||
- 点击"<<< Build Seamless Switching >>>"按钮
|
||||
- 系统会自动创建必要的属性和脚本节点
|
||||
|
||||
5. **使用切换功能**
|
||||
- 在切换控制器上找到"IKFK_Seamless"属性
|
||||
- 切换该属性值(IK=0, FK=1)即可实现无缝切换
|
||||
|
||||
### ADV绑定快速设置
|
||||
|
||||
如果使用ADV绑定系统,可以使用预设功能:
|
||||
1. 展开"Edit"面板
|
||||
2. 点击"<<< ADV Build >>>"
|
||||
3. 系统会自动为左右手臂和腿部设置IKFK切换
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **IKFKSwitchUI类**: UI界面管理
|
||||
- 创建和管理UI窗口
|
||||
- 处理用户输入
|
||||
- 调用切换逻辑
|
||||
|
||||
2. **seamless_switching函数**: 核心切换逻辑
|
||||
- 创建数据存储locator
|
||||
- 添加必要属性
|
||||
- 建立属性连接
|
||||
- 创建脚本节点
|
||||
|
||||
3. **create_switching_script函数**: 创建全局切换脚本
|
||||
- 使用MEL脚本实现切换逻辑
|
||||
- 处理约束和关键帧
|
||||
|
||||
4. **create_script_job函数**: 创建脚本任务
|
||||
- 监听属性变化
|
||||
- 自动触发切换
|
||||
|
||||
### 兼容性设计
|
||||
|
||||
- 使用`maya.cmds`和`maya.mel`模块,确保跨版本兼容
|
||||
- 避免使用版本特定的API
|
||||
- 使用f-string格式化(Python 3.6+)
|
||||
- 异常处理确保稳定性
|
||||
|
||||
## 工具架集成
|
||||
|
||||
该工具已集成到Nexus Animation工具架:
|
||||
- **图标**: ikfk_switch.png
|
||||
- **位置**: 动画工具架末尾
|
||||
- **调用**: `import animation_tools.ikfx_switch; animation_tools.ikfx_switch.show()`
|
||||
|
||||
## 文件位置
|
||||
|
||||
### Maya 2023
|
||||
- 脚本: `scripts/animation_tools/ikfx_switch.py`
|
||||
- 图标: `icons/ikfk_switch.png`
|
||||
- 工具架: `shelves/shelf_Nexus_Animation.mel`
|
||||
|
||||
### Maya 2025
|
||||
- 脚本: `scripts/animation_tools/ikfx_switch.py`
|
||||
- 图标: `icons/ikfk_switch.png`
|
||||
- 工具架: `shelves/shelf_Nexus_Animation.mel`
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **属性要求**: 切换属性必须有最小值和最大值设置
|
||||
2. **命名规范**: 建议使用清晰的命名规范以便识别
|
||||
3. **关键帧**: 切换会在当前帧和前一帧设置关键帧
|
||||
4. **选择保持**: 切换后会恢复原始选择
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **切换不工作**
|
||||
- 检查所有必需的对象是否存在
|
||||
- 确认切换属性已正确设置
|
||||
- 查看脚本编辑器的错误信息
|
||||
|
||||
2. **位置不匹配**
|
||||
- 确保FK和IK骨骼层级正确
|
||||
- 检查约束设置
|
||||
- 验证控制器方向
|
||||
|
||||
3. **属性加载失败**
|
||||
- 确保在通道盒中选择了属性
|
||||
- 检查属性是否可关键帧化
|
||||
@@ -1,301 +0,0 @@
|
||||
from maya import cmds
|
||||
from maya.api import OpenMaya as om2
|
||||
#import logging
|
||||
'''
|
||||
Create in ADV 6.040, select the corresponding IKFK controller, and simply run the script to switch
|
||||
by kangddan
|
||||
'''
|
||||
#logger = logging.getLogger(__name__)
|
||||
|
||||
def setAttrs(nodes, attrs, value):
|
||||
|
||||
for n in nodes:
|
||||
for ids, i in enumerate(attrs):
|
||||
try:
|
||||
if isinstance(value[ids], (list, tuple)):
|
||||
cmds.setAttr('{}.{}'.format(n, i), *value[ids])
|
||||
else:
|
||||
cmds.setAttr('{}.{}'.format(n, i), value[ids])
|
||||
except:
|
||||
pass
|
||||
|
||||
class Path:
|
||||
@classmethod
|
||||
def path(cls, fullPathName):
|
||||
return fullPathName.split("|")[-1]
|
||||
|
||||
@classmethod
|
||||
def name(cls, fullPathName):
|
||||
return cls.path(fullPathName).split(':')[-1]
|
||||
|
||||
@classmethod
|
||||
def namespace(cls, fullPathName):
|
||||
return cls.path(fullPathName).rpartition(':')[0]
|
||||
|
||||
class MOpenMaya:
|
||||
|
||||
@staticmethod
|
||||
def toMObject(nodeName):
|
||||
sl = om2.MSelectionList(); sl.add(nodeName)
|
||||
return sl.getDependNode(0)
|
||||
|
||||
@classmethod
|
||||
def getDagNode(cls, mobj) :
|
||||
if not mobj.hasFn(om2.MFn.kDagNode): return
|
||||
return om2.MDagPath.getAPathTo(mobj)
|
||||
|
||||
@classmethod
|
||||
def getGlobatMatrix(cls, dagNode):
|
||||
if not isinstance(dagNode, om2.MDagPath): return
|
||||
return dagNode.inclusiveMatrix()
|
||||
|
||||
|
||||
@classmethod
|
||||
def getGlobalPos(cls, dagNode):
|
||||
if not dagNode: return
|
||||
tm = om2.MTransformationMatrix(cls.getGlobatMatrix(dagNode))
|
||||
return tm.translation(om2.MSpace.kTransform)
|
||||
|
||||
@classmethod
|
||||
def ls(cls):
|
||||
sl = om2.MGlobal.getActiveSelectionList()
|
||||
return [cls.getDagNode(sl.getDependNode(o)) for o in range(sl.length())]
|
||||
|
||||
|
||||
class IkFk:
|
||||
ARM_IK_CTRLS = {'ik1':'IKArm', 'ik2':'PoleArm'} # 0
|
||||
ARM_FK_CTRLS = {'fk1':'FKShoulder', 'fk2':'FKElbow', 'fk3':'FKWrist'} # 1
|
||||
ARM_FKTOIK_LOCS = {'loc1':'IKX2Shoulder', 'loc2':'IKX2Elbow', 'loc3':'IKX2Wrist'} # 2
|
||||
ARM_IKTOFK_LOCS = {'loc1':'AlignIKToWrist'} # 3
|
||||
ARM_IKFK = {'switch':'FKIKArm'}
|
||||
|
||||
LEG_IK_CTRLS = {'ik1':'IKLeg', 'ik2':'PoleLeg', 'ik3':'RollHeel', 'ik4':'RollToesEnd', 'ik5':'RollToes', 'ik6':'IKToes'}
|
||||
LEG_FK_CTRLS = {'fk1':'FKHip', 'fk2':'FKKnee', 'fk3':'FKAnkle', 'fk4':'FKToes'}
|
||||
LEG_FKTOIK_LOCS = {'loc1':'IKX2Hip', 'loc2':'IKX2Knee', 'loc3':'IKX2Ankle', 'loc4':'IKX2Toes', 'loc5':'IKX2ToesEnd'}
|
||||
LEG_IKTOFK_LOCS = {'loc1':'AlignIKToAnkle', 'loc2':'AlignIKToToes'}
|
||||
leg_IKFK = {'switch':'FKIKLeg'}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
ARM_BASE_JNTS = {'base1':'Shoulder', 'base2':'Elbow', 'base3':'Wrist', 'root':'RootSystem'}
|
||||
LEG_BASE_JNTS = {'base1':'Hip', 'base2':'Knee', 'base3':'Ankle', 'root':'RootSystem'}
|
||||
|
||||
|
||||
# --------------------------------------------------------------
|
||||
|
||||
@staticmethod
|
||||
def getLRDict(_dict, namespace=''):
|
||||
|
||||
ldictList = [{k:'{}{}_L'.format(namespace, v) for k, v in d.items()} for d in _dict[:-2]]
|
||||
rdictList = [{k:'{}{}_R'.format(namespace, v) for k, v in d.items()} for d in _dict[:-2]]
|
||||
basedictList = [{k:'{}{}'.format(namespace, v) for k, v in d.items()} for d in _dict[-2:]]
|
||||
|
||||
return ldictList, rdictList, basedictList
|
||||
|
||||
@staticmethod
|
||||
def getLongName(sName, _dict):
|
||||
for k, v in _dict.items():
|
||||
longNames = cmds.ls(v, long=True)
|
||||
for ln in longNames:
|
||||
if sName == ln[0:len(sName)]:
|
||||
_dict[k] = ln
|
||||
# if ln[0].startswith(sName):
|
||||
# _dict[k] = ln
|
||||
break
|
||||
|
||||
# --------------------------------------------------------------
|
||||
|
||||
def newDict(self, fullPathName):
|
||||
dictList = [IkFk.ARM_IK_CTRLS, IkFk.ARM_FK_CTRLS, IkFk.ARM_FKTOIK_LOCS, IkFk.ARM_IKTOFK_LOCS, IkFk.ARM_IKFK,
|
||||
IkFk.LEG_IK_CTRLS, IkFk.LEG_FK_CTRLS, IkFk.LEG_FKTOIK_LOCS, IkFk.LEG_IKTOFK_LOCS, IkFk.leg_IKFK,
|
||||
IkFk.ARM_BASE_JNTS, IkFk.LEG_BASE_JNTS]
|
||||
|
||||
namespace = Path.namespace(fullPathName)
|
||||
if namespace:
|
||||
self.isRef = True
|
||||
return self.getLRDict(dictList, namespace+':')
|
||||
else:
|
||||
self.isRef = False
|
||||
sName = '|' + cmds.ls('*{}'.format(fullPathName), long=True)[0].split('|')[1]
|
||||
ldictList, rdictList, basedictList = self.getLRDict(dictList)
|
||||
|
||||
for dL, dR in zip(ldictList, rdictList):
|
||||
self.getLongName(sName, dL)
|
||||
self.getLongName(sName, dR)
|
||||
|
||||
for bS in basedictList: self.getLongName(sName, bS) #
|
||||
|
||||
return ldictList, rdictList, basedictList
|
||||
|
||||
@staticmethod
|
||||
def setPolePose(jntList, pole):
|
||||
jntPos = [MOpenMaya.getGlobalPos(MOpenMaya.getDagNode(MOpenMaya.toMObject(j))) for j in jntList]
|
||||
off = ((jntPos[1] - jntPos[0]).length() + (jntPos[2] - jntPos[1]).length())
|
||||
|
||||
midPos = ((jntPos[1] - jntPos[0]).normal() + ((jntPos[2] - jntPos[1]).normal() * -1)) * 0.5
|
||||
polePos = (midPos.normal() * off) + jntPos[1]
|
||||
cmds.xform(pole, t=polePos, ws=True)
|
||||
|
||||
@staticmethod
|
||||
def setIkStretch(jntList, locList):
|
||||
jntPos = [MOpenMaya.getGlobalPos(MOpenMaya.getDagNode(MOpenMaya.toMObject(j))) for j in jntList]
|
||||
locPos = [MOpenMaya.getGlobalPos(MOpenMaya.getDagNode(MOpenMaya.toMObject(l))) for l in locList]
|
||||
|
||||
upper = (jntPos[1] - jntPos[0]).length()
|
||||
mid = (jntPos[2] - jntPos[1]).length()
|
||||
|
||||
locUpper = (locPos[1] - locPos[0]).length()
|
||||
locMid = (locPos[2] - locPos[1]).length()
|
||||
|
||||
upperError = abs(upper / locUpper)
|
||||
midErrorr = abs(mid / locMid)
|
||||
return upperError, midErrorr
|
||||
|
||||
def getNewDict(self, fullPathName):
|
||||
ldictList, rdictList, basedictList = self.newDict(fullPathName)
|
||||
ALD = ldictList[0:5]; LLD = ldictList[5:]
|
||||
ARD = rdictList[0:5]; LRD = rdictList[5:]
|
||||
|
||||
AB = basedictList[0]; LB = basedictList[1] # base joint ref
|
||||
|
||||
return ALD, ARD, LLD, LRD, AB, LB
|
||||
|
||||
@staticmethod
|
||||
def _updateDict(_dict):
|
||||
return [value for d in [_dict[0], _dict[1], _dict[-1]] for value in d.values()]
|
||||
|
||||
def __init__(self, autoK=True):
|
||||
self.curTime , self.prevTime = cmds.currentTime(q=True), cmds.currentTime(q=True)-1
|
||||
self.autoK = autoK
|
||||
self.isRef = False
|
||||
self.ikfkSwitch()
|
||||
|
||||
|
||||
def autoKey(self, nodes=[], num=None):
|
||||
if not self.autoK:
|
||||
return
|
||||
for i in nodes:
|
||||
try:
|
||||
if cmds.keyframe(i, q=True, kc=True):
|
||||
cmds.setKeyframe(i, i=True, t=num)
|
||||
else:
|
||||
cmds.setKeyframe(i, i=False, t=num)
|
||||
except:
|
||||
continue
|
||||
|
||||
def ikfkSwitch(self):
|
||||
|
||||
sl = MOpenMaya.ls()
|
||||
for i in sl:
|
||||
if i is None:
|
||||
om2.MGlobal.displayWarning('Please select a DAG node')
|
||||
continue
|
||||
|
||||
ALD, ARD, LLD, LRD, AB, LB = self.getNewDict(i.fullPathName())
|
||||
path = Path.path(i.fullPathName()) if self.isRef else i.fullPathName()
|
||||
|
||||
if path in self._updateDict(ALD): # L arm
|
||||
self.armIKFk(ALD, AB)
|
||||
|
||||
elif path in self._updateDict(ARD): # R arm
|
||||
self.armIKFk(ARD, AB)
|
||||
|
||||
elif path in self._updateDict(LLD): # L leg
|
||||
self.legIkFk(LLD, LB)
|
||||
|
||||
elif path in self._updateDict(LRD): # R leg
|
||||
self.legIkFk(LRD, LB)
|
||||
|
||||
|
||||
def armIKFk(self, _dict, armBase):
|
||||
switch = _dict[-1]['switch']
|
||||
ikCtrl, poleCtrl = _dict[0]['ik1'], _dict[0]['ik2']
|
||||
fkCtrl1, fkCtrl2, fkCtrl3 = _dict[1]['fk1'], _dict[1]['fk2'], _dict[1]['fk3']
|
||||
fti1, fti2, fti3 = _dict[2]['loc1'], _dict[2]['loc2'], _dict[2]['loc3']
|
||||
itf = _dict[3]['loc1']
|
||||
baseJ1, baseJ2, baseJ3, root = armBase['base1'], armBase['base2'], armBase['base3'], armBase['root']
|
||||
|
||||
# -------------------------------------------
|
||||
scaleValue = cmds.getAttr('{}.sx'.format(root))
|
||||
# -------------------------------------------
|
||||
|
||||
value = cmds.getAttr(switch + '.FKIKBlend')
|
||||
|
||||
self.autoKey([switch, ikCtrl, poleCtrl, fkCtrl1, fkCtrl2, fkCtrl3], self.prevTime)
|
||||
|
||||
# fk to ik
|
||||
if value == 0.0:
|
||||
upperV, midV = self.setIkStretch([fkCtrl1, fkCtrl2, fkCtrl3], [baseJ1, baseJ2, baseJ3])
|
||||
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght1'), upperV / scaleValue)
|
||||
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght2'), midV/ scaleValue)
|
||||
cmds.setAttr('{}.{}'.format(ikCtrl, 'stretchy'), 0)
|
||||
cmds.setAttr('{}.{}'.format(poleCtrl, 'lock'), 0)
|
||||
|
||||
cmds.matchTransform(ikCtrl, itf)
|
||||
self.setPolePose([fkCtrl1, fkCtrl2, fkCtrl3], poleCtrl)
|
||||
cmds.setAttr(switch + '.FKIKBlend', 10.0)
|
||||
|
||||
# ik to fk
|
||||
elif value == 10.0:
|
||||
for fk, loc in zip([fkCtrl1, fkCtrl2, fkCtrl3], [fti1, fti2, fti3]):
|
||||
cmds.matchTransform(fk, loc)
|
||||
|
||||
cmds.setAttr(switch + '.FKIKBlend', 0.0)
|
||||
|
||||
self.autoKey([switch, ikCtrl, poleCtrl, fkCtrl1, fkCtrl2, fkCtrl3], self.curTime)
|
||||
|
||||
def legIkFk(self, _dict, legBase):
|
||||
switch = _dict[-1]['switch']
|
||||
ikCtrl, poleCtrl, ik3, ik4, ik5, ik6 = _dict[0]['ik1'], _dict[0]['ik2'], _dict[0]['ik3'], _dict[0]['ik4'], _dict[0]['ik5'], _dict[0]['ik6']
|
||||
fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4 = _dict[1]['fk1'], _dict[1]['fk2'], _dict[1]['fk3'], _dict[1]['fk4']
|
||||
fti1, fti2, fti3, fti4, fti5 = _dict[2]['loc1'], _dict[2]['loc2'], _dict[2]['loc3'], _dict[2]['loc4'], _dict[2]['loc5']
|
||||
itf1, itf2 = _dict[3]['loc1'], _dict[3]['loc2']
|
||||
baseJ1, baseJ2, baseJ3, root = legBase['base1'], legBase['base2'], legBase['base3'], legBase['root']
|
||||
|
||||
# -------------------------------------------
|
||||
scaleValue = cmds.getAttr('{}.sx'.format(root))
|
||||
# -------------------------------------------
|
||||
|
||||
value = cmds.getAttr(switch + '.FKIKBlend')
|
||||
|
||||
self.autoKey([switch, ikCtrl, poleCtrl, ik3, ik4, ik5, ik6, fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4], self.prevTime)
|
||||
|
||||
# fk to ik
|
||||
if value == 0.0:
|
||||
upperV, midV = self.setIkStretch([fkCtrl1, fkCtrl2, fkCtrl3], [baseJ1, baseJ2, baseJ3])
|
||||
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght1'), upperV / scaleValue)
|
||||
cmds.setAttr('{}.{}'.format(ikCtrl, 'Lenght2'), midV / scaleValue)
|
||||
cmds.setAttr('{}.{}'.format(ikCtrl, 'stretchy'), 0)
|
||||
cmds.setAttr('{}.{}'.format(poleCtrl, 'lock'), 0)
|
||||
|
||||
setAttrs([ikCtrl], ['swivel', 'roll', 'rock', 'antiPop'], [0, 0, 0, 0])
|
||||
setAttrs([ik3, ik4, ik5], ['t', 'r', 's'], [[0, 0, 0], [0, 0, 0], [1, 1, 1]])
|
||||
|
||||
cmds.matchTransform(ikCtrl, itf1)
|
||||
cmds.matchTransform(ik6, itf2) #
|
||||
self.setPolePose([fkCtrl1, fkCtrl2, fkCtrl3], poleCtrl)
|
||||
cmds.setAttr(switch + '.FKIKBlend', 10.0)
|
||||
|
||||
# ik to fk
|
||||
elif value == 10.0:
|
||||
for fk, loc in zip([fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4], [fti1, fti2, fti3, fti4]):
|
||||
cmds.matchTransform(fk, loc)
|
||||
|
||||
cmds.setAttr(switch + '.FKIKBlend', 0.0)
|
||||
|
||||
self.autoKey([switch, ikCtrl, poleCtrl, ik3, ik4, ik5, ik6, fkCtrl1, fkCtrl2, fkCtrl3, fkCtrl4], self.curTime)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
IkFk(False)
|
||||
|
||||
'''
|
||||
|
||||
Revision History
|
||||
Revision 1: 2023-08-11 : First publish
|
||||
Revision 2: 2023-08-14 : Fix a series of errors in the IK controller
|
||||
Revision 3: 2023-08-18 : Code Redundancy Optimization
|
||||
Revision 4: 2023-08-19 : Adding auto keyframe
|
||||
Revision 5: 2023-08-22 : Update IK Pole Position Algorithm
|
||||
Revision 6: 2023-09-15 : Optimize the transition from FK to IK
|
||||
|
||||
'''
|
||||
File diff suppressed because it is too large
Load Diff
603
2023/scripts/animation_tools/ikfk_switch.py
Normal file
603
2023/scripts/animation_tools/ikfk_switch.py
Normal file
@@ -0,0 +1,603 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import maya.cmds as cmds
|
||||
import maya.mel as mel
|
||||
import math
|
||||
import sys
|
||||
import inspect
|
||||
import importlib
|
||||
|
||||
# Crucial for fixing namespace issues:
|
||||
# Dynamically gets the module name (e.g., 'animation_tools.ikfk_switch')
|
||||
|
||||
MODULE_NAME = __name__
|
||||
|
||||
# ==============================================================================
|
||||
# UI Creation
|
||||
# ==============================================================================
|
||||
|
||||
def IKFK_Switch_UI():
|
||||
"""
|
||||
Creates the main IK/FK switch Maya window interface.
|
||||
"""
|
||||
WINDOW_NAME = "IKFK_Switch_UI"
|
||||
WINDOW_TITLE = "IK/FK Switch V2.9 (Final Button Fix)"
|
||||
WINDOW_WIDTH = 300
|
||||
|
||||
# Check if window exists and delete it
|
||||
if cmds.window(WINDOW_NAME, exists=True):
|
||||
cmds.deleteUI(WINDOW_NAME, window=True)
|
||||
|
||||
# Create window
|
||||
cmds.window(WINDOW_NAME, width=WINDOW_WIDTH, title=WINDOW_TITLE)
|
||||
|
||||
# Main Layout
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=1)
|
||||
|
||||
# --- Helper function for button commands ---
|
||||
# CORE FIX: Removed the 'python("...")' wrapper.
|
||||
# The function now returns a pure Python command string for button execution.
|
||||
def py_cmd(func_name, args=""):
|
||||
# Format: 'import module; module.function("args")' (Pure Python string)
|
||||
if args:
|
||||
# Use double quotes to avoid escaping issues
|
||||
return 'import {0}; {0}.{1}("{2}")'.format(MODULE_NAME, func_name, args)
|
||||
else:
|
||||
return 'import {0}; {0}.{1}()'.format(MODULE_NAME, func_name)
|
||||
|
||||
# --- Edit Section ---
|
||||
cmds.frameLayout(label="Edit", collapse=True, collapsable=True,
|
||||
collapseCommand="cmds.window('{}', edit=True, height=578)".format(WINDOW_NAME),
|
||||
expandCommand="cmds.window('{}', edit=True, height=578)".format(WINDOW_NAME))
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
cmds.button(label="<<< ADV Build >>>", command=py_cmd("sg_ADV_Build"))
|
||||
cmds.button(label="<<< Empty >>>", command=py_cmd("sg_Empty"))
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
# --- Load FK Ctrl Section ---
|
||||
cmds.frameLayout(label="Load FK Ctrl")
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
|
||||
# FK Joint Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_01", text="", placeholderText="FK Joint Root")
|
||||
cmds.button(label="< FK Joint Root", command=py_cmd("sg_setTextField", "target_01"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Joint Mid
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_02", text="", placeholderText="FK Joint Mid")
|
||||
cmds.button(label="< FK Joint Mid", command=py_cmd("sg_setTextField", "target_02"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Joint End
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_03", text="", placeholderText="FK Joint End")
|
||||
cmds.button(label="< FK Joint End", command=py_cmd("sg_setTextField", "target_03"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.separator(style="in", height=5)
|
||||
|
||||
# FK Ctrl Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_04", text="", placeholderText="FK Ctrl Root")
|
||||
cmds.button(label="< FK Ctrl Root", command=py_cmd("sg_setTextField", "target_04"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Ctrl Mid
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_05", text="", placeholderText="FK Ctrl Mid")
|
||||
cmds.button(label="< FK Ctrl Mid", command=py_cmd("sg_setTextField", "target_05"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Ctrl End
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_06", text="", placeholderText="FK Ctrl End")
|
||||
cmds.button(label="< FK Ctrl End", command=py_cmd("sg_setTextField", "target_06"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
# --- Load IK Ctrl Section ---
|
||||
cmds.frameLayout(label="Load IK Ctrl")
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
|
||||
# IK Joint Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_07", text="", placeholderText="IK Joint Root")
|
||||
cmds.button(label="< IK Joint Root", command=py_cmd("sg_setTextField", "target_07"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# IK Joint Mid
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_08", text="", placeholderText="IK Joint Mid")
|
||||
cmds.button(label="< IK Joint Mid", command=py_cmd("sg_setTextField", "target_08"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# IK Joint End
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_09", text="", placeholderText="IK Joint End")
|
||||
cmds.button(label="< IK Joint End", command=py_cmd("sg_setTextField", "target_09"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.separator(style="in", height=5)
|
||||
|
||||
# IK Ctrl Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_10", text="", placeholderText="IK Ctrl Root")
|
||||
cmds.button(label="< IK Ctrl Root", command=py_cmd("sg_setTextField", "target_10"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# IK Ctrl Pole
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_11", text="", placeholderText="IK Ctrl Pole")
|
||||
cmds.button(label="< IK Ctrl Pole", command=py_cmd("sg_setTextField", "target_11"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
# --- Load Switch Ctrl Section ---
|
||||
cmds.frameLayout(label="Load Switch Ctrl")
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
|
||||
# Switch Ctrl
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_12", text="", placeholderText="Switch Ctrl")
|
||||
cmds.button(label="< Switch Ctrl", command=py_cmd("sg_setTextField", "target_12"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# Switch Attr
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_13", text="", placeholderText="Switch Attr Name")
|
||||
cmds.button(label="< Switch Attr", command=py_cmd("sg_setLoadAttr"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.separator(style="in", height=5)
|
||||
|
||||
# Build Button
|
||||
cmds.button(height=32, label="<<< Build Seamless Switching >>>", command=py_cmd("sg_Execute"))
|
||||
|
||||
cmds.setParent("..")
|
||||
|
||||
# Adjust window height and show
|
||||
cmds.window(WINDOW_NAME, edit=True, height=578)
|
||||
cmds.showWindow(WINDOW_NAME)
|
||||
|
||||
|
||||
def sg_setTextField(target):
|
||||
"""
|
||||
Sets the name of the currently selected object into the specified text field.
|
||||
"""
|
||||
selection = cmds.ls(selection=True, long=False)
|
||||
|
||||
if selection:
|
||||
selected_object = selection[0]
|
||||
cmds.textField(target, edit=True, text=selected_object)
|
||||
else:
|
||||
cmds.warning("Please select an object.")
|
||||
|
||||
|
||||
def sg_setLoadAttr():
|
||||
"""
|
||||
Sets the name of the selected attribute in the Channel Box into the target_13 text field.
|
||||
"""
|
||||
# Query selected attributes in the Channel Box
|
||||
attrs = cmds.channelBox("mainChannelBox", query=True, selectedMainAttributes=True)
|
||||
|
||||
if not attrs:
|
||||
cmds.error("Select an attribute in the Channel Box...")
|
||||
return
|
||||
|
||||
# Take only the first selected attribute
|
||||
attribute_name = attrs[0]
|
||||
cmds.textField("target_13", edit=True, text=attribute_name)
|
||||
|
||||
|
||||
def sg_Execute():
|
||||
"""
|
||||
Reads all values from UI controls and calls the sg_SeamlessSwitching function.
|
||||
"""
|
||||
# Get values from UI controls
|
||||
FK_Joint_Root = cmds.textField("target_01", query=True, text=True)
|
||||
FK_Joint_Mid = cmds.textField("target_02", query=True, text=True)
|
||||
FK_Joint_End = cmds.textField("target_03", query=True, text=True)
|
||||
FK_Ctrl_Root = cmds.textField("target_04", query=True, text=True)
|
||||
FK_Ctrl_Mid = cmds.textField("target_05", query=True, text=True)
|
||||
FK_Ctrl_End = cmds.textField("target_06", query=True, text=True)
|
||||
IK_Joint_Root = cmds.textField("target_07", query=True, text=True)
|
||||
IK_Joint_Mid = cmds.textField("target_08", query=True, text=True)
|
||||
IK_Joint_End = cmds.textField("target_09", query=True, text=True)
|
||||
IK_Ctrl_Root = cmds.textField("target_10", query=True, text=True)
|
||||
IK_Ctrl_Pole = cmds.textField("target_11", query=True, text=True)
|
||||
Switch_Ctrl = cmds.textField("target_12", query=True, text=True)
|
||||
Switch_Attr = cmds.textField("target_13", query=True, text=True)
|
||||
|
||||
# Check for empty values
|
||||
all_targets = [FK_Joint_Root, FK_Joint_Mid, FK_Joint_End, FK_Ctrl_Root, FK_Ctrl_Mid,
|
||||
FK_Ctrl_End, IK_Joint_Root, IK_Joint_Mid, IK_Joint_End, IK_Ctrl_Root,
|
||||
IK_Ctrl_Pole, Switch_Ctrl, Switch_Attr]
|
||||
|
||||
for target in all_targets:
|
||||
if not target:
|
||||
cmds.error("All fields must be filled before building the switch.")
|
||||
return
|
||||
|
||||
# Call core build function
|
||||
sg_SeamlessSwitching(
|
||||
FK_Joint_Root,
|
||||
FK_Joint_Mid,
|
||||
FK_Joint_End,
|
||||
FK_Ctrl_Root,
|
||||
FK_Ctrl_Mid,
|
||||
FK_Ctrl_End,
|
||||
IK_Joint_Root,
|
||||
IK_Joint_Mid,
|
||||
IK_Joint_End,
|
||||
IK_Ctrl_Root,
|
||||
IK_Ctrl_Pole,
|
||||
Switch_Ctrl,
|
||||
Switch_Attr
|
||||
)
|
||||
|
||||
|
||||
def sg_ADV_Build():
|
||||
"""
|
||||
Builds IK/FK switching for sample limbs using preset naming conventions.
|
||||
NOTE: Modify names here to match your Rig naming convention.
|
||||
"""
|
||||
|
||||
# Right Arm
|
||||
sg_SeamlessSwitching(
|
||||
"FKXShoulder_R", "FKXElbow_R", "FKXWrist_R",
|
||||
"FKShoulder_R", "FKElbow_R", "FKWrist_R",
|
||||
"IKXShoulder_R", "IKXElbow_R", "IKXWrist_R",
|
||||
"IKArm_R", "PoleArm_R", "FKIKArm_R", "fkik"
|
||||
)
|
||||
|
||||
# Left Arm
|
||||
sg_SeamlessSwitching(
|
||||
"FKXShoulder_L", "FKXElbow_L", "FKXWrist_L",
|
||||
"FKShoulder_L", "FKElbow_L", "FKWrist_L",
|
||||
"IKXShoulder_L", "IKXElbow_L", "IKXWrist_L",
|
||||
"IKArm_L", "PoleArm_L", "FKIKArm_L", "fkik"
|
||||
)
|
||||
|
||||
# Right Leg
|
||||
sg_SeamlessSwitching(
|
||||
"FKXHip_R", "FKXKnee_R", "FKXAnkle_R",
|
||||
"FKHip_R", "FKKnee_R", "FKAnkle_R",
|
||||
"IKXHip_R", "IKXKnee_R", "IKXAnkle_R",
|
||||
"IKLeg_R", "PoleLeg_R", "FKIKLeg_R", "fkik"
|
||||
)
|
||||
|
||||
# Left Leg
|
||||
sg_SeamlessSwitching(
|
||||
"FKXHip_L", "FKXKnee_L", "FKXAnkle_L",
|
||||
"FKHip_L", "FKKnee_L", "FKAnkle_L",
|
||||
"IKXHip_L", "IKXKnee_L", "IKXAnkle_L",
|
||||
"IKLeg_L", "PoleLeg_L", "FKIKLeg_L", "fkik"
|
||||
)
|
||||
|
||||
def sg_Empty():
|
||||
"""
|
||||
Clears all text fields in the UI.
|
||||
"""
|
||||
for i in range(1, 14):
|
||||
target_name = "target_{:02d}".format(i)
|
||||
if cmds.textField(target_name, exists=True):
|
||||
cmds.textField(target_name, edit=True, text="")
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Core IK/FK Switching Logic
|
||||
# ==============================================================================
|
||||
|
||||
def sg_switching(locator_name):
|
||||
"""
|
||||
Executes the core IK/FK switching logic (seamless match).
|
||||
This function is called at runtime by the scriptJob.
|
||||
|
||||
Args:
|
||||
locator_name (str): The name of the Locator storing connection info.
|
||||
"""
|
||||
|
||||
# Get the Switch Control and attribute name (stored as string attributes)
|
||||
try:
|
||||
switch_ctrl = cmds.getAttr("{}.Switch_Ctrl".format(locator_name))
|
||||
switch_attr_name = cmds.getAttr("{}.Switch_Attr".format(locator_name))
|
||||
except:
|
||||
cmds.error("Locator {} does not have required attributes.".format(locator_name))
|
||||
return
|
||||
|
||||
# State check: Use optionVar to store the last state, preventing redundant execution
|
||||
option_var_name = locator_name
|
||||
current_state = cmds.getAttr("{}.IKFK_Seamless".format(switch_ctrl))
|
||||
|
||||
try:
|
||||
last_state = cmds.optionVar(query=option_var_name)
|
||||
except RuntimeError:
|
||||
last_state = -1
|
||||
|
||||
if last_state == current_state:
|
||||
# State has not changed, exit
|
||||
return
|
||||
|
||||
# Store current selection
|
||||
selection = cmds.ls(selection=True, long=False)
|
||||
|
||||
# Get all object names from the Locator (stored as string attributes)
|
||||
try:
|
||||
FK_Joint_Root = cmds.getAttr("{}.FK_Joint_Root".format(locator_name))
|
||||
FK_Joint_Mid = cmds.getAttr("{}.FK_Joint_Mid".format(locator_name))
|
||||
FK_Joint_End = cmds.getAttr("{}.FK_Joint_End".format(locator_name))
|
||||
FK_Ctrl_Root = cmds.getAttr("{}.FK_Ctrl_Root".format(locator_name))
|
||||
FK_Ctrl_Mid = cmds.getAttr("{}.FK_Ctrl_Mid".format(locator_name))
|
||||
FK_Ctrl_End = cmds.getAttr("{}.FK_Ctrl_End".format(locator_name))
|
||||
IK_Joint_Root = cmds.getAttr("{}.IK_Joint_Root".format(locator_name))
|
||||
IK_Joint_Mid = cmds.getAttr("{}.IK_Joint_Mid".format(locator_name))
|
||||
IK_Joint_End = cmds.getAttr("{}.IK_Joint_End".format(locator_name))
|
||||
IK_Ctrl_Root = cmds.getAttr("{}.IK_Ctrl_Root".format(locator_name))
|
||||
IK_Ctrl_Pole = cmds.getAttr("{}.IK_Ctrl_Pole".format(locator_name))
|
||||
except Exception as e:
|
||||
cmds.error("Failed to retrieve object names from Locator {}: {}".format(locator_name, e))
|
||||
return
|
||||
|
||||
# Get Min/Max values for the blend attribute
|
||||
try:
|
||||
attr_min = cmds.attributeQuery(switch_attr_name, node=switch_ctrl, minimum=True)
|
||||
attr_max = cmds.attributeQuery(switch_attr_name, node=switch_ctrl, maximum=True)
|
||||
min_val = attr_min[0] if attr_min else 0.0
|
||||
max_val = attr_max[0] if attr_max else 1.0
|
||||
except Exception:
|
||||
cmds.warning("Could not query min/max values for blend attribute. Defaulting to 0 and 1.")
|
||||
min_val = 0.0
|
||||
max_val = 1.0
|
||||
|
||||
current_time = cmds.currentTime(query=True)
|
||||
|
||||
# Set keyframes on all controls before switching (MEL lines 504-508)
|
||||
cmds.setKeyframe(FK_Ctrl_Root)
|
||||
cmds.setKeyframe(FK_Ctrl_Mid)
|
||||
cmds.setKeyframe(FK_Ctrl_End)
|
||||
cmds.setKeyframe(IK_Ctrl_Root)
|
||||
cmds.setKeyframe(IK_Ctrl_Pole)
|
||||
|
||||
# **MATCHING LOGIC**
|
||||
if current_state == 0: # Switch to IK (IKFK_Seamless: IK)
|
||||
|
||||
# 1. Match IK Ctrl Root (End Controller) - Using temp groups to preserve rotation offset
|
||||
# This matches the MEL version's approach (lines 510-519)
|
||||
tempGroup_A = cmds.group(empty=True)
|
||||
tempGroup_B = cmds.group(empty=True)
|
||||
|
||||
# Step 1: Match both temp groups to IK_Joint_End
|
||||
cmds.delete(cmds.parentConstraint(IK_Joint_End, tempGroup_A, weight=1))
|
||||
cmds.delete(cmds.parentConstraint(IK_Joint_End, tempGroup_B, weight=1))
|
||||
|
||||
# Step 2: Apply IK_Ctrl_Root's current orientation to tempGroup_B (preserve rotation offset)
|
||||
cmds.delete(cmds.orientConstraint(IK_Ctrl_Root, tempGroup_B, offset=(0, 0, 0), weight=1))
|
||||
|
||||
# Step 3: Parent tempGroup_B under tempGroup_A to create hierarchy
|
||||
cmds.parent(tempGroup_B, tempGroup_A)
|
||||
|
||||
# Step 4: Match tempGroup_A to FK_Joint_End (this moves the hierarchy)
|
||||
cmds.delete(cmds.parentConstraint(FK_Joint_End, tempGroup_A, weight=1))
|
||||
|
||||
# Step 5: Apply tempGroup_B's final transform to IK_Ctrl_Root
|
||||
con_A = cmds.parentConstraint(tempGroup_B, IK_Ctrl_Root, weight=1)
|
||||
cmds.setKeyframe(IK_Ctrl_Root)
|
||||
cmds.delete(con_A)
|
||||
|
||||
# 2. Calculate Pole Vector position based on FK chain bend direction
|
||||
# Get world space positions of FK joints
|
||||
pos_root = cmds.xform(FK_Joint_Root, query=True, worldSpace=True, translation=True)
|
||||
pos_mid = cmds.xform(FK_Joint_Mid, query=True, worldSpace=True, translation=True)
|
||||
pos_end = cmds.xform(FK_Joint_End, query=True, worldSpace=True, translation=True)
|
||||
|
||||
# Calculate the bend direction using vector projection
|
||||
# Vector from root to end (main chain direction)
|
||||
vec_RE = [pos_end[i] - pos_root[i] for i in range(3)]
|
||||
# Vector from root to mid
|
||||
vec_RM = [pos_mid[i] - pos_root[i] for i in range(3)]
|
||||
|
||||
# Length of main chain vector
|
||||
len_RE = math.sqrt(sum(vec_RE[i]**2 for i in range(3)))
|
||||
|
||||
if len_RE > 0.0001: # Avoid division by zero
|
||||
# Normalize the main chain vector
|
||||
norm_RE = [vec_RE[i] / len_RE for i in range(3)]
|
||||
|
||||
# Project vec_RM onto vec_RE to find the projection point
|
||||
proj_scalar = sum(vec_RM[i] * norm_RE[i] for i in range(3))
|
||||
proj_vec = [proj_scalar * norm_RE[i] for i in range(3)]
|
||||
|
||||
# Perpendicular vector (from projection point to mid joint)
|
||||
vec_perp = [vec_RM[i] - proj_vec[i] for i in range(3)]
|
||||
len_perp = math.sqrt(sum(vec_perp[i]**2 for i in range(3)))
|
||||
|
||||
if len_perp > 0.0001: # Chain has a bend
|
||||
# Normalize perpendicular vector
|
||||
norm_perp = [vec_perp[i] / len_perp for i in range(3)]
|
||||
|
||||
# Extension distance: use chain length as reference
|
||||
# Multiply by a factor to place pole vector at a reasonable distance
|
||||
extend_dist = len_RE * 0.5
|
||||
|
||||
# Pole Vector position = mid joint + perpendicular direction * extension distance
|
||||
pole_pos = [pos_mid[i] + norm_perp[i] * extend_dist for i in range(3)]
|
||||
|
||||
else: # Chain is straight, use a default offset
|
||||
# If straight, offset along Z-axis (or another appropriate axis)
|
||||
pole_pos = [pos_mid[0], pos_mid[1], pos_mid[2] + len_RE * 0.5]
|
||||
else:
|
||||
# Fallback: keep current position if chain length is zero
|
||||
pole_pos = cmds.xform(IK_Ctrl_Pole, query=True, worldSpace=True, translation=True)
|
||||
|
||||
# Apply calculated position to Pole Vector
|
||||
cmds.xform(IK_Ctrl_Pole, worldSpace=True, translation=pole_pos)
|
||||
cmds.setKeyframe(IK_Ctrl_Pole)
|
||||
|
||||
# Clean up temp groups
|
||||
cmds.delete(tempGroup_A)
|
||||
|
||||
# 3. Switch attribute: from FK (min_val) to IK (max_val)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), min_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time - 1)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), max_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time)
|
||||
|
||||
|
||||
elif current_state == 1: # Switch to FK (IKFK_Seamless: FK)
|
||||
|
||||
# Match FK controls to IK joints (MEL lines 530-538)
|
||||
# Use weight=1 instead of maintainOffset to match MEL behavior
|
||||
|
||||
# 1. Match FK Ctrl Root to IK Joint Root
|
||||
con_A = cmds.parentConstraint(IK_Joint_Root, FK_Ctrl_Root, weight=1)
|
||||
cmds.setKeyframe(FK_Ctrl_Root)
|
||||
cmds.delete(con_A)
|
||||
|
||||
# 2. Match FK Ctrl Mid to IK Joint Mid
|
||||
con_B = cmds.parentConstraint(IK_Joint_Mid, FK_Ctrl_Mid, weight=1)
|
||||
cmds.setKeyframe(FK_Ctrl_Mid)
|
||||
cmds.delete(con_B)
|
||||
|
||||
# 3. Match FK Ctrl End to IK Joint End
|
||||
con_C = cmds.parentConstraint(IK_Joint_End, FK_Ctrl_End, weight=1)
|
||||
cmds.setKeyframe(FK_Ctrl_End)
|
||||
cmds.delete(con_C)
|
||||
|
||||
# 4. Switch attribute: from IK (max_val) to FK (min_val)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), max_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time - 1)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), min_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time)
|
||||
|
||||
# Update optionVar
|
||||
cmds.optionVar(intValue=(option_var_name, current_state))
|
||||
|
||||
# Restore selection
|
||||
if selection:
|
||||
cmds.select(selection, replace=True)
|
||||
|
||||
|
||||
def sg_SeamlessSwitching(
|
||||
FK_Joint_Root, FK_Joint_Mid, FK_Joint_End,
|
||||
FK_Ctrl_Root, FK_Ctrl_Mid, FK_Ctrl_End,
|
||||
IK_Joint_Root, IK_Joint_Mid, IK_Joint_End,
|
||||
IK_Ctrl_Root, IK_Ctrl_Pole,
|
||||
Switch_Ctrl, Switch_Attr
|
||||
):
|
||||
"""
|
||||
Sets up the necessary nodes, attributes, and the scriptJob for IK/FK switching.
|
||||
"""
|
||||
|
||||
# 1. Ensure all objects exist
|
||||
all_objects = [FK_Joint_Root, FK_Joint_Mid, FK_Joint_End, FK_Ctrl_Root, FK_Ctrl_Mid,
|
||||
FK_Ctrl_End, IK_Joint_Root, IK_Joint_Mid, IK_Joint_End, IK_Ctrl_Root,
|
||||
IK_Ctrl_Pole, Switch_Ctrl]
|
||||
|
||||
for obj in all_objects:
|
||||
if not cmds.objExists(obj):
|
||||
cmds.error("Object not found: '{}'. Please verify all joint and controller names.".format(obj))
|
||||
return
|
||||
|
||||
# 2. Cleanup and Create Locator
|
||||
LOCATOR_NAME = "{}_Switch_Locator".format(Switch_Ctrl)
|
||||
|
||||
matching_locators = cmds.ls(LOCATOR_NAME + "*", type='transform')
|
||||
final_locators_to_delete = []
|
||||
for loc in matching_locators:
|
||||
if loc.startswith(LOCATOR_NAME) and cmds.listRelatives(loc, shapes=True, type='locator'):
|
||||
final_locators_to_delete.append(loc)
|
||||
|
||||
if final_locators_to_delete:
|
||||
cmds.warning("Found and deleting {} existing switch locators for {}: {}".format(
|
||||
len(final_locators_to_delete), Switch_Ctrl, final_locators_to_delete))
|
||||
try:
|
||||
cmds.delete(final_locators_to_delete)
|
||||
except Exception as e:
|
||||
cmds.warning("Failed to delete old locators: {}".format(e))
|
||||
|
||||
# Recreate Locator
|
||||
locator_shape = cmds.createNode("locator", name=LOCATOR_NAME + "Shape")
|
||||
locator = cmds.listRelatives(locator_shape, parent=True)[0]
|
||||
locator = cmds.rename(locator, LOCATOR_NAME)
|
||||
|
||||
cmds.parent(locator, Switch_Ctrl)
|
||||
|
||||
# Unlock and clear transform attributes
|
||||
for attr in ['tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz', 'v']:
|
||||
full_attr = "{}.{}".format(locator, attr)
|
||||
cmds.setAttr(full_attr, lock=False)
|
||||
connections = cmds.listConnections(full_attr, plugs=True, destination=False, source=True)
|
||||
if connections:
|
||||
cmds.disconnectAttr(connections[0], full_attr)
|
||||
|
||||
# Reset and hide Locator
|
||||
cmds.setAttr("{}.t".format(locator), 0, 0, 0)
|
||||
cmds.setAttr("{}.r".format(locator), 0, 0, 0)
|
||||
cmds.setAttr("{}.s".format(locator), 1, 1, 1)
|
||||
cmds.setAttr("{}.visibility".format(locator), 0)
|
||||
|
||||
|
||||
# 3. Add IKFK_Seamless attribute to Switch Ctrl
|
||||
if not cmds.attributeQuery("IKFK_Seamless", node=Switch_Ctrl, exists=True):
|
||||
cmds.addAttr(Switch_Ctrl, longName="IKFK_Seamless", attributeType="enum",
|
||||
enumName="IK:FK:", keyable=False)
|
||||
cmds.setAttr("{}.IKFK_Seamless".format(Switch_Ctrl), channelBox=True)
|
||||
|
||||
# 4. Add config attributes to Locator and connect
|
||||
|
||||
config_map = {
|
||||
"IKFK_Seamless_Switching": LOCATOR_NAME,
|
||||
"FK_Joint_Root": FK_Joint_Root,
|
||||
"FK_Joint_Mid": FK_Joint_Mid,
|
||||
"FK_Joint_End": FK_Joint_End,
|
||||
"FK_Ctrl_Root": FK_Ctrl_Root,
|
||||
"FK_Ctrl_Mid": FK_Ctrl_Mid,
|
||||
"FK_Ctrl_End": FK_Ctrl_End,
|
||||
"IK_Joint_Root": IK_Joint_Root,
|
||||
"IK_Joint_Mid": IK_Joint_Mid,
|
||||
"IK_Joint_End": IK_Joint_End,
|
||||
"IK_Ctrl_Root": IK_Ctrl_Root,
|
||||
"IK_Ctrl_Pole": IK_Ctrl_Pole,
|
||||
"Switch_Ctrl": Switch_Ctrl,
|
||||
"Switch_Attr": Switch_Attr,
|
||||
}
|
||||
|
||||
for attr_name, value in config_map.items():
|
||||
attr_full_name = "{}.{}".format(locator, attr_name)
|
||||
|
||||
if not cmds.attributeQuery(attr_name, node=locator, exists=True):
|
||||
cmds.addAttr(locator, longName=attr_name, dataType="string", keyable=True)
|
||||
|
||||
cmds.setAttr(attr_full_name, value, type="string")
|
||||
|
||||
# 6. Set up scriptJob (Uses the full module path)
|
||||
# Note: Old scriptJobs will be automatically replaced when the same attribute is monitored
|
||||
|
||||
# The command string for scriptJob - pure Python code
|
||||
# Format: 'import module; module.sg_switching("locator_name")'
|
||||
command = 'import {0}; {0}.sg_switching("{1}")'.format(MODULE_NAME, LOCATOR_NAME)
|
||||
|
||||
try:
|
||||
cmds.scriptJob(attributeChange=["{}.IKFK_Seamless".format(Switch_Ctrl), command],
|
||||
killWithScene=True)
|
||||
|
||||
cmds.inViewMessage(message='IK/FK Seamless Switching Built for: <hl>{}</hl>'.format(Switch_Ctrl),
|
||||
position='topCenter', fade=True)
|
||||
|
||||
except Exception as e:
|
||||
cmds.error("Failed to set up scriptJob: {}. Command was: {}".format(e, command))
|
||||
return
|
||||
|
||||
if __name__ == "__main__":
|
||||
IKFK_Switch_UI()
|
||||
@@ -49,62 +49,27 @@ global proc shelf_Nexus_Animation () {
|
||||
-manage 1
|
||||
-visible 1
|
||||
-preventOverride 0
|
||||
-annotation "IK/FK Switcher - Build temporary IK/FK setup on any rig"
|
||||
-annotation "IKFK Switch - Seamless switching between IK and FK animation"
|
||||
-enableBackground 0
|
||||
-backgroundColor 0 0 0
|
||||
-highlightColor 0.321569 0.521569 0.65098
|
||||
-align "center"
|
||||
-label "IK/FK"
|
||||
-label "IKFK"
|
||||
-labelOffset 0
|
||||
-rotation 0
|
||||
-flipX 0
|
||||
-flipY 0
|
||||
-useAlpha 1
|
||||
-font "plainLabelFont"
|
||||
-imageOverlayLabel "IK/FK"
|
||||
-imageOverlayLabel "IKFK"
|
||||
-overlayLabelColor 0.8 0.8 0.8
|
||||
-overlayLabelBackColor 0 0 0 0.5
|
||||
-image "ik_fk_switcher.png"
|
||||
-image1 "ik_fk_switcher.png"
|
||||
-image "ikfk_switch.png"
|
||||
-image1 "ikfk_switch.png"
|
||||
-style "iconOnly"
|
||||
-marginWidth 0
|
||||
-marginHeight 1
|
||||
-command "import animation_tools.ik_fk_switcher\nanimation_tools.ik_fk_switcher.user_interface()"
|
||||
-sourceType "python"
|
||||
-commandRepeatable 1
|
||||
-flat 1
|
||||
;
|
||||
shelfButton
|
||||
-enableCommandRepeat 1
|
||||
-flexibleWidthType 3
|
||||
-flexibleWidthValue 32
|
||||
-enable 1
|
||||
-width 35
|
||||
-height 34
|
||||
-manage 1
|
||||
-visible 1
|
||||
-preventOverride 0
|
||||
-annotation "Advanced IK/FK Switch - Switch between IK and FK for ADV rigs"
|
||||
-enableBackground 0
|
||||
-backgroundColor 0 0 0
|
||||
-highlightColor 0.321569 0.521569 0.65098
|
||||
-align "center"
|
||||
-label "AdvIKFK"
|
||||
-labelOffset 0
|
||||
-rotation 0
|
||||
-flipX 0
|
||||
-flipY 0
|
||||
-useAlpha 1
|
||||
-font "plainLabelFont"
|
||||
-imageOverlayLabel "AdvIKFK"
|
||||
-overlayLabelColor 0.8 0.8 0.8
|
||||
-overlayLabelBackColor 0 0 0 0.5
|
||||
-image "advIkFkSwitch.png"
|
||||
-image1 "advIkFkSwitch.png"
|
||||
-style "iconOnly"
|
||||
-marginWidth 0
|
||||
-marginHeight 1
|
||||
-command "import animation_tools.advIkFkSwitch\nanimation_tools.advIkFkSwitch.IkFk(False)"
|
||||
-command "from animation_tools import ikfk_switch\nikfk_switch.IKFK_Switch_UI()"
|
||||
-sourceType "python"
|
||||
-commandRepeatable 1
|
||||
-flat 1
|
||||
|
||||
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
144
2025/scripts/animation_tools/IKFK_SWITCH_README.md
Normal file
144
2025/scripts/animation_tools/IKFK_SWITCH_README.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# IKFK Switch Tool
|
||||
|
||||
## 概述
|
||||
IKFK Switch Tool 是一个用于在Maya中实现IK和FK动画之间无缝切换的工具。该工具从原始MEL脚本转换为Python版本,提供更好的跨版本兼容性。
|
||||
|
||||
## 版本信息
|
||||
- **版本**: V1.0
|
||||
- **兼容性**: Maya 2018+
|
||||
- **语言**: Python
|
||||
- **原始版本**: MEL (ikfk_switch.mel)
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 主要功能
|
||||
1. **无缝切换**: 在IK和FK动画之间进行无缝切换,保持动画连续性
|
||||
2. **自动匹配**: 切换时自动匹配控制器位置和旋转
|
||||
3. **关键帧设置**: 自动在切换点设置关键帧
|
||||
4. **预设支持**: 内置ADV绑定预设,支持快速设置
|
||||
|
||||
### UI功能
|
||||
- **FK控制器加载**: 加载FK骨骼和控制器
|
||||
- FK Joint Root/Mid/End
|
||||
- FK Ctrl Root/Mid/End
|
||||
|
||||
- **IK控制器加载**: 加载IK骨骼和控制器
|
||||
- IK Joint Root/Mid/End
|
||||
- IK Ctrl Root (IK手柄)
|
||||
- IK Ctrl Pole (极向量控制器)
|
||||
|
||||
- **切换控制器**: 设置切换属性
|
||||
- Switch Ctrl (切换控制器)
|
||||
- Switch Attr (切换属性名称)
|
||||
|
||||
### 辅助功能
|
||||
- **ADV Build**: 快速为ADV绑定设置IKFK切换
|
||||
- **Empty**: 清空所有输入字段
|
||||
- **BiliBili**: 打开教程链接
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本使用流程
|
||||
|
||||
1. **加载FK控制器**
|
||||
- 选择FK骨骼根节点,点击"< FK Joint Root"
|
||||
- 选择FK骨骼中间节点,点击"< FK Joint Mid"
|
||||
- 选择FK骨骼末端节点,点击"< FK Joint End"
|
||||
- 重复以上步骤加载FK控制器
|
||||
|
||||
2. **加载IK控制器**
|
||||
- 选择IK骨骼节点,点击相应按钮
|
||||
- 选择IK控制器和极向量控制器
|
||||
|
||||
3. **设置切换控制器**
|
||||
- 选择用于切换的控制器,点击"< Switch Ctrl"
|
||||
- 在通道盒中选择切换属性,点击"< Switch Attr"
|
||||
|
||||
4. **构建切换系统**
|
||||
- 点击"<<< Build Seamless Switching >>>"按钮
|
||||
- 系统会自动创建必要的属性和脚本节点
|
||||
|
||||
5. **使用切换功能**
|
||||
- 在切换控制器上找到"IKFK_Seamless"属性
|
||||
- 切换该属性值(IK=0, FK=1)即可实现无缝切换
|
||||
|
||||
### ADV绑定快速设置
|
||||
|
||||
如果使用ADV绑定系统,可以使用预设功能:
|
||||
1. 展开"Edit"面板
|
||||
2. 点击"<<< ADV Build >>>"
|
||||
3. 系统会自动为左右手臂和腿部设置IKFK切换
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **IKFKSwitchUI类**: UI界面管理
|
||||
- 创建和管理UI窗口
|
||||
- 处理用户输入
|
||||
- 调用切换逻辑
|
||||
|
||||
2. **seamless_switching函数**: 核心切换逻辑
|
||||
- 创建数据存储locator
|
||||
- 添加必要属性
|
||||
- 建立属性连接
|
||||
- 创建脚本节点
|
||||
|
||||
3. **create_switching_script函数**: 创建全局切换脚本
|
||||
- 使用MEL脚本实现切换逻辑
|
||||
- 处理约束和关键帧
|
||||
|
||||
4. **create_script_job函数**: 创建脚本任务
|
||||
- 监听属性变化
|
||||
- 自动触发切换
|
||||
|
||||
### 兼容性设计
|
||||
|
||||
- 使用`maya.cmds`和`maya.mel`模块,确保跨版本兼容
|
||||
- 避免使用版本特定的API
|
||||
- 使用f-string格式化(Python 3.6+)
|
||||
- 异常处理确保稳定性
|
||||
|
||||
## 工具架集成
|
||||
|
||||
该工具已集成到Nexus Animation工具架:
|
||||
- **图标**: ikfk_switch.png
|
||||
- **位置**: 动画工具架末尾
|
||||
- **调用**: `import animation_tools.ikfx_switch; animation_tools.ikfx_switch.show()`
|
||||
|
||||
## 文件位置
|
||||
|
||||
### Maya 2023
|
||||
- 脚本: `scripts/animation_tools/ikfx_switch.py`
|
||||
- 图标: `icons/ikfk_switch.png`
|
||||
- 工具架: `shelves/shelf_Nexus_Animation.mel`
|
||||
|
||||
### Maya 2025
|
||||
- 脚本: `scripts/animation_tools/ikfx_switch.py`
|
||||
- 图标: `icons/ikfk_switch.png`
|
||||
- 工具架: `shelves/shelf_Nexus_Animation.mel`
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **属性要求**: 切换属性必须有最小值和最大值设置
|
||||
2. **命名规范**: 建议使用清晰的命名规范以便识别
|
||||
3. **关键帧**: 切换会在当前帧和前一帧设置关键帧
|
||||
4. **选择保持**: 切换后会恢复原始选择
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **切换不工作**
|
||||
- 检查所有必需的对象是否存在
|
||||
- 确认切换属性已正确设置
|
||||
- 查看脚本编辑器的错误信息
|
||||
|
||||
2. **位置不匹配**
|
||||
- 确保FK和IK骨骼层级正确
|
||||
- 检查约束设置
|
||||
- 验证控制器方向
|
||||
|
||||
3. **属性加载失败**
|
||||
- 确保在通道盒中选择了属性
|
||||
- 检查属性是否可关键帧化
|
||||
File diff suppressed because it is too large
Load Diff
603
2025/scripts/animation_tools/ikfk_switch.py
Normal file
603
2025/scripts/animation_tools/ikfk_switch.py
Normal file
@@ -0,0 +1,603 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import maya.cmds as cmds
|
||||
import maya.mel as mel
|
||||
import math
|
||||
import sys
|
||||
import inspect
|
||||
import importlib
|
||||
|
||||
# Crucial for fixing namespace issues:
|
||||
# Dynamically gets the module name (e.g., 'animation_tools.ikfk_switch')
|
||||
|
||||
MODULE_NAME = __name__
|
||||
|
||||
# ==============================================================================
|
||||
# UI Creation
|
||||
# ==============================================================================
|
||||
|
||||
def IKFK_Switch_UI():
|
||||
"""
|
||||
Creates the main IK/FK switch Maya window interface.
|
||||
"""
|
||||
WINDOW_NAME = "IKFK_Switch_UI"
|
||||
WINDOW_TITLE = "IK/FK Switch V2.9 (Final Button Fix)"
|
||||
WINDOW_WIDTH = 300
|
||||
|
||||
# Check if window exists and delete it
|
||||
if cmds.window(WINDOW_NAME, exists=True):
|
||||
cmds.deleteUI(WINDOW_NAME, window=True)
|
||||
|
||||
# Create window
|
||||
cmds.window(WINDOW_NAME, width=WINDOW_WIDTH, title=WINDOW_TITLE)
|
||||
|
||||
# Main Layout
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=1)
|
||||
|
||||
# --- Helper function for button commands ---
|
||||
# CORE FIX: Removed the 'python("...")' wrapper.
|
||||
# The function now returns a pure Python command string for button execution.
|
||||
def py_cmd(func_name, args=""):
|
||||
# Format: 'import module; module.function("args")' (Pure Python string)
|
||||
if args:
|
||||
# Use double quotes to avoid escaping issues
|
||||
return 'import {0}; {0}.{1}("{2}")'.format(MODULE_NAME, func_name, args)
|
||||
else:
|
||||
return 'import {0}; {0}.{1}()'.format(MODULE_NAME, func_name)
|
||||
|
||||
# --- Edit Section ---
|
||||
cmds.frameLayout(label="Edit", collapse=True, collapsable=True,
|
||||
collapseCommand="cmds.window('{}', edit=True, height=578)".format(WINDOW_NAME),
|
||||
expandCommand="cmds.window('{}', edit=True, height=578)".format(WINDOW_NAME))
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
cmds.button(label="<<< ADV Build >>>", command=py_cmd("sg_ADV_Build"))
|
||||
cmds.button(label="<<< Empty >>>", command=py_cmd("sg_Empty"))
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
# --- Load FK Ctrl Section ---
|
||||
cmds.frameLayout(label="Load FK Ctrl")
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
|
||||
# FK Joint Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_01", text="", placeholderText="FK Joint Root")
|
||||
cmds.button(label="< FK Joint Root", command=py_cmd("sg_setTextField", "target_01"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Joint Mid
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_02", text="", placeholderText="FK Joint Mid")
|
||||
cmds.button(label="< FK Joint Mid", command=py_cmd("sg_setTextField", "target_02"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Joint End
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_03", text="", placeholderText="FK Joint End")
|
||||
cmds.button(label="< FK Joint End", command=py_cmd("sg_setTextField", "target_03"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.separator(style="in", height=5)
|
||||
|
||||
# FK Ctrl Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_04", text="", placeholderText="FK Ctrl Root")
|
||||
cmds.button(label="< FK Ctrl Root", command=py_cmd("sg_setTextField", "target_04"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Ctrl Mid
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_05", text="", placeholderText="FK Ctrl Mid")
|
||||
cmds.button(label="< FK Ctrl Mid", command=py_cmd("sg_setTextField", "target_05"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# FK Ctrl End
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_06", text="", placeholderText="FK Ctrl End")
|
||||
cmds.button(label="< FK Ctrl End", command=py_cmd("sg_setTextField", "target_06"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
# --- Load IK Ctrl Section ---
|
||||
cmds.frameLayout(label="Load IK Ctrl")
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
|
||||
# IK Joint Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_07", text="", placeholderText="IK Joint Root")
|
||||
cmds.button(label="< IK Joint Root", command=py_cmd("sg_setTextField", "target_07"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# IK Joint Mid
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_08", text="", placeholderText="IK Joint Mid")
|
||||
cmds.button(label="< IK Joint Mid", command=py_cmd("sg_setTextField", "target_08"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# IK Joint End
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_09", text="", placeholderText="IK Joint End")
|
||||
cmds.button(label="< IK Joint End", command=py_cmd("sg_setTextField", "target_09"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.separator(style="in", height=5)
|
||||
|
||||
# IK Ctrl Root
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_10", text="", placeholderText="IK Ctrl Root")
|
||||
cmds.button(label="< IK Ctrl Root", command=py_cmd("sg_setTextField", "target_10"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# IK Ctrl Pole
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_11", text="", placeholderText="IK Ctrl Pole")
|
||||
cmds.button(label="< IK Ctrl Pole", command=py_cmd("sg_setTextField", "target_11"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
# --- Load Switch Ctrl Section ---
|
||||
cmds.frameLayout(label="Load Switch Ctrl")
|
||||
cmds.columnLayout(adjustableColumn=True, rowSpacing=2)
|
||||
|
||||
# Switch Ctrl
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_12", text="", placeholderText="Switch Ctrl")
|
||||
cmds.button(label="< Switch Ctrl", command=py_cmd("sg_setTextField", "target_12"))
|
||||
cmds.setParent("..")
|
||||
|
||||
# Switch Attr
|
||||
cmds.rowLayout(numberOfColumns=2, columnWidth2=(45, 120), adjustableColumn=1)
|
||||
cmds.textField("target_13", text="", placeholderText="Switch Attr Name")
|
||||
cmds.button(label="< Switch Attr", command=py_cmd("sg_setLoadAttr"))
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.setParent("..")
|
||||
cmds.setParent("..")
|
||||
|
||||
cmds.separator(style="in", height=5)
|
||||
|
||||
# Build Button
|
||||
cmds.button(height=32, label="<<< Build Seamless Switching >>>", command=py_cmd("sg_Execute"))
|
||||
|
||||
cmds.setParent("..")
|
||||
|
||||
# Adjust window height and show
|
||||
cmds.window(WINDOW_NAME, edit=True, height=578)
|
||||
cmds.showWindow(WINDOW_NAME)
|
||||
|
||||
|
||||
def sg_setTextField(target):
|
||||
"""
|
||||
Sets the name of the currently selected object into the specified text field.
|
||||
"""
|
||||
selection = cmds.ls(selection=True, long=False)
|
||||
|
||||
if selection:
|
||||
selected_object = selection[0]
|
||||
cmds.textField(target, edit=True, text=selected_object)
|
||||
else:
|
||||
cmds.warning("Please select an object.")
|
||||
|
||||
|
||||
def sg_setLoadAttr():
|
||||
"""
|
||||
Sets the name of the selected attribute in the Channel Box into the target_13 text field.
|
||||
"""
|
||||
# Query selected attributes in the Channel Box
|
||||
attrs = cmds.channelBox("mainChannelBox", query=True, selectedMainAttributes=True)
|
||||
|
||||
if not attrs:
|
||||
cmds.error("Select an attribute in the Channel Box...")
|
||||
return
|
||||
|
||||
# Take only the first selected attribute
|
||||
attribute_name = attrs[0]
|
||||
cmds.textField("target_13", edit=True, text=attribute_name)
|
||||
|
||||
|
||||
def sg_Execute():
|
||||
"""
|
||||
Reads all values from UI controls and calls the sg_SeamlessSwitching function.
|
||||
"""
|
||||
# Get values from UI controls
|
||||
FK_Joint_Root = cmds.textField("target_01", query=True, text=True)
|
||||
FK_Joint_Mid = cmds.textField("target_02", query=True, text=True)
|
||||
FK_Joint_End = cmds.textField("target_03", query=True, text=True)
|
||||
FK_Ctrl_Root = cmds.textField("target_04", query=True, text=True)
|
||||
FK_Ctrl_Mid = cmds.textField("target_05", query=True, text=True)
|
||||
FK_Ctrl_End = cmds.textField("target_06", query=True, text=True)
|
||||
IK_Joint_Root = cmds.textField("target_07", query=True, text=True)
|
||||
IK_Joint_Mid = cmds.textField("target_08", query=True, text=True)
|
||||
IK_Joint_End = cmds.textField("target_09", query=True, text=True)
|
||||
IK_Ctrl_Root = cmds.textField("target_10", query=True, text=True)
|
||||
IK_Ctrl_Pole = cmds.textField("target_11", query=True, text=True)
|
||||
Switch_Ctrl = cmds.textField("target_12", query=True, text=True)
|
||||
Switch_Attr = cmds.textField("target_13", query=True, text=True)
|
||||
|
||||
# Check for empty values
|
||||
all_targets = [FK_Joint_Root, FK_Joint_Mid, FK_Joint_End, FK_Ctrl_Root, FK_Ctrl_Mid,
|
||||
FK_Ctrl_End, IK_Joint_Root, IK_Joint_Mid, IK_Joint_End, IK_Ctrl_Root,
|
||||
IK_Ctrl_Pole, Switch_Ctrl, Switch_Attr]
|
||||
|
||||
for target in all_targets:
|
||||
if not target:
|
||||
cmds.error("All fields must be filled before building the switch.")
|
||||
return
|
||||
|
||||
# Call core build function
|
||||
sg_SeamlessSwitching(
|
||||
FK_Joint_Root,
|
||||
FK_Joint_Mid,
|
||||
FK_Joint_End,
|
||||
FK_Ctrl_Root,
|
||||
FK_Ctrl_Mid,
|
||||
FK_Ctrl_End,
|
||||
IK_Joint_Root,
|
||||
IK_Joint_Mid,
|
||||
IK_Joint_End,
|
||||
IK_Ctrl_Root,
|
||||
IK_Ctrl_Pole,
|
||||
Switch_Ctrl,
|
||||
Switch_Attr
|
||||
)
|
||||
|
||||
|
||||
def sg_ADV_Build():
|
||||
"""
|
||||
Builds IK/FK switching for sample limbs using preset naming conventions.
|
||||
NOTE: Modify names here to match your Rig naming convention.
|
||||
"""
|
||||
|
||||
# Right Arm
|
||||
sg_SeamlessSwitching(
|
||||
"FKXShoulder_R", "FKXElbow_R", "FKXWrist_R",
|
||||
"FKShoulder_R", "FKElbow_R", "FKWrist_R",
|
||||
"IKXShoulder_R", "IKXElbow_R", "IKXWrist_R",
|
||||
"IKArm_R", "PoleArm_R", "FKIKArm_R", "fkik"
|
||||
)
|
||||
|
||||
# Left Arm
|
||||
sg_SeamlessSwitching(
|
||||
"FKXShoulder_L", "FKXElbow_L", "FKXWrist_L",
|
||||
"FKShoulder_L", "FKElbow_L", "FKWrist_L",
|
||||
"IKXShoulder_L", "IKXElbow_L", "IKXWrist_L",
|
||||
"IKArm_L", "PoleArm_L", "FKIKArm_L", "fkik"
|
||||
)
|
||||
|
||||
# Right Leg
|
||||
sg_SeamlessSwitching(
|
||||
"FKXHip_R", "FKXKnee_R", "FKXAnkle_R",
|
||||
"FKHip_R", "FKKnee_R", "FKAnkle_R",
|
||||
"IKXHip_R", "IKXKnee_R", "IKXAnkle_R",
|
||||
"IKLeg_R", "PoleLeg_R", "FKIKLeg_R", "fkik"
|
||||
)
|
||||
|
||||
# Left Leg
|
||||
sg_SeamlessSwitching(
|
||||
"FKXHip_L", "FKXKnee_L", "FKXAnkle_L",
|
||||
"FKHip_L", "FKKnee_L", "FKAnkle_L",
|
||||
"IKXHip_L", "IKXKnee_L", "IKXAnkle_L",
|
||||
"IKLeg_L", "PoleLeg_L", "FKIKLeg_L", "fkik"
|
||||
)
|
||||
|
||||
def sg_Empty():
|
||||
"""
|
||||
Clears all text fields in the UI.
|
||||
"""
|
||||
for i in range(1, 14):
|
||||
target_name = "target_{:02d}".format(i)
|
||||
if cmds.textField(target_name, exists=True):
|
||||
cmds.textField(target_name, edit=True, text="")
|
||||
|
||||
|
||||
# ==============================================================================
|
||||
# Core IK/FK Switching Logic
|
||||
# ==============================================================================
|
||||
|
||||
def sg_switching(locator_name):
|
||||
"""
|
||||
Executes the core IK/FK switching logic (seamless match).
|
||||
This function is called at runtime by the scriptJob.
|
||||
|
||||
Args:
|
||||
locator_name (str): The name of the Locator storing connection info.
|
||||
"""
|
||||
|
||||
# Get the Switch Control and attribute name (stored as string attributes)
|
||||
try:
|
||||
switch_ctrl = cmds.getAttr("{}.Switch_Ctrl".format(locator_name))
|
||||
switch_attr_name = cmds.getAttr("{}.Switch_Attr".format(locator_name))
|
||||
except:
|
||||
cmds.error("Locator {} does not have required attributes.".format(locator_name))
|
||||
return
|
||||
|
||||
# State check: Use optionVar to store the last state, preventing redundant execution
|
||||
option_var_name = locator_name
|
||||
current_state = cmds.getAttr("{}.IKFK_Seamless".format(switch_ctrl))
|
||||
|
||||
try:
|
||||
last_state = cmds.optionVar(query=option_var_name)
|
||||
except RuntimeError:
|
||||
last_state = -1
|
||||
|
||||
if last_state == current_state:
|
||||
# State has not changed, exit
|
||||
return
|
||||
|
||||
# Store current selection
|
||||
selection = cmds.ls(selection=True, long=False)
|
||||
|
||||
# Get all object names from the Locator (stored as string attributes)
|
||||
try:
|
||||
FK_Joint_Root = cmds.getAttr("{}.FK_Joint_Root".format(locator_name))
|
||||
FK_Joint_Mid = cmds.getAttr("{}.FK_Joint_Mid".format(locator_name))
|
||||
FK_Joint_End = cmds.getAttr("{}.FK_Joint_End".format(locator_name))
|
||||
FK_Ctrl_Root = cmds.getAttr("{}.FK_Ctrl_Root".format(locator_name))
|
||||
FK_Ctrl_Mid = cmds.getAttr("{}.FK_Ctrl_Mid".format(locator_name))
|
||||
FK_Ctrl_End = cmds.getAttr("{}.FK_Ctrl_End".format(locator_name))
|
||||
IK_Joint_Root = cmds.getAttr("{}.IK_Joint_Root".format(locator_name))
|
||||
IK_Joint_Mid = cmds.getAttr("{}.IK_Joint_Mid".format(locator_name))
|
||||
IK_Joint_End = cmds.getAttr("{}.IK_Joint_End".format(locator_name))
|
||||
IK_Ctrl_Root = cmds.getAttr("{}.IK_Ctrl_Root".format(locator_name))
|
||||
IK_Ctrl_Pole = cmds.getAttr("{}.IK_Ctrl_Pole".format(locator_name))
|
||||
except Exception as e:
|
||||
cmds.error("Failed to retrieve object names from Locator {}: {}".format(locator_name, e))
|
||||
return
|
||||
|
||||
# Get Min/Max values for the blend attribute
|
||||
try:
|
||||
attr_min = cmds.attributeQuery(switch_attr_name, node=switch_ctrl, minimum=True)
|
||||
attr_max = cmds.attributeQuery(switch_attr_name, node=switch_ctrl, maximum=True)
|
||||
min_val = attr_min[0] if attr_min else 0.0
|
||||
max_val = attr_max[0] if attr_max else 1.0
|
||||
except Exception:
|
||||
cmds.warning("Could not query min/max values for blend attribute. Defaulting to 0 and 1.")
|
||||
min_val = 0.0
|
||||
max_val = 1.0
|
||||
|
||||
current_time = cmds.currentTime(query=True)
|
||||
|
||||
# Set keyframes on all controls before switching (MEL lines 504-508)
|
||||
cmds.setKeyframe(FK_Ctrl_Root)
|
||||
cmds.setKeyframe(FK_Ctrl_Mid)
|
||||
cmds.setKeyframe(FK_Ctrl_End)
|
||||
cmds.setKeyframe(IK_Ctrl_Root)
|
||||
cmds.setKeyframe(IK_Ctrl_Pole)
|
||||
|
||||
# **MATCHING LOGIC**
|
||||
if current_state == 0: # Switch to IK (IKFK_Seamless: IK)
|
||||
|
||||
# 1. Match IK Ctrl Root (End Controller) - Using temp groups to preserve rotation offset
|
||||
# This matches the MEL version's approach (lines 510-519)
|
||||
tempGroup_A = cmds.group(empty=True)
|
||||
tempGroup_B = cmds.group(empty=True)
|
||||
|
||||
# Step 1: Match both temp groups to IK_Joint_End
|
||||
cmds.delete(cmds.parentConstraint(IK_Joint_End, tempGroup_A, weight=1))
|
||||
cmds.delete(cmds.parentConstraint(IK_Joint_End, tempGroup_B, weight=1))
|
||||
|
||||
# Step 2: Apply IK_Ctrl_Root's current orientation to tempGroup_B (preserve rotation offset)
|
||||
cmds.delete(cmds.orientConstraint(IK_Ctrl_Root, tempGroup_B, offset=(0, 0, 0), weight=1))
|
||||
|
||||
# Step 3: Parent tempGroup_B under tempGroup_A to create hierarchy
|
||||
cmds.parent(tempGroup_B, tempGroup_A)
|
||||
|
||||
# Step 4: Match tempGroup_A to FK_Joint_End (this moves the hierarchy)
|
||||
cmds.delete(cmds.parentConstraint(FK_Joint_End, tempGroup_A, weight=1))
|
||||
|
||||
# Step 5: Apply tempGroup_B's final transform to IK_Ctrl_Root
|
||||
con_A = cmds.parentConstraint(tempGroup_B, IK_Ctrl_Root, weight=1)
|
||||
cmds.setKeyframe(IK_Ctrl_Root)
|
||||
cmds.delete(con_A)
|
||||
|
||||
# 2. Calculate Pole Vector position based on FK chain bend direction
|
||||
# Get world space positions of FK joints
|
||||
pos_root = cmds.xform(FK_Joint_Root, query=True, worldSpace=True, translation=True)
|
||||
pos_mid = cmds.xform(FK_Joint_Mid, query=True, worldSpace=True, translation=True)
|
||||
pos_end = cmds.xform(FK_Joint_End, query=True, worldSpace=True, translation=True)
|
||||
|
||||
# Calculate the bend direction using vector projection
|
||||
# Vector from root to end (main chain direction)
|
||||
vec_RE = [pos_end[i] - pos_root[i] for i in range(3)]
|
||||
# Vector from root to mid
|
||||
vec_RM = [pos_mid[i] - pos_root[i] for i in range(3)]
|
||||
|
||||
# Length of main chain vector
|
||||
len_RE = math.sqrt(sum(vec_RE[i]**2 for i in range(3)))
|
||||
|
||||
if len_RE > 0.0001: # Avoid division by zero
|
||||
# Normalize the main chain vector
|
||||
norm_RE = [vec_RE[i] / len_RE for i in range(3)]
|
||||
|
||||
# Project vec_RM onto vec_RE to find the projection point
|
||||
proj_scalar = sum(vec_RM[i] * norm_RE[i] for i in range(3))
|
||||
proj_vec = [proj_scalar * norm_RE[i] for i in range(3)]
|
||||
|
||||
# Perpendicular vector (from projection point to mid joint)
|
||||
vec_perp = [vec_RM[i] - proj_vec[i] for i in range(3)]
|
||||
len_perp = math.sqrt(sum(vec_perp[i]**2 for i in range(3)))
|
||||
|
||||
if len_perp > 0.0001: # Chain has a bend
|
||||
# Normalize perpendicular vector
|
||||
norm_perp = [vec_perp[i] / len_perp for i in range(3)]
|
||||
|
||||
# Extension distance: use chain length as reference
|
||||
# Multiply by a factor to place pole vector at a reasonable distance
|
||||
extend_dist = len_RE * 0.5
|
||||
|
||||
# Pole Vector position = mid joint + perpendicular direction * extension distance
|
||||
pole_pos = [pos_mid[i] + norm_perp[i] * extend_dist for i in range(3)]
|
||||
|
||||
else: # Chain is straight, use a default offset
|
||||
# If straight, offset along Z-axis (or another appropriate axis)
|
||||
pole_pos = [pos_mid[0], pos_mid[1], pos_mid[2] + len_RE * 0.5]
|
||||
else:
|
||||
# Fallback: keep current position if chain length is zero
|
||||
pole_pos = cmds.xform(IK_Ctrl_Pole, query=True, worldSpace=True, translation=True)
|
||||
|
||||
# Apply calculated position to Pole Vector
|
||||
cmds.xform(IK_Ctrl_Pole, worldSpace=True, translation=pole_pos)
|
||||
cmds.setKeyframe(IK_Ctrl_Pole)
|
||||
|
||||
# Clean up temp groups
|
||||
cmds.delete(tempGroup_A)
|
||||
|
||||
# 3. Switch attribute: from FK (min_val) to IK (max_val)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), min_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time - 1)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), max_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time)
|
||||
|
||||
|
||||
elif current_state == 1: # Switch to FK (IKFK_Seamless: FK)
|
||||
|
||||
# Match FK controls to IK joints (MEL lines 530-538)
|
||||
# Use weight=1 instead of maintainOffset to match MEL behavior
|
||||
|
||||
# 1. Match FK Ctrl Root to IK Joint Root
|
||||
con_A = cmds.parentConstraint(IK_Joint_Root, FK_Ctrl_Root, weight=1)
|
||||
cmds.setKeyframe(FK_Ctrl_Root)
|
||||
cmds.delete(con_A)
|
||||
|
||||
# 2. Match FK Ctrl Mid to IK Joint Mid
|
||||
con_B = cmds.parentConstraint(IK_Joint_Mid, FK_Ctrl_Mid, weight=1)
|
||||
cmds.setKeyframe(FK_Ctrl_Mid)
|
||||
cmds.delete(con_B)
|
||||
|
||||
# 3. Match FK Ctrl End to IK Joint End
|
||||
con_C = cmds.parentConstraint(IK_Joint_End, FK_Ctrl_End, weight=1)
|
||||
cmds.setKeyframe(FK_Ctrl_End)
|
||||
cmds.delete(con_C)
|
||||
|
||||
# 4. Switch attribute: from IK (max_val) to FK (min_val)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), max_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time - 1)
|
||||
cmds.setAttr("{}.{}".format(switch_ctrl, switch_attr_name), min_val)
|
||||
cmds.setKeyframe(switch_ctrl, attribute=switch_attr_name, time=current_time)
|
||||
|
||||
# Update optionVar
|
||||
cmds.optionVar(intValue=(option_var_name, current_state))
|
||||
|
||||
# Restore selection
|
||||
if selection:
|
||||
cmds.select(selection, replace=True)
|
||||
|
||||
|
||||
def sg_SeamlessSwitching(
|
||||
FK_Joint_Root, FK_Joint_Mid, FK_Joint_End,
|
||||
FK_Ctrl_Root, FK_Ctrl_Mid, FK_Ctrl_End,
|
||||
IK_Joint_Root, IK_Joint_Mid, IK_Joint_End,
|
||||
IK_Ctrl_Root, IK_Ctrl_Pole,
|
||||
Switch_Ctrl, Switch_Attr
|
||||
):
|
||||
"""
|
||||
Sets up the necessary nodes, attributes, and the scriptJob for IK/FK switching.
|
||||
"""
|
||||
|
||||
# 1. Ensure all objects exist
|
||||
all_objects = [FK_Joint_Root, FK_Joint_Mid, FK_Joint_End, FK_Ctrl_Root, FK_Ctrl_Mid,
|
||||
FK_Ctrl_End, IK_Joint_Root, IK_Joint_Mid, IK_Joint_End, IK_Ctrl_Root,
|
||||
IK_Ctrl_Pole, Switch_Ctrl]
|
||||
|
||||
for obj in all_objects:
|
||||
if not cmds.objExists(obj):
|
||||
cmds.error("Object not found: '{}'. Please verify all joint and controller names.".format(obj))
|
||||
return
|
||||
|
||||
# 2. Cleanup and Create Locator
|
||||
LOCATOR_NAME = "{}_Switch_Locator".format(Switch_Ctrl)
|
||||
|
||||
matching_locators = cmds.ls(LOCATOR_NAME + "*", type='transform')
|
||||
final_locators_to_delete = []
|
||||
for loc in matching_locators:
|
||||
if loc.startswith(LOCATOR_NAME) and cmds.listRelatives(loc, shapes=True, type='locator'):
|
||||
final_locators_to_delete.append(loc)
|
||||
|
||||
if final_locators_to_delete:
|
||||
cmds.warning("Found and deleting {} existing switch locators for {}: {}".format(
|
||||
len(final_locators_to_delete), Switch_Ctrl, final_locators_to_delete))
|
||||
try:
|
||||
cmds.delete(final_locators_to_delete)
|
||||
except Exception as e:
|
||||
cmds.warning("Failed to delete old locators: {}".format(e))
|
||||
|
||||
# Recreate Locator
|
||||
locator_shape = cmds.createNode("locator", name=LOCATOR_NAME + "Shape")
|
||||
locator = cmds.listRelatives(locator_shape, parent=True)[0]
|
||||
locator = cmds.rename(locator, LOCATOR_NAME)
|
||||
|
||||
cmds.parent(locator, Switch_Ctrl)
|
||||
|
||||
# Unlock and clear transform attributes
|
||||
for attr in ['tx', 'ty', 'tz', 'rx', 'ry', 'rz', 'sx', 'sy', 'sz', 'v']:
|
||||
full_attr = "{}.{}".format(locator, attr)
|
||||
cmds.setAttr(full_attr, lock=False)
|
||||
connections = cmds.listConnections(full_attr, plugs=True, destination=False, source=True)
|
||||
if connections:
|
||||
cmds.disconnectAttr(connections[0], full_attr)
|
||||
|
||||
# Reset and hide Locator
|
||||
cmds.setAttr("{}.t".format(locator), 0, 0, 0)
|
||||
cmds.setAttr("{}.r".format(locator), 0, 0, 0)
|
||||
cmds.setAttr("{}.s".format(locator), 1, 1, 1)
|
||||
cmds.setAttr("{}.visibility".format(locator), 0)
|
||||
|
||||
|
||||
# 3. Add IKFK_Seamless attribute to Switch Ctrl
|
||||
if not cmds.attributeQuery("IKFK_Seamless", node=Switch_Ctrl, exists=True):
|
||||
cmds.addAttr(Switch_Ctrl, longName="IKFK_Seamless", attributeType="enum",
|
||||
enumName="IK:FK:", keyable=False)
|
||||
cmds.setAttr("{}.IKFK_Seamless".format(Switch_Ctrl), channelBox=True)
|
||||
|
||||
# 4. Add config attributes to Locator and connect
|
||||
|
||||
config_map = {
|
||||
"IKFK_Seamless_Switching": LOCATOR_NAME,
|
||||
"FK_Joint_Root": FK_Joint_Root,
|
||||
"FK_Joint_Mid": FK_Joint_Mid,
|
||||
"FK_Joint_End": FK_Joint_End,
|
||||
"FK_Ctrl_Root": FK_Ctrl_Root,
|
||||
"FK_Ctrl_Mid": FK_Ctrl_Mid,
|
||||
"FK_Ctrl_End": FK_Ctrl_End,
|
||||
"IK_Joint_Root": IK_Joint_Root,
|
||||
"IK_Joint_Mid": IK_Joint_Mid,
|
||||
"IK_Joint_End": IK_Joint_End,
|
||||
"IK_Ctrl_Root": IK_Ctrl_Root,
|
||||
"IK_Ctrl_Pole": IK_Ctrl_Pole,
|
||||
"Switch_Ctrl": Switch_Ctrl,
|
||||
"Switch_Attr": Switch_Attr,
|
||||
}
|
||||
|
||||
for attr_name, value in config_map.items():
|
||||
attr_full_name = "{}.{}".format(locator, attr_name)
|
||||
|
||||
if not cmds.attributeQuery(attr_name, node=locator, exists=True):
|
||||
cmds.addAttr(locator, longName=attr_name, dataType="string", keyable=True)
|
||||
|
||||
cmds.setAttr(attr_full_name, value, type="string")
|
||||
|
||||
# 6. Set up scriptJob (Uses the full module path)
|
||||
# Note: Old scriptJobs will be automatically replaced when the same attribute is monitored
|
||||
|
||||
# The command string for scriptJob - pure Python code
|
||||
# Format: 'import module; module.sg_switching("locator_name")'
|
||||
command = 'import {0}; {0}.sg_switching("{1}")'.format(MODULE_NAME, LOCATOR_NAME)
|
||||
|
||||
try:
|
||||
cmds.scriptJob(attributeChange=["{}.IKFK_Seamless".format(Switch_Ctrl), command],
|
||||
killWithScene=True)
|
||||
|
||||
cmds.inViewMessage(message='IK/FK Seamless Switching Built for: <hl>{}</hl>'.format(Switch_Ctrl),
|
||||
position='topCenter', fade=True)
|
||||
|
||||
except Exception as e:
|
||||
cmds.error("Failed to set up scriptJob: {}. Command was: {}".format(e, command))
|
||||
return
|
||||
|
||||
if __name__ == "__main__":
|
||||
IKFK_Switch_UI()
|
||||
@@ -49,27 +49,27 @@ global proc shelf_Nexus_Animation () {
|
||||
-manage 1
|
||||
-visible 1
|
||||
-preventOverride 0
|
||||
-annotation "IK/FK Switcher - Build temporary IK/FK setup on any rig"
|
||||
-annotation "IKFK Switch - Seamless switching between IK and FK animation"
|
||||
-enableBackground 0
|
||||
-backgroundColor 0 0 0
|
||||
-highlightColor 0.321569 0.521569 0.65098
|
||||
-align "center"
|
||||
-label "IK/FK"
|
||||
-label "IKFK"
|
||||
-labelOffset 0
|
||||
-rotation 0
|
||||
-flipX 0
|
||||
-flipY 0
|
||||
-useAlpha 1
|
||||
-font "plainLabelFont"
|
||||
-imageOverlayLabel "IK/FK"
|
||||
-imageOverlayLabel "IKFK"
|
||||
-overlayLabelColor 0.8 0.8 0.8
|
||||
-overlayLabelBackColor 0 0 0 0.5
|
||||
-image "ik_fk_switcher.png"
|
||||
-image1 "ik_fk_switcher.png"
|
||||
-image "ikfk_switch.png"
|
||||
-image1 "ikfk_switch.png"
|
||||
-style "iconOnly"
|
||||
-marginWidth 0
|
||||
-marginHeight 1
|
||||
-command "import animation_tools.ik_fk_switcher\nanimation_tools.ik_fk_switcher.user_interface()"
|
||||
-command "from animation_tools import ikfk_switch\nikfk_switch.IKFK_Switch_UI()"
|
||||
-sourceType "python"
|
||||
-commandRepeatable 1
|
||||
-flat 1
|
||||
@@ -249,5 +249,4 @@ global proc shelf_Nexus_Animation () {
|
||||
-commandRepeatable 1
|
||||
-flat 1
|
||||
;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user