This commit is contained in:
2025-11-26 00:15:31 +08:00
parent ce47cef161
commit 021c593241
3 changed files with 741 additions and 0 deletions

View File

@@ -0,0 +1,706 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
IKFK Seamless Switching Tool V1.0
IKFK无缝切换工具
使用scriptJob实现实时切换
"""
import maya.cmds as cmds
import maya.mel as mel
class IKFKSwitcher(object):
"""IKFK无缝切换工具主类"""
def __init__(self):
self.window_name = "ikfk_switcher_window"
self.window_title = "IKFK Seamless Switching Tool V1.0"
# 存储控制器和关节
self.joints = {
'shoulder': None,
'elbow': None,
'wrist': None
}
self.fk_ctrls = {
'shoulder': None,
'elbow': None,
'wrist': None
}
self.ik_ctrls = {
'wrist': None,
'pole': None
}
self.switch_ctrl = None
self.switch_attr = None
# scriptJob ID列表
self.script_jobs = []
# UI控件引用
self.ui_elements = {}
def create_ui(self):
"""创建UI界面"""
# 如果窗口存在则删除
if cmds.window(self.window_name, exists=True):
cmds.deleteUI(self.window_name)
# 创建窗口
self.window_name = cmds.window(
self.window_name,
title=self.window_title,
widthHeight=(280, 650),
sizeable=True
)
# 主布局
main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
# 标题栏
cmds.rowLayout(numberOfColumns=3, adjustableColumn=2, columnAttach=[(1, 'left', 5)])
cmds.text(label="IKFK Seamless Switching Tool V1.0", font="boldLabelFont")
cmds.button(label="?", width=25, height=25, command=self.show_help)
cmds.button(label="X", width=25, height=25,
backgroundColor=[0.8, 0.3, 0.3],
command=lambda x: cmds.deleteUI(self.window_name))
cmds.setParent('..')
cmds.separator(height=10, style='in')
# Edit菜单
cmds.frameLayout(label="Edit", collapsable=True, collapse=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
cmds.button(label="<<< ADV Build >>>", height=25,
command=self.adv_build,
backgroundColor=[0.4, 0.4, 0.4])
cmds.button(label="<<< Empty >>>", height=25,
command=self.empty_fields,
backgroundColor=[0.4, 0.4, 0.4])
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=10, style='in')
# Load Joint部分
cmds.frameLayout(label="Load Joint", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['joint_shoulder_field'] = self._create_load_field("Joint Shoulder", self.load_joint_shoulder)
self.ui_elements['joint_elbow_field'] = self._create_load_field("Joint Elbow", self.load_joint_elbow)
self.ui_elements['joint_wrist_field'] = self._create_load_field("Joint Wrist", self.load_joint_wrist)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5)
# Load FK Ctrl部分
cmds.frameLayout(label="Load FK Ctrl", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['fk_shoulder_field'] = self._create_load_field("FK Shoulder", self.load_fk_shoulder)
self.ui_elements['fk_elbow_field'] = self._create_load_field("FK Elbow", self.load_fk_elbow)
self.ui_elements['fk_wrist_field'] = self._create_load_field("FK Wrist", self.load_fk_wrist)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5)
# Load IK Ctrl部分
cmds.frameLayout(label="Load IK Ctrl", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['ik_wrist_field'] = self._create_load_field("IK Wrist", self.load_ik_wrist)
self.ui_elements['ik_pole_field'] = self._create_load_field("IK Pole", self.load_ik_pole)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5)
# Load Switch Ctrl部分
cmds.frameLayout(label="Load Switch Ctrl", collapsable=False,
borderStyle='etchedIn', marginWidth=5, marginHeight=5)
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
self.ui_elements['switch_ctrl_field'] = self._create_load_field("Switch Ctrl", self.load_switch_ctrl)
self.ui_elements['switch_attr_field'] = self._create_load_field("Switch Attr", self.load_switch_attr)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=10, style='in')
# 构建按钮
cmds.button(label=">>> Build Seamless Switching >>>",
height=35,
backgroundColor=[0.4, 0.7, 0.4],
command=self.build_seamless_switching)
cmds.separator(height=5)
# 显示窗口
cmds.showWindow(self.window_name)
def _create_load_field(self, label, command):
"""创建加载字段的辅助方法"""
cmds.rowLayout(numberOfColumns=2, adjustableColumn=1, columnAttach=[(2, 'right', 5)])
text_field = cmds.textField(editable=False, backgroundColor=[0.2, 0.2, 0.2])
cmds.button(label="<<< {} ".format(label), width=120, command=command)
cmds.setParent('..')
return text_field
def load_joint_shoulder(self, *args):
"""加载肩部关节"""
sel = cmds.ls(selection=True)
if sel:
self.joints['shoulder'] = sel[0]
cmds.textField(self.ui_elements['joint_shoulder_field'], edit=True, text=sel[0])
print("Loaded Joint Shoulder: {}".format(sel[0]))
else:
cmds.warning("Please select Joint Shoulder")
def load_joint_elbow(self, *args):
"""加载肘部关节"""
sel = cmds.ls(selection=True)
if sel:
self.joints['elbow'] = sel[0]
cmds.textField(self.ui_elements['joint_elbow_field'], edit=True, text=sel[0])
print("Loaded Joint Elbow: {}".format(sel[0]))
else:
cmds.warning("Please select Joint Elbow")
def load_joint_wrist(self, *args):
"""加载腕部关节"""
sel = cmds.ls(selection=True)
if sel:
self.joints['wrist'] = sel[0]
cmds.textField(self.ui_elements['joint_wrist_field'], edit=True, text=sel[0])
print("Loaded Joint Wrist: {}".format(sel[0]))
else:
cmds.warning("Please select Joint Wrist")
def load_fk_shoulder(self, *args):
"""加载FK肩部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.fk_ctrls['shoulder'] = sel[0]
cmds.textField(self.ui_elements['fk_shoulder_field'], edit=True, text=sel[0])
print("Loaded FK Shoulder: {}".format(sel[0]))
else:
cmds.warning("Please select FK Shoulder control")
def load_fk_elbow(self, *args):
"""加载FK肘部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.fk_ctrls['elbow'] = sel[0]
cmds.textField(self.ui_elements['fk_elbow_field'], edit=True, text=sel[0])
print("Loaded FK Elbow: {}".format(sel[0]))
else:
cmds.warning("Please select FK Elbow control")
def load_fk_wrist(self, *args):
"""加载FK腕部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.fk_ctrls['wrist'] = sel[0]
cmds.textField(self.ui_elements['fk_wrist_field'], edit=True, text=sel[0])
print("Loaded FK Wrist: {}".format(sel[0]))
else:
cmds.warning("Please select FK Wrist control")
def load_ik_wrist(self, *args):
"""加载IK腕部控制器"""
sel = cmds.ls(selection=True)
if sel:
self.ik_ctrls['wrist'] = sel[0]
cmds.textField(self.ui_elements['ik_wrist_field'], edit=True, text=sel[0])
print("Loaded IK Wrist: {}".format(sel[0]))
else:
cmds.warning("Please select IK Wrist control")
def load_ik_pole(self, *args):
"""加载IK极向量控制器"""
sel = cmds.ls(selection=True)
if sel:
self.ik_ctrls['pole'] = sel[0]
cmds.textField(self.ui_elements['ik_pole_field'], edit=True, text=sel[0])
print("Loaded IK Pole: {}".format(sel[0]))
else:
cmds.warning("Please select IK Pole control")
def load_switch_ctrl(self, *args):
"""加载切换控制器"""
sel = cmds.ls(selection=True)
if sel:
self.switch_ctrl = sel[0]
cmds.textField(self.ui_elements['switch_ctrl_field'], edit=True, text=sel[0])
print("Loaded Switch Control: {}".format(sel[0]))
else:
cmds.warning("Please select Switch control")
def load_switch_attr(self, *args):
"""加载切换属性"""
sel = cmds.ls(selection=True)
if sel and self.switch_ctrl:
# 获取选中的通道
channels = cmds.channelBox('mainChannelBox', query=True, selectedMainAttributes=True)
if channels:
self.switch_attr = channels[0]
cmds.textField(self.ui_elements['switch_attr_field'], edit=True, text=self.switch_attr)
print("Loaded Switch Attribute: {}".format(self.switch_attr))
else:
cmds.warning("Please select an attribute in the Channel Box")
else:
cmds.warning("Please load Switch Control first and select an attribute")
def build_seamless_switching(self, *args):
"""构建无缝切换系统"""
# 验证所有必需的控制器和关节是否已加载
if not self._validate_inputs():
return
# 清除现有的scriptJob
self.clear_script_jobs()
# 创建scriptJob来监听切换属性的变化
if self.switch_ctrl and self.switch_attr:
attr_full_name = "{}.{}".format(self.switch_ctrl, self.switch_attr)
# 创建属性变化监听
job_id = cmds.scriptJob(
attributeChange=[attr_full_name, self.on_switch_changed],
killWithScene=True,
protected=True
)
self.script_jobs.append(job_id)
print("Created scriptJob (ID: {}) for attribute: {}".format(job_id, attr_full_name))
cmds.inViewMessage(
amg='<hl>Seamless switching built successfully!</hl>',
pos='midCenter',
fade=True,
fadeStayTime=2000,
fadeOutTime=500
)
else:
cmds.warning("Switch control or attribute not loaded")
def _validate_inputs(self):
"""验证所有输入是否有效"""
# 检查关节
for key, joint in self.joints.items():
if not joint or not cmds.objExists(joint):
cmds.warning("Joint {} not loaded or doesn't exist".format(key))
return False
# 检查FK控制器
for key, ctrl in self.fk_ctrls.items():
if not ctrl or not cmds.objExists(ctrl):
cmds.warning("FK control {} not loaded or doesn't exist".format(key))
return False
# 检查IK控制器
for key, ctrl in self.ik_ctrls.items():
if not ctrl or not cmds.objExists(ctrl):
cmds.warning("IK control {} not loaded or doesn't exist".format(key))
return False
# 检查切换控制器
if not self.switch_ctrl or not cmds.objExists(self.switch_ctrl):
cmds.warning("Switch control not loaded or doesn't exist")
return False
if not self.switch_attr:
cmds.warning("Switch attribute not loaded")
return False
return True
def on_switch_changed(self):
"""当切换属性改变时调用"""
if not self.switch_ctrl or not self.switch_attr:
return
# 获取切换值 (假设0=FK, 1=IK)
switch_value = cmds.getAttr("{}.{}".format(self.switch_ctrl, self.switch_attr))
# 根据切换值进行匹配
if switch_value == 0:
# 切换到FK - 匹配FK到IK
self.match_fk_to_ik()
elif switch_value == 1:
# 切换到IK - 匹配IK到FK
self.match_ik_to_fk()
def match_fk_to_ik(self):
"""匹配FK控制器到IK位置"""
try:
# 匹配FK肩部到关节
if self.fk_ctrls['shoulder'] and self.joints['shoulder']:
cmds.matchTransform(self.fk_ctrls['shoulder'], self.joints['shoulder'],
position=False, rotation=True)
# 匹配FK肘部到关节
if self.fk_ctrls['elbow'] and self.joints['elbow']:
cmds.matchTransform(self.fk_ctrls['elbow'], self.joints['elbow'],
position=False, rotation=True)
# 匹配FK腕部到关节
if self.fk_ctrls['wrist'] and self.joints['wrist']:
cmds.matchTransform(self.fk_ctrls['wrist'], self.joints['wrist'],
position=False, rotation=True)
print("Matched FK to IK")
except Exception as e:
cmds.warning("Error matching FK to IK: {}".format(str(e)))
def match_ik_to_fk(self):
"""匹配IK控制器到FK位置"""
try:
# 匹配IK腕部到关节
if self.ik_ctrls['wrist'] and self.joints['wrist']:
cmds.matchTransform(self.ik_ctrls['wrist'], self.joints['wrist'],
position=True, rotation=True)
# 计算并设置极向量位置
if self.ik_ctrls['pole'] and all(self.joints.values()):
pole_pos = self._calculate_pole_vector_position()
if pole_pos:
cmds.xform(self.ik_ctrls['pole'], worldSpace=True,
translation=pole_pos)
print("Matched IK to FK")
except Exception as e:
cmds.warning("Error matching IK to FK: {}".format(str(e)))
def _calculate_pole_vector_position(self):
"""计算极向量位置"""
try:
# 获取三个关节的世界坐标
start_pos = cmds.xform(self.joints['shoulder'], query=True,
worldSpace=True, translation=True)
mid_pos = cmds.xform(self.joints['elbow'], query=True,
worldSpace=True, translation=True)
end_pos = cmds.xform(self.joints['wrist'], query=True,
worldSpace=True, translation=True)
# 使用向量计算极向量位置
import maya.OpenMaya as om
start_vec = om.MVector(start_pos[0], start_pos[1], start_pos[2])
mid_vec = om.MVector(mid_pos[0], mid_pos[1], mid_pos[2])
end_vec = om.MVector(end_pos[0], end_pos[1], end_pos[2])
# 计算从起点到终点的向量
start_to_end = end_vec - start_vec
start_to_mid = mid_vec - start_vec
# 投影
scale = (start_to_end * start_to_mid) / (start_to_end * start_to_end)
proj = start_to_end * scale + start_vec
# 计算极向量方向
pole_dir = mid_vec - proj
pole_dir.normalize()
# 计算链长度
length = (mid_vec - start_vec).length() + (end_vec - mid_vec).length()
# 极向量位置
pole_pos = mid_vec + pole_dir * length
return [pole_pos.x, pole_pos.y, pole_pos.z]
except Exception as e:
cmds.warning("Error calculating pole vector position: {}".format(str(e)))
return None
def clear_script_jobs(self):
"""清除所有scriptJob"""
for job_id in self.script_jobs:
if cmds.scriptJob(exists=job_id):
cmds.scriptJob(kill=job_id, force=True)
print("Killed scriptJob ID: {}".format(job_id))
self.script_jobs = []
def adv_build(self, *args):
"""高级构建功能 - 专门适配AdvanceSkeleton骨骼"""
# 获取当前选择
selection = cmds.ls(selection=True)
if not selection:
cmds.warning("Please select an AdvanceSkeleton control first")
return
# 尝试自动检测AdvanceSkeleton的IKFK_Seamless属性
ctrl = selection[0]
# 检查是否有IKFK_Seamless属性
if cmds.attributeQuery('IKFK_Seamless', node=ctrl, exists=True):
# 自动设置切换控制器和属性
self.switch_ctrl = ctrl
self.switch_attr = 'IKFK_Seamless'
cmds.textField(self.ui_elements['switch_ctrl_field'], edit=True, text=ctrl)
cmds.textField(self.ui_elements['switch_attr_field'], edit=True, text='IKFK_Seamless')
# 尝试自动查找相关的FK和IK控制器
self._auto_detect_adv_skeleton_controls(ctrl)
# 提示用户
result = cmds.confirmDialog(
title='AdvanceSkeleton Detected',
message='Detected AdvanceSkeleton control with IKFK_Seamless attribute.\n\nSwitch Control: {}\nSwitch Attribute: IKFK_Seamless\n\nPlease verify and load remaining controls manually if needed.'.format(ctrl),
button=['Build Now', 'Cancel'],
defaultButton='Build Now',
cancelButton='Cancel',
dismissString='Cancel'
)
if result == 'Build Now':
self.build_seamless_switching()
else:
# 没有找到IKFK_Seamless属性
cmds.confirmDialog(
title='Not AdvanceSkeleton',
message='Selected control does not have IKFK_Seamless attribute.\n\nThis ADV Build feature is designed for AdvanceSkeleton rigs.\n\nPlease select an AdvanceSkeleton FK or IK control.',
button=['OK'],
defaultButton='OK'
)
def _auto_detect_adv_skeleton_controls(self, ctrl):
"""自动检测AdvanceSkeleton控制器"""
# 获取命名空间和控制器名称
namespace = ''
ctrl_name = ctrl
if ':' in ctrl:
parts = ctrl.split(':')
namespace = ':'.join(parts[:-1]) + ':'
ctrl_name = parts[-1]
print("Auto-detecting AdvanceSkeleton controls for: {}".format(ctrl_name))
# 确定是左侧还是右侧
side = ''
if ctrl_name.endswith('_L'):
side = '_L'
elif ctrl_name.endswith('_R'):
side = '_R'
# 检测手臂控制器
if 'Arm' in ctrl_name:
self._load_adv_arm_controls(namespace, side)
# 检测腿部控制器
elif 'Leg' in ctrl_name:
self._load_adv_leg_controls(namespace, side)
else:
print("Unknown limb type, please load controls manually")
def _load_adv_arm_controls(self, namespace, side):
"""加载AdvanceSkeleton手臂控制器"""
print("Loading AdvanceSkeleton Arm controls for side: {}".format(side))
# AdvanceSkeleton手臂命名规范
# 关节
shoulder_jnt = "{}FKShoulder{}".format(namespace, side)
elbow_jnt = "{}FKElbow{}".format(namespace, side)
wrist_jnt = "{}FKWrist{}".format(namespace, side)
# FK控制器
fk_shoulder = "{}FKShoulder{}".format(namespace, side)
fk_elbow = "{}FKElbow{}".format(namespace, side)
fk_wrist = "{}FKWrist{}".format(namespace, side)
# IK控制器
ik_wrist = "{}IKArm{}".format(namespace, side)
ik_pole = "{}PoleArm{}".format(namespace, side)
# 尝试加载关节
self._try_load_control(shoulder_jnt, 'joint', 'shoulder')
self._try_load_control(elbow_jnt, 'joint', 'elbow')
self._try_load_control(wrist_jnt, 'joint', 'wrist')
# 尝试加载FK控制器
self._try_load_control(fk_shoulder, 'fk', 'shoulder')
self._try_load_control(fk_elbow, 'fk', 'elbow')
self._try_load_control(fk_wrist, 'fk', 'wrist')
# 尝试加载IK控制器
self._try_load_control(ik_wrist, 'ik', 'wrist')
self._try_load_control(ik_pole, 'ik', 'pole')
def _load_adv_leg_controls(self, namespace, side):
"""加载AdvanceSkeleton腿部控制器"""
print("Loading AdvanceSkeleton Leg controls for side: {}".format(side))
# AdvanceSkeleton腿部命名规范
# 关节
hip_jnt = "{}FKHip{}".format(namespace, side)
knee_jnt = "{}FKKnee{}".format(namespace, side)
ankle_jnt = "{}FKAnkle{}".format(namespace, side)
# FK控制器
fk_hip = "{}FKHip{}".format(namespace, side)
fk_knee = "{}FKKnee{}".format(namespace, side)
fk_ankle = "{}FKAnkle{}".format(namespace, side)
# IK控制器
ik_ankle = "{}IKLeg{}".format(namespace, side)
ik_pole = "{}PoleLeg{}".format(namespace, side)
# 尝试加载关节
self._try_load_control(hip_jnt, 'joint', 'shoulder') # 使用shoulder位置存储hip
self._try_load_control(knee_jnt, 'joint', 'elbow') # 使用elbow位置存储knee
self._try_load_control(ankle_jnt, 'joint', 'wrist') # 使用wrist位置存储ankle
# 尝试加载FK控制器
self._try_load_control(fk_hip, 'fk', 'shoulder')
self._try_load_control(fk_knee, 'fk', 'elbow')
self._try_load_control(fk_ankle, 'fk', 'wrist')
# 尝试加载IK控制器
self._try_load_control(ik_ankle, 'ik', 'wrist')
self._try_load_control(ik_pole, 'ik', 'pole')
def _try_load_control(self, ctrl_name, ctrl_type, position):
"""尝试加载控制器到对应位置"""
if cmds.objExists(ctrl_name):
if ctrl_type == 'joint':
self.joints[position] = ctrl_name
field_name = 'joint_{}_field'.format(position)
if field_name in self.ui_elements:
cmds.textField(self.ui_elements[field_name], edit=True, text=ctrl_name)
print(" Loaded Joint {}: {}".format(position, ctrl_name))
elif ctrl_type == 'fk':
self.fk_ctrls[position] = ctrl_name
field_name = 'fk_{}_field'.format(position)
if field_name in self.ui_elements:
cmds.textField(self.ui_elements[field_name], edit=True, text=ctrl_name)
print(" Loaded FK {}: {}".format(position, ctrl_name))
elif ctrl_type == 'ik':
self.ik_ctrls[position] = ctrl_name
field_name = 'ik_{}_field'.format(position)
if field_name in self.ui_elements:
cmds.textField(self.ui_elements[field_name], edit=True, text=ctrl_name)
print(" Loaded IK {}: {}".format(position, ctrl_name))
else:
print(" Warning: Control not found: {}".format(ctrl_name))
def empty_fields(self, *args):
"""清空所有字段"""
# 清空关节字段
for key in self.joints.keys():
self.joints[key] = None
# 清空FK控制器字段
for key in self.fk_ctrls.keys():
self.fk_ctrls[key] = None
# 清空IK控制器字段
for key in self.ik_ctrls.keys():
self.ik_ctrls[key] = None
# 清空切换控制器
self.switch_ctrl = None
self.switch_attr = None
# 清空UI文本框
for field_name, field_widget in self.ui_elements.items():
if cmds.textField(field_widget, exists=True):
cmds.textField(field_widget, edit=True, text='')
# 清除scriptJob
self.clear_script_jobs()
print("All fields cleared")
cmds.inViewMessage(
amg='<hl>All fields cleared!</hl>',
pos='midCenter',
fade=True,
fadeStayTime=1000,
fadeOutTime=500
)
def show_help(self, *args):
"""显示帮助信息"""
help_text = """
IKFK Seamless Switching Tool - Help
使用步骤:
1. 加载关节 (Load Joint)
- 依次选择并加载肩部、肘部、腕部关节
2. 加载FK控制器 (Load FK Ctrl)
- 依次选择并加载FK肩部、肘部、腕部控制器
3. 加载IK控制器 (Load IK Ctrl)
- 选择并加载IK腕部控制器和极向量控制器
4. 加载切换控制器 (Load Switch Ctrl)
- 选择切换控制器
- 在通道盒中选择切换属性后点击加载
5. 构建无缝切换
- 点击 "Build Seamless Switching" 按钮
- 系统将创建scriptJob监听切换属性
- 当切换属性改变时自动匹配IK/FK位置
注意:
- scriptJob会在场景关闭时自动清除
- 切换属性值: 0=FK, 1=IK
"""
result = cmds.confirmDialog(
title='Help',
message=help_text,
button=['OK'],
defaultButton='OK'
)
def close_window(self):
"""关闭窗口并清理"""
self.clear_script_jobs()
if cmds.window(self.window_name, exists=True):
cmds.deleteUI(self.window_name)
# 全局实例
_ikfk_switcher_instance = None
def show():
"""显示IKFK切换工具窗口"""
global _ikfk_switcher_instance
# 如果实例存在,先清理
if _ikfk_switcher_instance:
_ikfk_switcher_instance.close_window()
# 创建新实例
_ikfk_switcher_instance = IKFKSwitcher()
_ikfk_switcher_instance.create_ui()
return _ikfk_switcher_instance
def start():
"""启动函数(兼容性别名)"""
return show()
if __name__ == "__main__":
show()