#!/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 {}