#!/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='Seamless switching built successfully!', 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='All fields cleared!', 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()