#!/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()