Skip to main content

mittens_engine/engine/ecs/component/
ik_chain.rs

1use crate::engine::ecs::ComponentId;
2use crate::engine::ecs::component::{Component, ComponentRef};
3
4#[derive(Debug, Clone, Copy, PartialEq)]
5pub(crate) struct TwoBoneIkDebugVisuals {
6    pub target_line: ComponentId,
7    pub pole_line: ComponentId,
8    pub plane_normal_line: ComponentId,
9    pub elbow_line: ComponentId,
10    pub elbow_point: ComponentId,
11}
12
13/// Solver configuration for an `IKChainComponent`.
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum IKSolver {
16    /// Single-bone pose match.
17    ///
18    /// Sets the root joint's world rotation to match the target TC's world rotation,
19    /// post-multiplied by a fixed yaw offset.  Used for neck/head alignment from InputXR.
20    ///
21    /// `offset_yaw`: rotation applied after copying target world rotation.
22    /// Use `std::f32::consts::PI` for the OpenXR (−Z forward) → VRM (+Z forward) flip.
23    ///
24    /// `copy_position`: when true, also overrides the joint's world position to the
25    /// target's world position.  Required for HMD-driven head bones so the bone tracks
26    /// physical head translation (HMD moves forward+down when you pitch), not just
27    /// rotation.  Visually detaches the bone from its FK parent until a spine FABRIK
28    /// solver bends the chain to follow.  Default false (rotation-only behavior).
29    ///
30    /// `target_position_offset`: offset applied in the **target's local frame** before
31    /// copying its world position.  Used to compensate for the gap between the head
32    /// bone pivot (typically at the skull base) and the camera/eye position: passing
33    /// `(0, -eye_height, 0)` shifts the bone down so the eye mesh (which sits above
34    /// the bone pivot) lands at the HMD position.  Ignored when `copy_position` is
35    /// false.
36    AimConstraint {
37        offset_yaw: f32,
38        copy_position: bool,
39        target_position_offset: [f32; 3],
40    },
41
42    /// Closed-form 2-bone IK.
43    ///
44    /// Used for arms: UpperArm → LowerArm → Hand. All three joints are referenced
45    /// by explicit `ComponentId` — the solver does NO topology discovery and is
46    /// resilient to sibling helper / collider / cloth bones under the arm joints.
47    /// The chain's `parent_of` is ignored for this solver; `end_effector_id` (the
48    /// hand) lives on `IKChainComponent`, root + mid are here on the variant.
49    ///
50    /// `root_joint_id`: upper-arm TC (chain root).
51    /// `mid_joint_id`:  lower-arm TC (elbow).
52    /// `pole_direction`: direction hint for the middle joint (elbow / knee).
53    ///   Interpreted in body-local space when an ancestor `AvatarControlComponent`
54    ///   exists, otherwise world-space.  Body-local mode rotates the pole by the
55    ///   model root's world rotation each tick so the elbow stays anatomically
56    ///   correct when the body turns.
57    /// `copy_end_rotation`: if true, also aligns the end-effector bone to the target's rotation.
58    TwoBoneIK {
59        root_joint_id: ComponentId,
60        mid_joint_id: ComponentId,
61        pole_direction: [f32; 3],
62        copy_end_rotation: bool,
63    },
64
65    /// Iterative FABRIK solver — works for any chain length ≥ 2.
66    ///
67    /// Used for spine bending: chain hips → ... → splice_head with the head pose
68    /// driver as the target.  The spine rotates so the end-effector (splice_head)
69    /// FK-lands at the target position.
70    ///
71    /// `target_position_offset`: same semantics as `AimConstraint` — offset in the
72    /// target's local frame, added to its world position before chasing.  Used to
73    /// shift the target down by `eye_offset` so the head bone pivot (not the eye
74    /// mesh above it) lines up with the HMD position.
75    Fabrik {
76        max_iterations: u32,
77        tolerance: f32,
78        target_position_offset: [f32; 3],
79    },
80}
81
82/// Marks the root joint of an IK chain.
83///
84/// Place this as a **child of the root joint TC** (e.g. `J_Bip_L_UpperArm`, `splice_head`).
85/// The IKSystem finds this component, reads its parent TC as the root joint, walks down to
86/// `end_effector_id` to collect the chain, reads the target pose from `target_id`, solves,
87/// and emits `UpdateTransform` for each joint.
88///
89/// All three solver types are expressed through this single component; no separate
90/// end-effector or pole-vector marker components are required.
91#[derive(Debug, Clone)]
92pub struct IKChainComponent {
93    /// Which solver to run.
94    pub solver: IKSolver,
95
96    /// TC whose world pose is the IK target this frame.
97    ///
98    /// For `AimConstraint`: target world rotation is read here.
99    /// For `TwoBoneIK` / `Fabrik`: target world position (and optionally rotation) is read here.
100    pub target_id: ComponentId,
101
102    /// TC at the end of the bone chain.
103    ///
104    /// For `AimConstraint`: set to the root joint itself (chain length = 1).
105    /// For `TwoBoneIK`: set to the hand/foot bone (2 TCs below the root joint).
106    /// For `Fabrik`: set to the last bone in the spine/neck chain.
107    pub end_effector_id: ComponentId,
108
109    /// Blend weight: 0.0 = no IK applied, 1.0 = full solve.
110    pub weight: f32,
111
112    /// Authored form of `target_id` for round-trip dump. `None` for
113    /// IKChains wired purely at runtime (e.g. by `AvatarControlSystem`),
114    /// which have no MMS source to preserve.
115    pub target_source: Option<ComponentRef>,
116    /// Authored form of `end_effector_id` for round-trip dump.
117    pub end_effector_source: Option<ComponentRef>,
118
119    /// Cached ancestor `AvatarControlComponent` ID, discovered by `IKSystem`
120    /// on first tick via a parent-chain walk.  When `Some`, the solver
121    /// transforms `TwoBoneIK.pole_direction` from body-local to world space
122    /// using the AVC's model root rotation.  `None` → world-space pole
123    /// (current behavior for non-AVC chains).
124    pub(crate) avc_id: Option<ComponentId>,
125
126    /// Cached InputXR/XRHand component governing this runtime AVC target.
127    pub(crate) xr_pose_driver: Option<ComponentId>,
128
129    /// Lazily created runtime-only debug visual ids for TwoBoneIK inspection.
130    pub(crate) two_bone_debug_visuals: Option<TwoBoneIkDebugVisuals>,
131
132    component: Option<ComponentId>,
133}
134
135impl IKChainComponent {
136    pub fn new(solver: IKSolver, target_id: ComponentId, end_effector_id: ComponentId) -> Self {
137        Self {
138            solver,
139            target_id,
140            end_effector_id,
141            weight: 1.0,
142            target_source: None,
143            end_effector_source: None,
144            avc_id: None,
145            xr_pose_driver: None,
146            two_bone_debug_visuals: None,
147            component: None,
148        }
149    }
150
151    pub fn with_weight(mut self, w: f32) -> Self {
152        self.weight = w;
153        self
154    }
155
156    pub fn with_target_source(mut self, src: ComponentRef) -> Self {
157        self.target_source = Some(src);
158        self
159    }
160
161    pub fn with_end_effector_source(mut self, src: ComponentRef) -> Self {
162        self.end_effector_source = Some(src);
163        self
164    }
165}
166
167impl Component for IKChainComponent {
168    fn name(&self) -> &'static str {
169        "ik_chain"
170    }
171
172    fn set_id(&mut self, id: ComponentId) {
173        self.component = Some(id);
174    }
175
176    fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
177        emit.push_intent_now(
178            component,
179            crate::engine::ecs::IntentValue::RegisterIkChain {
180                component_ids: vec![component],
181            },
182        );
183    }
184
185    fn as_any(&self) -> &dyn std::any::Any {
186        self
187    }
188
189    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
190        self
191    }
192
193    fn to_mms_ast(
194        &self,
195        _world: &crate::engine::ecs::World,
196    ) -> crate::scripting::ast::ComponentExpression {
197        use crate::engine::ecs::component::ce_helpers::*;
198        use crate::scripting::ast::Expression;
199        let solver_call = match self.solver {
200            IKSolver::AimConstraint {
201                offset_yaw,
202                copy_position,
203                target_position_offset,
204            } => (
205                "aim_constraint",
206                vec![
207                    num(offset_yaw as f64),
208                    b(copy_position),
209                    array(nums(target_position_offset.iter().map(|&v| v as f64))),
210                ],
211            ),
212            IKSolver::TwoBoneIK {
213                pole_direction,
214                copy_end_rotation,
215                ..
216            } => (
217                "two_bone_ik",
218                vec![
219                    array(nums(pole_direction.iter().map(|&v| v as f64))),
220                    b(copy_end_rotation),
221                ],
222            ),
223            IKSolver::Fabrik {
224                max_iterations,
225                tolerance,
226                target_position_offset,
227            } => (
228                "fabrik",
229                vec![
230                    num(max_iterations as f64),
231                    num(tolerance as f64),
232                    array(nums(target_position_offset.iter().map(|&v| v as f64))),
233                ],
234            ),
235        };
236        fn target_expr(t: &ComponentRef) -> Expression {
237            match t {
238                ComponentRef::Guid(u) => Expression::String(format!("@uuid:{u}")),
239                ComponentRef::Query(s) => Expression::String(s.clone()),
240            }
241        }
242        let mut ce = ce_call("IKChain", solver_call.0, solver_call.1)
243            .with_call("weight", vec![num(self.weight as f64)]);
244        if let Some(src) = &self.target_source {
245            ce = ce.with_call("target", vec![target_expr(src)]);
246        }
247        if let Some(src) = &self.end_effector_source {
248            ce = ce.with_call("end_effector", vec![target_expr(src)]);
249        }
250        ce
251    }
252}