Weapons · Updated 2552.07.02.21.29

Left-Hand IK (LHIK) — Melee Suppression

How the FP weapon-layer left-hand IK is gated so it does not apply while a melee animation is playing.

How the FP weapon-layer left-hand IK is gated so it does not apply while a melee animation is playing.

#The problem

LHIK is driven by a Should Apply LHIK bool feeding the Active Value of a Blend Poses by bool node:

  • True Pose → FABRIK weapon IK applied (hand snapped to the weapon grip socket).
  • False Pose → cached non-IK pose.

We also want LHIK off while a melee swing animates. The obvious signal — bIsMeleeing — is wrong for this: that bool is tied to MeleeSwingDuration (0.6s, the GAS Meleeing tag lifetime), but the melee animation asset plays longer than 0.6s. Gating on !bIsMeleeing snaps LHIK back on mid-swing.

Melee here is not a montage — it's a state-machine state (the "Melee" state) playing a non-looping clip (MC_Rifle_Melee, MC_Pistol_Melee, etc.), so Is Any Montage Playing does not apply either.

#The fix — a curve baked on the melee anim

Drive the suppression off the animation asset's own timeline, not the tag.

#1. Author the curve (per melee clip)

Only the two-handed main weapons need this — rifle and shotgun, where the left hand
grips the foregrip via FABRIK and must release during a bash. The **pistol is the sidearm,
held one-handed in the left hand → it has no LHIK**, so its melee clips
(MC_FP_Pistol_Melee, MC_TP_Pistol_Melee) do not get this curve.

For every FP melee animation that plays through a weapon layer that uses left-hand foregrip IK (MC_Rifle_Melee, MC_Shotgun_Melee, …):

  1. Open the animation.
  1. Curves → Add Curve (float) → name it DisableLHIK (exact name, reused across clips).
  1. Set it to 1.0 across the whole clip.
    • Optional: ramp 1 → 0 over the recovery frames if you want the hand to return to
    • the weapon before the clip fully ends.

If an LHIK-using weapon's melee clip is missing the curve, LHIK snaps the off-hand to the
grip mid-swing. Add it to all the rifle/shotgun melee clips.

Because curves are evaluated through the pose with blend weights, the value reads ~1 while the Melee state dominates and ramps to 0 as it blends out — a natural fade, and exactly as long as the clip plays. No dependence on the 0.6s tag window.

#2. Wire it into Should Apply LHIK


[Get Curve Value]                [Less (<)]

  Curve Name: DisableLHIK  ──►  A         ──►┐

                                B: 0.5        │

                                              ├──►[AND Boolean]──►[Active Value]

[Should Apply LHIK] ──────────────────────────┘   (of Blend Poses by bool)

Node-by-node:

  1. Get Curve Value — Curve Name = DisableLHIK. Target defaults to self. (Thread-safe.)
  1. Less (<) (float) — A = curve value, B = 0.5. True when the curve is below
  2. threshold, i.e. melee anim is NOT playing.

  1. AND Boolean — inputs: existing Should Apply LHIK, and the < result.
  1. Wire the AND output into the Active Value pin of Blend Poses by bool
  2. (replacing the current direct connection).

Net: LHIK applies only when the original condition is true and the melee curve is inactive.

#Where to put the nodes — call Get Curve Value in Thread Safe Update Animation

Do this in the Blueprint Thread Safe Update Animation function, not inline in the AnimGraph / an Animation Layer. Read the curve there into a bool variable, then have the AnimGraph's Active Value pin read that variable:


(Thread Safe Update Animation)

  bShouldApplyLHIK = (existing logic) AND (GetCurveValue('DisableLHIK') < 0.5)

(AnimGraph)

  Active Value  ◄── bShouldApplyLHIK

Gotcha (cost us a session, 2026-06-13): calling Get Curve Value inline in the
AnimGraph or an Animation Layer returned 0 every frame even with the curve correctly
authored (flat 1.0 on MC_TP_AR_Melee2). Get Curve Value returns the curve from the
instance's evaluated pose; reading it from Thread Safe Update Animation and caching to
a bool is the path that reliably works. Switching to that fixed it immediately.

#Optional — smooth blend instead of binary

To ease LHIK back in during swing recovery (no pop):

  1. Ramp the DisableLHIK curve from 1 → 0 over the recovery frames of the clip.
  1. Replace Blend Poses by bool with a float/alpha blend node.
  1. Feed 1 - DisableLHIK (a OneMinus/subtract) — optionally `× ShouldApplyLHIK as
  2. float` — into the blend alpha.

#Why not the alternatives

  • AnimNotifyState across the clip → thread-safe bool — works, asset-authored, but
  • binary (no blend) and more plumbing.

  • State weight query — melee is a state, but cross-referencing its weight from the
  • IK section needs a state-machine ref + index lookup that breaks if states are reordered. The curve avoids that fragility and blends for free.


#TP Control Rig LHIK — CR_MC_TP_LeftHandIK

The third-person left-hand IK is moving from FABRIK (above) to a Control Rig (Two Bone IK, better elbow/pole control). Asset: /Game/SystemLink/Characters/MasterChief/Rigs/CR_MC_TP_LeftHandIK. Auto-generated via the systemlink-unreal bridge (see memory feedback_controlrig_python_membervar_crash for what is / isn't scriptable).

#Graph (Forwards Solve)


Forwards Solve ─exec→ Basic IK (RigUnit_TwoBoneIKSimplePerItem)

   ItemA=upperarm_l  ItemB=lowerarm_l  EffectorItem=hand_l



# Effector — POSITION-ONLY (keep the hand's animated rotation):

Get LeftHandTarget ──→ ComposeEffector.A ┐

Get Transform(hand_r) → ComposeEffector.B ┴→ Result.Translation → Effector.Translation

Get Transform(hand_l) ─────────────────────→ .Rotation ─────────→ Effector.Rotation



# Pole vector — LOCATION, composed from the weapon joint-target socket offset:

Get PoleVector (offset) → ComposePole.A.Translation ┐

Get Transform(hand_r)   → ComposePole.B             ┴→ Result.Translation → Pole Vector   (Kind = Location)



# Weight gate:

Get Apply → If.Condition ┐

Get Alpha → If.True       ┴→ If.Result → Weight     (If.False = 0.0)

Effector is position-only: the hand moves to the grip (ComposeEffector.Result translation) but keeps its animated rotation (hand_l's current transform). Forcing the socket rotation flipped the hand ~180° (the old FABRIK path was position-only, so the socket rotation was never authored to match). If you ever want exact finger-wrap alignment instead, feed the full composed transform and add a corrective rotation offset.

#Public variables (the ABP feeds these)

VariableTypeMeaning
LeftHandTargetTransformGrip target in hand_r bone space (FSLLeftHandIKTargets.EffectorBoneSpace_HandR). Only its translation is used (position-only).
PoleVectorVectorElbow-hint location offset in hand_r bone space (FSLLeftHandIKTargets.PoleBoneSpace_HandR), composed against hand_r → Location pole. Zero = no socket → pole at hand_r (unstable).
AlphaFloatIK blend 0..1.
ApplyBoolHard on/off gate — false forces Weight to 0 regardless of Alpha. Default should be true.

Per-weapon pole vector (data-driven): the pole is authored as the weapon's LeftHandIKJointTargetSocketName socket. CalculateLeftHandIKTargets reads that socket's world location and stores it relative to hand_r in PoleBoneSpace_HandR (mirrors the effector). Place the socket on the weapon mesh in the direction you want the elbow to point (down & out) — then every weapon reuses the same rig/ABP with its own elbow hint. Missing socket ⇒ zero offset ⇒ pole resolves to hand_r (degenerate). ABP feeds PoleBoneSpace_HandR → PoleVector.

#Why the Get Transform(hand_r) → Multiply composition

LHIK_TP_Snapshot stores the target in hand_r bone space, not world/component (it's computed as HandR_WS⁻¹ * Effector_WS in FSLWeaponViewDriverBase). The Two Bone IK's Effector needs a world transform, so the rig recomposes it internally: Effector = LeftHandTarget × hand_r(global) (UE relative×parent order). This keeps the ABP side trivial — just pass the raw snapshot; no transform math in the AnimGraph. (Assumes weapons use hand_r as RightHandBoneName — true for all current weapons.)

#Tuning with Direct Manipulation

Direct Manipulation is a Control Rig viewport feature: with a solver node selected (here Basic IK), it draws draggable gizmos in the 3D preview for that node's manipulatable inputs — the Effector (a transform) and the Pole Vector (a direction) — so you drag them and watch the arm solve live, instead of only typing values. The toolbar toggle is the move-gizmo button; the A dropdown next to it picks which target is active when a node exposes more than one. The green/blue arc it draws is the Two Bone IK debug visualization — the IK plane and reach, i.e. which way the elbow bends for the current pole vector.

Use it for the axis / pole-vector tuning pass:

  • Confirm the elbow bends backward naturally — if it snaps the wrong way, adjust the Pole Vector direction
  • (default (0,0,1)) and/or the node's Primary/Secondary Axis (defaults (1,0,0) / (0,1,0) — the left arm may need a sign flip).

Caveat for this rig: Effector is driven by a link (Multiply.Result) and PoleVector/Weight by
variables. Direct-manipulating a pin that's already connected/bound only edits its default, which the
link/variable overrides at runtime — so it's a preview/authoring aid here, not a live knob. To exercise the
solve, temporarily set the Apply/Alpha variable defaults (or feed values) so Weight > 0, then eyeball the
pole vector.

#Axis tuning — RESOLVED (2026-07-02)

Final values on the Basic IK node: PrimaryAxis (1,0,0), SecondaryAxis (0,-1,0).

Symptom that led here: with the default SecondaryAxis (0,1,0), the elbow bent the wrong way, and moving the pole to correct the elbow twisted the shoulder inside-out — i.e. you could get a correct elbow or a correct shoulder but never both, and flipping one inverted the other.

Diagnosis (computed from the skeleton ref pose via the bridge, not guessed):

  • Primary axis is fine(1,0,0) down the bone for both upperarm_l and lowerarm_l (the left arm is
  • NOT mirrored on the aim axis).

  • lowerarm_l is rolled ~39° relative to upperarm_l (dot(localY_upper, localY_lower) ≈ 0.777).
  • RigUnit_TwoBoneIKSimplePerItem ("Basic IK") assumes both bones share ONE Secondary Axis, so that roll mismatch makes the shared axis a compromise → the elbow-vs-shoulder "can't win both" behaviour.

  • Fix: the bend plane is X-Y (primary = X), so the secondary is ±Y; flipping it to (0,-1,0) resolved it.

Lesson: for a Two Bone IK arm, "elbow correct XOR shoulder inside-out, and flipping the pole swaps which is wrong" is a Secondary Axis problem, not a pole-position one. Don't chase it with the pole. Confirm the primary axis from the ref pose (inverse_transform_direction(bone, childDir)), then try the four ±secondary signs — one gives a clean elbow + shoulder. If none do (severe per-bone roll mismatch), switch the solver to Full Body IK (PBIK), which drives from an effector + pole with no explicit axes.

#ABP hookup (remaining — see Docs/EditorTasks.md)

Drop a Control Rig node in the TP AnimGraph set to CR_MC_TP_LeftHandIK, feed straight from LHIK_TP_Snapshot: LeftHandTarget ← EffectorBoneSpace_HandR, Alpha ← Alpha, Apply ← your suppression bool (e.g. the melee-curve gate that the FABRIK path uses above). Retire the FABRIK path once it matches.

  • Docs/Melee.md — melee ability, MeleeSwingDuration, bIsMeleeing snapshot.
  • Docs/WeaponClipping.md — FP weapon obstruction / pull-back (separate system).
  • Docs/EditorTasks.md — the CR_MC_TP_LeftHandIK build/hookup checklist.
  • Docs/Footguns.md — Control Rig via bridge (member-var crash; stale view when editing an open asset).