This commit is contained in:
2026-01-22 00:06:13 +08:00
parent f26fc95ea3
commit ed7476e54b
316 changed files with 4962 additions and 14039 deletions

View File

@@ -0,0 +1,737 @@
# -*- coding: utf-8 -*-
"""
StandardizeSkeleton.py
标准化骨骼工具 - 用于从绑定文件中提取干净的骨骼层级
功能:
1. 识别所有有蒙皮权重的骨骼
2. 复制这些骨骼并保持原有层级关系
3. 移除控制器和其他非骨骼节点
4. 批量添加/剔除骨骼前缀后缀
5. 创建干净的骨骼树,便于导出到游戏引擎
作者: Kilo Code
日期: 2025
"""
import maya.cmds as cmds
import maya.mel as mel
from functools import partial
try:
from PySide2 import QtWidgets, QtCore, QtGui
from shiboken2 import wrapInstance
except ImportError:
from PySide6 import QtWidgets, QtCore, QtGui
from shiboken6 import wrapInstance
import maya.OpenMayaUI as omui
def get_maya_main_window():
"""获取Maya主窗口"""
main_window_ptr = omui.MQtUtil.mainWindow()
return wrapInstance(int(main_window_ptr), QtWidgets.QWidget)
class StandardizeSkeletonUI(QtWidgets.QDialog):
"""标准化骨骼工具UI"""
def __init__(self, parent=get_maya_main_window()):
super(StandardizeSkeletonUI, self).__init__(parent)
self.setWindowTitle("标准化骨骼工具")
self.setMinimumWidth(500)
self.setMinimumHeight(600)
self.setWindowFlags(self.windowFlags() ^ QtCore.Qt.WindowContextHelpButtonHint)
self.geometry_list = [] # 存储几何体列表
self.create_widgets()
self.create_layouts()
self.create_connections()
def create_widgets(self):
"""创建UI控件"""
# 几何体列表区域
self.geo_group = QtWidgets.QGroupBox("几何体列表")
self.geo_list_widget = QtWidgets.QListWidget()
self.geo_list_widget.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
self.add_selected_btn = QtWidgets.QPushButton("添加选中对象")
self.add_selected_btn.setToolTip("选择对象后点击,会自动过滤出几何体")
self.remove_selected_btn = QtWidgets.QPushButton("移除选中项")
self.clear_list_btn = QtWidgets.QPushButton("清空列表")
# 骨骼信息区域
self.info_group = QtWidgets.QGroupBox("骨骼信息")
self.info_text = QtWidgets.QTextEdit()
self.info_text.setReadOnly(True)
self.info_text.setMaximumHeight(150)
self.analyze_btn = QtWidgets.QPushButton("分析权重骨骼")
self.analyze_btn.setStyleSheet("background-color: #4a90e2; color: white; font-weight: bold;")
# 导出选项区域
self.export_group = QtWidgets.QGroupBox("导出选项")
self.export_name_label = QtWidgets.QLabel("导出组名称:")
self.export_name_edit = QtWidgets.QLineEdit("export_skeleton_grp")
self.prefix_label = QtWidgets.QLabel("添加前缀:")
self.prefix_edit = QtWidgets.QLineEdit()
self.prefix_edit.setPlaceholderText("例如: SK_")
self.suffix_label = QtWidgets.QLabel("添加后缀:")
self.suffix_edit = QtWidgets.QLineEdit()
self.suffix_edit.setPlaceholderText("例如: _jnt")
self.remove_prefix_label = QtWidgets.QLabel("移除前缀:")
self.remove_prefix_edit = QtWidgets.QLineEdit()
self.remove_prefix_edit.setPlaceholderText("例如: L_, R_, M_")
self.remove_suffix_label = QtWidgets.QLabel("移除后缀:")
self.remove_suffix_edit = QtWidgets.QLineEdit()
self.remove_suffix_edit.setPlaceholderText("例如: _joint, _jnt")
self.exclude_keywords_label = QtWidgets.QLabel("排除关键词:")
self.exclude_keywords_edit = QtWidgets.QLineEdit()
self.exclude_keywords_edit.setPlaceholderText("例如: hair, lashe, zip, wrinkle")
self.exclude_keywords_edit.setToolTip("包含这些关键词的骨骼将被排除(逗号分隔,不区分大小写)")
self.root_joint_label = QtWidgets.QLabel("根骨骼限制:")
self.root_joint_edit = QtWidgets.QLineEdit()
self.root_joint_edit.setPlaceholderText("例如: rig|root_joint (留空=不限制)")
self.root_joint_edit.setToolTip("只导出此骨骼下的子骨骼(留空则导出所有有权重的骨骼)")
self.keep_hierarchy_cb = QtWidgets.QCheckBox("保持原始层级关系")
self.keep_hierarchy_cb.setChecked(True)
self.keep_hierarchy_cb.setToolTip("保持骨骼的父子层级关系")
self.freeze_transforms_cb = QtWidgets.QCheckBox("冻结变换")
self.freeze_transforms_cb.setChecked(False)
self.freeze_transforms_cb.setToolTip("冻结导出骨骼的变换属性")
self.delete_constraints_cb = QtWidgets.QCheckBox("删除所有约束")
self.delete_constraints_cb.setChecked(True)
self.delete_constraints_cb.setToolTip("删除复制骨骼上的所有约束节点")
# 执行按钮
self.create_skeleton_btn = QtWidgets.QPushButton("创建导出骨骼")
self.create_skeleton_btn.setStyleSheet("background-color: #5cb85c; color: white; font-weight: bold; padding: 10px;")
self.create_skeleton_btn.setMinimumHeight(40)
self.select_export_btn = QtWidgets.QPushButton("选择导出组")
self.delete_export_btn = QtWidgets.QPushButton("删除导出组")
# 帮助文本
self.help_text = QtWidgets.QLabel(
"使用说明:\n"
"1. 点击'添加选中对象'添加包含几何体的对象\n"
"2. 点击'分析权重骨骼'查看将要导出的骨骼信息\n"
"3. 设置导出选项(可选)\n"
"4. 点击'创建导出骨骼'生成干净的骨骼层级"
)
self.help_text.setStyleSheet("color: #666; padding: 10px; background-color: #f5f5f5; border-radius: 5px;")
self.help_text.setWordWrap(True)
def create_layouts(self):
"""创建布局"""
main_layout = QtWidgets.QVBoxLayout(self)
# 帮助文本
main_layout.addWidget(self.help_text)
# 几何体列表区域
geo_layout = QtWidgets.QVBoxLayout()
geo_layout.addWidget(self.geo_list_widget)
geo_btn_layout = QtWidgets.QHBoxLayout()
geo_btn_layout.addWidget(self.add_selected_btn)
geo_btn_layout.addWidget(self.remove_selected_btn)
geo_btn_layout.addWidget(self.clear_list_btn)
geo_layout.addLayout(geo_btn_layout)
self.geo_group.setLayout(geo_layout)
main_layout.addWidget(self.geo_group)
# 分析按钮
main_layout.addWidget(self.analyze_btn)
# 骨骼信息区域
info_layout = QtWidgets.QVBoxLayout()
info_layout.addWidget(self.info_text)
self.info_group.setLayout(info_layout)
main_layout.addWidget(self.info_group)
# 导出选项区域
export_layout = QtWidgets.QGridLayout()
row = 0
export_layout.addWidget(self.export_name_label, row, 0)
export_layout.addWidget(self.export_name_edit, row, 1)
row += 1
export_layout.addWidget(self.prefix_label, row, 0)
export_layout.addWidget(self.prefix_edit, row, 1)
row += 1
export_layout.addWidget(self.suffix_label, row, 0)
export_layout.addWidget(self.suffix_edit, row, 1)
row += 1
export_layout.addWidget(self.remove_prefix_label, row, 0)
export_layout.addWidget(self.remove_prefix_edit, row, 1)
row += 1
export_layout.addWidget(self.remove_suffix_label, row, 0)
export_layout.addWidget(self.remove_suffix_edit, row, 1)
row += 1
export_layout.addWidget(self.exclude_keywords_label, row, 0)
export_layout.addWidget(self.exclude_keywords_edit, row, 1)
row += 1
export_layout.addWidget(self.root_joint_label, row, 0)
export_layout.addWidget(self.root_joint_edit, row, 1)
row += 1
export_layout.addWidget(self.keep_hierarchy_cb, row, 0, 1, 2)
row += 1
export_layout.addWidget(self.freeze_transforms_cb, row, 0, 1, 2)
row += 1
export_layout.addWidget(self.delete_constraints_cb, row, 0, 1, 2)
self.export_group.setLayout(export_layout)
main_layout.addWidget(self.export_group)
# 执行按钮
main_layout.addWidget(self.create_skeleton_btn)
# 底部按钮
bottom_btn_layout = QtWidgets.QHBoxLayout()
bottom_btn_layout.addWidget(self.select_export_btn)
bottom_btn_layout.addWidget(self.delete_export_btn)
main_layout.addLayout(bottom_btn_layout)
def create_connections(self):
"""创建信号连接"""
self.add_selected_btn.clicked.connect(self.add_selected_geometry)
self.remove_selected_btn.clicked.connect(self.remove_selected_items)
self.clear_list_btn.clicked.connect(self.clear_geometry_list)
self.analyze_btn.clicked.connect(self.analyze_weighted_joints)
self.create_skeleton_btn.clicked.connect(self.create_export_skeleton)
self.select_export_btn.clicked.connect(self.select_export_group)
self.delete_export_btn.clicked.connect(self.delete_export_group)
def add_selected_geometry(self):
"""添加选中的几何体"""
selection = cmds.ls(selection=True, long=True)
if not selection:
QtWidgets.QMessageBox.warning(self, "警告", "请先选择对象!")
return
added_count = 0
for obj in selection:
# 获取所有子节点中的mesh
meshes = self.get_meshes_from_object(obj)
for mesh in meshes:
# 获取transform节点
transform = cmds.listRelatives(mesh, parent=True, fullPath=True)[0]
if transform not in self.geometry_list:
self.geometry_list.append(transform)
# 显示短名称
short_name = transform.split('|')[-1]
self.geo_list_widget.addItem(short_name)
added_count += 1
if added_count > 0:
self.info_text.append(f"添加了 {added_count} 个几何体")
else:
QtWidgets.QMessageBox.information(self, "提示", "没有找到新的几何体或几何体已存在")
def get_meshes_from_object(self, obj):
"""从对象中获取所有mesh"""
meshes = []
# 检查对象本身是否是mesh
shapes = cmds.listRelatives(obj, shapes=True, fullPath=True, type="mesh") or []
meshes.extend(shapes)
# 检查所有子节点
descendants = cmds.listRelatives(obj, allDescendents=True, fullPath=True, type="mesh") or []
meshes.extend(descendants)
return list(set(meshes)) # 去重
def remove_selected_items(self):
"""移除选中的列表项"""
selected_items = self.geo_list_widget.selectedItems()
if not selected_items:
QtWidgets.QMessageBox.warning(self, "警告", "请先选择要移除的项!")
return
for item in selected_items:
row = self.geo_list_widget.row(item)
self.geo_list_widget.takeItem(row)
if row < len(self.geometry_list):
self.geometry_list.pop(row)
def clear_geometry_list(self):
"""清空几何体列表"""
self.geo_list_widget.clear()
self.geometry_list = []
self.info_text.append("已清空几何体列表")
def analyze_weighted_joints(self):
"""分析有权重的骨骼"""
if not self.geometry_list:
QtWidgets.QMessageBox.warning(self, "警告", "请先添加几何体!")
return
self.info_text.clear()
self.info_text.append("正在分析权重骨骼...\n")
try:
# 获取排除关键词
exclude_keywords_text = self.exclude_keywords_edit.text().strip()
exclude_keywords = [k.strip() for k in exclude_keywords_text.split(',') if k.strip()] if exclude_keywords_text else None
# 获取根骨骼限制
root_joint = self.root_joint_edit.text().strip() or None
if exclude_keywords:
self.info_text.append(f"排除关键词: {', '.join(exclude_keywords)}")
if root_joint:
if cmds.objExists(root_joint):
self.info_text.append(f"根骨骼限制: {root_joint}\n")
else:
self.info_text.append(f"警告: 根骨骼 '{root_joint}' 不存在,将忽略此限制\n")
root_joint = None
weighted_joints = self.get_weighted_joints(self.geometry_list, exclude_keywords, root_joint)
if not weighted_joints:
self.info_text.append("未找到有权重的骨骼!")
return
# 分析骨骼层级
root_joints = self.get_root_joints(weighted_joints)
self.info_text.append(f"找到 {len(weighted_joints)} 个有权重的骨骼")
self.info_text.append(f"根骨骼数量: {len(root_joints)}")
self.info_text.append(f"\n根骨骼列表:")
for root in root_joints:
short_name = root.split('|')[-1]
self.info_text.append(f" - {short_name}")
# 显示前20个骨骼
self.info_text.append(f"\n前20个权重骨骼:")
for i, joint in enumerate(weighted_joints[:20]):
short_name = joint.split('|')[-1]
self.info_text.append(f" {i+1}. {short_name}")
if len(weighted_joints) > 20:
self.info_text.append(f" ... 还有 {len(weighted_joints) - 20} 个骨骼")
except Exception as e:
self.info_text.append(f"\n错误: {str(e)}")
import traceback
self.info_text.append(traceback.format_exc())
def get_weighted_joints(self, geometry_list, exclude_keywords=None, root_joint=None):
"""获取所有有权重的骨骼"""
weighted_joints = set()
# 如果指定了根骨骼,获取其所有子骨骼
root_joint_children = None
if root_joint and cmds.objExists(root_joint):
root_joint_children = set()
all_joints = cmds.listRelatives(root_joint, allDescendents=True, type="joint", fullPath=True) or []
root_joint_children = set(all_joints)
root_joint_children.add(cmds.ls(root_joint, long=True)[0])
for geo in geometry_list:
# 获取mesh shape
shapes = cmds.listRelatives(geo, shapes=True, fullPath=True, type="mesh") or []
for shape in shapes:
# 查找skinCluster
history = cmds.listHistory(shape, pruneDagObjects=True) or []
# 过滤出skinCluster节点
skin_clusters = [node for node in history if cmds.nodeType(node) == "skinCluster"]
for skin_cluster in skin_clusters:
# 获取影响的骨骼
influences = cmds.skinCluster(skin_cluster, query=True, influence=True) or []
# 转换为长名称
for influence in influences:
long_name = cmds.ls(influence, long=True)[0]
if cmds.nodeType(long_name) == "joint":
# 检查是否在根骨骼下
if root_joint_children is not None and long_name not in root_joint_children:
continue
# 检查是否需要排除
if exclude_keywords:
short_name = long_name.split('|')[-1].lower()
should_exclude = False
for keyword in exclude_keywords:
if keyword.lower() in short_name:
should_exclude = True
break
if not should_exclude:
weighted_joints.add(long_name)
else:
weighted_joints.add(long_name)
return list(weighted_joints)
def get_root_joints(self, joint_list):
"""获取根骨骼在joint_list中没有父骨骼的骨骼"""
root_joints = []
for joint in joint_list:
# 获取父节点
parent = cmds.listRelatives(joint, parent=True, fullPath=True)
# 如果没有父节点或者父节点不是joint或者父节点不在列表中
if not parent:
root_joints.append(joint)
else:
parent_joint = parent[0]
if cmds.nodeType(parent_joint) != "joint" or parent_joint not in joint_list:
root_joints.append(joint)
return root_joints
def create_export_skeleton(self):
"""创建导出骨骼"""
if not self.geometry_list:
QtWidgets.QMessageBox.warning(self, "警告", "请先添加几何体!")
return
export_name = self.export_name_edit.text().strip()
if not export_name:
QtWidgets.QMessageBox.warning(self, "警告", "请输入导出组名称!")
return
# 确认对话框
reply = QtWidgets.QMessageBox.question(
self,
"确认",
f"将创建导出骨骼组: {export_name}\n是否继续?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if reply == QtWidgets.QMessageBox.No:
return
try:
self.info_text.clear()
self.info_text.append("开始创建导出骨骼...\n")
# 获取排除关键词
exclude_keywords_text = self.exclude_keywords_edit.text().strip()
exclude_keywords = [k.strip() for k in exclude_keywords_text.split(',') if k.strip()] if exclude_keywords_text else None
# 获取根骨骼限制
root_joint = self.root_joint_edit.text().strip() or None
if exclude_keywords:
self.info_text.append(f"排除关键词: {', '.join(exclude_keywords)}")
if root_joint:
if cmds.objExists(root_joint):
self.info_text.append(f"根骨骼限制: {root_joint}")
else:
self.info_text.append(f"警告: 根骨骼 '{root_joint}' 不存在,将忽略此限制")
root_joint = None
# 获取有权重的骨骼
weighted_joints = self.get_weighted_joints(self.geometry_list, exclude_keywords, root_joint)
if not weighted_joints:
QtWidgets.QMessageBox.warning(self, "警告", "未找到有权重的骨骼!")
return
self.info_text.append(f"找到 {len(weighted_joints)} 个有权重的骨骼")
# 删除已存在的导出组
if cmds.objExists(export_name):
cmds.delete(export_name)
self.info_text.append(f"删除已存在的 {export_name}")
# 创建导出组
export_group = cmds.group(empty=True, name=export_name)
self.info_text.append(f"创建导出组: {export_group}")
# 复制骨骼
if self.keep_hierarchy_cb.isChecked():
# 保持层级关系
duplicated_joints = self.duplicate_joints_with_hierarchy(
weighted_joints,
export_group
)
else:
# 不保持层级,所有骨骼平铺
duplicated_joints = self.duplicate_joints_flat(
weighted_joints,
export_group
)
self.info_text.append(f"复制了 {len(duplicated_joints)} 个骨骼")
# 删除约束
if self.delete_constraints_cb.isChecked() and duplicated_joints:
deleted_constraints = self.delete_all_constraints(duplicated_joints)
if deleted_constraints > 0:
self.info_text.append(f"删除了 {deleted_constraints} 个约束")
# 重命名骨骼
if duplicated_joints:
self.rename_joints(
duplicated_joints,
self.prefix_edit.text().strip(),
self.suffix_edit.text().strip(),
self.remove_prefix_edit.text().strip(),
self.remove_suffix_edit.text().strip()
)
# 冻结变换
if self.freeze_transforms_cb.isChecked() and duplicated_joints:
try:
cmds.makeIdentity(duplicated_joints, apply=True, translate=True, rotate=True, scale=True)
self.info_text.append("已冻结变换")
except:
self.info_text.append("冻结变换失败(某些骨骼可能有约束)")
# 选择导出组
cmds.select(export_group)
self.info_text.append(f"\n成功创建导出骨骼!")
self.info_text.append(f"导出组: {export_group}")
QtWidgets.QMessageBox.information(
self,
"成功",
f"成功创建导出骨骼!\n导出组: {export_group}\n骨骼数量: {len(duplicated_joints)}"
)
except Exception as e:
error_msg = f"创建导出骨骼失败: {str(e)}"
self.info_text.append(f"\n错误: {error_msg}")
import traceback
self.info_text.append(traceback.format_exc())
QtWidgets.QMessageBox.critical(self, "错误", error_msg)
def duplicate_joints_with_hierarchy(self, joint_list, parent_group):
"""复制骨骼并保持层级关系(只复制有权重的骨骼)"""
duplicated_joints = []
joint_mapping = {} # 原始骨骼 -> 复制骨骼的映射
# 第一步:复制所有骨骼
for joint in joint_list:
# 复制单个骨骼(不复制子节点)
dup_joint = cmds.duplicate(joint, parentOnly=True, returnRootsOnly=True)[0]
# 重命名去掉Maya自动添加的数字后缀
short_name = joint.split('|')[-1]
dup_joint = cmds.rename(dup_joint, short_name + "_export")
joint_mapping[joint] = dup_joint
duplicated_joints.append(dup_joint)
# 第二步:重建层级关系(跳过没有权重的中间骨骼)
for original_joint, dup_joint in joint_mapping.items():
# 查找最近的有权重的父骨骼
parent_joint = self.find_weighted_parent(original_joint, joint_list)
if parent_joint and parent_joint in joint_mapping:
# 如果找到有权重的父骨骼,建立父子关系
try:
cmds.parent(dup_joint, joint_mapping[parent_joint])
except:
pass
else:
# 如果没有找到,放到导出组下
try:
cmds.parent(dup_joint, parent_group)
except:
pass # 可能已经在导出组下了
return duplicated_joints
def find_weighted_parent(self, joint, weighted_joint_list):
"""查找最近的有权重的父骨骼"""
current = joint
while True:
# 获取父节点
parent = cmds.listRelatives(current, parent=True, fullPath=True)
if not parent:
# 没有父节点了
return None
parent_node = parent[0]
# 检查父节点是否是joint
if cmds.nodeType(parent_node) != "joint":
# 父节点不是骨骼
return None
# 检查父骨骼是否在权重骨骼列表中
if parent_node in weighted_joint_list:
return parent_node
# 继续向上查找
current = parent_node
def duplicate_joints_flat(self, joint_list, parent_group):
"""复制骨骼(平铺,不保持层级)"""
duplicated_joints = []
for joint in joint_list:
# 复制单个骨骼
dup_joint = cmds.duplicate(joint, parentOnly=True, returnRootsOnly=True)[0]
# 重命名
short_name = joint.split('|')[-1]
dup_joint = cmds.rename(dup_joint, short_name + "_export")
# 放到导出组下
cmds.parent(dup_joint, parent_group)
duplicated_joints.append(dup_joint)
return duplicated_joints
def delete_all_constraints(self, joint_list):
"""删除所有约束"""
deleted_count = 0
# 获取所有骨骼(包括子骨骼)
all_joints = []
for joint in joint_list:
all_joints.append(joint)
descendants = cmds.listRelatives(joint, allDescendents=True, type="joint", fullPath=True) or []
all_joints.extend(descendants)
all_joints = list(set(all_joints)) # 去重
# 删除约束
for joint in all_joints:
# 获取连接到这个骨骼的所有约束
constraints = cmds.listConnections(joint, type="constraint") or []
for constraint in set(constraints): # 去重
if cmds.objExists(constraint):
try:
cmds.delete(constraint)
deleted_count += 1
except:
pass
return deleted_count
def rename_joints(self, joint_list, add_prefix, add_suffix, remove_prefix, remove_suffix):
"""重命名骨骼"""
if not any([add_prefix, add_suffix, remove_prefix, remove_suffix]):
return
# 解析要移除的前缀和后缀(支持逗号分隔)
remove_prefixes = [p.strip() for p in remove_prefix.split(',') if p.strip()]
remove_suffixes = [s.strip() for s in remove_suffix.split(',') if s.strip()]
renamed_count = 0
for joint in joint_list:
old_name = joint.split('|')[-1]
new_name = old_name
# 移除后缀
for suffix in remove_suffixes:
if new_name.endswith(suffix):
new_name = new_name[:-len(suffix)]
# 移除前缀
for prefix in remove_prefixes:
if new_name.startswith(prefix):
new_name = new_name[len(prefix):]
# 添加前缀
if add_prefix:
new_name = add_prefix + new_name
# 添加后缀
if add_suffix:
new_name = new_name + add_suffix
# 重命名
if new_name != old_name:
try:
cmds.rename(joint, new_name)
renamed_count += 1
except:
pass
if renamed_count > 0:
self.info_text.append(f"重命名了 {renamed_count} 个骨骼")
def select_export_group(self):
"""选择导出组"""
export_name = self.export_name_edit.text().strip()
if cmds.objExists(export_name):
cmds.select(export_name)
self.info_text.append(f"已选择: {export_name}")
else:
QtWidgets.QMessageBox.warning(self, "警告", f"导出组 '{export_name}' 不存在!")
def delete_export_group(self):
"""删除导出组"""
export_name = self.export_name_edit.text().strip()
if not cmds.objExists(export_name):
QtWidgets.QMessageBox.warning(self, "警告", f"导出组 '{export_name}' 不存在!")
return
reply = QtWidgets.QMessageBox.question(
self,
"确认删除",
f"确定要删除导出组 '{export_name}' 吗?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if reply == QtWidgets.QMessageBox.Yes:
cmds.delete(export_name)
self.info_text.append(f"已删除: {export_name}")
def show():
"""显示UI"""
global standardize_skeleton_ui
try:
standardize_skeleton_ui.close()
standardize_skeleton_ui.deleteLater()
except:
pass
standardize_skeleton_ui = StandardizeSkeletonUI()
standardize_skeleton_ui.show()
if __name__ == "__main__":
show()