Three.js Bone-Local Weapon Attachment: World vs Local Transform
How to attach a weapon to a Three.js character bone correctly, and how to diagnose double-applied transforms during idle, turn and attack.
A straightforward way to attach a weapon in Three.js is to parent the prop to the intended bone and apply a bone-local transform. This lets the scene hierarchy carry it with the animated character. A separate world-space attachment system can also work, but it must handle coordinate conversion and update order correctly. Problems arise when world-space values are applied as local values or when two systems both update the attachment.
GripForge's gripforge_attach tool returns a bind transform (position, rotation, rotationOrder, and a baseQuat correction) plus a ready-to-use Three.js snippet. Treat that exported snippet as the source of truth. Don't hand-roll quaternion multiplication order from scratch — small order mistakes are exactly what cause the "works at idle, breaks on turn" bug described below. See the attachment documentation for the exact fields the bind output contains.
The hierarchy you're actually placing the weapon in
Three.js bones participate in the normal scene graph, so a weapon's final position is the product of every ancestor transform above it:
Scene
└─ CharacterRoot (root scale / world position)
└─ Rig hierarchy
└─ Hand bone (e.g. RightHand, or a rig-specific alias)
└─ Weapon (bone-local position/rotation from bind output)
The diagram is conceptual: the bone objects belong to the scene hierarchy, while a THREE.Skeleton is the structure that tracks bones and their skinning data, not an extra scene node to parent under.
With ordinary scene-graph parenting, the weapon inherits its bone's transform when matrices are updated. Keep animation and matrix updates consistent with your application's render loop. You do not also need to copy the bone's world transform into the child's local transform each frame.
Diagnosing double-applied transforms
"Double-applied" means some correction — a rotation, an offset, a scale — is being added twice: once inside the bind data GripForge already computed, and again in your own code. The result is consistent but wrong, or wrong only under motion. Two common patterns:
Pattern 1 — a correction applied twice. A bind can contain position, rotation, a rotation order and baseQuat. Start with the generated snippet, which shows how those fields are used together. An extra correction copied from a different integration example may apply an adjustment twice. Establish a working baseline before introducing any deliberately authored per-character grip offset.
Pattern 2 — world values written into local properties. If a weapon is already a bone child, its position and orientation are relative to that parent. Writing the bone's world transform into those local properties mixes coordinate spaces. In a separate manual attachment system, reading the bone before animation or matrix updates can also introduce lag. Identify which system owns the final transform before changing its update order.
Validation: idle, turn, attack
Check these in order; each isolates a different failure mode.
- Idle. Attach the weapon at rest pose. Acceptance: no visible gap or clipping between grip and hand, matching the reference pose the bind was made for.
- Turn. Rotate the character root (yaw) through a full turn. Acceptance: the weapon's offset relative to the hand stays constant at every angle — no drift, no independent rotation. If it drifts, inspect the actual parent, inherited transforms and any other code writing the prop transform.
- Attack. Play the attack animation and watch the weapon through the fastest arm motion in the clip. Acceptance: the weapon tracks the hand rigidly with no lag, no flip, and no sudden re-orientation partway through. For lag, inspect animation and transform update order. For a flip, inspect rotation representation, the supplied rotation order and other code that changes orientation; the symptom alone does not identify one certain cause.
Symptom / cause checklist
| Symptom | Likely cause | What to check |
|---|---|---|
| Weapon floats away as character turns | Parented to scene/root instead of the bone | Confirm the weapon's parent is the bone object itself |
| Offset is wrong by the same amount at every pose | A correction was applied twice (own rotation stacked on baseQuat) | Start from the exported snippet; inspect additional rotations for duplicated corrections |
| Correct at idle, drifts during animation | Mixed coordinate spaces or conflicting updates | Inspect parenting, transform spaces and update order |
| Slight lag or jitter during fast attack motion | Manual sync runs on a different frame step than the animation mixer | Ensure any manual update runs after the animation mixer updates, or remove it entirely |
| Weapon scales oddly on some characters | Root scale conflated with prop scale | Check the attach output's own scale value separately from the character root's scale |
| Bind looks fine on one rig, wrong on another | Different bone mapping, axes or proportions | Inspect the actual target bone and fit on each rig |
Different rigs and proportions legitimately need different local offsets — getting identical numbers across two characters is not a correctness requirement, and isn't something to chase. If you're fitting one weapon across multiple body sizes, see fitting the same weapon to different character sizes.
FAQ
Do I need to write my own quaternion math to attach a weapon?
No. Use the Three.js snippet exported by gripforge_attach; it already encodes position, rotation, rotationOrder, and the baseQuat correction for that bind.
What if my rig doesn't use a standard bone name like RightHand?
Bone-name matching supports common humanoid naming conventions, but custom or non-standard skeletons should be checked directly — confirm the bone your prop is parented to is the one you expect before trusting the bind.
Is this the same process as Unreal socket attachment?
Conceptually similar — a prop is placed relative to a bone/socket rather than world space — but the mechanics differ. For the Unreal side, see the socket alignment transfer checklist.
Where do I get the exact field names in the bind output?
The attachment documentation lists the bind transform fields; if you're retrieving assets through an agent workflow, the MCP docs describe how hosted versus local tooling deliver those files into your project.