364 lines
13 KiB
Python
364 lines
13 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Tool - Maya MetaHuman Plugin
|
|
Main entry point for the Tool plugin
|
|
|
|
This module initializes the Tool plugin and provides the main UI and functionality
|
|
for working with MetaHuman models in Maya, including DNA editing, calibration,
|
|
custom binding, and more.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import traceback
|
|
import platform
|
|
import maya.cmds as cmds
|
|
import maya.mel as mel
|
|
import maya.OpenMayaUI as omui
|
|
|
|
# Detect Maya version and operating system
|
|
MAYA_VERSION = int(cmds.about(version=True))
|
|
OS_NAME = platform.system()
|
|
|
|
# Import configuration
|
|
try:
|
|
from config import TOOL_NAME, TOOL_VERSION, TOOL_AUTHOR, SCRIPTS_PATH, ASSETS_PATH, ICONS_PATH, PLUGINS_PATH, DNA_FILE_PATH, DNA_IMG_PATH, PYTHON_VERSION
|
|
except ImportError:
|
|
# If unable to import config, use default paths
|
|
TOOL_NAME = "Tool"
|
|
TOOL_VERSION = "1.0.0"
|
|
TOOL_AUTHOR = "Tool Team"
|
|
|
|
# Get script directory
|
|
METAHUMAN_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
SCRIPTS_PATH = os.path.join(METAHUMAN_PATH, "scripts")
|
|
ASSETS_PATH = os.path.join(METAHUMAN_PATH, "assets")
|
|
ICONS_PATH = os.path.join(METAHUMAN_PATH, "icons")
|
|
PLUGINS_PATH = os.path.join(METAHUMAN_PATH, "plugins")
|
|
DNA_FILE_PATH = os.path.join(ASSETS_PATH, "dna")
|
|
DNA_IMG_PATH = os.path.join(ASSETS_PATH, "images")
|
|
|
|
# Maya version information
|
|
PYTHON_VERSION = platform.python_version()
|
|
|
|
# Add scripts directory to Python path
|
|
if SCRIPTS_PATH not in sys.path:
|
|
sys.path.append(SCRIPTS_PATH)
|
|
|
|
# Select appropriate PySide version based on Maya version
|
|
QT_VERSION = None
|
|
|
|
# Maya 2025+ uses PySide6
|
|
if MAYA_VERSION >= 2025:
|
|
try:
|
|
from PySide6 import QtCore, QtGui, QtWidgets
|
|
from shiboken6 import wrapInstance
|
|
QT_VERSION = "PySide6"
|
|
print(f"Using PySide6 (Maya {MAYA_VERSION})")
|
|
except ImportError as e:
|
|
print(f"PySide6 import error: {str(e)}")
|
|
try:
|
|
from PySide2 import QtCore, QtGui, QtWidgets
|
|
from shiboken2 import wrapInstance
|
|
QT_VERSION = "PySide2"
|
|
print(f"Using PySide2 (fallback mode, Maya {MAYA_VERSION})")
|
|
except ImportError:
|
|
cmds.error("Unable to import PySide6 or PySide2, please ensure they are installed")
|
|
|
|
# Maya 2022-2024 uses PySide2
|
|
elif MAYA_VERSION >= 2022:
|
|
try:
|
|
from PySide2 import QtCore, QtGui, QtWidgets
|
|
from shiboken2 import wrapInstance
|
|
QT_VERSION = "PySide2"
|
|
print(f"Using PySide2 (Maya {MAYA_VERSION})")
|
|
except ImportError as e:
|
|
print(f"PySide2 import error: {str(e)}")
|
|
try:
|
|
# Try to fallback to PySide6 (some special installations may have PySide6)
|
|
from PySide6 import QtCore, QtGui, QtWidgets
|
|
from shiboken6 import wrapInstance
|
|
QT_VERSION = "PySide6"
|
|
print(f"Using PySide6 (non-standard configuration, Maya {MAYA_VERSION})")
|
|
except ImportError:
|
|
cmds.error("Unable to import PySide2 or PySide6, please ensure they are installed")
|
|
|
|
# Maya 2020-2021 uses PySide2 or PySide
|
|
else:
|
|
try:
|
|
from PySide2 import QtCore, QtGui, QtWidgets
|
|
from shiboken2 import wrapInstance
|
|
QT_VERSION = "PySide2"
|
|
print(f"Using PySide2 (Maya {MAYA_VERSION})")
|
|
except ImportError:
|
|
try:
|
|
from PySide import QtCore, QtGui, QtWidgets
|
|
from shiboken import wrapInstance
|
|
QT_VERSION = "PySide"
|
|
print(f"Using PySide (Maya {MAYA_VERSION})")
|
|
except ImportError:
|
|
cmds.error("Unable to import PySide or PySide2, please ensure they are installed")
|
|
|
|
# Import UI modules
|
|
try:
|
|
from ui.main_window import ToolWindow
|
|
from ui.dna_editor import DNAEditorWidget
|
|
from ui.calibration import CalibrationWidget
|
|
from ui.binding import BindingWidget
|
|
from ui.blendshape import BlendShapeWidget
|
|
except ImportError as e:
|
|
cmds.error(f"Error importing UI modules: {str(e)}\n{traceback.format_exc()}")
|
|
|
|
# Import utility modules
|
|
try:
|
|
from utils.dna_utils import DNAUtils
|
|
from utils.maya_utils import MayaUtils
|
|
from utils.mesh_utils import MeshUtils
|
|
from utils.joint_utils import JointUtils
|
|
from utils.blendshape_utils import BlendShapeUtils
|
|
except ImportError as e:
|
|
cmds.error(f"Error importing utility modules: {str(e)}\n{traceback.format_exc()}")
|
|
|
|
class Tool:
|
|
"""Main Tool class that initializes and manages the plugin"""
|
|
|
|
_instance = None
|
|
|
|
@classmethod
|
|
def instance(cls):
|
|
"""Singleton pattern to ensure only one instance of Tool exists"""
|
|
if cls._instance is None:
|
|
cls._instance = Tool()
|
|
return cls._instance
|
|
|
|
def __init__(self):
|
|
"""Initialize the Tool plugin"""
|
|
self.window = None
|
|
self.dna_utils = DNAUtils()
|
|
self.maya_utils = MayaUtils()
|
|
self.mesh_utils = MeshUtils()
|
|
self.joint_utils = JointUtils()
|
|
self.blendshape_utils = BlendShapeUtils()
|
|
|
|
# Initialize paths
|
|
self._init_paths()
|
|
|
|
# Load plugin dependencies
|
|
self._load_plugins()
|
|
|
|
print(f"{TOOL_NAME} {TOOL_VERSION} initialized")
|
|
|
|
def _init_paths(self):
|
|
"""Initialize necessary paths and directories"""
|
|
# Ensure all required directories exist
|
|
paths = [
|
|
SCRIPTS_PATH,
|
|
ASSETS_PATH,
|
|
ICONS_PATH,
|
|
PLUGINS_PATH,
|
|
DNA_FILE_PATH,
|
|
DNA_IMG_PATH
|
|
]
|
|
|
|
for path in paths:
|
|
if not os.path.exists(path):
|
|
try:
|
|
os.makedirs(path)
|
|
print(f"Created directory: {path}")
|
|
except Exception as e:
|
|
print(f"Error creating directory: {path}, Error: {str(e)}")
|
|
|
|
def _load_plugins(self):
|
|
"""Load required Maya plugins"""
|
|
required_plugins = ["fbxmaya", "matrixNodes"]
|
|
|
|
for plugin in required_plugins:
|
|
if not cmds.pluginInfo(plugin, query=True, loaded=True):
|
|
try:
|
|
cmds.loadPlugin(plugin)
|
|
print(f"Loaded plugin: {plugin}")
|
|
except Exception as e:
|
|
print(f"Error loading plugin: {plugin}, Error: {str(e)}")
|
|
|
|
def show_ui(self):
|
|
"""Show the main Tool UI window"""
|
|
if self.window is None or not self.window.isVisible():
|
|
self.window = ToolWindow()
|
|
|
|
self.window.show()
|
|
self.window.raise_()
|
|
return self.window
|
|
|
|
def create_metahuman_rig(self, mesh=None):
|
|
"""Create a MetaHuman rig from a mesh"""
|
|
if mesh is None:
|
|
# Get the selected mesh
|
|
selection = cmds.ls(selection=True)
|
|
if not selection:
|
|
cmds.warning("No mesh selected for rigging")
|
|
return False
|
|
mesh = selection[0]
|
|
|
|
# Validate the mesh
|
|
if not self.mesh_utils.validate_metahuman_topology(mesh):
|
|
cmds.warning(f"Selected mesh {mesh} does not have MetaHuman topology")
|
|
return False
|
|
|
|
# Create the rig
|
|
try:
|
|
result = self.joint_utils.create_metahuman_skeleton(mesh)
|
|
if result:
|
|
print(f"Successfully created MetaHuman rig for {mesh}")
|
|
return True
|
|
else:
|
|
cmds.warning(f"Failed to create MetaHuman rig for {mesh}")
|
|
return False
|
|
except Exception as e:
|
|
cmds.error(f"Error creating MetaHuman rig: {str(e)}")
|
|
return False
|
|
|
|
def load_dna(self, dna_file_path):
|
|
"""Load a DNA file into the scene"""
|
|
if not os.path.exists(dna_file_path):
|
|
cmds.warning(f"DNA file does not exist: {dna_file_path}")
|
|
return False
|
|
|
|
try:
|
|
result = self.dna_utils.load_dna(dna_file_path)
|
|
if result:
|
|
print(f"Successfully loaded DNA file: {dna_file_path}")
|
|
return True
|
|
else:
|
|
cmds.warning(f"Failed to load DNA file: {dna_file_path}")
|
|
return False
|
|
except Exception as e:
|
|
cmds.error(f"Error loading DNA file: {str(e)}")
|
|
return False
|
|
|
|
def save_dna(self, output_path):
|
|
"""Save the current scene as a DNA file"""
|
|
try:
|
|
result = self.dna_utils.save_dna(output_path)
|
|
if result:
|
|
print(f"Successfully saved DNA file to: {output_path}")
|
|
return True
|
|
else:
|
|
cmds.warning(f"Failed to save DNA file to: {output_path}")
|
|
return False
|
|
except Exception as e:
|
|
cmds.error(f"Error saving DNA file: {str(e)}")
|
|
return False
|
|
|
|
def export_fbx(self, output_path):
|
|
"""Export the current scene as an FBX file"""
|
|
try:
|
|
result = self.maya_utils.export_fbx(output_path)
|
|
if result:
|
|
print(f"Successfully exported FBX to: {output_path}")
|
|
return True
|
|
else:
|
|
cmds.warning(f"Failed to export FBX to: {output_path}")
|
|
return False
|
|
except Exception as e:
|
|
cmds.error(f"Error exporting FBX: {str(e)}")
|
|
return False
|
|
|
|
def batch_export_blendshapes(self, output_dir):
|
|
"""Batch export blendshapes to the specified directory"""
|
|
try:
|
|
# Check if output directory exists, if not create it
|
|
if not os.path.exists(output_dir):
|
|
os.makedirs(output_dir)
|
|
print(f"Created output directory: {output_dir}")
|
|
|
|
# Get all blendShape nodes in the scene
|
|
blendshape_nodes = cmds.ls(type="blendShape")
|
|
if not blendshape_nodes:
|
|
cmds.warning("No blendShape nodes found in the scene")
|
|
return False
|
|
|
|
# Export each blendShape node's targets
|
|
exported_files = []
|
|
for blendshape in blendshape_nodes:
|
|
try:
|
|
# Create subdirectory for each blendShape
|
|
blendshape_dir = os.path.join(output_dir, blendshape)
|
|
if not os.path.exists(blendshape_dir):
|
|
os.makedirs(blendshape_dir)
|
|
|
|
# Export targets
|
|
files = self.blendshape_utils.batch_export_targets(blendshape, blendshape_dir)
|
|
exported_files.extend(files)
|
|
except Exception as e:
|
|
cmds.warning(f"Error exporting blendShape {blendshape}: {str(e)}")
|
|
|
|
if exported_files:
|
|
print(f"Successfully exported {len(exported_files)} blendShape targets to: {output_dir}")
|
|
return True
|
|
else:
|
|
cmds.warning(f"No blendShape targets exported to: {output_dir}")
|
|
return False
|
|
except Exception as e:
|
|
error_msg = f"Error exporting blendShape: {str(e)}\n{traceback.format_exc()}"
|
|
cmds.error(error_msg)
|
|
return False
|
|
|
|
# Main function to initialize and show the Tool UI
|
|
def main():
|
|
"""Main function to initialize and show the Tool UI"""
|
|
try:
|
|
# Check Maya version compatibility
|
|
version_message = ""
|
|
version_warning = False
|
|
|
|
if MAYA_VERSION < 2022:
|
|
version_message = f"{TOOL_NAME} recommended to run in Maya 2022 or higher.\nCurrent version: Maya {MAYA_VERSION}\nSome features may be unavailable or perform abnormally."
|
|
version_warning = True
|
|
elif MAYA_VERSION > 2025:
|
|
version_message = f"{TOOL_NAME} has not been tested in Maya {MAYA_VERSION}.\nMay need updates to support new Maya features."
|
|
version_warning = True
|
|
|
|
if version_warning:
|
|
result = cmds.confirmDialog(
|
|
title="Version Compatibility Warning",
|
|
message=version_message + "\n\nDo you want to continue?",
|
|
button=["Continue", "Cancel"],
|
|
defaultButton="Continue",
|
|
cancelButton="Cancel",
|
|
dismissString="Cancel"
|
|
)
|
|
|
|
if result != "Continue":
|
|
print(f"{TOOL_NAME} startup cancelled")
|
|
return None
|
|
|
|
# Initialize and display UI
|
|
tool_instance = Tool.instance()
|
|
ui = tool_instance.show_ui()
|
|
|
|
# Print version information
|
|
print(f"\n{'-'*50}")
|
|
print(f"{TOOL_NAME} {TOOL_VERSION} started")
|
|
print(f"Maya version: {MAYA_VERSION}")
|
|
print(f"Operating system: {OS_NAME}")
|
|
print(f"Python version: {PYTHON_VERSION}")
|
|
print(f"Qt version: {QT_VERSION}")
|
|
print(f"{'-'*50}\n")
|
|
|
|
# In status bar display version information
|
|
if hasattr(ui, "status_bar"):
|
|
ui.status_bar.showMessage(f"Tool {TOOL_VERSION} | Maya {MAYA_VERSION} | {QT_VERSION} | {OS_NAME}")
|
|
|
|
return ui
|
|
except Exception as e:
|
|
error_msg = f"Error starting Tool: {str(e)}\n{traceback.format_exc()}"
|
|
cmds.error(error_msg)
|
|
return None
|
|
|
|
# If the script is run directly, show the UI
|
|
if __name__ == "__main__":
|
|
main()
|