738 lines
29 KiB
Python
738 lines
29 KiB
Python
# -*- 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()
|