Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 7 additions & 8 deletions release/scripts/mgear/shifter/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@

# mgear
import mgear
import mgear.core.utils
from . import guide, component

from mgear.core import primitive, attribute, skin, dag, icon, node
from mgear import shifter_classic_components
from mgear import shifter_epic_components
from mgear.shifter import naming
import importlib
from mgear.core import utils
from mgear.core import utils as core_utils

PY2 = sys.version_info[0] == 2

Expand Down Expand Up @@ -97,7 +96,7 @@ def log_window():
def getComponentDirectories():
"""Get the components directory"""
# TODO: ready to support multiple default directories
return mgear.core.utils.gatherCustomModuleDirectories(
return core_utils.gatherCustomModuleDirectories(
SHIFTER_COMPONENT_ENV_KEY,
[
os.path.join(os.path.dirname(shifter_classic_components.__file__)),
Expand All @@ -115,7 +114,7 @@ def importComponentGuide(comp_type):
defFmt = "mgear.shifter.component.{}.guide"
customFmt = "{}.guide"

module = mgear.core.utils.importFromStandardOrCustomDirectories(
module = core_utils.importFromStandardOrCustomDirectories(
dirs, defFmt, customFmt, comp_type
)
return module
Expand All @@ -127,7 +126,7 @@ def importComponent(comp_type):
defFmt = "mgear.shifter.component.{}"
customFmt = "{}"

module = mgear.core.utils.importFromStandardOrCustomDirectories(
module = core_utils.importFromStandardOrCustomDirectories(
dirs, defFmt, customFmt, comp_type
)
return module
Expand Down Expand Up @@ -184,7 +183,7 @@ def __init__(self):

self.build_data = {}

@utils.one_undo
@core_utils.one_undo
def buildFromDict(self, conf_dict):
log_window()
startTime = datetime.datetime.now()
Expand Down Expand Up @@ -231,7 +230,7 @@ def buildFromDict(self, conf_dict):

return build_data

@utils.one_undo
@core_utils.one_undo
def buildFromSelection(self):
"""Build the rig from selected guides."""

Expand Down Expand Up @@ -364,7 +363,7 @@ def postCustomStep(self):
customSteps = [cs.replace("\\", "/") for cs in customSteps]
self.customStep(customSteps)

# @utils.timeFunc
# @core_utils.timeFunc
def get_guide_data(self):
"""Get the guide data

Expand Down
266 changes: 193 additions & 73 deletions release/scripts/mgear/shifter/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,86 @@
import os
import json
import sys
import re
import logging

from typing import List, Tuple, Sequence, Optional

import mgear.pymaya as pm
from mgear import shifter
from mgear.shifter import utils as shifter_utils
from mgear.core import curve
from mgear.core import string

if sys.version_info[0] == 2:
string_types = (basestring, )
else:
string_types = (str,)


logger = logging.getLogger("mgear.shifter.io")
logger.setLevel(logging.INFO)


# -- Pair lists kept separate for readability and future edits
_MH_SPINE: List[Tuple[str, str]] = [("root_C0_root", "root"),
("body_C0_root", "pelvis"),
("spine_C0_spineBase", "spine_01"),
("spine_C0_tan0", "spine_02"),
("spine_C0_spineTop", "spine_04"),
("spine_C0_tan1", "spine_03"),
("spine_C0_chest", "spine_05")
]

_MH_LEG: List[Tuple[str, str]] = [("leg_L0_root", "thigh_l"),
("leg_L0_knee", "calf_l"),
("leg_L0_ankle", "foot_l"),
("foot_L0_0_loc", "ball_l")
]

_MH_ARM: List[Tuple[str, str]] = [("clavicle_L0_root", "clavicle_l"),
("clavicle_L0_tip", "upperarm_l"),
("arm_L0_elbow", "lowerarm_l"),
("arm_L0_wrist", "hand_l")
]

_MH_HAND: List[Tuple[str, str]] = [("index_metacarpal_L0_root", "index_metacarpal_l"),
("middle_metacarpal_L0_root", "middle_metacarpal_l"),
("ring_metacarpal_L0_root", "ring_metacarpal_l"),
("pinky_metacarpal_L0_root", "pinky_metacarpal_l"),
("thumb_L0_root", "thumb_01_l"),
("thumb_L0_0_loc", "thumb_02_l"),
("thumb_L0_1_loc", "thumb_03_l"),
("index_L0_root", "index_01_l"),
("index_L0_0_loc", "index_02_l"),
("index_L0_1_loc", "index_03_l"),
("middle_L0_root", "middle_01_l"),
("middle_L0_0_loc", "middle_02_l"),
("middle_L0_1_loc", "middle_03_l"),
("ring_L0_root", "ring_01_l"),
("ring_L0_0_loc", "ring_02_l"),
("ring_L0_1_loc", "ring_03_l"),
("pinky_L0_root", "pinky_01_l"),
("pinky_L0_0_loc", "pinky_02_l"),
("pinky_L0_1_loc", "pinky_03_l")
]

_MH_NECK: List[Tuple[str, str]] = [("neck_C0_root", "neck_01"),
("neck_C0_neck", "head"),
("neck_C0_tan0", "neck_02")
]

_MH_IKS: List[Tuple[str, str]] = [("ik_foot_L0_root", "foot_l"),
("ik_hand_gun_C0_root", "hand_r"),
("ik_hand_L0_root", "hand_l")
]


# -- Compiled once, used to discover skeleton root and mirroring
_RE_ROOT_NAME = re.compile(r"(^|_)root($|_|$)", re.IGNORECASE)
_RE_SUFFIX_FROM_ROOT = re.compile(r"root(_[A-Za-z0-9]+)?$", re.IGNORECASE)


def get_guide_template_dict(guide_node, meta=None):
"""Get the guide template dictionary from a guide node.

Expand Down Expand Up @@ -214,81 +284,131 @@ def import_sample_template(name, *args):
import_guide_template(path)


def _find_guide_root_name() -> Optional[str]:
"""
Return the mGear guide model name from utils.get_guide() or None.
"""
_guide = shifter_utils.get_guide()
if not _guide:
logger.warning("No guide found. Select a guide root or component.")
return None
return _guide[0].node().name()


def _snap_translation(src: pm.PyNode, dst: pm.PyNode) -> None:
"""
Snap source world translation to destination world translation.
"""
src.setTranslation(dst.getTranslation(space="world"), space="world")


def _get_suffix(root_nodes: List) -> str:
"""
Get the suffix (e.g. '_drv') from the first root joint in the list.

:param list[str] root_nodes: List of joint names containing 'root'.
:return: The detected suffix or an empty string if none is found.
:rtype: str
"""
root_short = root_nodes[0].split("|")[-1]
m = _RE_SUFFIX_FROM_ROOT.search(root_short)

return m.group(1) if (m and m.group(1)) else ""


def _set_roll_divisions_zero() -> None:
"""
Set roll division to 0 on upper and lower leg and arm components.

There is already rig logic from MetaHuman driving the twists, so there is
no need for mGear to build its own twist logic when driving a built MetaHuman.

:return: None
"""
for side in "LR":
for comp_name in ["arm", "leg"]:
comp_roots = [f"{comp_name}_{side}0_root.div0",
f"{comp_name}_{side}0_root.div1"]

for comp in comp_roots:
if not pm.objExists(comp_name):
continue
# -- Set upper and lower values to zero
pm.setAttr(comp, 0)


# Epic Metahuman snap to skeleton utility function
def metahuman_snap():
"""Snap and configure metahuman guide to attach to metahuman _drv skeleton
"""
if pm.ls("root_drv"):
spine = [[u'root_C0_root', u'root_drv'],
[u'body_C0_root', u'pelvis_drv'],
[u'spine_C0_spineBase', u'spine_01_drv'],
[u'spine_C0_tan0', u'spine_02_drv'],
[u'spine_C0_spineTop', u'spine_04_drv'],
[u'spine_C0_tan1', u'spine_03_drv'],
[u'spine_C0_chest', u'spine_05_drv']]
leg = [[u'leg_L0_root', u'thigh_l_drv'],
[u'leg_L0_knee', u'calf_l_drv'],
[u'leg_L0_ankle', u'foot_l_drv'],
[u'foot_L0_0_loc', u'ball_l_drv']]
arm = [[u'clavicle_L0_root', u'clavicle_l_drv'],
[u'clavicle_L0_tip', u'upperarm_l_drv'],
[u'arm_L0_elbow', u'lowerarm_l_drv'],
[u'arm_L0_wrist', u'hand_l_drv']]
hand = [[u'index_metacarpal_L0_root', u'index_metacarpal_l_drv'],
[u'middle_metacarpal_L0_root', u'middle_metacarpal_l_drv'],
[u'ring_metacarpal_L0_root', u'ring_metacarpal_l_drv'],
[u'pinky_metacarpal_L0_root', u'pinky_metacarpal_l_drv'],
[u'thumb_L0_root', u'thumb_01_l_drv'],
[u'thumb_L0_0_loc', u'thumb_02_l_drv'],
[u'thumb_L0_1_loc', u'thumb_03_l_drv'],
[u'index_L0_root', u'index_01_l_drv'],
[u'index_L0_0_loc', u'index_02_l_drv'],
[u'index_L0_1_loc', u'index_03_l_drv'],
[u'middle_L0_root', u'middle_01_l_drv'],
[u'middle_L0_0_loc', u'middle_02_l_drv'],
[u'middle_L0_1_loc', u'middle_03_l_drv'],
[u'ring_L0_root', u'ring_01_l_drv'],
[u'ring_L0_0_loc', u'ring_02_l_drv'],
[u'ring_L0_1_loc', u'ring_03_l_drv'],
[u'pinky_L0_root', u'pinky_01_l_drv'],
[u'pinky_L0_0_loc', u'pinky_02_l_drv'],
[u'pinky_L0_1_loc', u'pinky_03_l_drv']]
neck = [[u'neck_C0_root', u'neck_01_drv'],
[u'neck_C0_neck', u'head_drv'],
[u'neck_C0_tan0', u'neck_02_drv']]

def match(a, b):
a = pm.PyNode(a)
b = pm.PyNode(b)
a.setTranslation(b.getTranslation(
space="world"), space="world")

locs = spine + leg + arm + hand + neck
for loc in locs:
try:
a = loc[0]
b = loc[1]
match(a, b)

if "_l_" in b:
ar = a.replace("_L", "_R")
br = b.replace("_l_", "_r_")
match(ar, br)
except pm.MayaNodeError:
pm.displayWarning(
"Can't match position for locator {}. Please check if "
"the node exist and the name is not duplicated.".format(a))
Snap the MetaHuman guide to align with its corresponding skeleton.

Finds the skeleton root to extract the suffix - if there is one (e.g., `_drv`),
then snaps guide locators to their matching driver joints in world space.
Left-side elements are mirrored to the right automatically. Finally, updates
the joint naming rule and sets roll divisions for correct deformation.

:return: None
"""

guide_name = _find_guide_root_name()

# -- Exit early, if no guide in the scene.
if not guide_name:
logger.error("No guide found in the scene.")
return

all_joints = pm.ls(long=True, type="joint")
root_nodes = [n for n in all_joints if _RE_ROOT_NAME.search(n.split("|")[-1])]

# -- Exit early if no root node is found
if not root_nodes:
logger.error("No root or root_drv found in the scene.")
return

# -- extract suffix if there is one (backwards compatibility)
suffix = _get_suffix(root_nodes=root_nodes)

# -- Create one list with all paired items to match
# -- Build mapping once
left_pairs: Sequence[Tuple[str, str]] = (_MH_SPINE + _MH_LEG + _MH_ARM + _MH_HAND + _MH_NECK + _MH_IKS)
processed: List[Tuple[str, str]] = []

for src_left, tgt_left in left_pairs:
# -- Left pair - Adding the corrected suffix to the target skeleton
processed.append((src_left, f"{tgt_left}{suffix}"))

# -- Only mirror if the SOURCE actually has an L/R token
src_right = string.convertRLName(src_left)
if src_right != src_left:
tgt_right = string.convertRLName(tgt_left) + suffix
processed.append((src_right, tgt_right))

# -- Snapping
for src_name, dst_name in processed:
try:
pm.setAttr("guide.joint_name_rule", r"{description}{side}_drv")
except pm.MayaAttributeError:
pm.displayInfo("Please check joint Name Rule before build.")

# set roll division to 0 on upper leg and upper arm for correct deform
for side in "LR":
for comp in ["arm", "leg"]:
try:
pm.setAttr("{}_{}0_root.div0".format(comp, side), 0)
pm.setAttr("{}_{}0_root.div1".format(comp, side), 0)
except pm.MayaAttributeError:
pass
src = pm.PyNode(src_name)
dst = pm.PyNode(dst_name)
except pm.MayaNodeError:
logger.warning(f"Missing node. Skipping pair: {src_name} -> {dst_name}")
continue

try:
_snap_translation(src, dst)
except Exception as exc:
logger.error(f"Failed to snap {src_name} to {dst_name}: {exc}")

# -- Ensure to set the correct naming rule for mGear build.
name_pattern = f"{{description}}{{side}}{suffix}"
joint_name_rule = f"{guide_name}.joint_name_rule"

if not pm.objExists(joint_name_rule):
logger.error("Guide and attribute could not be found in the scene.")
return

# -- Set the value
pm.setAttr(joint_name_rule, name_pattern)

# -- Set the roll attributes
_set_roll_divisions_zero()

logger.info("Successfully aligned mGear guide to MetaHuman skeleton.")
7 changes: 7 additions & 0 deletions release/scripts/mgear/shifter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,10 @@ def get_root_joint(rigTopNode):
jnt_org = rigTopNode.jnt_vis.listConnections()[0]
root_jnt = jnt_org.child(0)
return root_jnt


def get_guide():
"""
Get the guide top node in the scene.
"""
return pm.ls("*.ismodel") or []
Loading