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, …):
- Open the animation.
- Curves → Add Curve (float) → name it
DisableLHIK(exact name, reused across clips).
- 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:
- Get Curve Value — Curve Name =
DisableLHIK. Target defaults to self. (Thread-safe.)
- Less (<) (float) — A = curve value, B = 0.5. True when the curve is below
threshold, i.e. melee anim is NOT playing.
- AND Boolean — inputs: existing
Should Apply LHIK, and the<result.
- Wire the AND output into the
Active Valuepin of Blend Poses by bool
(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 onMC_TP_AR_Melee2).Get Curve Valuereturns 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):
- Ramp the
DisableLHIKcurve from 1 → 0 over the recovery frames of the clip.
- Replace Blend Poses by bool with a float/alpha blend node.
- Feed
1 - DisableLHIK(aOneMinus/subtract) — optionally `× ShouldApplyLHIK as
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)
| Variable | Type | Meaning |
|---|---|---|
LeftHandTarget | Transform | Grip target in hand_r bone space (FSLLeftHandIKTargets.EffectorBoneSpace_HandR). Only its translation is used (position-only). |
PoleVector | Vector | Elbow-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). |
Alpha | Float | IK blend 0..1. |
Apply | Bool | Hard 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:Effectoris driven by a link (Multiply.Result) andPoleVector/Weightby
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 theApply/Alphavariable 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 bothupperarm_landlowerarm_l(the left arm is
NOT mirrored on the aim axis).
lowerarm_lis rolled ~39° relative toupperarm_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.
#Related
Docs/Melee.md— melee ability,MeleeSwingDuration,bIsMeleeingsnapshot.
Docs/WeaponClipping.md— FP weapon obstruction / pull-back (separate system).
Docs/EditorTasks.md— theCR_MC_TP_LeftHandIKbuild/hookup checklist.
Docs/Footguns.md— Control Rig via bridge (member-var crash; stale view when editing an open asset).