From 719058ff0197fddd8286cc439f8c34c27fae4f7f Mon Sep 17 00:00:00 2001 From: jeffreytsai1004 Date: Mon, 24 Nov 2025 22:26:56 +0800 Subject: [PATCH] Update --- .../scripts/animation_tools/ik_fk_switcher.py | 23 +++++ .../animation_tools/studiolibrary/README.md | 34 ++++++- .../animation_tools/studiolibrary/__init__.py | 89 +++++++++++++++++++ .../animation_tools/studiolibrary/launcher.py | 33 +++++-- .../animation_tools/test_ik_fk_switcher.py | 71 +++++++++++++++ 2023/scripts/rigging_tools/skin_api/README.md | 10 ++- .../rigging_tools/skin_api/Skinning.py | 74 ++++++++++----- 2023/scripts/rigging_tools/skin_api/Utils.py | 9 +- 8 files changed, 310 insertions(+), 33 deletions(-) create mode 100644 2023/scripts/animation_tools/studiolibrary/__init__.py create mode 100644 2023/scripts/animation_tools/test_ik_fk_switcher.py diff --git a/2023/scripts/animation_tools/ik_fk_switcher.py b/2023/scripts/animation_tools/ik_fk_switcher.py index 3f4c939..c60ebc0 100644 --- a/2023/scripts/animation_tools/ik_fk_switcher.py +++ b/2023/scripts/animation_tools/ik_fk_switcher.py @@ -1619,5 +1619,28 @@ def extraOptions(): cmds.showWindow("Extra_Options") + +# 导出主要函数,使其可以从外部调用 +__all__ = [ + 'fk_to_ik', + 'ik_to_fk', + 'delete_setup', + 'documentation', + 'extraOptions', + 'user_interface', +] + + +def show(): + """ + 便捷函数:启动 IK/FK Switcher UI + + 使用方法: + import animation_tools.ik_fk_switcher as ikfk + ikfk.show() + """ + user_interface() + + if __name__ == "__main__": user_interface() \ No newline at end of file diff --git a/2023/scripts/animation_tools/studiolibrary/README.md b/2023/scripts/animation_tools/studiolibrary/README.md index 805d028..2d340a0 100644 --- a/2023/scripts/animation_tools/studiolibrary/README.md +++ b/2023/scripts/animation_tools/studiolibrary/README.md @@ -25,20 +25,42 @@ Studio Library 2.20.2 ## 使用方法 -### 在 Python 中启动 +### 方式 1:使用 launcher(推荐) ```python +from animation_tools.studiolibrary import launcher +launcher.LaunchStudioLibrary() +``` + +### 方式 2:直接导入 + +```python +# 需要先添加路径到 sys.path import sys import os - studiolibrary_path = r'h:\Workspace\Raw\Tools\Plugins\Maya\2023\scripts\animation_tools\studiolibrary' if studiolibrary_path not in sys.path: sys.path.insert(0, studiolibrary_path) +# 然后导入并启动 import studiolibrary studiolibrary.main() ``` +### 方式 3:从 animation_tools 导入 + +```python +# 如果 animation_tools 已在 PYTHONPATH 中 +from animation_tools import studiolibrary +studiolibrary.main() +``` + +### 在 MEL 中启动 + +```mel +python("from animation_tools.studiolibrary import launcher; launcher.LaunchStudioLibrary()"); +``` + ### 从工具架启动 点击动画工具架上的 **StudioLib** 按钮即可启动。 @@ -62,9 +84,15 @@ studiolibrary.main() ## 🔧 版本兼容性 ### 支持的 Maya 版本 -- **Maya 2017+** - 支持所有现代版本的 Maya +- **Maya 2017-2024** - 使用 PySide2 +- **Maya 2025+** - 使用 PySide6 - **自动适配** - 运行时自动检测 Maya 环境和 Qt 版本 +### Maya 2025 特别说明 +- ✅ 已创建外层 `__init__.py` 文件,解决模块导入问题 +- ✅ 自动检测并使用 PySide6 +- ✅ 完全兼容新版本的 Python 3.11 + ### Qt 兼容性 Studio Library 使用 `Qt.py` 兼容层,支持多种 Qt 绑定: - **PySide6** - Maya 2025+ (优先) diff --git a/2023/scripts/animation_tools/studiolibrary/__init__.py b/2023/scripts/animation_tools/studiolibrary/__init__.py new file mode 100644 index 0000000..25202c7 --- /dev/null +++ b/2023/scripts/animation_tools/studiolibrary/__init__.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Studio Library Wrapper Module +用于简化 Studio Library 的导入和使用 +支持 Maya 2017-2026+ 所有版本 +""" + +import sys +import os + +# 获取当前目录 +_current_dir = os.path.dirname(os.path.abspath(__file__)) + +# 确保所有子模块路径都在 sys.path 中 +_required_paths = [ + _current_dir, + os.path.join(_current_dir, 'studiolibrary'), + os.path.join(_current_dir, 'studiolibrarymaya'), + os.path.join(_current_dir, 'mutils'), + os.path.join(_current_dir, 'studioqt'), + os.path.join(_current_dir, 'studiovendor'), +] + +for _path in _required_paths: + if _path not in sys.path: + sys.path.insert(0, _path) + +# 从内层 studiolibrary 模块导入所有功能 +try: + from studiolibrary import ( + __version__, + version, + config, + resource, + Library, + LibraryItem, + main, + ) + + # 导入工具函数 + from studiolibrary.utils import * + + # 导出所有公共接口 + __all__ = [ + '__version__', + 'version', + 'config', + 'resource', + 'Library', + 'LibraryItem', + 'main', + ] + +except ImportError as e: + import traceback + print("Failed to import studiolibrary:") + print(traceback.format_exc()) + raise + + +def show(*args, **kwargs): + """ + 便捷函数:启动 Studio Library + + Args: + *args: 传递给 main() 的位置参数 + **kwargs: 传递给 main() 的关键字参数 + + Returns: + LibraryWindow: Studio Library 窗口实例 + """ + return main(*args, **kwargs) + + +def isMaya(): + """ + 检查是否在 Maya 环境中运行 + + Returns: + bool: 如果在 Maya 中返回 True,否则返回 False + """ + try: + import maya.cmds + maya.cmds.about(batch=True) + return True + except ImportError: + return False diff --git a/2023/scripts/animation_tools/studiolibrary/launcher.py b/2023/scripts/animation_tools/studiolibrary/launcher.py index a1a8289..a4eeb22 100644 --- a/2023/scripts/animation_tools/studiolibrary/launcher.py +++ b/2023/scripts/animation_tools/studiolibrary/launcher.py @@ -4,7 +4,7 @@ """ Studio Library Launcher 用于从工具架快速启动 Studio Library -支持所有 Maya 版本 +支持所有 Maya 版本(2017-2026+) """ import sys @@ -15,6 +15,9 @@ def LaunchStudioLibrary(): """ 启动 Studio Library 主界面 自动检测 Maya 版本和 Qt 绑定 + + Returns: + LibraryWindow: Studio Library 窗口实例,失败时返回 None """ try: # 获取 Studio Library 路径 @@ -24,17 +27,28 @@ def LaunchStudioLibrary(): if current_dir not in sys.path: sys.path.insert(0, current_dir) - # 导入并启动 Studio Library - import studiolibrary + # 确保子模块路径也在 sys.path 中 + studiolibrary_subdir = os.path.join(current_dir, 'studiolibrary') + if studiolibrary_subdir not in sys.path: + sys.path.insert(0, studiolibrary_subdir) + + # 导入 Studio Library + try: + # 方式1:直接从外层包导入 + import studiolibrary + except ImportError: + # 方式2:从子目录导入 + sys.path.insert(0, studiolibrary_subdir) + import studiolibrary # 打印版本信息 print(f"Studio Library version: {studiolibrary.version()}") # 检测 Maya 环境 if studiolibrary.isMaya(): - print("Running in Maya environment") + print("Studio Library: Running in Maya environment") else: - print("Running in standalone mode") + print("Studio Library: Running in standalone mode") # 启动主窗口 window = studiolibrary.main() @@ -47,6 +61,15 @@ def LaunchStudioLibrary(): print(f"Failed to launch Studio Library: {e}") import traceback traceback.print_exc() + + # 提供详细的调试信息 + print("\n=== Debug Information ===") + print(f"Current directory: {os.path.dirname(os.path.abspath(__file__))}") + print(f"sys.path entries:") + for i, path in enumerate(sys.path[:10]): + print(f" [{i}] {path}") + print("=========================\n") + return None diff --git a/2023/scripts/animation_tools/test_ik_fk_switcher.py b/2023/scripts/animation_tools/test_ik_fk_switcher.py new file mode 100644 index 0000000..dd94a68 --- /dev/null +++ b/2023/scripts/animation_tools/test_ik_fk_switcher.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +IK/FK Switcher 测试脚本 +在 Maya 中运行此脚本来测试模块是否正常工作 +""" + +def test_ik_fk_switcher(): + """测试 ik_fk_switcher 模块的导入和函数""" + print("=" * 60) + print("IK/FK Switcher 测试") + print("=" * 60) + + try: + # 测试导入 + print("\n[1/5] 测试导入模块...") + import animation_tools.ik_fk_switcher as ikfk + print("✓ 成功导入 ik_fk_switcher") + + # 测试 __all__ 导出 + print("\n[2/5] 检查 __all__ 导出列表...") + if hasattr(ikfk, '__all__'): + print(f"✓ __all__ = {ikfk.__all__}") + else: + print("✗ 没有找到 __all__") + + # 测试主要函数 + print("\n[3/5] 检查主要函数...") + functions = ['fk_to_ik', 'ik_to_fk', 'delete_setup', 'documentation', 'extraOptions', 'user_interface', 'show'] + for func_name in functions: + if hasattr(ikfk, func_name): + func = getattr(ikfk, func_name) + print(f"✓ {func_name}: {type(func)}") + else: + print(f"✗ {func_name}: 未找到") + + # 测试 show() 函数 + print("\n[4/5] 测试 show() 函数...") + if hasattr(ikfk, 'show') and callable(ikfk.show): + print("✓ show() 函数可调用") + print(f" 文档: {ikfk.show.__doc__}") + else: + print("✗ show() 函数不可用") + + # 测试 dir() + print("\n[5/5] 列出所有可用属性...") + all_attrs = [attr for attr in dir(ikfk) if not attr.startswith('_')] + print(f"✓ 共有 {len(all_attrs)} 个公共属性/函数") + print(f" 主要函数: {[attr for attr in all_attrs if callable(getattr(ikfk, attr))][:10]}") + + print("\n" + "=" * 60) + print("所有测试通过!") + print("=" * 60) + print("\n使用方法:") + print(" import animation_tools.ik_fk_switcher as ikfk") + print(" ikfk.show() # 启动 UI") + print("=" * 60) + + return True + + except Exception as e: + print(f"\n✗ 测试失败: {e}") + import traceback + traceback.print_exc() + return False + + +if __name__ == "__main__": + # 在 Maya 中运行 + test_ik_fk_switcher() diff --git a/2023/scripts/rigging_tools/skin_api/README.md b/2023/scripts/rigging_tools/skin_api/README.md index 4b313a9..c76aef7 100644 --- a/2023/scripts/rigging_tools/skin_api/README.md +++ b/2023/scripts/rigging_tools/skin_api/README.md @@ -71,7 +71,8 @@ api.importSkinWeights(selected=False, stripJointNamespaces=False, addNewToHierar ## 🔧 版本兼容性 ### 支持的 Maya 版本 -- **所有 Maya 版本** - 从 Maya 2016 到最新版本 +- **所有 Maya 版本** - 从 Maya 2016 到 Maya 2025+ +- **Maya 2025** - 完全兼容,修复了 PyMEL 相关问题 ### API 兼容性 模块采用双重 API 支持策略: @@ -84,6 +85,8 @@ api.importSkinWeights(selected=False, stripJointNamespaces=False, addNewToHierar 2. **优雅降级** - PyMEL 不可用时自动使用 cmds 3. **相对导入** - 支持作为包导入或独立模块使用 4. **异常处理** - 完善的错误处理和用户提示 +5. **空值安全** - 处理节点无父节点等边界情况 +6. **Maya 2025 优化** - 修复 PyMEL 在新版本中的兼容性问题 ## 📝 文件格式 @@ -121,6 +124,11 @@ api.importSkinWeights(selected=False, stripJointNamespaces=False, addNewToHierar - 大量物体操作时会显示进度条 - 权重文件使用 pickle 格式,不同 Python 版本间可能存在兼容性问题 +### Maya 2025 特别说明 +- 已修复 PyMEL 在处理无父节点骨骼时的 `'NoneType' object has no attribute 'name'` 错误 +- 增强了所有 PyMEL 对象的空值检查 +- 建议使用 `saveJointInfo=True` 导出完整的骨骼信息 + ## 🐛 故障排除 ### 导入失败 diff --git a/2023/scripts/rigging_tools/skin_api/Skinning.py b/2023/scripts/rigging_tools/skin_api/Skinning.py index 43d523b..9f1fed7 100644 --- a/2023/scripts/rigging_tools/skin_api/Skinning.py +++ b/2023/scripts/rigging_tools/skin_api/Skinning.py @@ -111,26 +111,42 @@ def getSkinClusterInfo(objectName, saveJointInfo=False): return False def getSkinJointInformation(influences): + """ + 获取骨骼信息(父节点、矩阵、旋转、关节方向) + 兼容 PyMEL 和 cmds,处理无父节点的情况 + """ jointInformation = {} for inf in influences: jointInfo = {} - if pm: - infNode = pm.PyNode(inf) - jointInfo["parent"] = str(infNode.getParent().name()) - jointInfo["matrix"] = infNode.getMatrix(worldSpace=True) - jointInfo["rotation"] = infNode.getRotation() - jointInfo["jointOrient"] = infNode.getAttr("jointOrient") - jointInformation[str(infNode)] = copy.deepcopy(jointInfo) - else: - # cmds 版本 - infName = str(inf) - parents = cmds.listRelatives(infName, parent=True) - jointInfo["parent"] = parents[0] if parents else "" - jointInfo["matrix"] = cmds.xform(infName, q=True, matrix=True, worldSpace=True) - jointInfo["rotation"] = cmds.xform(infName, q=True, rotation=True) - jointInfo["jointOrient"] = cmds.getAttr(infName + ".jointOrient")[0] - jointInformation[infName] = copy.deepcopy(jointInfo) + try: + if pm: + infNode = pm.PyNode(inf) + # 安全获取父节点,避免 None.name() 错误 + parent = infNode.getParent() + jointInfo["parent"] = str(parent.name()) if parent else "" + jointInfo["matrix"] = infNode.getMatrix(worldSpace=True) + jointInfo["rotation"] = infNode.getRotation() + jointInfo["jointOrient"] = infNode.getAttr("jointOrient") + jointInformation[str(infNode)] = copy.deepcopy(jointInfo) + else: + # cmds 版本 + infName = str(inf) + parents = cmds.listRelatives(infName, parent=True) + jointInfo["parent"] = parents[0] if parents else "" + jointInfo["matrix"] = cmds.xform(infName, q=True, matrix=True, worldSpace=True) + jointInfo["rotation"] = cmds.xform(infName, q=True, rotation=True) + jointInfo["jointOrient"] = cmds.getAttr(infName + ".jointOrient")[0] + jointInformation[infName] = copy.deepcopy(jointInfo) + except Exception as e: + print(f"Warning: Failed to get joint information for {inf}: {e}") + # 使用默认值 + jointInfo["parent"] = "" + jointInfo["matrix"] = [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] + jointInfo["rotation"] = [0, 0, 0] + jointInfo["jointOrient"] = [0, 0, 0] + jointInformation[str(inf)] = copy.deepcopy(jointInfo) + return jointInformation def getMPlugObjects(MFnSkinCluster): @@ -350,10 +366,16 @@ def buildSkinWeightsDict(objectList, showLoadingBar=True, saveJointInfo=False): sourceWeightDict = {} for object in objectList: - if pm: - objectAsString = pm.PyNode(object).name() - else: - # cmds 版本 - object 已经是字符串 + try: + if pm: + # 安全转换为字符串,处理可能的 None 或无效对象 + obj_node = pm.PyNode(object) if not isinstance(object, pm.PyNode) else object + objectAsString = str(obj_node.name()) if obj_node else str(object) + else: + # cmds 版本 - object 已经是字符串 + objectAsString = str(object) + except Exception as e: + print(f"Warning: Failed to process object {object}: {e}") objectAsString = str(object) if showLoadingBar: @@ -394,7 +416,11 @@ def transferSkinWeights(transferNodes=None, showLoadingBar=True): if len(transferNodes): sourceObj = transferNodes[0] - sourceName = sourceObj.name() + # 安全获取名称 + try: + sourceName = str(sourceObj.name()) if hasattr(sourceObj, 'name') else str(sourceObj) + except: + sourceName = str(sourceObj) targetNameList = transferNodes[1:] loadBarMaxVal = len(targetNameList) @@ -410,7 +436,11 @@ def transferSkinWeights(transferNodes=None, showLoadingBar=True): # deep copy because: Mutable datatypes sourceWeightDictCopy = copy.deepcopy(sourceWeightDict) - targetName = str(tgtObject.name()) + # 安全获取名称 + try: + targetName = str(tgtObject.name()) if hasattr(tgtObject, 'name') else str(tgtObject) + except: + targetName = str(tgtObject) barycentrWeightDict = apiUtils.getBarycentricWeights(sourceName, targetName) diff --git a/2023/scripts/rigging_tools/skin_api/Utils.py b/2023/scripts/rigging_tools/skin_api/Utils.py index 2494ce0..185b4fe 100644 --- a/2023/scripts/rigging_tools/skin_api/Utils.py +++ b/2023/scripts/rigging_tools/skin_api/Utils.py @@ -399,8 +399,13 @@ def matchDictionaryToSceneMeshes(weightDictionary, selected=False): if vtxCountMatch(sceneNode, weightDictionary[dictNodeName]["vtxCount"]): # found match on both name and vtxcount, copy info to the local sceneWeightDict if pm: - sceneWeightDict[sceneNode.name()] = weightDictionary[dictNodeName] - validNodeList.append(sceneNode.name()) + # 安全获取节点名称 + try: + node_name = str(sceneNode.name()) if hasattr(sceneNode, 'name') else str(sceneNode) + except: + node_name = str(sceneNode) + sceneWeightDict[node_name] = weightDictionary[dictNodeName] + validNodeList.append(node_name) else: sceneWeightDict[sceneNode] = weightDictionary[dictNodeName] validNodeList.append(sceneNode)