Skip to content

Commit 6bf96ee

Browse files
committed
Updated MetaHuman snap method to support both legacy _drv naming and the newer MetaHuman standard without the suffix. - Fixed a namespace conflict between mGear core and shifter utils modules. - Added a helper function to retrieve the mGear guide directly from the scene.
1 parent f6dad4b commit 6bf96ee

3 files changed

Lines changed: 207 additions & 81 deletions

File tree

release/scripts/mgear/shifter/__init__.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,14 @@
1212

1313
# mgear
1414
import mgear
15-
import mgear.core.utils
1615
from . import guide, component
1716

1817
from mgear.core import primitive, attribute, skin, dag, icon, node
1918
from mgear import shifter_classic_components
2019
from mgear import shifter_epic_components
2120
from mgear.shifter import naming
2221
import importlib
23-
from mgear.core import utils
22+
from mgear.core import utils as core_utils
2423

2524
PY2 = sys.version_info[0] == 2
2625

@@ -97,7 +96,7 @@ def log_window():
9796
def getComponentDirectories():
9897
"""Get the components directory"""
9998
# TODO: ready to support multiple default directories
100-
return mgear.core.utils.gatherCustomModuleDirectories(
99+
return core_utils.gatherCustomModuleDirectories(
101100
SHIFTER_COMPONENT_ENV_KEY,
102101
[
103102
os.path.join(os.path.dirname(shifter_classic_components.__file__)),
@@ -115,7 +114,7 @@ def importComponentGuide(comp_type):
115114
defFmt = "mgear.shifter.component.{}.guide"
116115
customFmt = "{}.guide"
117116

118-
module = mgear.core.utils.importFromStandardOrCustomDirectories(
117+
module = core_utils.importFromStandardOrCustomDirectories(
119118
dirs, defFmt, customFmt, comp_type
120119
)
121120
return module
@@ -127,7 +126,7 @@ def importComponent(comp_type):
127126
defFmt = "mgear.shifter.component.{}"
128127
customFmt = "{}"
129128

130-
module = mgear.core.utils.importFromStandardOrCustomDirectories(
129+
module = core_utils.importFromStandardOrCustomDirectories(
131130
dirs, defFmt, customFmt, comp_type
132131
)
133132
return module
@@ -184,7 +183,7 @@ def __init__(self):
184183

185184
self.build_data = {}
186185

187-
@utils.one_undo
186+
@core_utils.one_undo
188187
def buildFromDict(self, conf_dict):
189188
log_window()
190189
startTime = datetime.datetime.now()
@@ -231,7 +230,7 @@ def buildFromDict(self, conf_dict):
231230

232231
return build_data
233232

234-
@utils.one_undo
233+
@core_utils.one_undo
235234
def buildFromSelection(self):
236235
"""Build the rig from selected guides."""
237236

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

367-
# @utils.timeFunc
366+
# @core_utils.timeFunc
368367
def get_guide_data(self):
369368
"""Get the guide data
370369

release/scripts/mgear/shifter/io.py

Lines changed: 193 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,86 @@
22
import os
33
import json
44
import sys
5+
import re
6+
import logging
7+
8+
from typing import List, Tuple, Sequence, Optional
9+
510
import mgear.pymaya as pm
611
from mgear import shifter
12+
from mgear.shifter import utils as shifter_utils
713
from mgear.core import curve
14+
from mgear.core import string
815

916
if sys.version_info[0] == 2:
1017
string_types = (basestring, )
1118
else:
1219
string_types = (str,)
1320

1421

22+
logger = logging.getLogger("mgear.shifter.io")
23+
logger.setLevel(logging.INFO)
24+
25+
26+
# -- Pair lists kept separate for readability and future edits
27+
_MH_SPINE: List[Tuple[str, str]] = [("root_C0_root", "root"),
28+
("body_C0_root", "pelvis"),
29+
("spine_C0_spineBase", "spine_01"),
30+
("spine_C0_tan0", "spine_02"),
31+
("spine_C0_spineTop", "spine_04"),
32+
("spine_C0_tan1", "spine_03"),
33+
("spine_C0_chest", "spine_05")
34+
]
35+
36+
_MH_LEG: List[Tuple[str, str]] = [("leg_L0_root", "thigh_l"),
37+
("leg_L0_knee", "calf_l"),
38+
("leg_L0_ankle", "foot_l"),
39+
("foot_L0_0_loc", "ball_l")
40+
]
41+
42+
_MH_ARM: List[Tuple[str, str]] = [("clavicle_L0_root", "clavicle_l"),
43+
("clavicle_L0_tip", "upperarm_l"),
44+
("arm_L0_elbow", "lowerarm_l"),
45+
("arm_L0_wrist", "hand_l")
46+
]
47+
48+
_MH_HAND: List[Tuple[str, str]] = [("index_metacarpal_L0_root", "index_metacarpal_l"),
49+
("middle_metacarpal_L0_root", "middle_metacarpal_l"),
50+
("ring_metacarpal_L0_root", "ring_metacarpal_l"),
51+
("pinky_metacarpal_L0_root", "pinky_metacarpal_l"),
52+
("thumb_L0_root", "thumb_01_l"),
53+
("thumb_L0_0_loc", "thumb_02_l"),
54+
("thumb_L0_1_loc", "thumb_03_l"),
55+
("index_L0_root", "index_01_l"),
56+
("index_L0_0_loc", "index_02_l"),
57+
("index_L0_1_loc", "index_03_l"),
58+
("middle_L0_root", "middle_01_l"),
59+
("middle_L0_0_loc", "middle_02_l"),
60+
("middle_L0_1_loc", "middle_03_l"),
61+
("ring_L0_root", "ring_01_l"),
62+
("ring_L0_0_loc", "ring_02_l"),
63+
("ring_L0_1_loc", "ring_03_l"),
64+
("pinky_L0_root", "pinky_01_l"),
65+
("pinky_L0_0_loc", "pinky_02_l"),
66+
("pinky_L0_1_loc", "pinky_03_l")
67+
]
68+
69+
_MH_NECK: List[Tuple[str, str]] = [("neck_C0_root", "neck_01"),
70+
("neck_C0_neck", "head"),
71+
("neck_C0_tan0", "neck_02")
72+
]
73+
74+
_MH_IKS: List[Tuple[str, str]] = [("ik_foot_L0_root", "foot_l"),
75+
("ik_hand_gun_C0_root", "hand_r"),
76+
("ik_hand_L0_root", "hand_l")
77+
]
78+
79+
80+
# -- Compiled once, used to discover skeleton root and mirroring
81+
_RE_ROOT_NAME = re.compile(r"(^|_)root($|_|$)", re.IGNORECASE)
82+
_RE_SUFFIX_FROM_ROOT = re.compile(r"root(_[A-Za-z0-9]+)?$", re.IGNORECASE)
83+
84+
1585
def get_guide_template_dict(guide_node, meta=None):
1686
"""Get the guide template dictionary from a guide node.
1787
@@ -214,81 +284,131 @@ def import_sample_template(name, *args):
214284
import_guide_template(path)
215285

216286

287+
def _find_guide_root_name() -> Optional[str]:
288+
"""
289+
Return the mGear guide model name from utils.get_guide() or None.
290+
"""
291+
_guide = shifter_utils.get_guide()
292+
if not _guide:
293+
logger.warning("No guide found. Select a guide root or component.")
294+
return None
295+
return _guide[0].node().name()
296+
297+
298+
def _snap_translation(src: pm.PyNode, dst: pm.PyNode) -> None:
299+
"""
300+
Snap source world translation to destination world translation.
301+
"""
302+
src.setTranslation(dst.getTranslation(space="world"), space="world")
303+
304+
305+
def _get_suffix(root_nodes: List) -> str:
306+
"""
307+
Get the suffix (e.g. '_drv') from the first root joint in the list.
308+
309+
:param list[str] root_nodes: List of joint names containing 'root'.
310+
:return: The detected suffix or an empty string if none is found.
311+
:rtype: str
312+
"""
313+
root_short = root_nodes[0].split("|")[-1]
314+
m = _RE_SUFFIX_FROM_ROOT.search(root_short)
315+
316+
return m.group(1) if (m and m.group(1)) else ""
317+
318+
319+
def _set_roll_divisions_zero() -> None:
320+
"""
321+
Set roll division to 0 on upper and lower leg and arm components.
322+
323+
There is already rig logic from MetaHuman driving the twists, so there is
324+
no need for mGear to build its own twist logic when driving a built MetaHuman.
325+
326+
:return: None
327+
"""
328+
for side in "LR":
329+
for comp_name in ["arm", "leg"]:
330+
comp_roots = [f"{comp_name}_{side}0_root.div0",
331+
f"{comp_name}_{side}0_root.div1"]
332+
333+
for comp in comp_roots:
334+
if not pm.objExists(comp_name):
335+
continue
336+
# -- Set upper and lower values to zero
337+
pm.setAttr(comp, 0)
338+
339+
217340
# Epic Metahuman snap to skeleton utility function
218341
def metahuman_snap():
219-
"""Snap and configure metahuman guide to attach to metahuman _drv skeleton
220342
"""
221-
if pm.ls("root_drv"):
222-
spine = [[u'root_C0_root', u'root_drv'],
223-
[u'body_C0_root', u'pelvis_drv'],
224-
[u'spine_C0_spineBase', u'spine_01_drv'],
225-
[u'spine_C0_tan0', u'spine_02_drv'],
226-
[u'spine_C0_spineTop', u'spine_04_drv'],
227-
[u'spine_C0_tan1', u'spine_03_drv'],
228-
[u'spine_C0_chest', u'spine_05_drv']]
229-
leg = [[u'leg_L0_root', u'thigh_l_drv'],
230-
[u'leg_L0_knee', u'calf_l_drv'],
231-
[u'leg_L0_ankle', u'foot_l_drv'],
232-
[u'foot_L0_0_loc', u'ball_l_drv']]
233-
arm = [[u'clavicle_L0_root', u'clavicle_l_drv'],
234-
[u'clavicle_L0_tip', u'upperarm_l_drv'],
235-
[u'arm_L0_elbow', u'lowerarm_l_drv'],
236-
[u'arm_L0_wrist', u'hand_l_drv']]
237-
hand = [[u'index_metacarpal_L0_root', u'index_metacarpal_l_drv'],
238-
[u'middle_metacarpal_L0_root', u'middle_metacarpal_l_drv'],
239-
[u'ring_metacarpal_L0_root', u'ring_metacarpal_l_drv'],
240-
[u'pinky_metacarpal_L0_root', u'pinky_metacarpal_l_drv'],
241-
[u'thumb_L0_root', u'thumb_01_l_drv'],
242-
[u'thumb_L0_0_loc', u'thumb_02_l_drv'],
243-
[u'thumb_L0_1_loc', u'thumb_03_l_drv'],
244-
[u'index_L0_root', u'index_01_l_drv'],
245-
[u'index_L0_0_loc', u'index_02_l_drv'],
246-
[u'index_L0_1_loc', u'index_03_l_drv'],
247-
[u'middle_L0_root', u'middle_01_l_drv'],
248-
[u'middle_L0_0_loc', u'middle_02_l_drv'],
249-
[u'middle_L0_1_loc', u'middle_03_l_drv'],
250-
[u'ring_L0_root', u'ring_01_l_drv'],
251-
[u'ring_L0_0_loc', u'ring_02_l_drv'],
252-
[u'ring_L0_1_loc', u'ring_03_l_drv'],
253-
[u'pinky_L0_root', u'pinky_01_l_drv'],
254-
[u'pinky_L0_0_loc', u'pinky_02_l_drv'],
255-
[u'pinky_L0_1_loc', u'pinky_03_l_drv']]
256-
neck = [[u'neck_C0_root', u'neck_01_drv'],
257-
[u'neck_C0_neck', u'head_drv'],
258-
[u'neck_C0_tan0', u'neck_02_drv']]
259-
260-
def match(a, b):
261-
a = pm.PyNode(a)
262-
b = pm.PyNode(b)
263-
a.setTranslation(b.getTranslation(
264-
space="world"), space="world")
265-
266-
locs = spine + leg + arm + hand + neck
267-
for loc in locs:
268-
try:
269-
a = loc[0]
270-
b = loc[1]
271-
match(a, b)
272-
273-
if "_l_" in b:
274-
ar = a.replace("_L", "_R")
275-
br = b.replace("_l_", "_r_")
276-
match(ar, br)
277-
except pm.MayaNodeError:
278-
pm.displayWarning(
279-
"Can't match position for locator {}. Please check if "
280-
"the node exist and the name is not duplicated.".format(a))
343+
Snap the MetaHuman guide to align with its corresponding skeleton.
344+
345+
Finds the skeleton root to extract the suffix - if there is one (e.g., `_drv`),
346+
then snaps guide locators to their matching driver joints in world space.
347+
Left-side elements are mirrored to the right automatically. Finally, updates
348+
the joint naming rule and sets roll divisions for correct deformation.
349+
350+
:return: None
351+
"""
352+
353+
guide_name = _find_guide_root_name()
354+
355+
# -- Exit early, if no guide in the scene.
356+
if not guide_name:
357+
logger.error("No guide found in the scene.")
358+
return
359+
360+
all_joints = pm.ls(long=True, type="joint")
361+
root_nodes = [n for n in all_joints if _RE_ROOT_NAME.search(n.split("|")[-1])]
362+
363+
# -- Exit early if no root node is found
364+
if not root_nodes:
365+
logger.error("No root or root_drv found in the scene.")
366+
return
367+
368+
# -- extract suffix if there is one (backwards compatibility)
369+
suffix = _get_suffix(root_nodes=root_nodes)
281370

371+
# -- Create one list with all paired items to match
372+
# -- Build mapping once
373+
left_pairs: Sequence[Tuple[str, str]] = (_MH_SPINE + _MH_LEG + _MH_ARM + _MH_HAND + _MH_NECK + _MH_IKS)
374+
processed: List[Tuple[str, str]] = []
375+
376+
for src_left, tgt_left in left_pairs:
377+
# -- Left pair - Adding the corrected suffix to the target skeleton
378+
processed.append((src_left, f"{tgt_left}{suffix}"))
379+
380+
# -- Only mirror if the SOURCE actually has an L/R token
381+
src_right = string.convertRLName(src_left)
382+
if src_right != src_left:
383+
tgt_right = string.convertRLName(tgt_left) + suffix
384+
processed.append((src_right, tgt_right))
385+
386+
# -- Snapping
387+
for src_name, dst_name in processed:
282388
try:
283-
pm.setAttr("guide.joint_name_rule", r"{description}{side}_drv")
284-
except pm.MayaAttributeError:
285-
pm.displayInfo("Please check joint Name Rule before build.")
286-
287-
# set roll division to 0 on upper leg and upper arm for correct deform
288-
for side in "LR":
289-
for comp in ["arm", "leg"]:
290-
try:
291-
pm.setAttr("{}_{}0_root.div0".format(comp, side), 0)
292-
pm.setAttr("{}_{}0_root.div1".format(comp, side), 0)
293-
except pm.MayaAttributeError:
294-
pass
389+
src = pm.PyNode(src_name)
390+
dst = pm.PyNode(dst_name)
391+
except pm.MayaNodeError:
392+
logger.warning(f"Missing node. Skipping pair: {src_name} -> {dst_name}")
393+
continue
394+
395+
try:
396+
_snap_translation(src, dst)
397+
except Exception as exc:
398+
logger.error(f"Failed to snap {src_name} to {dst_name}: {exc}")
399+
400+
# -- Ensure to set the correct naming rule for mGear build.
401+
name_pattern = f"{{description}}{{side}}{suffix}"
402+
joint_name_rule = f"{guide_name}.joint_name_rule"
403+
404+
if not pm.objExists(joint_name_rule):
405+
logger.error("Guide and attribute could not be found in the scene.")
406+
return
407+
408+
# -- Set the value
409+
pm.setAttr(joint_name_rule, name_pattern)
410+
411+
# -- Set the roll attributes
412+
_set_roll_divisions_zero()
413+
414+
logger.info("Successfully aligned mGear guide to MetaHuman skeleton.")

release/scripts/mgear/shifter/utils.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,10 @@ def get_root_joint(rigTopNode):
6060
jnt_org = rigTopNode.jnt_vis.listConnections()[0]
6161
root_jnt = jnt_org.child(0)
6262
return root_jnt
63+
64+
65+
def get_guide():
66+
"""
67+
Get the guide top node in the scene.
68+
"""
69+
return pm.ls("*.ismodel") or []

0 commit comments

Comments
 (0)