#!/usr/bin/env python # -*- coding: utf-8 -*- """ Tool - Joint Utilities Provides utilities for working with joints and skeletons in Maya """ import os import sys import json import maya.cmds as cmds import maya.mel as mel # Import configuration try: import config except ImportError: # Try relative import if direct import fails try: from ... import config except ImportError: # If both fail, try to get from sys.modules import sys if 'config' in sys.modules: config = sys.modules['config'] else: raise ImportError("Could not import config module") class JointUtils: """Utilities for working with joints and skeletons in Maya""" def __init__(self): """Initialize joint utilities""" # MetaHuman joint mapping for different versions self.mh_joint_mapping = { "MH.4": { # Core skeleton joints "root": "root", "spine": "spine_01", "spine1": "spine_02", "spine2": "spine_03", "neck": "neck_01", "head": "head", # Left arm "l_shoulder": "clavicle_l", "l_arm": "upperarm_l", "l_elbow": "lowerarm_l", "l_wrist": "hand_l", # Right arm "r_shoulder": "clavicle_r", "r_arm": "upperarm_r", "r_elbow": "lowerarm_r", "r_wrist": "hand_r", # Left leg "l_hip": "thigh_l", "l_knee": "calf_l", "l_ankle": "foot_l", "l_toe": "ball_l", # Right leg "r_hip": "thigh_r", "r_knee": "calf_r", "r_ankle": "foot_r", "r_toe": "ball_r", # Face joints (core) "jaw": "jaw", "l_eye": "eye_l", "r_eye": "eye_r" }, "MH.3": { # Similar structure with some naming differences "root": "root", "spine": "spine_01", "spine1": "spine_02", "spine2": "spine_03", "neck": "neck_01", "head": "head", # Left arm "l_shoulder": "clavicle_l", "l_arm": "upperarm_l", "l_elbow": "lowerarm_l", "l_wrist": "hand_l", # Right arm "r_shoulder": "clavicle_r", "r_arm": "upperarm_r", "r_elbow": "lowerarm_r", "r_wrist": "hand_r", # Left leg "l_hip": "thigh_l", "l_knee": "calf_l", "l_ankle": "foot_l", "l_toe": "ball_l", # Right leg "r_hip": "thigh_r", "r_knee": "calf_r", "r_ankle": "foot_r", "r_toe": "ball_r", # Face joints (core) "jaw": "jaw", "l_eye": "eye_l", "r_eye": "eye_r" } } # Current calibration data self.current_calibration = { "reference": None, "target": None, "mapping": {}, "status": {} } def create_metahuman_skeleton(self, mesh): """ Create a MetaHuman skeleton for a mesh 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 does not exist: {mesh}") return False # Get mesh shape shapes = cmds.listRelatives(mesh, shapes=True, type="mesh") if not shapes: cmds.warning(f"Object is not a mesh: {mesh}") return False # Create root joint root_joint = cmds.joint(name=f"{mesh}_root", position=(0, 0, 0)) # Create MetaHuman skeleton structure # This is a simplified version - a real implementation would create # a complete skeleton based on MetaHuman joint structure spine_joint = cmds.joint(name=f"{mesh}_spine", position=(0, 100, 0)) neck_joint = cmds.joint(name=f"{mesh}_neck", position=(0, 120, 0)) head_joint = cmds.joint(name=f"{mesh}_head", position=(0, 140, 0)) # Return to spine to create left arm cmds.select(spine_joint) l_shoulder_joint = cmds.joint(name=f"{mesh}_l_shoulder", position=(20, 110, 0)) l_elbow_joint = cmds.joint(name=f"{mesh}_l_elbow", position=(40, 100, 0)) l_wrist_joint = cmds.joint(name=f"{mesh}_l_wrist", position=(60, 90, 0)) # Return to spine to create right arm cmds.select(spine_joint) r_shoulder_joint = cmds.joint(name=f"{mesh}_r_shoulder", position=(-20, 110, 0)) r_elbow_joint = cmds.joint(name=f"{mesh}_r_elbow", position=(-40, 100, 0)) r_wrist_joint = cmds.joint(name=f"{mesh}_r_wrist", position=(-60, 90, 0)) # Return to root to create left leg cmds.select(root_joint) l_hip_joint = cmds.joint(name=f"{mesh}_l_hip", position=(10, 80, 0)) l_knee_joint = cmds.joint(name=f"{mesh}_l_knee", position=(10, 40, 0)) l_ankle_joint = cmds.joint(name=f"{mesh}_l_ankle", position=(10, 10, 0)) # Return to root to create right leg cmds.select(root_joint) r_hip_joint = cmds.joint(name=f"{mesh}_r_hip", position=(-10, 80, 0)) r_knee_joint = cmds.joint(name=f"{mesh}_r_knee", position=(-10, 40, 0)) r_ankle_joint = cmds.joint(name=f"{mesh}_r_ankle", position=(-10, 10, 0)) # Orient joints cmds.select(root_joint) cmds.joint(edit=True, orientJoint="xyz", secondaryAxisOrient="yup") # Center skeleton to mesh self._center_skeleton_to_mesh(root_joint, mesh) print(f"Successfully created MetaHuman skeleton for {mesh}") return True except Exception as e: cmds.warning(f"Error creating MetaHuman skeleton: {str(e)}") return False def load_metahuman_reference(self): """ Load MetaHuman reference skeleton Returns: bool: True if successful, False otherwise """ try: # Check if reference file exists reference_file = os.path.join(config.METAHUMAN_PATH, "reference", "metahuman_skeleton.mb") if not os.path.exists(reference_file): cmds.warning(f"MetaHuman reference skeleton file does not exist: {reference_file}") return False # Import reference file cmds.file(reference_file, reference=True, namespace="metahuman_reference") print(f"Successfully loaded MetaHuman reference skeleton") return True except Exception as e: cmds.warning(f"Error loading MetaHuman reference skeleton: {str(e)}") return False def load_custom_reference(self, file_path): """ Load custom reference skeleton Args: file_path (str): Path to the reference file Returns: bool: True if successful, False otherwise """ try: # Check if file exists if not os.path.exists(file_path): cmds.warning(f"Reference skeleton file does not exist: {file_path}") return False # Determine file type if file_path.lower().endswith(".json"): # Load from JSON with open(file_path, "r") as f: reference_data = json.load(f) # Create reference skeleton from JSON data self._create_skeleton_from_json(reference_data) else: # Import reference file cmds.file(file_path, reference=True, namespace="custom_reference") print(f"Successfully loaded custom reference skeleton: {file_path}") return True except Exception as e: cmds.warning(f"Error loading custom reference skeleton: {str(e)}") return False def auto_calibrate(self, skeleton): """ Auto-calibrate a skeleton Args: skeleton (str): Name of the skeleton root Returns: bool: True if successful, False otherwise """ try: # Check if skeleton exists if not cmds.objExists(skeleton): cmds.warning(f"骨架不存在: {skeleton}") return False # Get reference skeleton reference_skeleton = None references = cmds.ls("*:*_root", type="joint") if not references: cmds.warning("未找到参考骨架,请先加载参考骨架") return False reference_skeleton = references[0] # Get all joints in the skeleton joints = self._get_all_joints(skeleton) reference_joints = self._get_all_joints(reference_skeleton) # Map joints by name joint_map = {} for joint in joints: joint_name = joint.split("|")[-1].split(":")[-1] # Find matching reference joint for ref_joint in reference_joints: ref_joint_name = ref_joint.split("|")[-1].split(":")[-1] if joint_name == ref_joint_name: joint_map[joint] = ref_joint break # Calibrate joints for joint, ref_joint in joint_map.items(): # Get reference joint position ref_pos = cmds.xform(ref_joint, query=True, worldSpace=True, translation=True) # Set joint position to reference cmds.xform(joint, worldSpace=True, translation=ref_pos) # Get reference joint orientation ref_rot = cmds.xform(ref_joint, query=True, worldSpace=True, rotation=True) # Set joint orientation to reference cmds.xform(joint, worldSpace=True, rotation=ref_rot) print(f"成功自动校准骨架: {skeleton}") return True except Exception as e: cmds.warning(f"自动校准骨架出错: {str(e)}") return False def show_manual_calibration_ui(self, joint): """ Show manual calibration UI for a joint Args: joint (str): Name of the joint Returns: bool: True if successful, False otherwise """ try: # Check if joint exists if not cmds.objExists(joint): cmds.warning(f"关节不存在: {joint}") return False # Create UI if cmds.window("manualCalibrationUI", exists=True): cmds.deleteUI("manualCalibrationUI") window = cmds.window("manualCalibrationUI", title=f"手动校准 - {joint}", width=300) cmds.columnLayout(adjustableColumn=True) # Position controls cmds.frameLayout(label="位置", collapsable=True, collapse=False) cmds.columnLayout(adjustableColumn=True) pos = cmds.xform(joint, query=True, worldSpace=True, translation=True) cmds.floatSliderGrp("posX", label="X", field=True, minValue=-100, maxValue=100, value=pos[0], step=0.1, changeCommand=lambda x: self._update_joint_position(joint, 0, x)) cmds.floatSliderGrp("posY", label="Y", field=True, minValue=-100, maxValue=100, value=pos[1], step=0.1, changeCommand=lambda x: self._update_joint_position(joint, 1, x)) cmds.floatSliderGrp("posZ", label="Z", field=True, minValue=-100, maxValue=100, value=pos[2], step=0.1, changeCommand=lambda x: self._update_joint_position(joint, 2, x)) cmds.setParent("..") cmds.setParent("..") # Rotation controls cmds.frameLayout(label="旋转", collapsable=True, collapse=False) cmds.columnLayout(adjustableColumn=True) rot = cmds.xform(joint, query=True, worldSpace=True, rotation=True) cmds.floatSliderGrp("rotX", label="X", field=True, minValue=-180, maxValue=180, value=rot[0], step=1, changeCommand=lambda x: self._update_joint_rotation(joint, 0, x)) cmds.floatSliderGrp("rotY", label="Y", field=True, minValue=-180, maxValue=180, value=rot[1], step=1, changeCommand=lambda x: self._update_joint_rotation(joint, 1, x)) cmds.floatSliderGrp("rotZ", label="Z", field=True, minValue=-180, maxValue=180, value=rot[2], step=1, changeCommand=lambda x: self._update_joint_rotation(joint, 2, x)) cmds.setParent("..") cmds.setParent("..") # Buttons cmds.rowLayout(numberOfColumns=3, columnWidth3=(100, 100, 100), adjustableColumn=2, columnAlign=(1, "center"), columnAttach=[(1, "both", 0), (2, "both", 0), (3, "both", 0)]) cmds.button(label="重置", command=lambda x: self._reset_joint(joint)) cmds.button(label="应用", command=lambda x: self._apply_joint_calibration(joint)) cmds.button(label="关闭", command=lambda x: cmds.deleteUI("manualCalibrationUI")) cmds.setParent("..") cmds.showWindow(window) return True except Exception as e: cmds.warning(f"显示手动校准界面出错: {str(e)}") return False def reset_calibration(self, skeleton): """ Reset calibration for a skeleton Args: skeleton (str): Name of the skeleton root Returns: bool: True if successful, False otherwise """ try: # Check if skeleton exists if not cmds.objExists(skeleton): cmds.warning(f"骨架不存在: {skeleton}") return False # Get all joints in the skeleton joints = self._get_all_joints(skeleton) # Reset joint positions and orientations for joint in joints: # Reset position cmds.setAttr(f"{joint}.translateX", 0) cmds.setAttr(f"{joint}.translateY", 0) cmds.setAttr(f"{joint}.translateZ", 0) # Reset rotation cmds.setAttr(f"{joint}.rotateX", 0) cmds.setAttr(f"{joint}.rotateY", 0) cmds.setAttr(f"{joint}.rotateZ", 0) # Reset scale cmds.setAttr(f"{joint}.scaleX", 1) cmds.setAttr(f"{joint}.scaleY", 1) cmds.setAttr(f"{joint}.scaleZ", 1) print(f"成功重置骨架校准: {skeleton}") return True except Exception as e: cmds.warning(f"重置骨架校准出错: {str(e)}") return False def apply_calibration(self, skeleton): """ Apply calibration to a skeleton Args: skeleton (str): Name of the skeleton root Returns: bool: True if successful, False otherwise """ try: # Check if skeleton exists if not cmds.objExists(skeleton): cmds.warning(f"骨架不存在: {skeleton}") return False # Get all joints in the skeleton joints = self._get_all_joints(skeleton) # Apply calibration for joint in joints: # Store current position and orientation pos = cmds.xform(joint, query=True, worldSpace=True, translation=True) rot = cmds.xform(joint, query=True, worldSpace=True, rotation=True) # Store calibration data calibration_data = { "position": pos, "rotation": rot } # Add calibration data as attributes if not cmds.attributeQuery("calibrationData", node=joint, exists=True): cmds.addAttr(joint, longName="calibrationData", dataType="string") cmds.setAttr(f"{joint}.calibrationData", json.dumps(calibration_data), type="string") print(f"Successfully applied skeleton calibration: {skeleton}") return True except Exception as e: cmds.warning(f"Error applying skeleton calibration: {str(e)}") return False def get_joints_info(self, skeleton): """ Get information about joints in a skeleton Args: skeleton (str): Name of the skeleton root Returns: dict: Dictionary of joint information """ try: # Check if skeleton exists if not cmds.objExists(skeleton): cmds.warning(f"Skeleton does not exist: {skeleton}") return {} # Get all joints in the skeleton joints = self._get_all_joints(skeleton) # Get joint information joints_info = {} for joint in joints: # Get joint name joint_name = joint.split("|")[-1] # Get joint position pos = cmds.xform(joint, query=True, worldSpace=True, translation=True) # Get joint rotation rot = cmds.xform(joint, query=True, worldSpace=True, rotation=True) # Get calibration status status = "未校准" if cmds.attributeQuery("calibrationData", node=joint, exists=True): status = "已校准" # Add joint information joints_info[joint_name] = { "position": pos, "rotation": rot, "status": status } return joints_info except Exception as e: cmds.warning(f"Error getting joint information: {str(e)}") return {} def update_visualization(self, show_reference=True, show_current=True, show_differences=True): """ Update visualization of skeletons Args: show_reference (bool, optional): Show reference skeleton show_current (bool, optional): Show current skeleton show_differences (bool, optional): Show differences between skeletons Returns: bool: True if successful, False otherwise """ try: # Get reference skeleton reference_skeleton = None references = cmds.ls("*:*_root", type="joint") if references: reference_skeleton = references[0] # Get current skeleton current_skeleton = None skeletons = cmds.ls("*_root", type="joint") if skeletons: # Filter out reference skeletons for skeleton in skeletons: if ":" not in skeleton: current_skeleton = skeleton break # Show/hide reference skeleton if reference_skeleton: cmds.setAttr(f"{reference_skeleton}.visibility", show_reference) # Show/hide current skeleton if current_skeleton: cmds.setAttr(f"{current_skeleton}.visibility", show_current) # Show/hide differences if show_differences and reference_skeleton and current_skeleton: self._visualize_differences(reference_skeleton, current_skeleton) else: # Remove difference visualization if cmds.objExists("difference_visualization"): cmds.delete("difference_visualization") return True except Exception as e: cmds.warning(f"Error updating visualization: {str(e)}") return False def _center_skeleton_to_mesh(self, root_joint, mesh): """ Center a skeleton to a mesh Args: root_joint (str): Name of the root joint mesh (str): Name of the mesh Returns: bool: True if successful, False otherwise """ try: # Get mesh bounding box bbox = cmds.exactWorldBoundingBox(mesh) # Calculate center center_x = (bbox[0] + bbox[3]) / 2 center_y = bbox[1] # Bottom of the mesh center_z = (bbox[2] + bbox[5]) / 2 # Move root joint to center cmds.xform(root_joint, worldSpace=True, translation=(center_x, center_y, center_z)) return True except Exception as e: cmds.warning(f"Error centering skeleton to mesh: {str(e)}") return False def _create_skeleton_from_json(self, data): """ Create a skeleton from JSON data Args: data (dict): Skeleton data Returns: bool: True if successful, False otherwise """ try: # Create root joint root_joint = cmds.joint(name=data["root"]["name"], position=data["root"]["position"]) # Create child joints self._create_child_joints(root_joint, data["root"]["children"]) # Orient joints cmds.select(root_joint) cmds.joint(edit=True, orientJoint="xyz", secondaryAxisOrient="yup") return True except Exception as e: cmds.warning(f"Error creating skeleton from JSON: {str(e)}") return False def _create_child_joints(self, parent_joint, children): """ Create child joints Args: parent_joint (str): Name of the parent joint children (list): List of child joint data Returns: bool: True if successful, False otherwise """ try: for child in children: # Select parent cmds.select(parent_joint) # Create child joint child_joint = cmds.joint(name=child["name"], position=child["position"]) # Create grandchildren if "children" in child: self._create_child_joints(child_joint, child["children"]) return True except Exception as e: cmds.warning(f"Error creating child joints: {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 _update_joint_position(self, joint, axis, value): """ Update joint position Args: joint (str): Name of the joint axis (int): Axis index (0=X, 1=Y, 2=Z) value (float): New value Returns: bool: True if successful, False otherwise """ try: # Get current position pos = cmds.xform(joint, query=True, worldSpace=True, translation=True) # Update position pos[axis] = value # Set new position cmds.xform(joint, worldSpace=True, translation=pos) return True except Exception as e: cmds.warning(f"Error updating joint position: {str(e)}") return False def _update_joint_rotation(self, joint, axis, value): """ Update joint rotation Args: joint (str): Name of the joint axis (int): Axis index (0=X, 1=Y, 2=Z) value (float): New value Returns: bool: True if successful, False otherwise """ try: # Get current rotation rot = cmds.xform(joint, query=True, worldSpace=True, rotation=True) # Update rotation rot[axis] = value # Set new rotation cmds.xform(joint, worldSpace=True, rotation=rot) return True except Exception as e: cmds.warning(f"Error updating joint rotation: {str(e)}") return False def _reset_joint(self, joint): """ Reset joint position and rotation Args: joint (str): Name of the joint Returns: bool: True if successful, False otherwise """ try: # Reset position cmds.setAttr(f"{joint}.translateX", 0) cmds.setAttr(f"{joint}.translateY", 0) cmds.setAttr(f"{joint}.translateZ", 0) # Reset rotation cmds.setAttr(f"{joint}.rotateX", 0) cmds.setAttr(f"{joint}.rotateY", 0) cmds.setAttr(f"{joint}.rotateZ", 0) # Update UI if cmds.window("manualCalibrationUI", exists=True): pos = cmds.xform(joint, query=True, worldSpace=True, translation=True) rot = cmds.xform(joint, query=True, worldSpace=True, rotation=True) cmds.floatSliderGrp("posX", edit=True, value=pos[0]) cmds.floatSliderGrp("posY", edit=True, value=pos[1]) cmds.floatSliderGrp("posZ", edit=True, value=pos[2]) cmds.floatSliderGrp("rotX", edit=True, value=rot[0]) cmds.floatSliderGrp("rotY", edit=True, value=rot[1]) cmds.floatSliderGrp("rotZ", edit=True, value=rot[2]) return True except Exception as e: cmds.warning(f"Error resetting joint: {str(e)}") return False def _apply_joint_calibration(self, joint): """ Apply joint calibration Args: joint (str): Name of the joint Returns: bool: True if successful, False otherwise """ try: # Get current position and rotation pos = cmds.xform(joint, query=True, worldSpace=True, translation=True) rot = cmds.xform(joint, query=True, worldSpace=True, rotation=True) # Store calibration data calibration_data = { "position": pos, "rotation": rot } # Add calibration data as attributes if not cmds.attributeQuery("calibrationData", node=joint, exists=True): cmds.addAttr(joint, longName="calibrationData", dataType="string") cmds.setAttr(f"{joint}.calibrationData", json.dumps(calibration_data), type="string") print(f"Successfully applied joint calibration: {joint}") return True except Exception as e: cmds.warning(f"Error applying joint calibration: {str(e)}") return False def _visualize_differences(self, reference_skeleton, current_skeleton): """ Visualize differences between skeletons Args: reference_skeleton (str): Name of the reference skeleton root current_skeleton (str): Name of the current skeleton root Returns: bool: True if successful, False otherwise """ try: # Remove existing visualization if cmds.objExists("difference_visualization"): cmds.delete("difference_visualization") # Create visualization group vis_group = cmds.group(empty=True, name="difference_visualization") # Get all joints in both skeletons ref_joints = self._get_all_joints(reference_skeleton) cur_joints = self._get_all_joints(current_skeleton) # Map joints by name joint_map = {} # Try to use existing mapping if available if self.current_calibration["mapping"] and self.current_calibration["reference"] == reference_skeleton and self.current_calibration["target"] == current_skeleton: # Use existing mapping for cur_joint in cur_joints: cur_joint_name = cur_joint.split("|")[-1].split(":")[-1] if cur_joint_name in self.current_calibration["mapping"]: ref_joint_name = self.current_calibration["mapping"][cur_joint_name] # Find the full path to the reference joint for ref_joint in ref_joints: if ref_joint.endswith(ref_joint_name): joint_map[cur_joint] = ref_joint break else: # Create mapping by name for cur_joint in cur_joints: cur_joint_name = cur_joint.split("|")[-1].split(":")[-1] # Find matching reference joint for ref_joint in ref_joints: ref_joint_name = ref_joint.split("|")[-1].split(":")[-1] if cur_joint_name == ref_joint_name: joint_map[cur_joint] = ref_joint break # Create lines between corresponding joints for cur_joint, ref_joint in joint_map.items(): # Get positions cur_pos = cmds.xform(cur_joint, query=True, worldSpace=True, translation=True) ref_pos = cmds.xform(ref_joint, query=True, worldSpace=True, translation=True) # Calculate distance distance = ((cur_pos[0] - ref_pos[0]) ** 2 + (cur_pos[1] - ref_pos[1]) ** 2 + (cur_pos[2] - ref_pos[2]) ** 2) ** 0.5 # Skip if too close if distance < 0.001: continue # Create curve curve = cmds.curve(degree=1, point=[cur_pos, ref_pos]) # Set color based on distance cmds.setAttr(f"{curve}.overrideEnabled", 1) if distance < 1.0: cmds.setAttr(f"{curve}.overrideColor", 14) # Green elif distance < 5.0: cmds.setAttr(f"{curve}.overrideColor", 17) # Yellow else: cmds.setAttr(f"{curve}.overrideColor", 13) # Red # Add to group cmds.parent(curve, vis_group) # Add distance text text_pos = [(cur_pos[0] + ref_pos[0]) / 2, (cur_pos[1] + ref_pos[1]) / 2, (cur_pos[2] + ref_pos[2]) / 2] text = cmds.textCurves(text=f"{distance:.2f}", font="smallPlainText")[0] cmds.scale(0.2, 0.2, 0.2, text) cmds.move(text_pos[0], text_pos[1], text_pos[2], text) cmds.parent(text, vis_group) return True except Exception as e: cmds.warning(f"Error visualizing differences: {str(e)}") return False def detect_skeleton_type(self, skeleton_root): """ Detect the type of skeleton (MetaHuman, Custom, etc.) Args: skeleton_root (str): Name of the skeleton root Returns: str: Skeleton type ("MH.4", "MH.3", "Custom", etc.) """ try: # Get all joints joints = self._get_all_joints(skeleton_root) if not joints: return "Unknown" # Count total joints joint_count = len(joints) # Check for MetaHuman specific joints mh4_specific = ["spine_04", "thumb_03_l", "thumb_03_r"] mh3_specific = ["spine_03", "thumb_02_l", "thumb_02_r"] mh4_count = 0 mh3_count = 0 for joint in joints: joint_name = joint.split("|")[-1].split(":")[-1] for specific in mh4_specific: if specific in joint_name: mh4_count += 1 for specific in mh3_specific: if specific in joint_name: mh3_count += 1 # MetaHuman 4 typically has 411 joints if joint_count >= 400 and joint_count <= 420 and mh4_count >= 2: return "MH.4" # MetaHuman 3 typically has 245 joints elif joint_count >= 240 and joint_count <= 260 and mh3_count >= 2: return "MH.3" # Generic MetaHuman elif "spine_01" in str(joints) and "clavicle_l" in str(joints) and "clavicle_r" in str(joints): return "MetaHuman" else: return "Custom" except Exception as e: cmds.warning(f"Error detecting skeleton type: {str(e)}") return "Unknown" def create_joint_mapping(self, source_skeleton, target_skeleton, mapping_type="auto"): """ Create a mapping between joints in two skeletons Args: source_skeleton (str): Name of the source skeleton root target_skeleton (str): Name of the target skeleton root mapping_type (str): Type of mapping ("auto", "name", "position", "hierarchy") Returns: dict: Mapping from source joint names to target joint names """ try: # Get all joints source_joints = self._get_all_joints(source_skeleton) target_joints = self._get_all_joints(target_skeleton) if not source_joints or not target_joints: return {} # Detect skeleton types source_type = self.detect_skeleton_type(source_skeleton) target_type = self.detect_skeleton_type(target_skeleton) # Initialize mapping mapping = {} # If both are MetaHuman, use predefined mapping if source_type in ["MH.4", "MH.3", "MetaHuman"] and target_type in ["MH.4", "MH.3", "MetaHuman"]: # Get source and target mappings source_map = self.mh_joint_mapping.get(source_type, self.mh_joint_mapping["MH.4"]) target_map = self.mh_joint_mapping.get(target_type, self.mh_joint_mapping["MH.4"]) # Create mapping from source to target for generic_name, source_name in source_map.items(): if generic_name in target_map: target_name = target_map[generic_name] # Find full joint names source_full = None target_full = None for joint in source_joints: if joint.endswith(source_name): source_full = joint.split("|")[-1].split(":")[-1] break for joint in target_joints: if joint.endswith(target_name): target_full = joint.split("|")[-1].split(":")[-1] break if source_full and target_full: mapping[source_full] = target_full else: # Use the specified mapping type if mapping_type == "name" or mapping_type == "auto": # Map by name for source_joint in source_joints: source_name = source_joint.split("|")[-1].split(":")[-1] for target_joint in target_joints: target_name = target_joint.split("|")[-1].split(":")[-1] if source_name == target_name: mapping[source_name] = target_name break if mapping_type == "position" or (mapping_type == "auto" and len(mapping) < len(source_joints) * 0.5): # Map by position (for joints not mapped by name) for source_joint in source_joints: source_name = source_joint.split("|")[-1].split(":")[-1] if source_name in mapping: continue # Get source position source_pos = cmds.xform(source_joint, query=True, worldSpace=True, translation=True) # Find closest target joint closest_joint = None closest_distance = float("inf") for target_joint in target_joints: target_name = target_joint.split("|")[-1].split(":")[-1] # Skip if already mapped if target_name in mapping.values(): continue # Get target position target_pos = cmds.xform(target_joint, query=True, worldSpace=True, translation=True) # Calculate distance distance = ((source_pos[0] - target_pos[0]) ** 2 + (source_pos[1] - target_pos[1]) ** 2 + (source_pos[2] - target_pos[2]) ** 2) ** 0.5 if distance < closest_distance: closest_distance = distance closest_joint = target_name if closest_joint and closest_distance < 10.0: # Threshold for position mapping mapping[source_name] = closest_joint if mapping_type == "hierarchy" or (mapping_type == "auto" and len(mapping) < len(source_joints) * 0.7): # Map by hierarchy (for remaining joints) # This is a simplified implementation - a real one would be more complex source_hierarchy = self._get_joint_hierarchy(source_skeleton) target_hierarchy = self._get_joint_hierarchy(target_skeleton) # Map root joints if source_hierarchy and target_hierarchy: source_root = list(source_hierarchy.keys())[0] target_root = list(target_hierarchy.keys())[0] if source_root not in mapping: mapping[source_root] = target_root # Map children based on relative positions in hierarchy self._map_children_by_hierarchy(source_hierarchy, target_hierarchy, source_root, target_root, mapping) # Store the mapping self.current_calibration["reference"] = source_skeleton self.current_calibration["target"] = target_skeleton self.current_calibration["mapping"] = mapping return mapping except Exception as e: cmds.warning(f"Error creating joint mapping: {str(e)}") return {} def _get_joint_hierarchy(self, root_joint): """ Get the hierarchy of joints Args: root_joint (str): Name of the root joint Returns: dict: Hierarchy of joints """ try: # Initialize hierarchy hierarchy = {} # Get the root joint name root_name = root_joint.split("|")[-1].split(":")[-1] # Get children children = cmds.listRelatives(root_joint, children=True, type="joint") or [] # Add to hierarchy hierarchy[root_name] = [] # Process children for child in children: child_hierarchy = self._get_joint_hierarchy(child) hierarchy[root_name].append(child_hierarchy) return hierarchy except Exception as e: cmds.warning(f"Error getting joint hierarchy: {str(e)}") return {} def _map_children_by_hierarchy(self, source_hierarchy, target_hierarchy, source_parent, target_parent, mapping): """ Map children by hierarchy Args: source_hierarchy (dict): Source hierarchy target_hierarchy (dict): Target hierarchy source_parent (str): Source parent joint target_parent (str): Target parent joint mapping (dict): Current mapping to update Returns: None """ try: # Get source and target children source_children = source_hierarchy.get(source_parent, []) target_children = target_hierarchy.get(target_parent, []) if not source_children or not target_children: return # If same number of children, map directly if len(source_children) == len(target_children): for i in range(len(source_children)): source_child = list(source_children[i].keys())[0] target_child = list(target_children[i].keys())[0] if source_child not in mapping: mapping[source_child] = target_child # Recursively map grandchildren self._map_children_by_hierarchy(source_hierarchy, target_hierarchy, source_child, target_child, mapping) else: # Different number of children, try to map by name or position for source_child_dict in source_children: source_child = list(source_child_dict.keys())[0] if source_child in mapping: continue # Try to find by name found = False for target_child_dict in target_children: target_child = list(target_child_dict.keys())[0] if target_child in mapping.values(): continue if source_child == target_child or source_child.lower() == target_child.lower(): mapping[source_child] = target_child found = True # Recursively map grandchildren self._map_children_by_hierarchy(source_hierarchy, target_hierarchy, source_child, target_child, mapping) break # If not found by name, leave unmapped for now # A more advanced implementation would try position-based mapping here except Exception as e: cmds.warning(f"Error mapping children by hierarchy: {str(e)}") return