MetaWhiz/scripts/utils/mesh_utils.py
2025-04-17 13:00:39 +08:00

654 lines
23 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
Tool - Mesh Utilities
Provides utilities for working with mesh objects in Maya
"""
import os
import sys
import json
import maya.cmds as cmds
import maya.mel as mel
# Import configuration
import config
class MeshUtils:
"""Utilities for working with mesh objects in Maya"""
def __init__(self):
"""Initialize mesh utilities"""
pass
def validate_metahuman_topology(self, mesh):
"""
Validate if a mesh has MetaHuman topology
Args:
mesh (str): Name of the mesh
Returns:
bool: True if the mesh has MetaHuman topology, False otherwise
"""
try:
# Check if mesh exists
if not cmds.objExists(mesh):
cmds.warning(f"Mesh model not found: {mesh}")
return False
# Get mesh shape
shapes = cmds.listRelatives(mesh, shapes=True, type="mesh")
if not shapes:
cmds.warning(f"Mesh model not found: {mesh}")
return False
mesh_shape = shapes[0]
# Get vertex count
vertex_count = cmds.polyEvaluate(mesh, vertex=True)
# MetaHuman topology has a specific vertex count
# For MetaHuman 4.0, the vertex count is typically around 6700-6800
# This is a very basic check and should be enhanced with more detailed topology checks
if vertex_count < 6000 or vertex_count > 7000:
print(f"Mesh {mesh} has vertex count ({vertex_count}) that does not meet MetaHuman topology requirements (6000-7000)")
return False
# TODO: Add more detailed topology checks
# - Check for specific edge loops
# - Check for specific vertex positions
# - Check for specific face structure
return True
except Exception as e:
cmds.warning(f"Error validating MetaHuman topology: {str(e)}")
return False
def bind_mesh(self, mesh, skeleton, method="Smooth Bind", max_influences=4,
maintain_max_influences=True, normalize_weights=True,
post_normalize=True, use_metahuman_weights=True):
"""
Bind a mesh to a skeleton
Args:
mesh (str): Name of the mesh
skeleton (str): Name of the skeleton root
method (str, optional): Binding method
max_influences (int, optional): Maximum number of joint influences per vertex
maintain_max_influences (bool, optional): Maintain maximum influences
normalize_weights (bool, optional): Normalize weights
post_normalize (bool, optional): Post normalize weights
use_metahuman_weights (bool, optional): Use MetaHuman weights if available
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if mesh and skeleton exist
if not cmds.objExists(mesh):
cmds.warning(f"Mesh model not found: {mesh}")
return False
if not cmds.objExists(skeleton):
cmds.warning(f"Skeleton not found: {skeleton}")
return False
# Get all joints in the skeleton
joints = self._get_all_joints(skeleton)
if not joints:
cmds.warning(f"Skeleton {skeleton} has no joints")
return False
# Check if mesh is already bound
skin_clusters = self._get_skin_clusters(mesh)
if skin_clusters:
# Ask if we should unbind first
result = cmds.confirmDialog(
title="Mesh already bound",
message=f"Mesh {mesh} is already bound to skeleton. Do you want to unbind first?",
button=["Yes", "No"],
defaultButton="Yes",
cancelButton="No",
dismissString="No"
)
if result == "Yes":
self.unbind_mesh(mesh)
else:
cmds.warning(f"Mesh {mesh} is already bound to skeleton")
return False
# Bind mesh to skeleton
skin_cluster = None
if method == "Smooth Bind":
skin_cluster = cmds.skinCluster(
joints,
mesh,
name=f"{mesh}_skinCluster",
toSelectedBones=True,
bindMethod=0, # Closest distance
skinMethod=0, # Linear
normalizeWeights=2 if normalize_weights else 0, # 0=None, 1=Interactive, 2=Post
maximumInfluences=max_influences,
obeyMaxInfluences=maintain_max_influences,
dropoffRate=4.0
)[0]
elif method == "Rigid Bind":
skin_cluster = cmds.skinCluster(
joints,
mesh,
name=f"{mesh}_skinCluster",
toSelectedBones=True,
bindMethod=1, # Closest joint
skinMethod=0, # Linear
normalizeWeights=2 if normalize_weights else 0,
maximumInfluences=1,
obeyMaxInfluences=True,
dropoffRate=4.0
)[0]
elif method == "Heat Map Bind":
# Heat Map binding requires Maya 2020 or later
skin_cluster = cmds.skinCluster(
joints,
mesh,
name=f"{mesh}_skinCluster",
toSelectedBones=True,
bindMethod=0, # Closest distance
skinMethod=1, # Dual quaternion
normalizeWeights=2 if normalize_weights else 0,
maximumInfluences=max_influences,
obeyMaxInfluences=maintain_max_influences,
dropoffRate=4.0,
heatmapFalloff=0.68
)[0]
elif method == "Geodesic Voxel Bind":
# Geodesic Voxel binding requires Maya 2018 or later
skin_cluster = cmds.skinCluster(
joints,
mesh,
name=f"{mesh}_skinCluster",
toSelectedBones=True,
bindMethod=2, # Geodesic Voxel
skinMethod=0, # Linear
normalizeWeights=2 if normalize_weights else 0,
maximumInfluences=max_influences,
obeyMaxInfluences=maintain_max_influences,
dropoffRate=4.0,
volumeType=1, # 0=Volume, 1=Surface
volumeBinding=0.5
)[0]
if not skin_cluster:
cmds.warning(f"Failed to create skin cluster")
return False
# Post-normalize weights if needed
if post_normalize and normalize_weights:
cmds.skinPercent(skin_cluster, mesh, normalize=True)
# Load MetaHuman weights if available and requested
if use_metahuman_weights:
weights_file = os.path.join(config.METAHUMAN_PATH, "weights", f"{mesh}_weights.xml")
if os.path.exists(weights_file):
self.import_weights(mesh, weights_file)
print(f"Successfully bound {mesh} to {skeleton}")
return True
except Exception as e:
cmds.warning(f"Error binding mesh: {str(e)}")
return False
def unbind_mesh(self, mesh):
"""
Unbind a mesh from its skeleton
Args:
mesh (str): Name of the mesh
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if mesh exists
if not cmds.objExists(mesh):
cmds.warning(f"Mesh model not found: {mesh}")
return False
# Get skin clusters
skin_clusters = self._get_skin_clusters(mesh)
if not skin_clusters:
print(f"Mesh {mesh} is not bound to skeleton")
return True
# Unbind mesh
for skin_cluster in skin_clusters:
cmds.skinCluster(skin_cluster, edit=True, unbind=True)
print(f"Successfully unbound {mesh}")
return True
except Exception as e:
cmds.warning(f"Error unbinding mesh: {str(e)}")
return False
def export_weights(self, mesh, file_path):
"""
Export skin weights to a file
Args:
mesh (str): Name of the mesh
file_path (str): Path to save the weights file
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if mesh exists
if not cmds.objExists(mesh):
cmds.warning(f"Mesh model not found: {mesh}")
return False
# Get skin clusters
skin_clusters = self._get_skin_clusters(mesh)
if not skin_clusters:
cmds.warning(f"Mesh {mesh} is not bound to skeleton")
return False
# Determine file format
if file_path.lower().endswith(".xml"):
# Export as XML
cmds.deformerWeights(
file_path,
export=True,
deformer=skin_clusters[0],
format="XML"
)
else:
# Export as JSON
weights_data = self._get_skin_weights(mesh, skin_clusters[0])
with open(file_path, "w") as f:
json.dump(weights_data, f, indent=2)
print(f"Successfully exported weights for {mesh} to {file_path}")
return True
except Exception as e:
cmds.warning(f"Error exporting weights: {str(e)}")
return False
def import_weights(self, mesh, file_path):
"""
Import skin weights from a file
Args:
mesh (str): Name of the mesh
file_path (str): Path to the weights file
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if mesh exists
if not cmds.objExists(mesh):
cmds.warning(f"Mesh model not found: {mesh}")
return False
# Check if file exists
if not os.path.exists(file_path):
cmds.warning(f"Weights file not found: {file_path}")
return False
# Get skin clusters
skin_clusters = self._get_skin_clusters(mesh)
if not skin_clusters:
cmds.warning(f"Mesh {mesh} is not bound to skeleton")
return False
# Determine file format
if file_path.lower().endswith(".xml"):
# Import from XML
cmds.deformerWeights(
file_path,
im=True,
deformer=skin_clusters[0],
format="XML"
)
else:
# Import from JSON
with open(file_path, "r") as f:
weights_data = json.load(f)
self._set_skin_weights(mesh, skin_clusters[0], weights_data)
print(f"Successfully imported weights to {mesh}")
return True
except Exception as e:
cmds.warning(f"Error importing weights: {str(e)}")
return False
def mirror_weights(self, mesh):
"""
Mirror skin weights across the X axis
Args:
mesh (str): Name of the mesh
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if mesh exists
if not cmds.objExists(mesh):
cmds.warning(f"Mesh model not found: {mesh}")
return False
# Get skin clusters
skin_clusters = self._get_skin_clusters(mesh)
if not skin_clusters:
cmds.warning(f"Mesh {mesh} is not bound to skeleton")
return False
# Mirror weights
cmds.copySkinWeights(
sourceSkin=skin_clusters[0],
destinationSkin=skin_clusters[0],
mirrorMode="YZ",
surfaceAssociation="closestPoint",
influenceAssociation=["label", "oneToOne"]
)
print(f"Successfully mirrored weights for {mesh}")
return True
except Exception as e:
cmds.warning(f"Error mirroring weights: {str(e)}")
return False
def prune_weights(self, mesh, threshold=0.01):
"""
Prune small skin weights
Args:
mesh (str): Name of the mesh
threshold (float, optional): Threshold for pruning
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if mesh exists
if not cmds.objExists(mesh):
cmds.warning(f"Mesh model not found: {mesh}")
return False
# Get skin clusters
skin_clusters = self._get_skin_clusters(mesh)
if not skin_clusters:
cmds.warning(f"Mesh {mesh} is not bound to skeleton")
return False
# Prune weights
cmds.skinPercent(
skin_clusters[0],
mesh,
pruneWeights=threshold
)
print(f"Successfully pruned weights for {mesh}")
return True
except Exception as e:
cmds.warning(f"Error pruning weights: {str(e)}")
return False
def transfer_weights(self, source_mesh, target_mesh):
"""
Transfer skin weights from one mesh to another
Args:
source_mesh (str): Name of the source mesh
target_mesh (str): Name of the target mesh
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if meshes exist
if not cmds.objExists(source_mesh):
cmds.warning(f"Source mesh model not found: {source_mesh}")
return False
if not cmds.objExists(target_mesh):
cmds.warning(f"Target mesh model not found: {target_mesh}")
return False
# Get source skin cluster
source_skin_clusters = self._get_skin_clusters(source_mesh)
if not source_skin_clusters:
cmds.warning(f"Source mesh {source_mesh} is not bound to skeleton")
return False
# Get target skin cluster
target_skin_clusters = self._get_skin_clusters(target_mesh)
if not target_skin_clusters:
# Get joints from source skin cluster
joints = cmds.skinCluster(source_skin_clusters[0], query=True, influence=True)
# Bind target mesh to the same joints
target_skin_clusters = [cmds.skinCluster(
joints,
target_mesh,
name=f"{target_mesh}_skinCluster",
toSelectedBones=True,
bindMethod=0,
skinMethod=0,
normalizeWeights=2,
maximumInfluences=4,
obeyMaxInfluences=True,
dropoffRate=4.0
)[0]]
# Transfer weights
cmds.copySkinWeights(
sourceSkin=source_skin_clusters[0],
destinationSkin=target_skin_clusters[0],
noMirror=True,
surfaceAssociation="closestPoint",
influenceAssociation=["label", "name"]
)
print(f"Successfully transferred weights from {source_mesh} to {target_mesh}")
return True
except Exception as e:
cmds.warning(f"Error transferring weights: {str(e)}")
return False
def _get_all_joints(self, root_joint):
"""
Get all joints in a hierarchy
Args:
root_joint (str): Name of the root joint
Returns:
list: List of joint names
"""
try:
# Check if root joint exists
if not cmds.objExists(root_joint):
return []
# Get all joints in the hierarchy
joints = cmds.listRelatives(root_joint, allDescendents=True, type="joint") or []
joints.append(root_joint)
return joints
except Exception as e:
cmds.warning(f"Error getting joints: {str(e)}")
return []
def _get_skin_clusters(self, mesh):
"""
Get skin clusters for a mesh
Args:
mesh (str): Name of the mesh
Returns:
list: List of skin cluster names
"""
try:
# Check if mesh exists
if not cmds.objExists(mesh):
return []
# Get mesh shape
shapes = cmds.listRelatives(mesh, shapes=True, type="mesh")
if not shapes:
return []
mesh_shape = shapes[0]
# Get skin clusters
skin_clusters = []
# Get all deformers
deformers = cmds.listHistory(mesh_shape, pruneDagObjects=True)
if deformers:
# Filter for skin clusters
skin_clusters = cmds.ls(deformers, type="skinCluster")
return skin_clusters
except Exception as e:
cmds.warning(f"Error getting skin clusters: {str(e)}")
return []
def _get_skin_weights(self, mesh, skin_cluster):
"""
Get skin weights for a mesh
Args:
mesh (str): Name of the mesh
skin_cluster (str): Name of the skin cluster
Returns:
dict: Dictionary of skin weights
"""
try:
# Check if mesh and skin cluster exist
if not cmds.objExists(mesh) or not cmds.objExists(skin_cluster):
return {}
# Get influences
influences = cmds.skinCluster(skin_cluster, query=True, influence=True)
if not influences:
return {}
# Get vertex count
vertex_count = cmds.polyEvaluate(mesh, vertex=True)
# Get weights for each vertex
weights_data = {
"influences": influences,
"weights": {}
}
for i in range(vertex_count):
vertex_weights = {}
# Get weights for this vertex
for influence in influences:
weight = cmds.skinPercent(
skin_cluster,
f"{mesh}.vtx[{i}]",
query=True,
transform=influence
)
if weight > 0.0001: # Only store non-zero weights
vertex_weights[influence] = weight
weights_data["weights"][i] = vertex_weights
return weights_data
except Exception as e:
cmds.warning(f"Error getting skin weights: {str(e)}")
return {}
def _set_skin_weights(self, mesh, skin_cluster, weights_data):
"""
Set skin weights for a mesh
Args:
mesh (str): Name of the mesh
skin_cluster (str): Name of the skin cluster
weights_data (dict): Dictionary of skin weights
Returns:
bool: True if successful, False otherwise
"""
try:
# Check if mesh and skin cluster exist
if not cmds.objExists(mesh) or not cmds.objExists(skin_cluster):
return False
# Get influences from skin cluster
current_influences = cmds.skinCluster(skin_cluster, query=True, influence=True)
# Get influences from weights data
data_influences = weights_data.get("influences", [])
# Map influences if needed
influence_map = {}
for data_influence in data_influences:
# Check if influence exists
if cmds.objExists(data_influence) and data_influence in current_influences:
influence_map[data_influence] = data_influence
else:
# Try to find a matching influence by name
for current_influence in current_influences:
if current_influence.endswith(data_influence.split("|")[-1]):
influence_map[data_influence] = current_influence
break
# Set weights for each vertex
weights = weights_data.get("weights", {})
for vertex_index, vertex_weights in weights.items():
# Convert to transformation value pairs
transform_value_pairs = []
for influence, weight in vertex_weights.items():
# Map influence if needed
mapped_influence = influence_map.get(influence)
if mapped_influence:
transform_value_pairs.extend([mapped_influence, weight])
if transform_value_pairs:
# Set weights for this vertex
cmds.skinPercent(
skin_cluster,
f"{mesh}.vtx[{vertex_index}]",
transformValue=transform_value_pairs
)
# Normalize weights
cmds.skinPercent(skin_cluster, mesh, normalize=True)
return True
except Exception as e:
cmds.warning(f"Error setting skin weights: {str(e)}")
return False