1395 lines
54 KiB
Python
1395 lines
54 KiB
Python
#!/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()
|