diff --git a/scripts/utils/blendshape_utils.py b/scripts/utils/blendshape_utils.py index ddf6a95..0da348d 100644 --- a/scripts/utils/blendshape_utils.py +++ b/scripts/utils/blendshape_utils.py @@ -623,3 +623,314 @@ class BlendShapeUtils: 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 {}