MetaWhiz/scripts/utils/joint_utils.py
2025-04-17 13:00:39 +08:00

1170 lines
45 KiB
Python

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