Files
Nexus/2025/scripts/rigging_tools/PickUpWeightedBones.py
2026-01-22 00:06:13 +08:00

1395 lines
54 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
PickUpWeightedBones.py
从选中对象中筛选出几何体,获取这些几何体蒙皮权重对应的骨骼,并整理到一个集合里
带有完整的UI界面
"""
import maya.cmds as cmds
import maya.mel as mel
from functools import partial
# 全局变量
WINDOW_NAME = "pickUpWeightedBonesWindow"
WINDOW_TITLE = "骨骼拾取工具"
WINDOW_WIDTH = 450
WINDOW_HEIGHT = 700
class PickUpWeightedBonesUI(object):
"""骨骼拾取工具UI类"""
def __init__(self):
self.window = WINDOW_NAME
self.geometry_list = []
self.bone_list = []
self.selected_bones = []
# UI控件引用
self.geometry_text_scroll = None
self.bone_text_scroll = None
self.info_text_scroll = None
self.constraint_checkboxes = {}
# 约束类型
self.constraint_types = {
'parentConstraint': True,
'pointConstraint': True,
'orientConstraint': True,
'scaleConstraint': True,
'aimConstraint': True,
'poleVectorConstraint': True,
'geometryConstraint': True,
'normalConstraint': True,
'tangentConstraint': True
}
def create_ui(self):
"""创建UI界面"""
# 如果窗口已存在,删除它
if cmds.window(self.window, exists=True):
cmds.deleteUI(self.window)
# 创建窗口
self.window = cmds.window(
self.window,
title=WINDOW_TITLE,
widthHeight=(WINDOW_WIDTH, WINDOW_HEIGHT),
sizeable=True
)
# 主布局
main_layout = cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
# 标题
cmds.text(
label="从Maya场景中筛选带权重的骨骼",
font="boldLabelFont",
height=30,
backgroundColor=[0.3, 0.3, 0.3]
)
cmds.separator(height=10, style='none')
# 使用说明
cmds.frameLayout(label="使用说明", collapsable=True, collapse=False, borderStyle='etchedIn')
cmds.columnLayout(adjustableColumn=True, rowSpacing=3)
cmds.text(label="1. 选择场景中的对象或几何体", align='left')
cmds.text(label="2. 点击'拾取几何体'按钮(会自动筛选出几何体)", align='left')
cmds.text(label="3. 点击'分析骨骼'查看带权重的骨骼列表", align='left')
cmds.text(label="4. 选择要清理的约束类型", align='left')
cmds.text(label="5. 点击'清理约束''整理骨骼'按钮", align='left')
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=10, style='in')
# 几何体拾取区域
cmds.frameLayout(label="几何体拾取器", collapsable=True, collapse=False, borderStyle='etchedIn')
cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
cmds.rowLayout(numberOfColumns=3, columnWidth3=(150, 150, 100), adjustableColumn=3)
cmds.button(label="拾取几何体", command=self.pick_geometry, backgroundColor=[0.4, 0.6, 0.4])
cmds.button(label="选择模型", command=self.select_geometry_in_scene, backgroundColor=[0.5, 0.5, 0.6])
cmds.button(label="清空", command=self.clear_geometry, width=70)
cmds.setParent('..')
cmds.text(label="已拾取的几何体:", align='left', font='smallBoldLabelFont')
self.geometry_text_scroll = cmds.scrollField(
editable=False,
wordWrap=False,
height=80,
backgroundColor=[0.2, 0.2, 0.2],
font='smallFixedWidthFont'
)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5, style='in')
# 骨骼列表区域
cmds.frameLayout(label="骨骼列表", collapsable=True, collapse=False, borderStyle='etchedIn')
cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
cmds.rowLayout(numberOfColumns=3, columnWidth3=(150, 150, 100), adjustableColumn=3)
cmds.button(label="分析骨骼", command=self.analyze_bones, backgroundColor=[0.4, 0.5, 0.6])
cmds.button(label="选择骨骼", command=self.select_bones_in_scene)
cmds.button(label="清空", command=self.clear_bones)
cmds.setParent('..')
cmds.text(label="带权重的骨骼:", align='left', font='smallBoldLabelFont')
self.bone_text_scroll = cmds.scrollField(
editable=False,
wordWrap=False,
height=120,
backgroundColor=[0.2, 0.2, 0.2],
font='smallFixedWidthFont'
)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5, style='in')
# 约束清理器
cmds.frameLayout(label="约束清理器", collapsable=True, collapse=False, borderStyle='etchedIn')
cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
# 快速选择按钮
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-20, WINDOW_WIDTH/2-20))
cmds.button(label="全选", command=self.select_all_constraints, backgroundColor=[0.3, 0.4, 0.3])
cmds.button(label="全不选", command=self.deselect_all_constraints, backgroundColor=[0.4, 0.3, 0.3])
cmds.setParent('..')
cmds.separator(height=5, style='none')
# 约束类型复选框 - 两列布局
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-20, WINDOW_WIDTH/2-20))
# 左列
left_column = cmds.columnLayout(adjustableColumn=True)
self.constraint_checkboxes['parentConstraint'] = cmds.checkBox(
label='parentConstraint', value=True
)
self.constraint_checkboxes['orientConstraint'] = cmds.checkBox(
label='orientConstraint', value=True
)
self.constraint_checkboxes['aimConstraint'] = cmds.checkBox(
label='aimConstraint', value=True
)
self.constraint_checkboxes['geometryConstraint'] = cmds.checkBox(
label='geometryConstraint', value=True
)
self.constraint_checkboxes['tangentConstraint'] = cmds.checkBox(
label='tangentConstraint', value=True
)
cmds.setParent('..')
# 右列
right_column = cmds.columnLayout(adjustableColumn=True)
self.constraint_checkboxes['pointConstraint'] = cmds.checkBox(
label='pointConstraint', value=True
)
self.constraint_checkboxes['scaleConstraint'] = cmds.checkBox(
label='scaleConstraint', value=True
)
self.constraint_checkboxes['poleVectorConstraint'] = cmds.checkBox(
label='poleVectorConstraint', value=True
)
self.constraint_checkboxes['normalConstraint'] = cmds.checkBox(
label='normalConstraint', value=True
)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5, style='in')
# 约束操作按钮区域
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-10, WINDOW_WIDTH/2-10), height=40)
cmds.button(
label="扫描模型约束",
command=self.scan_geometry_constraints,
backgroundColor=[0.4, 0.5, 0.6],
height=35
)
cmds.button(
label="扫描骨骼约束",
command=self.scan_bone_constraints,
backgroundColor=[0.5, 0.4, 0.6],
height=35
)
cmds.setParent('..')
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-10, WINDOW_WIDTH/2-10), height=40)
cmds.button(
label="扫描模型和骨骼约束",
command=self.scan_all_constraints,
backgroundColor=[0.6, 0.5, 0.4],
height=35
)
cmds.button(
label="清理约束",
command=self.clean_constraints,
backgroundColor=[0.6, 0.4, 0.3],
height=35
)
cmds.setParent('..')
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5, style='in')
# 矩阵节点清理器
cmds.frameLayout(label="矩阵节点清理器", collapsable=True, collapse=False, borderStyle='etchedIn')
cmds.columnLayout(adjustableColumn=True, rowSpacing=5)
# 安全模式警告
cmds.text(
label="警告: 矩阵节点是绑定系统的核心组件,删除可能导致绑定失效!",
align='left',
font='smallBoldLabelFont',
backgroundColor=[0.6, 0.3, 0.3]
)
# 安全模式复选框
self.safe_mode_checkbox = cmds.checkBox(
label='启用安全模式 (推荐)',
value=True,
annotation='安全模式下会保护绑定系统的核心节点,只删除控制约束相关的矩阵节点'
)
cmds.separator(height=5, style='none')
# 快速选择按钮
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-20, WINDOW_WIDTH/2-20))
cmds.button(label="全选", command=self.select_all_matrix_nodes, backgroundColor=[0.3, 0.4, 0.3])
cmds.button(label="全不选", command=self.deselect_all_matrix_nodes, backgroundColor=[0.4, 0.3, 0.3])
cmds.setParent('..')
cmds.separator(height=5, style='none')
# 矩阵节点类型复选框 - 两列布局
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-20, WINDOW_WIDTH/2-20))
# 左列
left_column = cmds.columnLayout(adjustableColumn=True)
self.matrix_checkboxes = {}
self.matrix_checkboxes['multMatrix'] = cmds.checkBox(
label='multMatrix', value=True
)
self.matrix_checkboxes['decomposeMatrix'] = cmds.checkBox(
label='decomposeMatrix', value=True
)
self.matrix_checkboxes['multiplyDivide'] = cmds.checkBox(
label='multiplyDivide', value=True
)
self.matrix_checkboxes['plusMinusAverage'] = cmds.checkBox(
label='plusMinusAverage', value=True
)
self.matrix_checkboxes['blendMatrix'] = cmds.checkBox(
label='blendMatrix', value=True
)
cmds.setParent('..')
# 右列
right_column = cmds.columnLayout(adjustableColumn=True)
self.matrix_checkboxes['pickMatrix'] = cmds.checkBox(
label='pickMatrix', value=True
)
self.matrix_checkboxes['composeMatrix'] = cmds.checkBox(
label='composeMatrix', value=True
)
self.matrix_checkboxes['inverseMatrix'] = cmds.checkBox(
label='inverseMatrix', value=True
)
self.matrix_checkboxes['transposeMatrix'] = cmds.checkBox(
label='transposeMatrix', value=True
)
self.matrix_checkboxes['blendColors'] = cmds.checkBox(
label='blendColors', value=True
)
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5, style='in')
# 矩阵节点操作按钮区域
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-10, WINDOW_WIDTH/2-10), height=40)
cmds.button(
label="扫描模型矩阵节点",
command=self.scan_geometry_matrix_nodes,
backgroundColor=[0.4, 0.5, 0.6],
height=35
)
cmds.button(
label="扫描骨骼矩阵节点",
command=self.scan_bone_matrix_nodes,
backgroundColor=[0.5, 0.4, 0.6],
height=35
)
cmds.setParent('..')
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-10, WINDOW_WIDTH/2-10), height=40)
cmds.button(
label="扫描模型和骨骼矩阵节点",
command=self.scan_all_matrix_nodes,
backgroundColor=[0.6, 0.5, 0.4],
height=35
)
cmds.button(
label="清理矩阵节点",
command=self.clean_matrix_nodes,
backgroundColor=[0.6, 0.4, 0.3],
height=35
)
cmds.setParent('..')
cmds.setParent('..')
cmds.setParent('..')
cmds.separator(height=5, style='none')
# 骨骼集合操作按钮
cmds.rowLayout(numberOfColumns=2, columnWidth2=(WINDOW_WIDTH/2-10, WINDOW_WIDTH/2-10), height=40)
cmds.button(
label="创建骨骼选择集",
command=self.create_bone_set_only,
backgroundColor=[0.5, 0.6, 0.4],
height=35
)
cmds.button(
label="整理骨骼",
command=self.organize_bones,
backgroundColor=[0.5, 0.6, 0.4],
height=35
)
cmds.setParent('..')
cmds.separator(height=10, style='in')
# 信息输出区域
cmds.text(label="信息", align='left', font='smallBoldLabelFont')
self.info_text_scroll = cmds.scrollField(
editable=False,
wordWrap=True,
height=100,
backgroundColor=[0.15, 0.15, 0.15],
font='smallFixedWidthFont'
)
# 显示窗口
cmds.showWindow(self.window)
# 初始化信息
self.log_info("工具已就绪,请选择对象后点击'拾取几何体'")
def log_info(self, message):
"""输出信息到信息框"""
if self.info_text_scroll:
current_text = cmds.scrollField(self.info_text_scroll, query=True, text=True)
new_text = current_text + "\n" + message if current_text else message
cmds.scrollField(self.info_text_scroll, edit=True, text=new_text)
print(message)
def clear_info(self):
"""清空信息框"""
if self.info_text_scroll:
cmds.scrollField(self.info_text_scroll, edit=True, text="")
def pick_geometry(self, *args):
"""拾取几何体 - 自动扫描子层级"""
self.geometry_list = []
selection = cmds.ls(selection=True, long=True)
if not selection:
self.log_info("警告: 请先选择对象")
cmds.warning("请先选择对象")
return
for obj in selection:
# 检查是否是transform节点
if cmds.nodeType(obj) == 'transform':
# 获取所有子层级中的transform节点递归扫描
all_descendants = cmds.listRelatives(obj, allDescendents=True, fullPath=True, type='transform') or []
# 包含自身和所有子层级
all_transforms = [obj] + all_descendants
for transform in all_transforms:
# 获取shape节点
shapes = cmds.listRelatives(transform, shapes=True, fullPath=True, noIntermediate=True)
if shapes:
for shape in shapes:
shape_type = cmds.nodeType(shape)
# 只拾取mesh类型几何体剔除曲线和Nurbs
if shape_type == 'mesh':
self.geometry_list.append(transform)
break
# 如果直接选择的是shape节点
elif cmds.nodeType(obj) == 'mesh':
parent = cmds.listRelatives(obj, parent=True, fullPath=True)
if parent:
self.geometry_list.append(parent[0])
# 去重
self.geometry_list = list(set(self.geometry_list))
# 更新UI
if self.geometry_list:
geo_names = [geo.split('|')[-1] for geo in self.geometry_list]
cmds.scrollField(self.geometry_text_scroll, edit=True, text='\n'.join(geo_names))
self.log_info(f"成功拾取 {len(self.geometry_list)} 个几何体")
else:
cmds.scrollField(self.geometry_text_scroll, edit=True, text="")
self.log_info("警告: 选择中没有找到几何体对象")
cmds.warning("选择中没有找到几何体对象")
def clear_geometry(self, *args):
"""清空几何体列表"""
self.geometry_list = []
cmds.scrollField(self.geometry_text_scroll, edit=True, text="")
self.log_info("已清空几何体列表")
def analyze_bones(self, *args):
"""分析骨骼"""
if not self.geometry_list:
self.log_info("警告: 请先拾取几何体")
cmds.warning("请先拾取几何体")
return
self.bone_list = []
geometry_with_skin = []
for geo in self.geometry_list:
skin_cluster = self.get_skincluster_from_geometry(geo)
if skin_cluster:
bones = self.get_weighted_bones_from_skincluster(skin_cluster)
if bones:
self.bone_list.extend(bones)
geometry_with_skin.append(geo)
self.log_info(f"{geo.split('|')[-1]} 找到 {len(bones)} 个影响骨骼")
else:
self.log_info(f"几何体 {geo.split('|')[-1]} 没有skinCluster")
# 去重
self.bone_list = list(set(self.bone_list))
# 更新UI
if self.bone_list:
bone_names = [bone.split('|')[-1] for bone in self.bone_list]
cmds.scrollField(self.bone_text_scroll, edit=True, text='\n'.join(bone_names))
self.log_info(f"总共找到 {len(self.bone_list)} 个唯一骨骼")
else:
cmds.scrollField(self.bone_text_scroll, edit=True, text="")
self.log_info("警告: 没有找到任何带权重的骨骼")
cmds.warning("没有找到任何带权重的骨骼")
def clear_bones(self, *args):
"""清空骨骼列表"""
self.bone_list = []
cmds.scrollField(self.bone_text_scroll, edit=True, text="")
self.log_info("已清空骨骼列表")
def select_bones_in_scene(self, *args):
"""在场景中选择骨骼"""
if not self.bone_list:
self.log_info("警告: 骨骼列表为空")
cmds.warning("骨骼列表为空")
return
existing_bones = [bone for bone in self.bone_list if cmds.objExists(bone)]
if existing_bones:
cmds.select(existing_bones, replace=True)
self.log_info(f"已选择 {len(existing_bones)} 个骨骼")
else:
self.log_info("警告: 没有找到可选择的骨骼")
def select_geometry_in_scene(self, *args):
"""在场景中选择几何体"""
if not self.geometry_list:
self.log_info("警告: 几何体列表为空")
cmds.warning("几何体列表为空")
return
existing_geos = [geo for geo in self.geometry_list if cmds.objExists(geo)]
if existing_geos:
cmds.select(existing_geos, replace=True)
self.log_info(f"已选择 {len(existing_geos)} 个几何体")
else:
self.log_info("警告: 没有找到可选择的几何体")
def select_all_constraints(self, *args):
"""全选约束类型"""
for checkbox in self.constraint_checkboxes.values():
cmds.checkBox(checkbox, edit=True, value=True)
self.log_info("已全选所有约束类型")
def deselect_all_constraints(self, *args):
"""全不选约束类型"""
for checkbox in self.constraint_checkboxes.values():
cmds.checkBox(checkbox, edit=True, value=False)
self.log_info("已取消选择所有约束类型")
def get_selected_constraint_types(self):
"""获取选中的约束类型"""
selected_types = []
for constraint_type, checkbox in self.constraint_checkboxes.items():
if cmds.checkBox(checkbox, query=True, value=True):
selected_types.append(constraint_type)
return selected_types
def scan_geometry_constraints(self, *args):
"""扫描模型约束 - 扫描几何体上的约束"""
if not self.geometry_list:
self.log_info("警告: 请先拾取几何体")
cmds.warning("请先拾取几何体")
return
self._scan_constraints_on_objects(self.geometry_list, "几何体")
def scan_bone_constraints(self, *args):
"""扫描骨骼约束 - 扫描骨骼上的约束"""
if not self.bone_list:
self.log_info("警告: 请先分析骨骼")
cmds.warning("请先分析骨骼")
return
self._scan_constraints_on_objects(self.bone_list, "骨骼")
def scan_all_constraints(self, *args):
"""扫描模型和骨骼约束 - 扫描所有对象的约束"""
if not self.geometry_list and not self.bone_list:
self.log_info("警告: 请先拾取几何体或分析骨骼")
cmds.warning("请先拾取几何体或分析骨骼")
return
all_objects = list(set(self.geometry_list + self.bone_list))
self._scan_constraints_on_objects(all_objects, "模型和骨骼")
def _scan_constraints_on_objects(self, objects, object_type_name):
"""扫描对象上的约束 - 通用方法"""
# 所有约束类型
all_constraint_types = [
'parentConstraint',
'pointConstraint',
'orientConstraint',
'scaleConstraint',
'aimConstraint',
'poleVectorConstraint',
'geometryConstraint',
'normalConstraint',
'tangentConstraint'
]
self.log_info(f"\n开始扫描{object_type_name}约束...")
# 首先检查场景中是否有约束节点
scene_constraints = {}
for constraint_type in all_constraint_types:
constraints = cmds.ls(type=constraint_type)
if constraints:
scene_constraints[constraint_type] = len(constraints)
if not scene_constraints:
self.log_info("\n场景中没有找到任何约束节点!")
self.log_info("可能原因:")
self.log_info(" 1. 此绑定没有使用约束系统")
self.log_info(" 2. 约束已经被烘焙或删除")
self.log_info(" 3. 使用了其他控制方式(表达式、脚本等)")
return
self.log_info("场景中找到约束节点:")
for constraint_type, count in sorted(scene_constraints.items()):
self.log_info(f" {constraint_type}: {count}")
# 扫描对象上的约束
total_constraints = 0
constraint_summary = {}
found_any = False
for obj in objects:
if not cmds.objExists(obj):
continue
obj_constraints = []
for constraint_type in all_constraint_types:
# 查找连接到对象的约束节点(作为被约束对象)
constraints = cmds.listConnections(obj, type=constraint_type, source=True, destination=False)
if constraints:
constraints = list(set(constraints))
obj_constraints.extend(constraints)
if constraint_type not in constraint_summary:
constraint_summary[constraint_type] = 0
constraint_summary[constraint_type] += len(constraints)
# 也查找对象作为约束目标的情况
target_constraints = cmds.listConnections(obj, type=constraint_type, source=False, destination=True)
if target_constraints:
target_constraints = list(set(target_constraints))
for tc in target_constraints:
if tc not in obj_constraints:
obj_constraints.append(tc)
if constraint_type not in constraint_summary:
constraint_summary[constraint_type] = 0
constraint_summary[constraint_type] += 1
# 去重
unique_constraints = list(set(obj_constraints))
if unique_constraints:
found_any = True
total_constraints += len(unique_constraints)
constraint_names = [c.split('|')[-1] for c in unique_constraints]
self.log_info(f" {obj.split('|')[-1]}{len(unique_constraints)} 个约束: {', '.join(constraint_names)}")
# 输出统计信息
self.log_info("\n扫描完成!")
self.log_info(f"{object_type_name}数: {len(objects)}")
self.log_info(f"{object_type_name}上的约束数: {total_constraints}")
if constraint_summary:
self.log_info(f"\n{object_type_name}约束类型统计:")
for constraint_type, count in sorted(constraint_summary.items()):
self.log_info(f" {constraint_type}: {count}")
else:
self.log_info(f"\n这些{object_type_name}上没有找到约束")
self.log_info("提示: 场景中有约束,但不在选中的对象上")
def clean_constraints(self, *args):
"""清理约束"""
if not self.bone_list:
self.log_info("警告: 请先分析骨骼")
cmds.warning("请先分析骨骼")
return
selected_types = self.get_selected_constraint_types()
if not selected_types:
self.log_info("警告: 请至少选择一种约束类型")
cmds.warning("请至少选择一种约束类型")
return
# 确认对话框
result = cmds.confirmDialog(
title='确认清理',
message='确定要删除选中骨骼上的约束吗?\n这个操作不可撤销!',
button=['确定', '取消'],
defaultButton='确定',
cancelButton='取消',
dismissString='取消'
)
if result != '确定':
self.log_info("操作已取消")
return
removed_count = self.remove_constraints_from_bones(self.bone_list, selected_types)
if removed_count > 0:
self.log_info(f"成功清理了 {removed_count} 个约束")
else:
self.log_info("没有找到需要清理的约束")
def create_bone_set_only(self, *args):
"""创建骨骼选择集 - 只创建选择集"""
if not self.bone_list:
self.log_info("警告: 请先分析骨骼")
cmds.warning("请先分析骨骼")
return
self.log_info("\n开始创建骨骼选择集...")
# 创建选择集
set_name = "weighted_bones_set"
bone_set = self.create_bone_set(self.bone_list, set_name)
if not bone_set:
self.log_info("操作已取消")
return
self.log_info(f"\n成功!")
self.log_info(f"- 骨骼选择集已创建: {bone_set}")
self.log_info(f"- 添加的骨骼数量: {len(self.bone_list)}")
def organize_bones(self, *args):
"""整理骨骼 - 复制骨骼层级结构到新的root骨骼下"""
if not self.bone_list:
self.log_info("警告: 请先分析骨骼")
cmds.warning("请先分析骨骼")
return
# 确认对话框
result = cmds.confirmDialog(
title='确认整理骨骼',
message='将复制骨骼层级结构到新的root骨骼下。\n\n这将:\n1. 创建新的root骨骼\n2. 复制骨骼及其层级关系\n3. 保持原始骨骼的位置和旋转\n4. 原始骨骼不会被删除\n\n确定要继续吗?',
button=['确定', '取消'],
defaultButton='确定',
cancelButton='取消',
dismissString='取消'
)
if result != '确定':
self.log_info("操作已取消")
return
self.log_info("\n开始整理骨骼...")
try:
# 1. 分析骨骼层级关系
bone_hierarchy = self._analyze_bone_hierarchy(self.bone_list)
if not bone_hierarchy:
self.log_info("警告: 无法分析骨骼层级关系")
return
self.log_info(f"分析了 {len(bone_hierarchy)} 个根骨骼")
# 2. 创建新的root骨骼
cmds.select(clear=True)
root_name = "organized_root"
# 生成唯一名称
i = 1
while cmds.objExists(root_name):
root_name = f"organized_root_{i}"
i += 1
root_bone = cmds.joint(name=root_name, position=[0, 0, 0])
self.log_info(f"创建了根骨骼: {root_name}")
# 3. 复制骨骼层级
created_count = self._copy_bone_hierarchy(bone_hierarchy, root_bone)
self.log_info(f"\n成功!")
self.log_info(f"- 骨骼已整理到新的root骨骼下")
self.log_info(f"- 创建的骨骼数量: {created_count}")
self.log_info(f"- 新的root骨骼: {root_name}")
# 4. 选择新的root骨骼
cmds.select(root_bone, replace=True, hierarchy=True)
except Exception as e:
import traceback
error_msg = traceback.format_exc()
self.log_info(f"错误: 整理骨骼时发生异常:\n{error_msg}")
cmds.warning(f"整理骨骼时发生异常: {str(e)}")
def _analyze_bone_hierarchy(self, bone_list):
"""分析骨骼层级关系
返回根骨骼列表,每个根骨骼包含其子骨骼的层级结构
考虑组的层级结构,保持原始的层级关系
"""
# 过滤出存在的骨骼 - 移除 | 前缀后检查
existing_bones = []
for bone in bone_list:
# 移除 | 前缀
clean_bone = bone[1:] if bone.startswith('|') else bone
if cmds.objExists(clean_bone):
existing_bones.append(clean_bone)
if not existing_bones:
return None
# 构建骨骼映射表:骨骼名 -> 完整路径列表(处理同名骨骼)
bone_map = {}
for bone in existing_bones:
bone_name = bone.split('|')[-1]
if bone_name not in bone_map:
bone_map[bone_name] = []
bone_map[bone_name].append(bone)
# 构建完整的父子关系(包括通过组连接的骨骼)
parent_map = {} # 子骨骼 -> 父骨骼(完整路径)
children_map = {} # 父骨骼 -> 子骨骼列表(完整路径列表)
for bone in existing_bones:
bone_name = bone.split('|')[-1]
# 向上遍历父对象链,找到最近的骨骼父对象
current_parent = bone
found_bone_parent = False
while True:
parents = cmds.listRelatives(current_parent, parent=True, fullPath=True)
if not parents:
break
current_parent = parents[0]
parent_name = current_parent.split('|')[-1]
# 检查父对象是否是骨骼
if parent_name in bone_map:
# 找到骨骼父对象
parent_map[bone] = current_parent
# 添加到子骨骼列表
if current_parent not in children_map:
children_map[current_parent] = []
children_map[current_parent].append(bone)
found_bone_parent = True
break
# 如果父对象不是骨骼,继续向上查找
# 这样可以保持组的层级关系
# 找出根骨骼没有在bone_list中的父骨骼
root_bones = []
for bone in existing_bones:
if bone not in parent_map:
root_bones.append(bone)
# 构建层级结构
hierarchy = []
for root_bone in root_bones:
root_name = root_bone.split('|')[-1]
hierarchy.append({
'name': root_name,
'path': root_bone,
'children': self._build_hierarchy_tree(root_bone, children_map, bone_map)
})
return hierarchy
def _build_hierarchy_tree(self, bone_path, children_map, bone_map):
"""递归构建层级树
Args:
bone_path: 骨骼完整路径
children_map: 子骨骼映射表(完整路径 -> 完整路径列表)
bone_map: 骨骼名到完整路径列表的映射表
"""
if bone_path not in children_map:
return []
bone_name = bone_path.split('|')[-1]
tree = []
for child_path in children_map[bone_path]:
child_name = child_path.split('|')[-1]
tree.append({
'name': child_name,
'path': child_path, # 使用完整路径
'children': self._build_hierarchy_tree(child_path, children_map, bone_map)
})
return tree
def _rebuild_bone_hierarchy(self, hierarchy, root_parent):
"""重建骨骼层级结构
Args:
hierarchy: 层级结构列表
root_parent: 根父骨骼新的root骨骼
"""
for root_node in hierarchy:
self._create_bone_hierarchy_node(root_node, root_parent)
def _create_bone_hierarchy_node(self, node, parent):
"""递归创建骨骼层级节点
Args:
node: 骨骼节点字典 {'name': str, 'path': str, 'children': list}
parent: 父骨骼对象
"""
# 获取原始骨骼的完整路径
original_bone_path = node.get('path', node['name'])
# 移除路径前导的 | 符号,确保 Maya 能正确识别对象
if original_bone_path and original_bone_path.startswith('|'):
original_bone_path = original_bone_path[1:]
# 检查骨骼是否存在
if not original_bone_path or not cmds.objExists(original_bone_path):
self.log_info(f" 警告: 原始骨骼不存在: {original_bone_path}")
return
# 获取变换属性 - 使用相对空间local space
try:
translation = cmds.xform(original_bone_path, query=True, translation=True, worldSpace=False)
rotation = cmds.xform(original_bone_path, query=True, rotation=True, worldSpace=False)
scale = cmds.xform(original_bone_path, query=True, scale=True, worldSpace=False)
except Exception as e:
self.log_info(f" 警告: 无法获取 {original_bone_path} 的变换信息: {str(e)}")
# 设置默认值,避免后续错误
translation = [0, 0, 0]
rotation = [0, 0, 0]
scale = [1, 1, 1]
# 清除选择状态,避免创建冲突
cmds.select(clear=True)
# 创建新的骨骼 - 如果有父对象,在父对象下创建
if parent and cmds.objExists(parent):
# 选择父对象,确保新骨骼在父对象下创建
cmds.select(parent, replace=True)
new_bone = cmds.joint(name=node['name'])
else:
# 没有父对象,直接创建
new_bone = cmds.joint(name=node['name'])
# 应用变换
try:
cmds.xform(new_bone, translation=translation, rotation=rotation, scale=scale, worldSpace=False)
except Exception as e:
self.log_info(f" 警告: 无法应用变换到 {new_bone}: {str(e)}")
# 递归创建子骨骼
for child_node in node['children']:
self._create_bone_hierarchy_node(child_node, new_bone)
def _copy_bone_hierarchy(self, hierarchy, root_parent):
"""复制骨骼层级结构
Args:
hierarchy: 层级结构列表
root_parent: 根父骨骼新的root骨骼
Returns:
int: 创建的骨骼数量
"""
created_count = 0
for root_node in hierarchy:
created_count += self._copy_bone_node(root_node, root_parent)
return created_count
def _copy_bone_node(self, node, parent):
"""递归复制骨骼节点
Args:
node: 骨骼节点字典 {'name': str, 'path': str, 'children': list}
parent: 父骨骼对象
Returns:
int: 创建的骨骼数量
"""
# 获取原始骨骼的完整路径
original_bone = node['path']
# 移除路径前导的 | 符号
if original_bone and original_bone.startswith('|'):
original_bone = original_bone[1:]
# 检查骨骼是否存在
if not original_bone or not cmds.objExists(original_bone):
self.log_info(f" 警告: 原始骨骼不存在: {original_bone}")
return 0
# 获取世界空间的变换信息
try:
world_pos = cmds.xform(original_bone, query=True, translation=True, worldSpace=True)
world_rot = cmds.xform(original_bone, query=True, rotation=True, worldSpace=True)
except Exception as e:
self.log_info(f" 警告: 无法获取 {original_bone} 的世界空间变换: {str(e)}")
world_pos = [0, 0, 0]
world_rot = [0, 0, 0]
# 获取相对缩放
try:
scale = cmds.getAttr(f"{original_bone}.scale")
except:
scale = [1, 1, 1]
# 清除选择
cmds.select(clear=True)
# 选择父骨骼(如果有)
if parent and cmds.objExists(parent):
cmds.select(parent, replace=True)
# 创建新骨骼
new_bone_name = f"copy_{node['name']}"
new_bone = cmds.joint(name=new_bone_name)
# 设置世界空间位置和旋转
try:
cmds.xform(new_bone, translation=world_pos, worldSpace=True)
cmds.xform(new_bone, rotation=world_rot, worldSpace=True)
except Exception as e:
self.log_info(f" 警告: 无法设置 {new_bone} 的世界空间变换: {str(e)}")
# 设置缩放
try:
cmds.setAttr(f"{new_bone}.scale", *scale)
except:
pass
created_count = 1
# 递归复制子骨骼
for child_node in node['children']:
created_count += self._copy_bone_node(child_node, new_bone)
return created_count
# ========== 矩阵节点清理器功能 ==========
def select_all_matrix_nodes(self, *args):
"""全选矩阵节点类型"""
for checkbox in self.matrix_checkboxes.values():
cmds.checkBox(checkbox, edit=True, value=True)
self.log_info("已全选所有矩阵节点类型")
def deselect_all_matrix_nodes(self, *args):
"""全不选矩阵节点类型"""
for checkbox in self.matrix_checkboxes.values():
cmds.checkBox(checkbox, edit=True, value=False)
self.log_info("已取消选择所有矩阵节点类型")
def get_selected_matrix_node_types(self):
"""获取选中的矩阵节点类型"""
selected_types = []
for node_type, checkbox in self.matrix_checkboxes.items():
if cmds.checkBox(checkbox, query=True, value=True):
selected_types.append(node_type)
return selected_types
def scan_geometry_matrix_nodes(self, *args):
"""扫描模型矩阵节点 - 扫描几何体上的矩阵节点"""
if not self.geometry_list:
self.log_info("警告: 请先拾取几何体")
cmds.warning("请先拾取几何体")
return
self._scan_matrix_nodes_on_objects(self.geometry_list, "几何体")
def scan_bone_matrix_nodes(self, *args):
"""扫描骨骼矩阵节点 - 扫描骨骼上的矩阵节点"""
if not self.bone_list:
self.log_info("警告: 请先分析骨骼")
cmds.warning("请先分析骨骼")
return
self._scan_matrix_nodes_on_objects(self.bone_list, "骨骼")
def scan_all_matrix_nodes(self, *args):
"""扫描模型和骨骼矩阵节点 - 扫描所有对象的矩阵节点"""
if not self.geometry_list and not self.bone_list:
self.log_info("警告: 请先拾取几何体或分析骨骼")
cmds.warning("请先拾取几何体或分析骨骼")
return
all_objects = list(set(self.geometry_list + self.bone_list))
self._scan_matrix_nodes_on_objects(all_objects, "模型和骨骼")
def _scan_matrix_nodes_on_objects(self, objects, object_type_name):
"""扫描对象上的矩阵节点 - 通用方法"""
# 所有矩阵节点类型
all_matrix_types = [
'multMatrix',
'decomposeMatrix',
'multiplyDivide',
'plusMinusAverage',
'blendMatrix',
'pickMatrix',
'composeMatrix',
'inverseMatrix',
'transposeMatrix',
'blendColors'
]
self.log_info(f"\n开始扫描{object_type_name}矩阵节点...")
# 首先检查场景中是否有矩阵节点
scene_matrix_nodes = {}
for matrix_type in all_matrix_types:
nodes = cmds.ls(type=matrix_type)
if nodes:
scene_matrix_nodes[matrix_type] = len(nodes)
if not scene_matrix_nodes:
self.log_info("\n场景中没有找到任何矩阵节点!")
return
self.log_info("场景中找到矩阵节点:")
for matrix_type, count in sorted(scene_matrix_nodes.items()):
self.log_info(f" {matrix_type}: {count}")
# 扫描对象上的矩阵节点
total_matrix_nodes = 0
matrix_summary = {}
found_any = False
for obj in objects:
if not cmds.objExists(obj):
continue
obj_matrix_nodes = []
for matrix_type in all_matrix_types:
# 查找连接到对象的矩阵节点(作为源)
matrix_nodes = cmds.listConnections(obj, type=matrix_type, source=True, destination=False)
if matrix_nodes:
matrix_nodes = list(set(matrix_nodes))
obj_matrix_nodes.extend(matrix_nodes)
if matrix_type not in matrix_summary:
matrix_summary[matrix_type] = 0
matrix_summary[matrix_type] += len(matrix_nodes)
# 也查找对象作为矩阵节点目标的情况
target_matrix_nodes = cmds.listConnections(obj, type=matrix_type, source=False, destination=True)
if target_matrix_nodes:
target_matrix_nodes = list(set(target_matrix_nodes))
for tm in target_matrix_nodes:
if tm not in obj_matrix_nodes:
obj_matrix_nodes.append(tm)
if matrix_type not in matrix_summary:
matrix_summary[matrix_type] = 0
matrix_summary[matrix_type] += 1
# 去重
unique_matrix_nodes = list(set(obj_matrix_nodes))
if unique_matrix_nodes:
found_any = True
total_matrix_nodes += len(unique_matrix_nodes)
node_names = [n.split('|')[-1] for n in unique_matrix_nodes]
self.log_info(f" {obj.split('|')[-1]}{len(unique_matrix_nodes)} 个矩阵节点: {', '.join(node_names)}")
# 输出统计信息
self.log_info("\n扫描完成!")
self.log_info(f"{object_type_name}数: {len(objects)}")
self.log_info(f"{object_type_name}上的矩阵节点数: {total_matrix_nodes}")
if matrix_summary:
self.log_info(f"\n{object_type_name}矩阵节点类型统计:")
for matrix_type, count in sorted(matrix_summary.items()):
self.log_info(f" {matrix_type}: {count}")
else:
self.log_info(f"\n这些{object_type_name}上没有找到矩阵节点")
self.log_info("提示: 场景中有矩阵节点,但不在选中的对象上")
def clean_matrix_nodes(self, *args):
"""清理矩阵节点"""
if not self.bone_list:
self.log_info("警告: 请先分析骨骼")
cmds.warning("请先分析骨骼")
return
selected_types = self.get_selected_matrix_node_types()
if not selected_types:
self.log_info("警告: 请至少选择一种矩阵节点类型")
cmds.warning("请至少选择一种矩阵节点类型")
return
# 检查安全模式
safe_mode = cmds.checkBox(self.safe_mode_checkbox, query=True, value=True)
if safe_mode:
warning_msg = (
'安全模式已启用!\n'
'只删除控制约束相关的矩阵节点,保护绑定系统的核心节点。\n'
'如果需要删除所有矩阵节点,请先禁用安全模式。\n\n'
'确定要继续吗?'
)
else:
warning_msg = (
'危险操作!\n'
'安全模式已禁用,将删除所有选中的矩阵节点!\n'
'这可能会完全破坏绑定系统的连接关系!\n\n'
'确定要继续吗?'
)
# 确认对话框
result = cmds.confirmDialog(
title='确认清理',
message=warning_msg,
button=['确定', '取消'],
defaultButton='取消',
cancelButton='取消',
dismissString='取消'
)
if result != '确定':
self.log_info("操作已取消")
return
removed_count = self.remove_matrix_nodes_from_bones(self.bone_list, selected_types, safe_mode)
if removed_count > 0:
self.log_info(f"成功清理了 {removed_count} 个矩阵节点")
else:
self.log_info("没有找到需要清理的矩阵节点")
# ========== 核心功能函数 ==========
def get_skincluster_from_geometry(self, geometry):
"""获取几何体的skinCluster节点"""
shapes = cmds.listRelatives(geometry, shapes=True, fullPath=True, noIntermediate=True)
if not shapes:
return None
for shape in shapes:
history = cmds.listHistory(shape, pruneDagObjects=True)
if history:
skin_clusters = cmds.ls(history, type='skinCluster')
if skin_clusters:
return skin_clusters[0]
return None
def get_weighted_bones_from_skincluster(self, skin_cluster):
"""从skinCluster获取所有有权重的骨骼"""
if not skin_cluster:
return []
influences = cmds.skinCluster(skin_cluster, query=True, influence=True)
return influences if influences else []
def remove_constraints_from_bones(self, bones, constraint_types):
"""移除骨骼上的指定约束"""
if not bones:
return 0
removed_count = 0
self.log_info("\n开始移除骨骼约束...")
for bone in bones:
if not cmds.objExists(bone):
continue
# 查找骨骼上的所有约束
constraints = []
for constraint_type in constraint_types:
# 查找连接到骨骼的约束节点
bone_constraints = cmds.listConnections(bone, type=constraint_type)
if bone_constraints:
constraints.extend(bone_constraints)
# 去重
constraints = list(set(constraints))
# 删除约束
for constraint in constraints:
if cmds.objExists(constraint):
try:
cmds.delete(constraint)
removed_count += 1
self.log_info(f" 移除约束: {constraint.split('|')[-1]} (来自 {bone.split('|')[-1]})")
except Exception as e:
self.log_info(f" 警告: 无法删除约束 {constraint}: {str(e)}")
return removed_count
def remove_matrix_nodes_from_bones(self, bones, matrix_types, safe_mode=True):
"""移除骨骼上的指定矩阵节点
Args:
bones: 骨骼列表
matrix_types: 矩阵节点类型列表
safe_mode: 安全模式True时保护绑定系统的核心节点
"""
if not bones:
return 0
removed_count = 0
skipped_count = 0
self.log_info("\n开始移除骨骼矩阵节点...")
if safe_mode:
self.log_info("安全模式已启用,将保护绑定系统的核心节点")
# 定义保护模式列表(绑定系统的核心节点)
protected_patterns = [
'*_parent_multMatrix', # 父子关系矩阵
'*_child_decomposeMatrix', # 子节点解压矩阵
'*_child_decomposeMatrix1', # 子节点解压矩阵变体
'*_worldparent_multMatrix', # 世界父矩阵
'*_scale_parent_multMatrix', # 缩放父矩阵
'*_psd_*_multMatrix', # PSD相关矩阵
'*_psd_*_pickMatrix', # PSD pick矩阵
'*_psd_*_composeMatrix', # PSD组合矩阵
'*_tb_*_multMatrix', # Twist Bone相关矩阵
'*_tb_*_child_decomposeMatrix', # Twist Bone解压矩阵
'*_skin_joint_multMatrix', # 蒙皮骨骼矩阵
'*_skin_child_decomposeMatrix', # 蒙皮骨骼解压矩阵
'*_scale_child_decomposeMatrix', # 缩放子节点解压矩阵
'*_parent_multMatrix1', # 父子关系矩阵变体
'*_parent_multMatrix2', # 父子关系矩阵变体
'*_parent_multMatrix3', # 父子关系矩阵变体
]
# 导入fnmatch模块用于通配符匹配
import fnmatch
for bone in bones:
if not cmds.objExists(bone):
continue
# 查找骨骼上的所有矩阵节点
matrix_nodes = []
for matrix_type in matrix_types:
# 查找连接到骨骼的矩阵节点
bone_matrix_nodes = cmds.listConnections(bone, type=matrix_type)
if bone_matrix_nodes:
matrix_nodes.extend(bone_matrix_nodes)
# 去重
matrix_nodes = list(set(matrix_nodes))
# 删除矩阵节点
for matrix_node in matrix_nodes:
if not cmds.objExists(matrix_node):
continue
# 检查是否在保护列表中
node_name = matrix_node.split('|')[-1]
should_skip = False
if safe_mode:
for pattern in protected_patterns:
if fnmatch.fnmatch(node_name, pattern):
should_skip = True
break
if should_skip:
skipped_count += 1
if skipped_count <= 5: # 只显示前5个跳过的节点
self.log_info(f" [保护] 跳过核心节点: {node_name}")
continue
try:
cmds.delete(matrix_node)
removed_count += 1
self.log_info(f" 移除矩阵节点: {node_name} (来自 {bone.split('|')[-1]})")
except Exception as e:
self.log_info(f" 警告: 无法删除矩阵节点 {matrix_node}: {str(e)}")
if safe_mode and skipped_count > 5:
self.log_info(f" ... 还有 {skipped_count - 5} 个核心节点被保护")
return removed_count
def create_bone_set(self, bones, set_name):
"""创建集合并添加骨骼"""
if not bones:
return None
# 检查集合是否已存在
if cmds.objExists(set_name):
result = cmds.confirmDialog(
title='集合已存在',
message=f'集合 "{set_name}" 已存在,是否要添加到现有集合中?',
button=['添加到现有集合', '创建新集合', '取消'],
defaultButton='添加到现有集合',
cancelButton='取消',
dismissString='取消'
)
if result == '取消':
return None
elif result == '创建新集合':
i = 1
while cmds.objExists(f"{set_name}{i}"):
i += 1
set_name = f"{set_name}{i}"
bone_set = cmds.sets(name=set_name, empty=True)
else:
bone_set = set_name
else:
bone_set = cmds.sets(name=set_name, empty=True)
# 将骨骼添加到集合中
for bone in bones:
if cmds.objExists(bone):
try:
cmds.sets(bone, addElement=bone_set)
except Exception as e:
self.log_info(f"警告: 无法将 {bone} 添加到集合中: {str(e)}")
return bone_set
def show_ui():
"""显示UI"""
ui = PickUpWeightedBonesUI()
ui.create_ui()
# 执行函数
if __name__ == "__main__":
show_ui()