Skip to content

Commit c9286ed

Browse files
committed
Core: Anim Utils: Fix IK/FK match for 3jnt leg and roll control fixes #626
Generalize match_fk_to_ik_scale_slide, place_upv_from_fk, and match_fk_to_ik_arbitrary_lengths to support chains with 3+ FK controls instead of hardcoded 3-way unpacking. Fixes ValueError on leg_3jnt_01 components which have 4 FK controls. Fix roll_ctl matching: use match_ref connection to snap to roll_mth when available (leg_3jnt_01), fall back to rotateX reset when no match reference exists (arm components).
1 parent 2defc9d commit c9286ed

1 file changed

Lines changed: 126 additions & 99 deletions

File tree

release/scripts/mgear/core/anim_utils.py

Lines changed: 126 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -957,28 +957,52 @@ def _get_mth(name):
957957

958958
# if is IK then snap FK
959959
if val == ik_val:
960-
961960
for target, ctl in zip(fk_targets, fk_ctrls):
962961
transform.matchWorldTransform(target, ctl)
963962
pm.setAttr(o_attr, fk_val)
964963

965964
# if is FK then snap IK
966965
elif val == fk_val:
967-
transform.matchWorldTransform(ik_targets["ik_control"], ik_ctrl["ik_control"])
968-
transform.matchWorldTransform(ik_targets["pole_vector"], ik_ctrl["pole_vector"])
966+
transform.matchWorldTransform(
967+
ik_targets["ik_control"], ik_ctrl["ik_control"]
968+
)
969+
transform.matchWorldTransform(
970+
ik_targets["pole_vector"], ik_ctrl["pole_vector"]
971+
)
972+
969973
try:
970-
transform.matchWorldTransform(ik_targets["toes_ik"], ik_ctrl["toes_ik"])
971-
transform.matchWorldTransform(ik_targets["toeRollIk"], ik_ctrl["toeRollIk"])
972-
transform.matchWorldTransform(ik_targets["heelIk"], ik_ctrl["heelIk"])
973974
transform.matchWorldTransform(
974-
ik_targets["reverse_ankle_ik"], ik_ctrl["reverse_ankle_ik"]
975+
ik_targets["toes_ik"], ik_ctrl["toes_ik"]
976+
)
977+
transform.matchWorldTransform(
978+
ik_targets["toeRollIk"], ik_ctrl["toeRollIk"]
979+
)
980+
transform.matchWorldTransform(
981+
ik_targets["heelIk"], ik_ctrl["heelIk"]
982+
)
983+
transform.matchWorldTransform(
984+
ik_targets["reverse_ankle_ik"],
985+
ik_ctrl["reverse_ankle_ik"],
986+
)
987+
match_fk_to_ik_arbitrary_lengths(
988+
fk_controls, ui_node, ikfk_attr,
989+
ik_ctrl["pole_vector"],
975990
)
976-
match_fk_to_ik_arbitrary_lengths(fk_controls, ui_node,
977-
ikfk_attr, ik_ctrl["pole_vector"])
978991
except KeyError:
979992
pass
993+
994+
# Set the blend attr to IK BEFORE matching roll, because
995+
# the rig needs to evaluate in IK mode first so the roll
996+
# control's parent hierarchy is in its final state.
980997
pm.setAttr(o_attr, ik_val)
981998

999+
# Match roll control after blend switch
1000+
if "roll" in ik_ctrl and ik_ctrl["roll"] is not None:
1001+
if "roll" in ik_targets and ik_targets["roll"] is not None:
1002+
transform.matchWorldTransform(
1003+
ik_targets["roll"], ik_ctrl["roll"]
1004+
)
1005+
9821006
# sets keyframes
9831007
if keyframe:
9841008
[
@@ -1234,10 +1258,20 @@ def _get_mth(name):
12341258
roll_att = ik_ctrl.attr(roll_att_name)
12351259
roll_att.set(0.0)
12361260

1237-
# reset roll ctl if exist
1261+
# match roll ctl to its reference if it exists,
1262+
# otherwise reset rotateX (arm components have no roll_mth)
12381263
if ik_controls and "roll" in ik_controls.keys():
1239-
roll_ctl = _get_node(ik_controls["roll"])
1240-
roll_ctl.rotateX.set(0)
1264+
roll_ctl_node = _get_node(ik_controls["roll"])
1265+
if roll_ctl_node.hasAttr("match_ref"):
1266+
match_nodes = roll_ctl_node.match_ref.listConnections()
1267+
if match_nodes:
1268+
transform.matchWorldTransform(
1269+
match_nodes[0], roll_ctl_node
1270+
)
1271+
else:
1272+
roll_ctl_node.rotateX.set(0)
1273+
else:
1274+
roll_ctl_node.rotateX.set(0)
12411275

12421276
# reset IK foot ctls
12431277
if foot_cnx:
@@ -2614,99 +2648,86 @@ def bakeAnimation(
26142648

26152649
# Functions to support arbitraty limb length for FK to IK
26162650

2617-
def match_fk_to_ik_scale_slide(arm_ctl, forearm_ctl, hand_ctl,
2618-
ui_host, scale_attr='scale',
2619-
slide_attr='slide'):
2651+
def match_fk_to_ik_scale_slide(fk_controls, ui_host,
2652+
scale_attr="scale",
2653+
slide_attr="slide"):
26202654
"""Match FK limb to IK using scale and slide on a uiHost node.
26212655
2656+
Supports chains with 2 or more segments (3+ FK controls).
2657+
26222658
Args:
2623-
arm_ctl (str): Arm or upper leg FK control.
2624-
forearm_ctl (str): Forearm or lower leg FK control.
2625-
hand_ctl (str): Hand or foot FK control.
2659+
fk_controls (list): FK control names in chain order.
26262660
ui_host (str): Node where scale/slide attrs live.
26272661
scale_attr (str): Name of scale attribute.
26282662
slide_attr (str): Name of slide attribute.
26292663
26302664
Raises:
2631-
RuntimeError: On missing nodes or zerolength setup.
2665+
RuntimeError: On missing nodes or zero-length setup.
26322666
"""
2633-
# get default max values and rest values
26342667
s_path = "{}.{}".format(ui_host, scale_attr)
26352668
sl_path = "{}.{}".format(ui_host, slide_attr)
26362669
s_path_info = attribute.get_attr_info(ui_host, scale_attr)
26372670
sl_path_info = attribute.get_attr_info(ui_host, slide_attr)
26382671
s_default_val = s_path_info["default"]
2639-
s_min_val = s_path_info["min"]
2640-
s_max_val = s_path_info["max"]
2641-
2642-
sl_default_val = sl_path_info["default"]
26432672
sl_min_val = sl_path_info["min"]
26442673
sl_max_val = sl_path_info["max"]
26452674

2646-
# verify controls & parents
26472675
def parent_of(obj):
2648-
p = cmds.listRelatives(obj, parent=True, f=True)
2676+
p = cmds.listRelatives(obj, parent=True, fullPath=True)
26492677
if not p:
26502678
raise RuntimeError("No parent for {}".format(obj))
26512679
return p[0]
26522680

2653-
for ctl in (arm_ctl, forearm_ctl, hand_ctl):
2681+
for ctl in fk_controls:
26542682
if not cmds.objExists(ctl):
26552683
raise RuntimeError("Control not found: {}".format(ctl))
26562684

2657-
# arm_p = parent_of(arm_ctl)
2658-
# arm_p_p = parent_of(arm_p)
2659-
fore_p = parent_of(forearm_ctl)
2660-
fore_p_p = parent_of(fore_p)
2661-
hand_p = parent_of(hand_ctl)
2662-
hand_p_p = parent_of(hand_p)
2663-
2664-
# rest lengths
2665-
rest_upper = vector.getDistance2(fore_p_p, fore_p)
2666-
rest_lower = vector.getDistance2(hand_p_p, hand_p)
2667-
# rest_lower = 2.0
2668-
rest_total = rest_upper + rest_lower
2669-
# print("Rest lengths: upper={:.3f}, lower={:.3f}, total={:.3f}"
2670-
# .format(rest_upper, rest_lower, rest_total))
2685+
# Rest lengths from parent hierarchy for each segment
2686+
rest_segments = []
2687+
for ctl in fk_controls[1:]:
2688+
ctl_p = parent_of(ctl)
2689+
ctl_pp = parent_of(ctl_p)
2690+
rest_segments.append(vector.getDistance2(ctl_pp, ctl_p))
2691+
rest_total = sum(rest_segments)
26712692

26722693
if rest_total == 0:
26732694
raise RuntimeError("Rest pose total length is zero.")
26742695

2675-
# current lengths
2676-
cur_upper = vector.getDistance2(arm_ctl, forearm_ctl)
2677-
cur_lower = vector.getDistance2(forearm_ctl, hand_ctl)
2678-
cur_total = cur_upper + cur_lower
2679-
# print("Cur lengths: upper={:.3f}, lower={:.3f}, total={:.3f}"
2680-
# .format(cur_upper, cur_lower, cur_total))
2696+
# Current lengths between consecutive FK controls
2697+
cur_segments = []
2698+
for i in range(len(fk_controls) - 1):
2699+
cur_segments.append(
2700+
vector.getDistance2(fk_controls[i], fk_controls[i + 1])
2701+
)
2702+
cur_total = sum(cur_segments)
26812703

2682-
# scale
2704+
# Scale
26832705
scale_val = cur_total / rest_total
2684-
# print("Scale value: {:.3f}".format(scale_val))
26852706

2686-
# slide: piecewise around rest ratio
2707+
# Slide: ratio of upper portion vs total
2708+
mid_idx = len(fk_controls) // 2
2709+
rest_upper = sum(rest_segments[:mid_idx])
2710+
cur_upper = sum(cur_segments[:mid_idx])
26872711
rest_ratio = rest_upper / rest_total
2688-
cur_ratio = cur_upper / cur_total
2689-
# print("Ratios: rest_ratio={:.3f}, cur_ratio={:.3f}"
2690-
# .format(rest_ratio, cur_ratio))
2712+
cur_ratio = cur_upper / cur_total if cur_total > 0 else rest_ratio
26912713

26922714
if cur_ratio <= rest_ratio:
26932715
slide_val = (cur_ratio / rest_ratio) * 0.5
26942716
else:
2695-
slide_val = 0.5 + ((cur_ratio - rest_ratio) / (1 - rest_ratio)) \
2696-
* 0.5
2717+
slide_val = (
2718+
0.5
2719+
+ ((cur_ratio - rest_ratio) / (1 - rest_ratio)) * 0.5
2720+
)
26972721

26982722
slide_val = max(0.0, min(1.0, slide_val))
2699-
# print("Slide value: {:.3f}".format(slide_val))
27002723

2701-
# map normalized values to attribute ranges
2724+
# Map normalized values to attribute ranges
27022725
if s_default_val is not None:
27032726
scale_val = scale_val * s_default_val
27042727

27052728
if sl_min_val is not None and sl_max_val is not None:
27062729
slide_val = (sl_max_val - sl_min_val) * slide_val + sl_min_val
27072730

2708-
# set attrs
2709-
27102731
for p in (s_path, sl_path):
27112732
if not cmds.objExists(p):
27122733
raise RuntimeError("Missing attribute: {}".format(p))
@@ -2716,33 +2737,35 @@ def parent_of(obj):
27162737

27172738

27182739

2719-
def place_upv_from_fk(arm_ctl, forearm_ctl, hand_ctl,
2720-
upv_ctl, distance_multiplier=2.0):
2740+
def place_upv_from_fk(fk_controls, upv_ctl, distance_multiplier=2.0):
27212741
"""Place up vector control based on the FK plane.
27222742
2723-
Calculates the pole vector (up vector) position defined by the FK
2724-
controls and places the upv_ctl at that position.
2743+
Uses the first, middle, and last FK controls to define the limb
2744+
plane and places the pole vector perpendicular to it.
2745+
Supports chains with 3 or more FK controls.
27252746
27262747
Args:
2727-
arm_ctl (str): Arm or upper leg FK control.
2728-
forearm_ctl (str): Forearm or lower leg FK control.
2729-
hand_ctl (str): Hand or foot FK control.
2748+
fk_controls (list): FK control names in chain order.
27302749
upv_ctl (str): Up vector control to be moved.
27312750
distance_multiplier (float): Distance scale factor.
27322751
27332752
Raises:
27342753
RuntimeError: If any control does not exist.
27352754
"""
2736-
for ctl in [arm_ctl, forearm_ctl, hand_ctl, upv_ctl]:
2755+
for ctl in list(fk_controls) + [upv_ctl]:
27372756
if not cmds.objExists(ctl):
27382757
raise RuntimeError("Control not found: {}".format(ctl))
27392758

2740-
v1 = vector.get_mvector(arm_ctl)
2741-
v2 = vector.get_mvector(forearm_ctl)
2742-
v3 = vector.get_mvector(hand_ctl)
2759+
first = fk_controls[0]
2760+
mid = fk_controls[len(fk_controls) // 2]
2761+
last = fk_controls[-1]
27432762

2744-
a = v2 - v1 # vector from arm to elbow
2745-
b = v3 - v1 # vector from arm to wrist
2763+
v1 = vector.get_mvector(first)
2764+
v2 = vector.get_mvector(mid)
2765+
v3 = vector.get_mvector(last)
2766+
2767+
a = v2 - v1
2768+
b = v3 - v1
27462769

27472770
b_normalized = b.normal()
27482771
proj = a * b_normalized
@@ -2761,23 +2784,26 @@ def match_fk_to_ik_arbitrary_lengths(fk_controls, ui_host,
27612784
blend_attr, upv_ctl):
27622785
"""Match FK to IK for arbitrary limb lengths.
27632786
2787+
Supports chains with 3 or more FK controls (2jnt, 3jnt, etc.).
2788+
27642789
Args:
2765-
fk_controls (list[str or PyNode]): [arm_ctl, forearm_ctl, hand_ctl].
2790+
fk_controls (list): FK controls as strings or PyNodes.
27662791
ui_host (str or PyNode): Node with blend attr.
27672792
blend_attr (str): Name of blend or switch attr.
27682793
upv_ctl (str or PyNode): Up-vector control.
27692794
27702795
Returns:
27712796
bool: True if match ran, False if attrs missing.
27722797
"""
2773-
arm_str, fore_str, hand_str = [], [], []
27742798
names = []
27752799
for c in fk_controls:
27762800
if not isinstance(c, str):
27772801
names.append(c.name())
27782802
else:
27792803
names.append(c)
2780-
arm_str, fore_str, hand_str = names
2804+
2805+
if len(names) < 3:
2806+
return False
27812807

27822808
if not isinstance(ui_host, str):
27832809
ui_node = ui_host
@@ -2791,45 +2817,46 @@ def match_fk_to_ik_arbitrary_lengths(fk_controls, ui_host,
27912817
else:
27922818
upv_str = upv_ctl
27932819

2794-
# Derive scale/slide attr names
2795-
if blend_attr.endswith('_blend'):
2820+
if blend_attr.endswith("_blend"):
27962821
base = blend_attr[:-6]
2797-
elif blend_attr.endswith('_Switch'):
2822+
elif blend_attr.endswith("_Switch"):
27982823
base = blend_attr[:-7]
27992824
else:
28002825
base = blend_attr
2801-
scale_attr = base + '_ikscale'
2802-
slide_attr = base + '_slide'
2826+
scale_attr = base + "_ikscale"
2827+
slide_attr = base + "_slide"
28032828

2804-
# Check required attrs on PyNode
28052829
if not ui_node.hasAttr(scale_attr) or not ui_node.hasAttr(
2806-
slide_attr):
2830+
slide_attr
2831+
):
28072832
return False
28082833

2809-
keyframe = pm.keyframe(f"{ui_str}.{blend_attr}",
2810-
query=True,
2811-
keyframeCount=True)
2812-
2813-
# if keyframe:
2814-
# cmds.setKeyframe(f"{ui_str}.{scale_attr}", time=(cmds.currentTime(query=True) - 1.0))
2815-
# cmds.setKeyframe(f"{ui_str}.{slide_attr}", time=(cmds.currentTime(query=True) - 1.0))
2834+
keyframe = pm.keyframe(
2835+
"{}.{}".format(ui_str, blend_attr),
2836+
query=True,
2837+
keyframeCount=True,
2838+
)
28162839

2817-
# Run FK to IK match
28182840
match_fk_to_ik_scale_slide(
2819-
arm_ctl=arm_str, forearm_ctl=fore_str,
2820-
hand_ctl=hand_str, ui_host=ui_str,
2821-
scale_attr=scale_attr, slide_attr=slide_attr
2841+
fk_controls=names,
2842+
ui_host=ui_str,
2843+
scale_attr=scale_attr,
2844+
slide_attr=slide_attr,
28222845
)
28232846

2824-
# Place up-vector
28252847
place_upv_from_fk(
2826-
arm_ctl=arm_str, forearm_ctl=fore_str,
2827-
hand_ctl=hand_str, upv_ctl=upv_str,
2828-
distance_multiplier=1.0
2848+
fk_controls=names,
2849+
upv_ctl=upv_str,
2850+
distance_multiplier=1.0,
28292851
)
28302852

28312853
if keyframe:
2832-
cmds.setKeyframe(f"{ui_str}.{scale_attr}", time=(cmds.currentTime(query=True)))
2833-
cmds.setKeyframe(f"{ui_str}.{slide_attr}", time=(cmds.currentTime(query=True)))
2854+
cur_time = cmds.currentTime(query=True)
2855+
cmds.setKeyframe(
2856+
"{}.{}".format(ui_str, scale_attr), time=cur_time
2857+
)
2858+
cmds.setKeyframe(
2859+
"{}.{}".format(ui_str, slide_attr), time=cur_time
2860+
)
28342861

28352862
return True

0 commit comments

Comments
 (0)