MetaWhiz/scripts/utils/blendshape_utils.py

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 {}