#!/usr/bin/env python # -*- coding: utf-8 -*- """ Tool - Binding UI Provides the UI for custom binding of MetaHuman models """ import os import sys import maya.cmds as cmds try: from PySide2 import QtCore, QtGui, QtWidgets except ImportError: try: from PySide6 import QtCore, QtGui, QtWidgets except ImportError: from PySide import QtCore, QtGui, QtWidgets # Import configuration import config class BindingWidget(QtWidgets.QWidget): """Widget for custom binding of MetaHuman models""" def __init__(self, parent=None): """Initialize the binding widget""" super(BindingWidget, self).__init__(parent) # Initialize UI self._create_widgets() self._create_layouts() self._create_connections() def _create_widgets(self): """Create widgets for the binding interface""" # Mesh Selection section self.mesh_group = QtWidgets.QGroupBox("Mesh Selection") self.mesh_label = QtWidgets.QLabel("Target Mesh:") self.mesh_combo = QtWidgets.QComboBox() self.mesh_combo.setEditable(True) self.mesh_combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.refresh_mesh_button = QtWidgets.QPushButton() self.refresh_mesh_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "refresh.png"))) self.refresh_mesh_button.setToolTip("Refresh meshes in the scene") self.refresh_mesh_button.setFixedSize(24, 24) self.select_mesh_button = QtWidgets.QPushButton("Select Mesh") self.select_mesh_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "select.png"))) # Skeleton Selection section self.skeleton_group = QtWidgets.QGroupBox("Skeleton Selection") self.skeleton_label = QtWidgets.QLabel("Target Skeleton:") self.skeleton_combo = QtWidgets.QComboBox() self.skeleton_combo.setEditable(True) self.skeleton_combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert) self.refresh_skeleton_button = QtWidgets.QPushButton() self.refresh_skeleton_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "refresh.png"))) self.refresh_skeleton_button.setToolTip("Refresh skeletons in the scene") self.refresh_skeleton_button.setFixedSize(24, 24) self.select_skeleton_button = QtWidgets.QPushButton("Select Skeleton") self.select_skeleton_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "select.png"))) # Binding Options section self.options_group = QtWidgets.QGroupBox("Binding Options") self.method_label = QtWidgets.QLabel("Binding Method:") self.method_combo = QtWidgets.QComboBox() self.method_combo.addItems(["Smooth Bind", "Rigid Bind", "Heat Map Bind", "Geodesic Voxel Bind"]) self.max_influences_label = QtWidgets.QLabel("Max Influences:") self.max_influences_spin = QtWidgets.QSpinBox() self.max_influences_spin.setRange(1, 16) self.max_influences_spin.setValue(4) self.maintain_influences_check = QtWidgets.QCheckBox("Maintain Max Influences") self.maintain_influences_check.setChecked(True) self.normalize_weights_check = QtWidgets.QCheckBox("Normalize Weights") self.normalize_weights_check.setChecked(True) self.post_normalize_check = QtWidgets.QCheckBox("Post Normalize") self.post_normalize_check.setChecked(True) self.use_metahuman_weights_check = QtWidgets.QCheckBox("Use MetaHuman Weights") self.use_metahuman_weights_check.setChecked(True) # Binding Operations section self.operations_group = QtWidgets.QGroupBox("Binding Operations") self.bind_button = QtWidgets.QPushButton("Bind") self.bind_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "bind.png"))) self.unbind_button = QtWidgets.QPushButton("Unbind") self.unbind_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "unbind.png"))) self.export_weights_button = QtWidgets.QPushButton("导出权重") self.export_weights_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "export.png"))) self.import_weights_button = QtWidgets.QPushButton("导入权重") self.import_weights_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "import.png"))) # Weight Painting section self.painting_group = QtWidgets.QGroupBox("权重绘制") self.paint_weights_button = QtWidgets.QPushButton("绘制权重") self.paint_weights_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "paint.png"))) self.smooth_weights_button = QtWidgets.QPushButton("平滑权重") self.smooth_weights_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "smooth.png"))) self.prune_weights_button = QtWidgets.QPushButton("裁剪权重") self.prune_weights_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "prune.png"))) self.mirror_weights_button = QtWidgets.QPushButton("镜像权重") self.mirror_weights_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "mirror.png"))) def _create_layouts(self): """Create layouts for the binding interface""" # Mesh Selection layout mesh_layout = QtWidgets.QGridLayout() mesh_layout.addWidget(self.mesh_label, 0, 0) mesh_layout.addWidget(self.mesh_combo, 0, 1) mesh_layout.addWidget(self.refresh_mesh_button, 0, 2) mesh_layout.addWidget(self.select_mesh_button, 1, 1) self.mesh_group.setLayout(mesh_layout) # Skeleton Selection layout skeleton_layout = QtWidgets.QGridLayout() skeleton_layout.addWidget(self.skeleton_label, 0, 0) skeleton_layout.addWidget(self.skeleton_combo, 0, 1) skeleton_layout.addWidget(self.refresh_skeleton_button, 0, 2) skeleton_layout.addWidget(self.select_skeleton_button, 1, 1) self.skeleton_group.setLayout(skeleton_layout) # Binding Options layout options_layout = QtWidgets.QGridLayout() options_layout.addWidget(self.method_label, 0, 0) options_layout.addWidget(self.method_combo, 0, 1) options_layout.addWidget(self.max_influences_label, 1, 0) options_layout.addWidget(self.max_influences_spin, 1, 1) options_layout.addWidget(self.maintain_influences_check, 2, 0, 1, 2) options_layout.addWidget(self.normalize_weights_check, 3, 0, 1, 2) options_layout.addWidget(self.post_normalize_check, 4, 0, 1, 2) options_layout.addWidget(self.use_metahuman_weights_check, 5, 0, 1, 2) self.options_group.setLayout(options_layout) # Binding Operations layout operations_layout = QtWidgets.QGridLayout() operations_layout.addWidget(self.bind_button, 0, 0) operations_layout.addWidget(self.unbind_button, 0, 1) operations_layout.addWidget(self.export_weights_button, 1, 0) operations_layout.addWidget(self.import_weights_button, 1, 1) self.operations_group.setLayout(operations_layout) # Weight Painting layout painting_layout = QtWidgets.QGridLayout() painting_layout.addWidget(self.paint_weights_button, 0, 0) painting_layout.addWidget(self.smooth_weights_button, 0, 1) painting_layout.addWidget(self.prune_weights_button, 1, 0) painting_layout.addWidget(self.mirror_weights_button, 1, 1) self.painting_group.setLayout(painting_layout) # Main layout main_layout = QtWidgets.QVBoxLayout() main_layout.addWidget(self.mesh_group) main_layout.addWidget(self.skeleton_group) main_layout.addWidget(self.options_group) main_layout.addWidget(self.operations_group) main_layout.addWidget(self.painting_group) main_layout.addStretch() self.setLayout(main_layout) def _create_connections(self): """Create signal/slot connections for the binding interface""" # Connect buttons self.refresh_mesh_button.clicked.connect(self._refresh_meshes) self.select_mesh_button.clicked.connect(self._select_mesh) self.refresh_skeleton_button.clicked.connect(self._refresh_skeletons) self.select_skeleton_button.clicked.connect(self._select_skeleton) self.bind_button.clicked.connect(self._bind) self.unbind_button.clicked.connect(self._unbind) self.export_weights_button.clicked.connect(self._export_weights) self.import_weights_button.clicked.connect(self._import_weights) self.paint_weights_button.clicked.connect(self._paint_weights) self.smooth_weights_button.clicked.connect(self._smooth_weights) self.prune_weights_button.clicked.connect(self._prune_weights) self.mirror_weights_button.clicked.connect(self._mirror_weights) # Connect combo boxes self.mesh_combo.currentIndexChanged.connect(self._on_mesh_changed) self.skeleton_combo.currentIndexChanged.connect(self._on_skeleton_changed) # Initial refresh self._refresh_meshes() self._refresh_skeletons() def _refresh_meshes(self): """Refresh the list of meshes in the scene""" current_text = self.mesh_combo.currentText() self.mesh_combo.clear() # Get all mesh shapes in the scene meshes = cmds.ls(type="mesh") mesh_transforms = [] for mesh in meshes: # Get the transform node for the mesh transform = cmds.listRelatives(mesh, parent=True) if transform: mesh_transforms.append(transform[0]) # Add unique mesh transforms to the combo box unique_transforms = list(set(mesh_transforms)) unique_transforms.sort() self.mesh_combo.addItems(unique_transforms) # Restore previous selection if possible index = self.mesh_combo.findText(current_text) if index >= 0: self.mesh_combo.setCurrentIndex(index) def _select_mesh(self): """Select the current mesh in the scene""" mesh = self.mesh_combo.currentText() if mesh: cmds.select(mesh) def _refresh_skeletons(self): """Refresh the list of skeletons in the scene""" current_text = self.skeleton_combo.currentText() self.skeleton_combo.clear() # Get all joint hierarchies in the scene root_joints = [] all_joints = cmds.ls(type="joint") for joint in all_joints: # Check if it's a root joint (no joint parent) parent = cmds.listRelatives(joint, parent=True) if not parent or cmds.nodeType(parent[0]) != "joint": root_joints.append(joint) # Add root joints to the combo box root_joints.sort() self.skeleton_combo.addItems(root_joints) # Restore previous selection if possible index = self.skeleton_combo.findText(current_text) if index >= 0: self.skeleton_combo.setCurrentIndex(index) def _select_skeleton(self): """Select the current skeleton in the scene""" skeleton = self.skeleton_combo.currentText() if skeleton: cmds.select(skeleton) def _on_mesh_changed(self, index): """Handle mesh combo box change""" if index >= 0: mesh = self.mesh_combo.itemText(index) # Select the mesh in the scene cmds.select(mesh) def _on_skeleton_changed(self, index): """Handle skeleton combo box change""" if index >= 0: skeleton = self.skeleton_combo.itemText(index) # Select the skeleton in the scene cmds.select(skeleton) def _bind(self): """Bind the mesh to the skeleton""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return skeleton = self.skeleton_combo.currentText() if not skeleton: QtWidgets.QMessageBox.warning( self, "Warning", "请先选择一个骨骼" ) return # Get binding options method = self.method_combo.currentText() max_influences = self.max_influences_spin.value() maintain_max_influences = self.maintain_influences_check.isChecked() normalize_weights = self.normalize_weights_check.isChecked() post_normalize = self.post_normalize_check.isChecked() use_metahuman_weights = self.use_metahuman_weights_check.isChecked() # Import the mesh utils here to avoid circular imports from utils.mesh_utils import MeshUtils mesh_utils = MeshUtils() try: result = mesh_utils.bind_mesh( mesh, skeleton, method=method, max_influences=max_influences, maintain_max_influences=maintain_max_influences, normalize_weights=normalize_weights, post_normalize=post_normalize, use_metahuman_weights=use_metahuman_weights ) if result: QtWidgets.QMessageBox.information( self, "Success", f"成功将 {mesh} 绑定到 {skeleton}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"将 {mesh} 绑定到 {skeleton} 失败" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"绑定出错: {str(e)}" ) def _unbind(self): """Unbind the mesh from the skeleton""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return # Confirm unbind result = QtWidgets.QMessageBox.question( self, "确认", f"确定要解除 {mesh} 的绑定吗?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No ) if result == QtWidgets.QMessageBox.Yes: # Import the mesh utils here to avoid circular imports from utils.mesh_utils import MeshUtils mesh_utils = MeshUtils() try: result = mesh_utils.unbind_mesh(mesh) if result: QtWidgets.QMessageBox.information( self, "Success", f"成功解除 {mesh} 的绑定" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"解除 {mesh} 的绑定失败" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"解除绑定出错: {str(e)}" ) def _export_weights(self): """Export skin weights""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return file_path, _ = QtWidgets.QFileDialog.getSaveFileName( self, "导出权重文件", "", "Weight Files (*.xml *.json);;All Files (*.*)" ) if file_path: # Import the mesh utils here to avoid circular imports from utils.mesh_utils import MeshUtils mesh_utils = MeshUtils() try: result = mesh_utils.export_weights(mesh, file_path) if result: QtWidgets.QMessageBox.information( self, "Success", f"成功导出 {mesh} 的权重到 {os.path.basename(file_path)}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"导出 {mesh} 的权重失败" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"导出权重出错: {str(e)}" ) def _import_weights(self): """Import skin weights""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return file_path, _ = QtWidgets.QFileDialog.getOpenFileName( self, "导入权重文件", "", "Weight Files (*.xml *.json);;All Files (*.*)" ) if file_path: # Import the mesh utils here to avoid circular imports from utils.mesh_utils import MeshUtils mesh_utils = MeshUtils() try: result = mesh_utils.import_weights(mesh, file_path) if result: QtWidgets.QMessageBox.information( self, "Success", f"Successfully imported weights to {mesh}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"Failed to import weights to {mesh}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error importing weights: {str(e)}" ) def _paint_weights(self): """Open the weight painting tool""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return try: cmds.select(mesh) mel.eval("ArtPaintSkinWeightsToolOptions") except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error opening weight painting tool: {str(e)}" ) def _smooth_weights(self): """Smooth skin weights""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return try: cmds.select(mesh) mel.eval("SmoothBindSkinOptions") except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error smoothing weights: {str(e)}" ) def _prune_weights(self): """Prune small weights""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return # Import the mesh utils here to avoid circular imports from utils.mesh_utils import MeshUtils mesh_utils = MeshUtils() try: result = mesh_utils.prune_weights(mesh) if result: QtWidgets.QMessageBox.information( self, "Success", f"Successfully pruned weights for {mesh}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"Failed to prune weights for {mesh}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error pruning weights: {str(e)}" ) def _mirror_weights(self): """Mirror skin weights""" mesh = self.mesh_combo.currentText() if not mesh: QtWidgets.QMessageBox.warning( self, "Warning", "Please select a mesh first" ) return # Import the mesh utils here to avoid circular imports from utils.mesh_utils import MeshUtils mesh_utils = MeshUtils() try: result = mesh_utils.mirror_weights(mesh) if result: QtWidgets.QMessageBox.information( self, "Success", f"Successfully mirrored weights for {mesh}" ) else: QtWidgets.QMessageBox.warning( self, "Warning", f"Failed to mirror weights for {mesh}" ) except Exception as e: QtWidgets.QMessageBox.critical( self, "Error", f"Error mirroring weights: {str(e)}" )