MetaWhiz/scripts/ui/binding.py
2025-04-17 13:00:39 +08:00

594 lines
22 KiB
Python

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