594 lines
22 KiB
Python
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)}"
|
|
)
|