1170 lines
45 KiB
Python
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
|