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

752 lines
28 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tool - BlendShape UI
Provides the UI for editing and managing BlendShapes
"""
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 BlendShapeWidget(QtWidgets.QWidget):
"""Widget for editing and managing BlendShapes"""
def __init__(self, parent=None):
"""Initialize the BlendShape widget"""
super(BlendShapeWidget, self).__init__(parent)
# Initialize UI
self._create_widgets()
self._create_layouts()
self._create_connections()
def _create_widgets(self):
"""Create widgets for the BlendShape interface"""
# BlendShape Selection section
self.blendshape_group = QtWidgets.QGroupBox("BlendShape Selection")
self.blendshape_label = QtWidgets.QLabel("BlendShape Node:")
self.blendshape_combo = QtWidgets.QComboBox()
self.blendshape_combo.setEditable(True)
self.blendshape_combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
self.refresh_blendshape_button = QtWidgets.QPushButton()
self.refresh_blendshape_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "refresh.png")))
self.refresh_blendshape_button.setToolTip("Refresh BlendShape nodes in the scene")
self.refresh_blendshape_button.setFixedSize(24, 24)
self.create_blendshape_button = QtWidgets.QPushButton("Create BlendShape")
self.create_blendshape_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "create.png")))
# Target Mesh section
self.target_group = QtWidgets.QGroupBox("Target Mesh")
self.target_label = QtWidgets.QLabel("Target Mesh:")
self.target_combo = QtWidgets.QComboBox()
self.target_combo.setEditable(True)
self.target_combo.setInsertPolicy(QtWidgets.QComboBox.NoInsert)
self.refresh_target_button = QtWidgets.QPushButton()
self.refresh_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "refresh.png")))
self.refresh_target_button.setToolTip("Refresh meshes in the scene")
self.refresh_target_button.setFixedSize(24, 24)
self.select_target_button = QtWidgets.QPushButton("Select Target")
self.select_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "select.png")))
# BlendShape Targets section
self.targets_group = QtWidgets.QGroupBox("BlendShape Targets")
self.targets_tree = QtWidgets.QTreeWidget()
self.targets_tree.setHeaderLabels(["Target Name", "Weight", "Status"])
self.targets_tree.setColumnWidth(0, 200)
self.targets_tree.setColumnWidth(1, 100)
self.targets_tree.setAlternatingRowColors(True)
# Target Controls section
self.controls_group = QtWidgets.QGroupBox("Target Controls")
self.add_target_button = QtWidgets.QPushButton("Add Target")
self.add_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "add.png")))
self.remove_target_button = QtWidgets.QPushButton("Remove Target")
self.remove_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "remove.png")))
self.edit_target_button = QtWidgets.QPushButton("Edit Target")
self.edit_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "edit.png")))
self.duplicate_target_button = QtWidgets.QPushButton("Duplicate Target")
self.duplicate_target_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "duplicate.png")))
# Batch Operations section
self.batch_group = QtWidgets.QGroupBox("Batch Operations")
self.export_all_button = QtWidgets.QPushButton("Export All Targets")
self.export_all_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "export.png")))
self.import_targets_button = QtWidgets.QPushButton("导入目标")
self.import_targets_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "import.png")))
self.connect_to_rig_button = QtWidgets.QPushButton("连接到控制器")
self.connect_to_rig_button.setIcon(QtGui.QIcon(os.path.join(config.ICONS_PATH, "connect.png")))
def _create_layouts(self):
"""Create layouts for the BlendShape interface"""
# BlendShape Selection layout
blendshape_layout = QtWidgets.QGridLayout()
blendshape_layout.addWidget(self.blendshape_label, 0, 0)
blendshape_layout.addWidget(self.blendshape_combo, 0, 1)
blendshape_layout.addWidget(self.refresh_blendshape_button, 0, 2)
blendshape_layout.addWidget(self.create_blendshape_button, 1, 1)
self.blendshape_group.setLayout(blendshape_layout)
# Target Mesh layout
target_layout = QtWidgets.QGridLayout()
target_layout.addWidget(self.target_label, 0, 0)
target_layout.addWidget(self.target_combo, 0, 1)
target_layout.addWidget(self.refresh_target_button, 0, 2)
target_layout.addWidget(self.select_target_button, 1, 1)
self.target_group.setLayout(target_layout)
# BlendShape Targets layout
targets_layout = QtWidgets.QVBoxLayout()
targets_layout.addWidget(self.targets_tree)
self.targets_group.setLayout(targets_layout)
# Target Controls layout
controls_layout = QtWidgets.QGridLayout()
controls_layout.addWidget(self.add_target_button, 0, 0)
controls_layout.addWidget(self.remove_target_button, 0, 1)
controls_layout.addWidget(self.edit_target_button, 1, 0)
controls_layout.addWidget(self.duplicate_target_button, 1, 1)
self.controls_group.setLayout(controls_layout)
# Batch Operations layout
batch_layout = QtWidgets.QHBoxLayout()
batch_layout.addWidget(self.export_all_button)
batch_layout.addWidget(self.import_targets_button)
batch_layout.addWidget(self.connect_to_rig_button)
self.batch_group.setLayout(batch_layout)
# Main layout
main_layout = QtWidgets.QVBoxLayout()
main_layout.addWidget(self.blendshape_group)
main_layout.addWidget(self.target_group)
main_layout.addWidget(self.targets_group)
main_layout.addWidget(self.controls_group)
main_layout.addWidget(self.batch_group)
self.setLayout(main_layout)
def _create_connections(self):
"""Create signal/slot connections for the BlendShape interface"""
# Connect buttons
self.refresh_blendshape_button.clicked.connect(self._refresh_blendshapes)
self.create_blendshape_button.clicked.connect(self._create_blendshape)
self.refresh_target_button.clicked.connect(self._refresh_targets)
self.select_target_button.clicked.connect(self._select_target)
self.add_target_button.clicked.connect(self._add_target)
self.remove_target_button.clicked.connect(self._remove_target)
self.edit_target_button.clicked.connect(self._edit_target)
self.duplicate_target_button.clicked.connect(self._duplicate_target)
self.export_all_button.clicked.connect(self._export_all_targets)
self.import_targets_button.clicked.connect(self._import_targets)
self.connect_to_rig_button.clicked.connect(self._connect_to_rig)
# Connect combo boxes
self.blendshape_combo.currentIndexChanged.connect(self._on_blendshape_changed)
self.target_combo.currentIndexChanged.connect(self._on_target_changed)
# Connect tree widget
self.targets_tree.itemClicked.connect(self._on_target_selected)
self.targets_tree.itemDoubleClicked.connect(self._on_target_double_clicked)
# Initial refresh
self._refresh_blendshapes()
self._refresh_targets()
def _refresh_blendshapes(self):
"""Refresh the list of BlendShape nodes in the scene"""
current_text = self.blendshape_combo.currentText()
self.blendshape_combo.clear()
# Get all BlendShape nodes in the scene
blendshapes = cmds.ls(type="blendShape")
blendshapes.sort()
self.blendshape_combo.addItems(blendshapes)
# Restore previous selection if possible
index = self.blendshape_combo.findText(current_text)
if index >= 0:
self.blendshape_combo.setCurrentIndex(index)
# Update targets tree
self._update_targets_tree()
def _create_blendshape(self):
"""Create a new BlendShape node"""
target = self.target_combo.currentText()
if not target:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先选择一个目标网格"
)
return
# Get a name for the new BlendShape node
name, ok = QtWidgets.QInputDialog.getText(
self,
"创建 BlendShape",
"BlendShape 名称:",
QtWidgets.QLineEdit.Normal,
f"{target}_blendShape"
)
if ok and name:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.create_blendshape(target, name)
if result:
# Refresh BlendShape list
self._refresh_blendshapes()
# Select the new BlendShape
index = self.blendshape_combo.findText(name)
if index >= 0:
self.blendshape_combo.setCurrentIndex(index)
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功创建 BlendShape: {name}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"创建 BlendShape 失败: {name}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"创建 BlendShape 出错: {str(e)}"
)
def _refresh_targets(self):
"""Refresh the list of target meshes in the scene"""
current_text = self.target_combo.currentText()
self.target_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.target_combo.addItems(unique_transforms)
# Restore previous selection if possible
index = self.target_combo.findText(current_text)
if index >= 0:
self.target_combo.setCurrentIndex(index)
def _select_target(self):
"""Select the current target mesh in the scene"""
target = self.target_combo.currentText()
if target:
cmds.select(target)
def _on_blendshape_changed(self, index):
"""Handle BlendShape combo box change"""
if index >= 0:
# Update targets tree
self._update_targets_tree()
def _on_target_changed(self, index):
"""Handle target combo box change"""
if index >= 0:
target = self.target_combo.itemText(index)
# Select the target in the scene
cmds.select(target)
def _update_targets_tree(self):
"""Update the targets tree with current BlendShape information"""
self.targets_tree.clear()
blendshape = self.blendshape_combo.currentText()
if not blendshape:
return
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
# Get targets information
targets_info = blendshape_utils.get_targets_info(blendshape)
if targets_info:
# Add targets to tree
for target_name, target_info in targets_info.items():
weight = target_info.get("weight", 0.0)
status = target_info.get("status", "正常")
item = QtWidgets.QTreeWidgetItem(self.targets_tree, [
target_name,
str(weight),
status
])
# Create weight slider
slider = QtWidgets.QSlider(QtCore.Qt.Horizontal)
slider.setRange(0, 100)
slider.setValue(int(weight * 100))
slider.setProperty("target_name", target_name)
slider.valueChanged.connect(self._on_weight_changed)
self.targets_tree.setItemWidget(item, 1, slider)
except Exception as e:
print(f"更新目标树出错: {str(e)}")
def _on_target_selected(self, item, column):
"""Handle target tree item selection"""
target_name = item.text(0)
blendshape = self.blendshape_combo.currentText()
if blendshape and target_name:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
# Select the target in the scene if it exists as a separate mesh
target_mesh = blendshape_utils.get_target_mesh(blendshape, target_name)
if target_mesh:
cmds.select(target_mesh)
except Exception:
pass
def _on_target_double_clicked(self, item, column):
"""Handle target tree item double click"""
if column == 0: # Target name column
self._edit_target()
def _on_weight_changed(self, value):
"""Handle weight slider change"""
sender = self.sender()
if not sender:
return
target_name = sender.property("target_name")
blendshape = self.blendshape_combo.currentText()
if blendshape and target_name:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
weight = value / 100.0
blendshape_utils.set_target_weight(blendshape, target_name, weight)
except Exception as e:
print(f"设置目标权重出错: {str(e)}")
def _add_target(self):
"""Add a new target to the BlendShape"""
blendshape = self.blendshape_combo.currentText()
if not blendshape:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a BlendShape node first"
)
return
# Get selected mesh
selection = cmds.ls(selection=True)
if not selection:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先选择一个网格模型作为目标"
)
return
target_mesh = selection[0]
# Get a name for the new target
name, ok = QtWidgets.QInputDialog.getText(
self,
"添加目标",
"目标名称:",
QtWidgets.QLineEdit.Normal,
f"Target_{len(cmds.blendShape(blendshape, query=True, target=True) or [])}"
)
if ok and name:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.add_target(blendshape, target_mesh, name)
if result:
# Update targets tree
self._update_targets_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功添加目标: {name}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"添加目标失败: {name}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"添加目标出错: {str(e)}"
)
def _remove_target(self):
"""Remove a target from the BlendShape"""
blendshape = self.blendshape_combo.currentText()
if not blendshape:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a BlendShape node first"
)
return
# Get selected target
selected_items = self.targets_tree.selectedItems()
if not selected_items:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先在目标列表中选择一个目标"
)
return
target_name = selected_items[0].text(0)
# Confirm removal
result = QtWidgets.QMessageBox.question(
self,
"确认",
f"确定要移除目标 {target_name} 吗?",
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
)
if result == QtWidgets.QMessageBox.Yes:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.remove_target(blendshape, target_name)
if result:
# Update targets tree
self._update_targets_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功移除目标: {target_name}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"移除目标失败: {target_name}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"移除目标出错: {str(e)}"
)
def _edit_target(self):
"""Edit a target in the BlendShape"""
blendshape = self.blendshape_combo.currentText()
if not blendshape:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a BlendShape node first"
)
return
# Get selected target
selected_items = self.targets_tree.selectedItems()
if not selected_items:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先在目标列表中选择一个目标"
)
return
target_name = selected_items[0].text(0)
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.edit_target(blendshape, target_name)
if result:
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功编辑目标: {target_name}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"编辑目标失败: {target_name}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"编辑目标出错: {str(e)}"
)
def _duplicate_target(self):
"""Duplicate a target in the BlendShape"""
blendshape = self.blendshape_combo.currentText()
if not blendshape:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a BlendShape node first"
)
return
# Get selected target
selected_items = self.targets_tree.selectedItems()
if not selected_items:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"请先在目标列表中选择一个目标"
)
return
target_name = selected_items[0].text(0)
# Get a name for the new target
new_name, ok = QtWidgets.QInputDialog.getText(
self,
"复制目标",
"新目标名称:",
QtWidgets.QLineEdit.Normal,
f"{target_name}_copy"
)
if ok and new_name:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.duplicate_target(blendshape, target_name, new_name)
if result:
# Update targets tree
self._update_targets_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"成功复制目标 {target_name}{new_name}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"复制目标失败: {target_name}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"复制目标出错: {str(e)}"
)
def _export_all_targets(self):
"""Export all targets to separate files"""
blendshape = self.blendshape_combo.currentText()
if not blendshape:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a BlendShape node first"
)
return
dir_path = QtWidgets.QFileDialog.getExistingDirectory(
self,
"选择导出目录",
""
)
if dir_path:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.export_all_targets(blendshape, dir_path)
if result:
QtWidgets.QMessageBox.information(
self,
"Success",
f"Successfully exported all targets to: {dir_path}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"Failed to export targets: {dir_path}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"Error exporting targets: {str(e)}"
)
def _import_targets(self):
"""Import targets from files"""
blendshape = self.blendshape_combo.currentText()
if not blendshape:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a BlendShape node first"
)
return
file_paths, _ = QtWidgets.QFileDialog.getOpenFileNames(
self,
"Select Target Files",
"",
"Maya Files (*.ma *.mb);;OBJ Files (*.obj);;All Files (*.*)"
)
if file_paths:
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.import_targets(blendshape, file_paths)
if result:
# Update targets tree
self._update_targets_tree()
QtWidgets.QMessageBox.information(
self,
"Success",
f"Successfully imported {len(file_paths)} targets"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Failed to import targets"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"Error importing targets: {str(e)}"
)
def _connect_to_rig(self):
"""Connect BlendShape targets to rig controls"""
blendshape = self.blendshape_combo.currentText()
if not blendshape:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a BlendShape node first"
)
return
# Get the rig control
selection = cmds.ls(selection=True)
if not selection:
QtWidgets.QMessageBox.warning(
self,
"Warning",
"Please select a controller first"
)
return
control = selection[0]
# Import the blendshape utils here to avoid circular imports
from utils.blendshape_utils import BlendShapeUtils
blendshape_utils = BlendShapeUtils()
try:
result = blendshape_utils.connect_to_rig(blendshape, control)
if result:
QtWidgets.QMessageBox.information(
self,
"Success",
f"Successfully connected {blendshape} to {control}"
)
else:
QtWidgets.QMessageBox.warning(
self,
"Warning",
f"Failed to connect to controller: {control}"
)
except Exception as e:
QtWidgets.QMessageBox.critical(
self,
"Error",
f"Error connecting to controller: {str(e)}"
)