937 lines
36 KiB
Python
937 lines
36 KiB
Python
#!/usr/bin/env python
|
|
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
Tool - BlendShape Utilities
|
|
Provides utilities for working with BlendShapes in Maya
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import json
|
|
import maya.cmds as cmds
|
|
import maya.mel as mel
|
|
|
|
# Import configuration
|
|
import config
|
|
|
|
class BlendShapeUtils:
|
|
"""Utilities for working with BlendShapes in Maya"""
|
|
|
|
def __init__(self):
|
|
"""Initialize BlendShape utilities"""
|
|
# MetaHuman BlendShape categories
|
|
self.mh_blendshape_categories = {
|
|
"FACS": ["browInnerUp", "browDown", "browOuterUp", "eyesClosed", "eyesLookUp", "eyesLookDown",
|
|
"eyesLookLeft", "eyesLookRight", "eyeWideLeft", "eyeWideRight", "cheekPuff", "cheekSquint",
|
|
"noseSneer", "jawOpen", "jawForward", "jawLeft", "jawRight", "mouthClose", "mouthFunnel",
|
|
"mouthPucker", "mouthLeft", "mouthRight", "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft",
|
|
"mouthFrownRight", "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight",
|
|
"mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper", "mouthPressLeft",
|
|
"mouthPressRight", "mouthLowerDownLeft", "mouthLowerDownRight", "mouthUpperUpLeft", "mouthUpperUpRight"],
|
|
"Expressions": ["browRaiseLeft", "browRaiseRight", "browRaiseBoth", "browLowerLeft", "browLowerRight",
|
|
"browLowerBoth", "eyeBlinkLeft", "eyeBlinkRight", "eyeSquintLeft", "eyeSquintRight",
|
|
"noseWrinkle", "nostrilDilate", "cheekRaiseLeft", "cheekRaiseRight", "mouthOpen",
|
|
"mouthSmile", "mouthFrown", "mouthSad", "mouthAngry", "mouthFear", "mouthSurprise",
|
|
"mouthDisgust", "mouthHappy", "mouthSad", "mouthWhistle", "tongueOut"],
|
|
"Phonemes": ["viseme_aa", "viseme_ch", "viseme_dd", "viseme_ee", "viseme_ff", "viseme_ih",
|
|
"viseme_kk", "viseme_nn", "viseme_oh", "viseme_ou", "viseme_pp", "viseme_rr",
|
|
"viseme_sh", "viseme_ss", "viseme_th", "viseme_uh"]
|
|
}
|
|
|
|
# Current working data
|
|
self.current_blendshape = None
|
|
self.current_base_mesh = None
|
|
|
|
def create_blendshape(self, base_mesh, target_meshes=None, name=None):
|
|
"""
|
|
Create a BlendShape deformer
|
|
|
|
Args:
|
|
base_mesh (str): Name of the base mesh
|
|
target_meshes (list, optional): List of target meshes
|
|
name (str, optional): Name of the BlendShape node
|
|
|
|
Returns:
|
|
str: Name of the created BlendShape node
|
|
"""
|
|
try:
|
|
# Check if base mesh exists
|
|
if not cmds.objExists(base_mesh):
|
|
cmds.warning(f"Base mesh not found: {base_mesh}")
|
|
return None
|
|
|
|
# Create name if not provided
|
|
if not name:
|
|
name = f"{base_mesh}_blendShape"
|
|
|
|
# Create BlendShape
|
|
if target_meshes:
|
|
blendshape = cmds.blendShape(target_meshes, base_mesh, name=name)[0]
|
|
else:
|
|
blendshape = cmds.blendShape(base_mesh, name=name)[0]
|
|
|
|
print(f"Successfully created BlendShape: {blendshape}")
|
|
return blendshape
|
|
except Exception as e:
|
|
cmds.warning(f"Error creating BlendShape: {str(e)}")
|
|
return None
|
|
|
|
def add_target(self, blendshape, target_mesh, target_name=None, target_weight=1.0):
|
|
"""
|
|
Add a target to a BlendShape
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_mesh (str): Name of the target mesh
|
|
target_name (str, optional): Name of the target
|
|
target_weight (float, optional): Weight of the target
|
|
|
|
Returns:
|
|
int: Index of the added target
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return -1
|
|
|
|
# Check if target mesh exists
|
|
if not cmds.objExists(target_mesh):
|
|
cmds.warning(f"Target mesh not found: {target_mesh}")
|
|
return -1
|
|
|
|
# Get base mesh
|
|
base_mesh = cmds.blendShape(blendshape, query=True, geometry=True)[0]
|
|
|
|
# Create target name if not provided
|
|
if not target_name:
|
|
target_name = target_mesh
|
|
|
|
# Get current target count
|
|
target_count = len(cmds.blendShape(blendshape, query=True, target=True) or [])
|
|
|
|
# Add target
|
|
cmds.blendShape(blendshape, edit=True, target=(base_mesh, target_count, target_mesh, 1.0))
|
|
|
|
# Rename target
|
|
cmds.aliasAttr(target_name, f"{blendshape}.weight[{target_count}]")
|
|
|
|
# Set target weight
|
|
cmds.setAttr(f"{blendshape}.{target_name}", target_weight)
|
|
|
|
print(f"Successfully added target {target_name} to {blendshape}")
|
|
return target_count
|
|
except Exception as e:
|
|
cmds.warning(f"Error adding target: {str(e)}")
|
|
return -1
|
|
|
|
def remove_target(self, blendshape, target_name):
|
|
"""
|
|
Remove a target from a BlendShape
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_name (str): Name of the target
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return False
|
|
|
|
# Get target index
|
|
target_index = self._get_target_index(blendshape, target_name)
|
|
|
|
if target_index == -1:
|
|
cmds.warning(f"Target not found: {target_name}")
|
|
return False
|
|
|
|
# Remove target
|
|
cmds.removeMultiInstance(f"{blendshape}.weight[{target_index}]", b=True)
|
|
cmds.aliasAttr(f"{blendshape}.weight[{target_index}]", remove=True)
|
|
|
|
print(f"Successfully removed target {target_name} from {blendshape}")
|
|
return True
|
|
except Exception as e:
|
|
cmds.warning(f"Error removing target: {str(e)}")
|
|
return False
|
|
|
|
def get_targets(self, blendshape):
|
|
"""
|
|
Get all targets in a BlendShape
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
|
|
Returns:
|
|
list: List of target names
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return []
|
|
|
|
# Get targets
|
|
targets = cmds.blendShape(blendshape, query=True, target=True) or []
|
|
|
|
return targets
|
|
except Exception as e:
|
|
cmds.warning(f"Error getting targets: {str(e)}")
|
|
return []
|
|
|
|
def set_target_weight(self, blendshape, target_name, weight):
|
|
"""
|
|
Set the weight of a target
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_name (str): Name of the target
|
|
weight (float): Weight value
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return False
|
|
|
|
# Get target index
|
|
target_index = self._get_target_index(blendshape, target_name)
|
|
|
|
if target_index == -1:
|
|
cmds.warning(f"Target not found: {target_name}")
|
|
return False
|
|
|
|
# Set weight
|
|
cmds.setAttr(f"{blendshape}.{target_name}", weight)
|
|
|
|
return True
|
|
except Exception as e:
|
|
cmds.warning(f"Error setting target weight: {str(e)}")
|
|
return False
|
|
|
|
def connect_to_controller(self, blendshape, target_name, controller, attribute=None):
|
|
"""
|
|
Connect a BlendShape target to a controller
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_name (str): Name of the target
|
|
controller (str): Name of the controller
|
|
attribute (str, optional): Name of the attribute on the controller
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return False
|
|
|
|
# Check if controller exists
|
|
if not cmds.objExists(controller):
|
|
cmds.warning(f"Controller not found: {controller}")
|
|
return False
|
|
|
|
# Get target index
|
|
target_index = self._get_target_index(blendshape, target_name)
|
|
|
|
if target_index == -1:
|
|
cmds.warning(f"Target not found: {target_name}")
|
|
return False
|
|
|
|
# Create attribute if not provided
|
|
if not attribute:
|
|
attribute = target_name
|
|
|
|
# Add attribute to controller if it doesn't exist
|
|
if not cmds.attributeQuery(attribute, node=controller, exists=True):
|
|
cmds.addAttr(controller, longName=attribute, attributeType="float", minValue=0.0, maxValue=1.0, defaultValue=0.0, keyable=True)
|
|
|
|
# Connect controller to BlendShape
|
|
cmds.connectAttr(f"{controller}.{attribute}", f"{blendshape}.{target_name}")
|
|
|
|
print(f"Successfully connected {target_name} to {controller}.{attribute}")
|
|
return True
|
|
except Exception as e:
|
|
cmds.warning(f"Error connecting to controller: {str(e)}")
|
|
return False
|
|
|
|
def batch_export_targets(self, blendshape, export_dir=None):
|
|
"""
|
|
Batch export all targets in a BlendShape
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
export_dir (str, optional): Directory to export targets to
|
|
|
|
Returns:
|
|
list: List of exported file paths
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return []
|
|
|
|
# Get base mesh
|
|
base_mesh = cmds.blendShape(blendshape, query=True, geometry=True)[0]
|
|
|
|
# Get targets
|
|
targets = self.get_targets(blendshape)
|
|
|
|
if not targets:
|
|
cmds.warning(f"BlendShape node has no targets: {blendshape}")
|
|
return []
|
|
|
|
# Create export directory if not provided
|
|
if not export_dir:
|
|
export_dir = os.path.join(cmds.workspace(query=True, rootDirectory=True), "exports", f"{blendshape}_targets")
|
|
|
|
if not os.path.exists(export_dir):
|
|
os.makedirs(export_dir)
|
|
|
|
# Export targets
|
|
exported_files = []
|
|
|
|
for target in targets:
|
|
# Create temporary mesh
|
|
temp_mesh = cmds.duplicate(base_mesh, name=f"{target}_export")[0]
|
|
|
|
# Reset all target weights
|
|
for other_target in targets:
|
|
cmds.setAttr(f"{blendshape}.{other_target}", 0.0)
|
|
|
|
# Set current target weight to 1.0
|
|
cmds.setAttr(f"{blendshape}.{target}", 1.0)
|
|
|
|
# Export mesh
|
|
export_path = os.path.join(export_dir, f"{target}.obj")
|
|
cmds.select(temp_mesh)
|
|
cmds.file(export_path, force=True, options="groups=0;ptgroups=0;materials=0;smoothing=0;normals=1", type="OBJexport", exportSelected=True)
|
|
|
|
# Delete temporary mesh
|
|
cmds.delete(temp_mesh)
|
|
|
|
exported_files.append(export_path)
|
|
|
|
# Reset all target weights
|
|
for target in targets:
|
|
cmds.setAttr(f"{blendshape}.{target}", 0.0)
|
|
|
|
print(f"Successfully exported {len(exported_files)} targets to {export_dir}")
|
|
return exported_files
|
|
except Exception as e:
|
|
cmds.warning(f"Error exporting targets: {str(e)}")
|
|
return []
|
|
|
|
def import_targets_from_directory(self, blendshape, import_dir):
|
|
"""
|
|
Import targets from a directory
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
import_dir (str): Directory to import targets from
|
|
|
|
Returns:
|
|
list: List of imported target names
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return []
|
|
|
|
# Check if directory exists
|
|
if not os.path.exists(import_dir):
|
|
cmds.warning(f"Import directory not found: {import_dir}")
|
|
return []
|
|
|
|
# Get base mesh
|
|
base_mesh = cmds.blendShape(blendshape, query=True, geometry=True)[0]
|
|
|
|
# Get all OBJ files in directory
|
|
obj_files = [f for f in os.listdir(import_dir) if f.lower().endswith(".obj")]
|
|
|
|
if not obj_files:
|
|
cmds.warning(f"Import directory has no OBJ files: {import_dir}")
|
|
return []
|
|
|
|
# Import targets
|
|
imported_targets = []
|
|
|
|
for obj_file in obj_files:
|
|
# Get target name from file name
|
|
target_name = os.path.splitext(obj_file)[0]
|
|
|
|
# Import mesh
|
|
imported_nodes = cmds.file(os.path.join(import_dir, obj_file), i=True, returnNewNodes=True)
|
|
|
|
# Get imported mesh
|
|
imported_mesh = None
|
|
|
|
for node in imported_nodes:
|
|
if cmds.objectType(node) == "transform" and cmds.listRelatives(node, shapes=True, type="mesh"):
|
|
imported_mesh = node
|
|
break
|
|
|
|
if not imported_mesh:
|
|
cmds.warning(f"Failed to import mesh: {obj_file}")
|
|
continue
|
|
|
|
# Add target to BlendShape
|
|
target_index = self.add_target(blendshape, imported_mesh, target_name, 0.0)
|
|
|
|
if target_index != -1:
|
|
imported_targets.append(target_name)
|
|
|
|
# Delete imported mesh
|
|
cmds.delete(imported_mesh)
|
|
|
|
print(f"Successfully imported {len(imported_targets)} targets to {blendshape}")
|
|
return imported_targets
|
|
except Exception as e:
|
|
cmds.warning(f"Error importing targets from directory: {str(e)}")
|
|
return []
|
|
|
|
def create_corrective_blendshape(self, base_mesh, deformed_mesh, name=None):
|
|
"""
|
|
Create a corrective BlendShape
|
|
|
|
Args:
|
|
base_mesh (str): Name of the base mesh
|
|
deformed_mesh (str): Name of the deformed mesh
|
|
name (str, optional): Name of the corrective BlendShape
|
|
|
|
Returns:
|
|
str: Name of the created corrective BlendShape
|
|
"""
|
|
try:
|
|
# Check if meshes exist
|
|
if not cmds.objExists(base_mesh):
|
|
cmds.warning(f"Base mesh not found: {base_mesh}")
|
|
return None
|
|
|
|
if not cmds.objExists(deformed_mesh):
|
|
cmds.warning(f"Deformed mesh not found: {deformed_mesh}")
|
|
return None
|
|
|
|
# Create name if not provided
|
|
if not name:
|
|
name = f"{base_mesh}_corrective"
|
|
|
|
# Duplicate base mesh
|
|
corrective_mesh = cmds.duplicate(base_mesh, name=f"{name}_target")[0]
|
|
|
|
# Create delta mesh
|
|
delta_node = cmds.createNode("plusMinusAverage", name=f"{name}_delta")
|
|
cmds.setAttr(f"{delta_node}.operation", 2) # Subtract
|
|
|
|
# Connect deformed mesh to delta
|
|
deformed_shape = cmds.listRelatives(deformed_mesh, shapes=True, type="mesh")[0]
|
|
cmds.connectAttr(f"{deformed_shape}.outMesh", f"{delta_node}.input3D[0].input3Dpt")
|
|
|
|
# Connect base mesh to delta
|
|
base_shape = cmds.listRelatives(base_mesh, shapes=True, type="mesh")[0]
|
|
cmds.connectAttr(f"{base_shape}.outMesh", f"{delta_node}.input3D[1].input3Dpt")
|
|
|
|
# Connect delta to corrective mesh
|
|
corrective_shape = cmds.listRelatives(corrective_mesh, shapes=True, type="mesh")[0]
|
|
cmds.connectAttr(f"{delta_node}.output3D", f"{corrective_shape}.inMesh")
|
|
|
|
# Break connections
|
|
cmds.disconnectAttr(f"{delta_node}.output3D", f"{corrective_shape}.inMesh")
|
|
|
|
# Create BlendShape
|
|
blendshape = self.create_blendshape(base_mesh, [corrective_mesh], name)
|
|
|
|
# Delete corrective mesh
|
|
cmds.delete(corrective_mesh)
|
|
|
|
print(f"Successfully created corrective BlendShape: {blendshape}")
|
|
return blendshape
|
|
except Exception as e:
|
|
cmds.warning(f"Error creating corrective BlendShape: {str(e)}")
|
|
return None
|
|
|
|
def mirror_target(self, blendshape, target_name, axis="X", new_target_name=None):
|
|
"""
|
|
Mirror a BlendShape target
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_name (str): Name of the target to mirror
|
|
axis (str, optional): Axis to mirror across (X, Y, or Z)
|
|
new_target_name (str, optional): Name of the new mirrored target
|
|
|
|
Returns:
|
|
str: Name of the mirrored target
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return None
|
|
|
|
# Get target index
|
|
target_index = self._get_target_index(blendshape, target_name)
|
|
|
|
if target_index == -1:
|
|
cmds.warning(f"Target not found: {target_name}")
|
|
return None
|
|
|
|
# Get base mesh
|
|
base_mesh = cmds.blendShape(blendshape, query=True, geometry=True)[0]
|
|
|
|
# Create temporary meshes
|
|
temp_base = cmds.duplicate(base_mesh, name="temp_base")[0]
|
|
temp_target = cmds.duplicate(base_mesh, name="temp_target")[0]
|
|
|
|
# Set target weight to 1.0
|
|
cmds.setAttr(f"{blendshape}.{target_name}", 1.0)
|
|
|
|
# Create mirror mesh
|
|
mirror_mesh = cmds.duplicate(base_mesh, name="mirror_mesh")[0]
|
|
|
|
# Reset target weight
|
|
cmds.setAttr(f"{blendshape}.{target_name}", 0.0)
|
|
|
|
# Mirror mesh
|
|
scale_vector = [1, 1, 1]
|
|
|
|
if axis.upper() == "X":
|
|
scale_vector[0] = -1
|
|
elif axis.upper() == "Y":
|
|
scale_vector[1] = -1
|
|
elif axis.upper() == "Z":
|
|
scale_vector[2] = -1
|
|
|
|
cmds.scale(scale_vector[0], scale_vector[1], scale_vector[2], mirror_mesh)
|
|
|
|
# Create mirrored target name if not provided
|
|
if not new_target_name:
|
|
# Replace left/right prefixes
|
|
if target_name.startswith("l_"):
|
|
new_target_name = f"r_{target_name[2:]}"
|
|
elif target_name.startswith("r_"):
|
|
new_target_name = f"l_{target_name[2:]}"
|
|
elif "_l_" in target_name:
|
|
new_target_name = target_name.replace("_l_", "_r_")
|
|
elif "_r_" in target_name:
|
|
new_target_name = target_name.replace("_r_", "_l_")
|
|
elif "_left_" in target_name.lower():
|
|
new_target_name = target_name.lower().replace("_left_", "_right_")
|
|
elif "_right_" in target_name.lower():
|
|
new_target_name = target_name.lower().replace("_right_", "_left_")
|
|
else:
|
|
new_target_name = f"{target_name}_mirrored"
|
|
|
|
# Add mirrored target to BlendShape
|
|
target_index = self.add_target(blendshape, mirror_mesh, new_target_name, 0.0)
|
|
|
|
# Delete temporary meshes
|
|
cmds.delete(temp_base, temp_target, mirror_mesh)
|
|
|
|
if target_index != -1:
|
|
print(f"Successfully mirrored target {target_name} to {new_target_name}")
|
|
return new_target_name
|
|
else:
|
|
return None
|
|
except Exception as e:
|
|
cmds.warning(f"Error mirroring target: {str(e)}")
|
|
return None
|
|
|
|
def _get_target_index(self, blendshape, target_name):
|
|
"""
|
|
Get the index of a target
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_name (str): Name of the target
|
|
|
|
Returns:
|
|
int: Index of the target, or -1 if not found
|
|
"""
|
|
try:
|
|
# Check if target exists
|
|
if not cmds.attributeQuery(target_name, node=blendshape, exists=True):
|
|
return -1
|
|
|
|
# Get weight alias
|
|
weight_alias = cmds.aliasAttr(f"{blendshape}.{target_name}", query=True)
|
|
|
|
# Extract index from weight alias
|
|
if weight_alias:
|
|
import re
|
|
match = re.search(r"weight\[(\d+)\]", weight_alias)
|
|
|
|
if match:
|
|
return int(match.group(1))
|
|
|
|
return -1
|
|
except Exception as e:
|
|
cmds.warning(f"Error getting target index: {str(e)}")
|
|
return -1
|
|
|
|
def create_inbetween_target(self, blendshape, target_name, inbetween_mesh, weight=0.5):
|
|
"""
|
|
Create an in-between target
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_name (str): Name of the target
|
|
inbetween_mesh (str): Name of the in-between mesh
|
|
weight (float, optional): Weight of the in-between target
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return False
|
|
|
|
# Get target index
|
|
target_index = self._get_target_index(blendshape, target_name)
|
|
|
|
if target_index == -1:
|
|
cmds.warning(f"Target not found: {target_name}")
|
|
return False
|
|
|
|
# Check if in-between mesh exists
|
|
if not cmds.objExists(inbetween_mesh):
|
|
cmds.warning(f"In-between mesh not found: {inbetween_mesh}")
|
|
return False
|
|
|
|
# Get base mesh
|
|
base_mesh = cmds.blendShape(blendshape, query=True, geometry=True)[0]
|
|
|
|
# Add in-between target
|
|
cmds.blendShape(blendshape, edit=True, target=(base_mesh, target_index, inbetween_mesh, weight))
|
|
|
|
print(f"Successfully added in-between target {inbetween_mesh} to {target_name} at weight {weight}")
|
|
return True
|
|
except Exception as e:
|
|
cmds.warning(f"Error creating in-between target: {str(e)}")
|
|
return False
|
|
|
|
def categorize_targets(self, blendshape):
|
|
"""
|
|
Categorize BlendShape targets based on MetaHuman categories
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
|
|
Returns:
|
|
dict: Dictionary of categorized targets
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return {}
|
|
|
|
# Get all targets
|
|
targets = self.get_targets(blendshape)
|
|
|
|
if not targets:
|
|
return {}
|
|
|
|
# Initialize categories
|
|
categorized = {
|
|
"FACS": [],
|
|
"Expressions": [],
|
|
"Phonemes": [],
|
|
"Other": []
|
|
}
|
|
|
|
# Categorize targets
|
|
for target in targets:
|
|
categorized_flag = False
|
|
|
|
# Check each category
|
|
for category, category_targets in self.mh_blendshape_categories.items():
|
|
for cat_target in category_targets:
|
|
if cat_target.lower() in target.lower():
|
|
categorized[category].append(target)
|
|
categorized_flag = True
|
|
break
|
|
|
|
if categorized_flag:
|
|
break
|
|
|
|
# If not categorized, add to Other
|
|
if not categorized_flag:
|
|
categorized["Other"].append(target)
|
|
|
|
return categorized
|
|
except Exception as e:
|
|
cmds.warning(f"Error categorizing targets: {str(e)}")
|
|
return {}
|
|
|
|
def create_metahuman_blendshape_set(self, base_mesh, name=None):
|
|
"""
|
|
Create a standard MetaHuman BlendShape set with predefined categories
|
|
|
|
Args:
|
|
base_mesh (str): Name of the base mesh
|
|
name (str, optional): Name of the BlendShape node
|
|
|
|
Returns:
|
|
str: Name of the created BlendShape node
|
|
"""
|
|
try:
|
|
# Check if base mesh exists
|
|
if not cmds.objExists(base_mesh):
|
|
cmds.warning(f"Base mesh not found: {base_mesh}")
|
|
return None
|
|
|
|
# Create name if not provided
|
|
if not name:
|
|
name = f"{base_mesh}_metahuman_blendShape"
|
|
|
|
# Create BlendShape
|
|
blendshape = self.create_blendshape(base_mesh, None, name)
|
|
|
|
if not blendshape:
|
|
return None
|
|
|
|
# Create standard MetaHuman targets (placeholder targets)
|
|
# In a real implementation, you would create actual target meshes
|
|
# Here we're just creating the attributes
|
|
|
|
# Add FACS targets
|
|
for target in self.mh_blendshape_categories["FACS"]:
|
|
# Create a temporary duplicate of the base mesh
|
|
temp_mesh = cmds.duplicate(base_mesh, name=f"temp_{target}")[0]
|
|
|
|
# Add as target
|
|
self.add_target(blendshape, temp_mesh, target, 0.0)
|
|
|
|
# Delete temporary mesh
|
|
cmds.delete(temp_mesh)
|
|
|
|
# Store as current working blendshape
|
|
self.current_blendshape = blendshape
|
|
self.current_base_mesh = base_mesh
|
|
|
|
print(f"Successfully created MetaHuman BlendShape set: {blendshape}")
|
|
return blendshape
|
|
except Exception as e:
|
|
cmds.warning(f"Error creating MetaHuman BlendShape set: {str(e)}")
|
|
return None
|
|
|
|
def import_metahuman_blendshapes(self, blendshape, import_dir):
|
|
"""
|
|
Import MetaHuman BlendShapes from a directory with automatic categorization
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
import_dir (str): Directory to import from
|
|
|
|
Returns:
|
|
dict: Dictionary of imported targets by category
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return {}
|
|
|
|
# Check if directory exists
|
|
if not os.path.exists(import_dir):
|
|
cmds.warning(f"Import directory not found: {import_dir}")
|
|
return {}
|
|
|
|
# Get all mesh files in the directory
|
|
mesh_files = []
|
|
for file in os.listdir(import_dir):
|
|
file_path = os.path.join(import_dir, file)
|
|
if os.path.isfile(file_path) and file.lower().endswith((".obj", ".fbx", ".ma", ".mb")):
|
|
mesh_files.append(file_path)
|
|
|
|
if not mesh_files:
|
|
cmds.warning(f"No mesh files found in: {import_dir}")
|
|
return {}
|
|
|
|
# Initialize import results
|
|
import_results = {
|
|
"FACS": [],
|
|
"Expressions": [],
|
|
"Phonemes": [],
|
|
"Other": []
|
|
}
|
|
|
|
# Import each file
|
|
for file_path in mesh_files:
|
|
try:
|
|
# Get file name without extension
|
|
file_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
|
|
# Import mesh
|
|
if file_path.lower().endswith(".obj"):
|
|
imported_nodes = cmds.file(file_path, i=True, type="OBJ", returnNewNodes=True)
|
|
elif file_path.lower().endswith(".fbx"):
|
|
imported_nodes = cmds.file(file_path, i=True, type="FBX", returnNewNodes=True)
|
|
else: # Maya files
|
|
imported_nodes = cmds.file(file_path, i=True, returnNewNodes=True)
|
|
|
|
# Find mesh in imported nodes
|
|
imported_mesh = None
|
|
for node in imported_nodes:
|
|
if cmds.objectType(node) == "transform" and cmds.listRelatives(node, shapes=True, type="mesh"):
|
|
imported_mesh = node
|
|
break
|
|
|
|
if not imported_mesh:
|
|
cmds.warning(f"No mesh found in imported file: {file_path}")
|
|
continue
|
|
|
|
# Add as target
|
|
target_name = file_name
|
|
self.add_target(blendshape, imported_mesh, target_name, 0.0)
|
|
|
|
# Delete imported mesh
|
|
cmds.delete(imported_mesh)
|
|
|
|
# Categorize target
|
|
categorized = False
|
|
for category, category_targets in self.mh_blendshape_categories.items():
|
|
for cat_target in category_targets:
|
|
if cat_target.lower() in target_name.lower():
|
|
import_results[category].append(target_name)
|
|
categorized = True
|
|
break
|
|
|
|
if categorized:
|
|
break
|
|
|
|
# If not categorized, add to Other
|
|
if not categorized:
|
|
import_results["Other"].append(target_name)
|
|
except Exception as e:
|
|
cmds.warning(f"Error importing file {file_path}: {str(e)}")
|
|
|
|
return import_results
|
|
except Exception as e:
|
|
cmds.warning(f"Error importing MetaHuman BlendShapes: {str(e)}")
|
|
return {}
|
|
|
|
def connect_to_rig(self, blendshape, controller):
|
|
"""
|
|
Connect all BlendShape targets to a controller with organized attributes
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
controller (str): Name of the controller
|
|
|
|
Returns:
|
|
bool: True if successful, False otherwise
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return False
|
|
|
|
# Check if controller exists
|
|
if not cmds.objExists(controller):
|
|
cmds.warning(f"Controller not found: {controller}")
|
|
return False
|
|
|
|
# Categorize targets
|
|
categorized = self.categorize_targets(blendshape)
|
|
|
|
if not categorized:
|
|
cmds.warning(f"No targets found in BlendShape: {blendshape}")
|
|
return False
|
|
|
|
# Create category attributes on controller
|
|
for category in categorized.keys():
|
|
if not cmds.attributeQuery(category, node=controller, exists=True):
|
|
cmds.addAttr(controller, longName=category, attributeType="enum", enumName="-----", keyable=True)
|
|
|
|
# Connect targets by category
|
|
for category, targets in categorized.items():
|
|
for target in targets:
|
|
# Create attribute if it doesn't exist
|
|
if not cmds.attributeQuery(target, node=controller, exists=True):
|
|
cmds.addAttr(controller, longName=target, attributeType="float", min=0.0, max=1.0, defaultValue=0.0, keyable=True)
|
|
|
|
# Connect attribute to BlendShape target
|
|
cmds.connectAttr(f"{controller}.{target}", f"{blendshape}.{target}", force=True)
|
|
|
|
print(f"Successfully connected {blendshape} to {controller}")
|
|
return True
|
|
except Exception as e:
|
|
cmds.warning(f"Error connecting to rig: {str(e)}")
|
|
return False
|
|
|
|
def extract_target_deltas(self, blendshape, target_name, output_file=None):
|
|
"""
|
|
Extract vertex deltas from a BlendShape target
|
|
|
|
Args:
|
|
blendshape (str): Name of the BlendShape node
|
|
target_name (str): Name of the target
|
|
output_file (str, optional): Path to save deltas to JSON
|
|
|
|
Returns:
|
|
dict: Dictionary of vertex deltas
|
|
"""
|
|
try:
|
|
# Check if BlendShape exists
|
|
if not cmds.objExists(blendshape):
|
|
cmds.warning(f"BlendShape node not found: {blendshape}")
|
|
return {}
|
|
|
|
# Get target index
|
|
target_index = self._get_target_index(blendshape, target_name)
|
|
|
|
if target_index == -1:
|
|
cmds.warning(f"Target not found: {target_name}")
|
|
return {}
|
|
|
|
# Get base mesh
|
|
base_mesh = cmds.blendShape(blendshape, query=True, geometry=True)[0]
|
|
|
|
# Get vertex count
|
|
vertex_count = cmds.polyEvaluate(base_mesh, vertex=True)
|
|
|
|
# Get deltas
|
|
deltas = {}
|
|
for i in range(vertex_count):
|
|
# Get target weight points
|
|
point_data = cmds.getAttr(f"{blendshape}.inputTarget[0].inputTargetGroup[{target_index}].targetPoints[{i}]")
|
|
|
|
# Only store non-zero deltas
|
|
if point_data[0][0] != 0 or point_data[0][1] != 0 or point_data[0][2] != 0:
|
|
deltas[i] = {
|
|
"x": point_data[0][0],
|
|
"y": point_data[0][1],
|
|
"z": point_data[0][2]
|
|
}
|
|
|
|
# Save to file if requested
|
|
if output_file:
|
|
with open(output_file, "w") as f:
|
|
json.dump({
|
|
"blendshape": blendshape,
|
|
"target": target_name,
|
|
"deltas": deltas
|
|
}, f, indent=2)
|
|
|
|
return deltas
|
|
except Exception as e:
|
|
cmds.warning(f"Error extracting target deltas: {str(e)}")
|
|
return {}
|