|
2 | 2 | import os |
3 | 3 | import json |
4 | 4 | import sys |
| 5 | +import re |
| 6 | +import logging |
| 7 | + |
| 8 | +from typing import List, Tuple, Sequence, Optional |
| 9 | + |
5 | 10 | import mgear.pymaya as pm |
6 | 11 | from mgear import shifter |
| 12 | +from mgear.shifter import utils as shifter_utils |
7 | 13 | from mgear.core import curve |
| 14 | +from mgear.core import string |
8 | 15 |
|
9 | 16 | if sys.version_info[0] == 2: |
10 | 17 | string_types = (basestring, ) |
11 | 18 | else: |
12 | 19 | string_types = (str,) |
13 | 20 |
|
14 | 21 |
|
| 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 | + |
15 | 85 | def get_guide_template_dict(guide_node, meta=None): |
16 | 86 | """Get the guide template dictionary from a guide node. |
17 | 87 |
|
@@ -214,81 +284,131 @@ def import_sample_template(name, *args): |
214 | 284 | import_guide_template(path) |
215 | 285 |
|
216 | 286 |
|
| 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 | + |
217 | 340 | # Epic Metahuman snap to skeleton utility function |
218 | 341 | def metahuman_snap(): |
219 | | - """Snap and configure metahuman guide to attach to metahuman _drv skeleton |
220 | 342 | """ |
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) |
281 | 370 |
|
| 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: |
282 | 388 | 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.") |
0 commit comments