Update
This commit is contained in:
737
2025/scripts/rigging_tools/StandardizeSkeleton.py
Normal file
737
2025/scripts/rigging_tools/StandardizeSkeleton.py
Normal 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()
|
||||
Reference in New Issue
Block a user