1use glam::{Mat4, Quat, Vec3A};
2
3use crate::IkAngleLimit;
4
5#[derive(Clone, Debug, PartialEq)]
6pub struct IkChainLinkDefinition {
7 pub bone_slot: usize,
8 pub angle_limit: Option<IkAngleLimit>,
9}
10
11#[derive(Clone, Debug, PartialEq)]
12pub struct IkChainDefinition {
13 pub parent_slots: Vec<Option<usize>>,
14 pub rest_positions: Vec<Vec3A>,
15 pub fixed_axes: Vec<Option<Vec3A>>,
18 pub target_slot: usize,
19 pub links: Vec<IkChainLinkDefinition>,
20 pub iteration_count: u32,
21 pub limit_angle: f32,
22}
23
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct IkChainPoseInput<'a> {
26 pub parent_world_matrix: Option<Mat4>,
27 pub local_position_offsets: &'a [Vec3A],
28 pub local_rotations: &'a [Quat],
29 pub goal_position: Vec3A,
30 pub tolerance: f32,
31 pub max_iterations_cap: Option<u32>,
32}
33
34#[derive(Clone, Debug, PartialEq)]
35pub struct IkChainSolveOutput {
36 pub solved_link_rotations: Vec<Quat>,
37 pub final_distance: f32,
38 pub executed_iterations: u32,
39 pub link_steps: u32,
40}
41
42#[derive(Debug)]
43pub struct IkChainSolver {
44 definition: IkChainDefinition,
45 local_axis_bases: Vec<Option<Quat>>,
48 world_matrices: Vec<Mat4>,
49 local_rotations: Vec<Quat>,
50 base_rotations: Vec<Quat>,
51 ik_rotations: Vec<Quat>,
52 best_ik_rotations: Vec<Quat>,
53 chain_states: Vec<ChainLinkState>,
54}
55
56impl IkChainSolver {
57 pub fn new(definition: IkChainDefinition) -> Self {
59 let bone_count = definition.rest_positions.len();
60 Self::new_with_local_axis_bases(definition, vec![None; bone_count])
61 }
62
63 pub fn new_with_local_axis_bases(
67 definition: IkChainDefinition,
68 local_axis_bases: Vec<Option<Quat>>,
69 ) -> Self {
70 let bone_count = definition.rest_positions.len();
71 let link_count = definition.links.len();
72 let mut bases = local_axis_bases
73 .into_iter()
74 .map(|basis| {
75 basis.filter(|basis| basis.is_finite() && basis.length_squared() > f32::EPSILON)
76 })
77 .collect::<Vec<_>>();
78 bases.resize(bone_count, None);
79 bases.truncate(bone_count);
80 Self {
81 definition,
82 local_axis_bases: bases,
83 world_matrices: vec![Mat4::IDENTITY; bone_count],
84 local_rotations: vec![Quat::IDENTITY; bone_count],
85 base_rotations: Vec::with_capacity(link_count),
86 ik_rotations: Vec::with_capacity(link_count),
87 best_ik_rotations: Vec::with_capacity(link_count),
88 chain_states: Vec::with_capacity(link_count),
89 }
90 }
91
92 pub fn solve(&mut self, input: IkChainPoseInput<'_>) -> IkChainSolveOutput {
93 let tolerance = input.tolerance.max(0.0);
94 let iteration_count = input
95 .max_iterations_cap
96 .map(|cap| self.definition.iteration_count.min(cap))
97 .unwrap_or(self.definition.iteration_count)
98 .max(1) as usize;
99 let limit_angle = self.definition.limit_angle.max(0.0);
100 let link_count = self.definition.links.len();
101
102 self.local_rotations.copy_from_slice(input.local_rotations);
103 self.update_world_matrices(input);
104
105 self.base_rotations.clear();
106 self.base_rotations.extend(
107 self.definition
108 .links
109 .iter()
110 .map(|link| self.local_rotations[link.bone_slot]),
111 );
112 self.ik_rotations.clear();
113 self.ik_rotations.resize(link_count, Quat::IDENTITY);
114 self.best_ik_rotations.clear();
115 self.best_ik_rotations.resize(link_count, Quat::IDENTITY);
116 self.chain_states.clear();
117 self.chain_states
118 .resize_with(link_count, ChainLinkState::default);
119
120 self.apply_link_rotations();
121 self.update_world_matrices(input);
122
123 let mut final_distance = f32::MAX;
124 let mut best_distance = f32::MAX;
125 let mut executed_iterations = 0u32;
126 let mut link_steps = 0u32;
127
128 for iteration in 0..iteration_count {
129 let eff_pos = translation(self.world_matrices[self.definition.target_slot]);
130 final_distance = (eff_pos - input.goal_position).length();
131 if final_distance <= tolerance {
132 break;
133 }
134 executed_iterations += 1;
135
136 for link_index in 0..link_count {
137 let link = &self.definition.links[link_index];
138 let link_slot = link.bone_slot;
139
140 if link_slot == self.definition.target_slot {
141 continue;
142 }
143
144 let link_world = self.world_matrices[link_slot];
145 let link_pos = translation(link_world);
146 let eff_pos = translation(self.world_matrices[self.definition.target_slot]);
147 let link_world_rot = rotation(link_world);
148 let local_effector = link_world_rot.inverse().mul_vec3a(eff_pos - link_pos);
149 let local_target = link_world_rot
150 .inverse()
151 .mul_vec3a(input.goal_position - link_pos);
152
153 if local_effector.length_squared() <= f32::EPSILON
154 || local_target.length_squared() <= f32::EPSILON
155 {
156 continue;
157 }
158
159 let bone_slot = link.bone_slot;
160 let fixed_axis = self.definition.fixed_axes.get(bone_slot).copied().flatten();
161 let local_axis_basis = self.local_axis_bases.get(bone_slot).copied().flatten();
162 solve_link_step(LinkStepInput {
163 local_effector: &local_effector,
164 local_target: &local_target,
165 link_index,
166 base_rotations: &self.base_rotations,
167 ik_rotations: &mut self.ik_rotations,
168 chain_states: &mut self.chain_states,
169 angle_limit: link.angle_limit,
170 iteration,
171 limit_angle,
172 local_axis_basis,
173 fixed_axis,
174 });
175
176 self.apply_link_rotations();
177 self.update_world_matrices(input);
178 link_steps += 1;
179 }
180
181 let current_distance = {
182 let eff = translation(self.world_matrices[self.definition.target_slot]);
183 (eff - input.goal_position).length()
184 };
185 final_distance = current_distance;
186 if current_distance < best_distance {
187 best_distance = current_distance;
188 self.best_ik_rotations.copy_from_slice(&self.ik_rotations);
189 if current_distance <= tolerance {
190 break;
191 }
192 } else {
193 self.ik_rotations.copy_from_slice(&self.best_ik_rotations);
194 self.apply_link_rotations();
195 self.update_world_matrices(input);
196 break;
197 }
198 }
199
200 self.ik_rotations.copy_from_slice(&self.best_ik_rotations);
201 self.apply_link_rotations();
202 self.update_world_matrices(input);
203
204 let solved_link_rotations = self
205 .definition
206 .links
207 .iter()
208 .map(|link| self.local_rotations[link.bone_slot])
209 .collect();
210
211 IkChainSolveOutput {
212 solved_link_rotations,
213 final_distance,
214 executed_iterations,
215 link_steps,
216 }
217 }
218
219 pub(crate) fn update_world_matrices(&mut self, input: IkChainPoseInput<'_>) {
220 update_mini_chain_world_matrices(
221 &self.definition,
222 input.parent_world_matrix.unwrap_or(Mat4::IDENTITY),
223 input.local_position_offsets,
224 &self.local_rotations,
225 &mut self.world_matrices,
226 );
227 }
228
229 fn apply_link_rotations(&mut self) {
230 for (i, link) in self.definition.links.iter().enumerate() {
231 let effective = (self.ik_rotations[i] * self.base_rotations[i]).normalize();
232 self.local_rotations[link.bone_slot] = effective;
235 }
236 }
237}
238
239pub(crate) fn update_mini_chain_world_matrices(
240 definition: &IkChainDefinition,
241 parent_world_matrix: Mat4,
242 local_position_offsets: &[Vec3A],
243 local_rotations: &[Quat],
244 world_matrices: &mut [Mat4],
245) {
246 for slot in 0..definition.rest_positions.len() {
247 let local_position = definition.rest_positions[slot] + local_position_offsets[slot];
248 let local_rotation = local_rotations[slot];
249 let local_matrix = Mat4::from_scale_rotation_translation(
250 Vec3A::ONE.into(),
251 local_rotation,
252 local_position.into(),
253 );
254 world_matrices[slot] = match definition.parent_slots[slot] {
255 Some(parent) => world_matrices[parent] * local_matrix,
256 None => parent_world_matrix * local_matrix,
257 };
258 }
259}
260
261pub(crate) fn solve_link_step(input: LinkStepInput<'_>) {
262 if let Some(fixed_axis) = input.fixed_axis {
266 let prior_ik_rotation = input.ik_rotations[input.link_index];
267 let prior_chain_state = input.chain_states[input.link_index];
268 solve_unconstrained_link_step(UnconstrainedLinkStepInput {
269 local_effector: input.local_effector,
270 local_target: input.local_target,
271 link_index: input.link_index,
272 base_rotations: input.base_rotations,
273 ik_rotations: input.ik_rotations,
274 limit_angle: input.limit_angle,
275 fixed_axis: Some(fixed_axis),
276 });
277 if let Some(angle_limit) = input.angle_limit {
278 if link_rotation_within_angle_limits(
282 input.link_index,
283 input.base_rotations,
284 input.ik_rotations,
285 angle_limit,
286 input.local_axis_basis,
287 ) {
288 return;
289 }
290 clamp_link_rotation_to_angle_limits(ClampAngleLimitInput {
291 link_index: input.link_index,
292 base_rotations: input.base_rotations,
293 ik_rotations: input.ik_rotations,
294 chain_states: input.chain_states,
295 limits: angle_limit,
296 local_axis_basis: input.local_axis_basis,
297 });
298 project_link_rotation_onto_fixed_axis(
301 input.link_index,
302 input.base_rotations,
303 input.ik_rotations,
304 fixed_axis,
305 );
306 if !link_rotation_within_angle_limits(
310 input.link_index,
311 input.base_rotations,
312 input.ik_rotations,
313 angle_limit,
314 input.local_axis_basis,
315 ) {
316 input.ik_rotations[input.link_index] = prior_ik_rotation;
317 input.chain_states[input.link_index] = prior_chain_state;
318 }
319 }
320 return;
321 }
322
323 let single_axis = get_single_axis_limit(input.angle_limit);
324 if let (Some(angle_limit), Some(axis_index)) = (input.angle_limit, single_axis) {
325 solve_plane_link_step(PlaneLinkStepInput {
326 local_effector: input.local_effector,
327 local_target: input.local_target,
328 link_index: input.link_index,
329 base_rotations: input.base_rotations,
330 ik_rotations: input.ik_rotations,
331 chain_states: input.chain_states,
332 axis_index,
333 limits: angle_limit,
334 iteration: input.iteration,
335 limit_angle: input.limit_angle,
336 local_axis_basis: input.local_axis_basis,
337 });
338 } else if let Some(angle_limit) = input.angle_limit {
339 solve_limited_axes_link_step(LimitedAxesLinkStepInput {
340 local_effector: input.local_effector,
341 local_target: input.local_target,
342 link_index: input.link_index,
343 base_rotations: input.base_rotations,
344 ik_rotations: input.ik_rotations,
345 chain_states: input.chain_states,
346 limits: angle_limit,
347 limit_angle: input.limit_angle,
348 local_axis_basis: input.local_axis_basis,
349 });
350 } else {
351 solve_unconstrained_link_step(UnconstrainedLinkStepInput {
352 local_effector: input.local_effector,
353 local_target: input.local_target,
354 link_index: input.link_index,
355 base_rotations: input.base_rotations,
356 ik_rotations: input.ik_rotations,
357 limit_angle: input.limit_angle,
358 fixed_axis: input.fixed_axis,
359 });
360 }
361}
362
363fn link_rotation_within_angle_limits(
364 link_index: usize,
365 base_rotations: &[Quat],
366 ik_rotations: &[Quat],
367 limits: IkAngleLimit,
368 local_axis_basis: Option<Quat>,
369) -> bool {
370 let chain = ik_rotations[link_index] * base_rotations[link_index];
371 if !chain.is_finite() || chain.length_squared() <= f32::EPSILON {
372 return false;
373 }
374 let basis =
375 local_axis_basis.filter(|basis| basis.is_finite() && basis.length_squared() > f32::EPSILON);
376 let (q_b, q_b_inv) = basis.map_or((Quat::IDENTITY, Quat::IDENTITY), |basis| {
377 let basis = basis.normalize();
378 (basis, basis.inverse())
379 });
380 let local = (q_b_inv * chain.normalize() * q_b).normalize();
381 if !local.is_finite() {
382 return false;
383 }
384 let euler = decompose_euler_xyz(&quat_to_rotation_mat3(local), &[0.0; 3]);
385 euler.iter().enumerate().all(|(axis, value)| {
386 let (lower, upper) = limit_axis_bounds(limits, axis);
387 *value >= lower - 1.0e-5 && *value <= upper + 1.0e-5
388 })
389}
390
391struct ClampAngleLimitInput<'a> {
392 link_index: usize,
393 base_rotations: &'a [Quat],
394 ik_rotations: &'a mut [Quat],
395 chain_states: &'a mut [ChainLinkState],
396 limits: IkAngleLimit,
397 local_axis_basis: Option<Quat>,
398}
399
400fn clamp_link_rotation_to_angle_limits(input: ClampAngleLimitInput<'_>) {
403 let base = input.base_rotations[input.link_index];
404 let current = input.ik_rotations[input.link_index] * base;
405 if !current.is_finite() || current.length_squared() <= f32::EPSILON {
406 return;
407 }
408 let current = current.normalize();
409 let (q_b, q_b_inv) = match input.local_axis_basis {
410 Some(basis) if basis.is_finite() => (basis.normalize(), basis.normalize().inverse()),
411 _ => (Quat::IDENTITY, Quat::IDENTITY),
412 };
413 let current_la = q_b_inv * current * q_b;
414 if !current_la.is_finite() || current_la.length_squared() <= f32::EPSILON {
415 return;
416 }
417 let current_la = current_la.normalize();
418 let current_mat = quat_to_rotation_mat3(current_la);
419 let state = &mut input.chain_states[input.link_index];
420 let mut euler = decompose_euler_xyz(¤t_mat, &state.previous_euler);
421 if !euler.iter().all(|v| v.is_finite()) {
422 return;
423 }
424 for (axis_index, value) in euler.iter_mut().enumerate() {
425 let (lower, upper) = limit_axis_bounds(input.limits, axis_index);
426 *value = value.clamp(lower, upper);
427 }
428 state.previous_euler = euler;
429 if let Some(axis_index) = get_single_axis_limit(Some(input.limits)) {
430 state.plane_mode_angle = euler[axis_index];
431 }
432 let chain_rotation_la = euler_xyz_to_quat(&euler);
433 if !chain_rotation_la.is_finite() || chain_rotation_la.length_squared() <= f32::EPSILON {
434 return;
435 }
436 let chain_rotation = q_b * chain_rotation_la.normalize() * q_b_inv;
437 if !chain_rotation.is_finite() || chain_rotation.length_squared() <= f32::EPSILON {
438 return;
439 }
440 let chain_rotation = chain_rotation.normalize();
441 let next_ik = chain_rotation * base.inverse();
442 if !next_ik.is_finite() || next_ik.length_squared() <= f32::EPSILON {
443 return;
444 }
445 input.ik_rotations[input.link_index] = next_ik.normalize();
446}
447
448fn project_link_rotation_onto_fixed_axis(
449 link_index: usize,
450 base_rotations: &[Quat],
451 ik_rotations: &mut [Quat],
452 fixed_axis: Vec3A,
453) {
454 if fixed_axis.length_squared() <= f32::EPSILON || !fixed_axis.is_finite() {
455 return;
456 }
457 let ik_rotation = ik_rotations[link_index];
458 if !ik_rotation.is_finite() || ik_rotation.length_squared() <= f32::EPSILON {
459 return;
460 }
461 let base = base_rotations[link_index];
462 if !base.is_finite() || base.length_squared() <= f32::EPSILON {
463 return;
464 }
465 let correction_axis = base.normalize().mul_vec3a(fixed_axis.normalize());
469 let constrained = constrain_rotation_to_axis(ik_rotation.normalize(), correction_axis);
470 if !constrained.is_finite() || constrained.length_squared() <= f32::EPSILON {
471 return;
472 }
473 ik_rotations[link_index] = constrained.normalize();
474}
475
476pub(crate) struct LinkStepInput<'a> {
477 pub local_effector: &'a Vec3A,
478 pub local_target: &'a Vec3A,
479 pub link_index: usize,
480 pub base_rotations: &'a [Quat],
481 pub ik_rotations: &'a mut [Quat],
482 pub chain_states: &'a mut [ChainLinkState],
483 pub angle_limit: Option<IkAngleLimit>,
484 pub iteration: usize,
485 pub limit_angle: f32,
486 pub local_axis_basis: Option<Quat>,
488 pub fixed_axis: Option<Vec3A>,
490}
491
492struct UnconstrainedLinkStepInput<'a> {
493 local_effector: &'a Vec3A,
494 local_target: &'a Vec3A,
495 link_index: usize,
496 base_rotations: &'a [Quat],
497 ik_rotations: &'a mut [Quat],
498 limit_angle: f32,
499 fixed_axis: Option<Vec3A>,
500}
501
502fn solve_unconstrained_link_step(input: UnconstrainedLinkStepInput<'_>) {
503 let local_eff_n = input.local_effector.normalize();
504 let local_tgt_n = input.local_target.normalize();
505
506 let tiny_angle = 1e-3 * std::f32::consts::PI / 180.0;
507
508 let (axis_vec, mut angle) = if let Some(fixed) = input.fixed_axis {
509 let axis = if fixed.length_squared() > f32::EPSILON && fixed.is_finite() {
510 fixed.normalize()
511 } else {
512 return;
513 };
514 let signed = signed_projected_angle(local_eff_n, local_tgt_n, axis);
515 if signed.abs() < tiny_angle {
516 return;
517 }
518 (axis, signed)
519 } else {
520 let dot = local_eff_n.dot(local_tgt_n).clamp(-1.0, 1.0);
521 let angle = dot.acos();
522 if angle < tiny_angle {
523 return;
524 }
525 let axis = local_eff_n.cross(local_tgt_n);
526 let axis_vec = if axis.length() < 1e-5 {
527 if dot > -1.0 + 1e-5 {
528 return;
529 }
530 let basis = if local_eff_n.x.abs() < 0.9 {
531 Vec3A::new(1.0, 0.0, 0.0)
532 } else {
533 Vec3A::new(0.0, 1.0, 0.0)
534 };
535 local_eff_n.cross(basis).normalize()
536 } else {
537 axis.normalize()
538 };
539 (axis_vec, angle)
540 };
541
542 if input.limit_angle > 0.0 {
543 angle = angle.clamp(-input.limit_angle, input.limit_angle);
544 }
545
546 let delta = Quat::from_axis_angle(axis_vec.into(), angle);
547 let base = input.base_rotations[input.link_index];
548 let ik = input.ik_rotations[input.link_index];
549 let chain_rotation = (ik * base * delta).normalize();
550
551 input.ik_rotations[input.link_index] = (chain_rotation * base.inverse()).normalize();
552}
553
554pub(crate) fn translation(matrix: Mat4) -> Vec3A {
555 Vec3A::from_vec4(matrix.w_axis)
556}
557
558pub(crate) fn rotation(matrix: Mat4) -> Quat {
559 matrix.to_scale_rotation_translation().1
560}
561
562pub(crate) fn constrain_rotation_to_axis(rotation: Quat, axis: Vec3A) -> Quat {
563 let axis = axis.normalize();
564 let vector = Vec3A::new(rotation.x, rotation.y, rotation.z);
565 let projected = axis * vector.dot(axis);
566 let twist = Quat::from_xyzw(projected.x, projected.y, projected.z, rotation.w);
567 if twist.length_squared() <= f32::EPSILON {
568 Quat::IDENTITY
569 } else {
570 twist.normalize()
571 }
572}
573
574#[derive(Clone, Copy, Debug, Default, PartialEq)]
575pub(crate) struct ChainLinkState {
576 pub previous_euler: [f32; 3],
577 pub plane_mode_angle: f32,
578}
579
580pub(crate) fn get_single_axis_limit(limit: Option<IkAngleLimit>) -> Option<usize> {
581 let limit = limit?;
582 let has = [
583 limit.min.x != 0.0 || limit.max.x != 0.0,
584 limit.min.y != 0.0 || limit.max.y != 0.0,
585 limit.min.z != 0.0 || limit.max.z != 0.0,
586 ];
587 if has[0]
588 && limit.min.y == 0.0
589 && limit.max.y == 0.0
590 && limit.min.z == 0.0
591 && limit.max.z == 0.0
592 {
593 return Some(0);
594 }
595 if has[1]
596 && limit.min.x == 0.0
597 && limit.max.x == 0.0
598 && limit.min.z == 0.0
599 && limit.max.z == 0.0
600 {
601 return Some(1);
602 }
603 if has[2]
604 && limit.min.x == 0.0
605 && limit.max.x == 0.0
606 && limit.min.y == 0.0
607 && limit.max.y == 0.0
608 {
609 return Some(2);
610 }
611 None
612}
613
614pub(crate) fn quat_to_rotation_mat3(rotation: Quat) -> [f32; 9] {
615 let [x, y, z, w] = rotation.normalize().to_array();
616 let x2 = x + x;
617 let y2 = y + y;
618 let z2 = z + z;
619 let xx = x * x2;
620 let xy = x * y2;
621 let xz = x * z2;
622 let yy = y * y2;
623 let yz = y * z2;
624 let zz = z * z2;
625 let wx = w * x2;
626 let wy = w * y2;
627 let wz = w * z2;
628 [
629 1.0 - (yy + zz),
630 xy + wz,
631 xz - wy,
632 xy - wz,
633 1.0 - (xx + zz),
634 yz + wx,
635 xz + wy,
636 yz - wx,
637 1.0 - (xx + yy),
638 ]
639}
640
641pub(crate) fn decompose_euler_xyz(mat: &[f32; 9], before: &[f32; 3]) -> [f32; 3] {
642 let sy = -mat[2];
643 let mut result: [f32; 3];
644 if 1.0 - sy.abs() < 1e-6 {
645 let y = sy.asin();
646 let sx = before[0].sin();
647 let sz = before[2].sin();
648 if sx.abs() < sz.abs() {
649 let cx = before[0].cos();
650 result = if cx > 0.0 {
651 [0.0, y, (-mat[3]).asin()]
652 } else {
653 [std::f32::consts::PI, y, mat[3].asin()]
654 };
655 } else {
656 let cz = before[2].cos();
657 result = if cz > 0.0 {
658 [(-mat[7]).asin(), y, 0.0]
659 } else {
660 [mat[7].asin(), y, std::f32::consts::PI]
661 };
662 }
663 } else {
664 result = [mat[5].atan2(mat[8]), (-mat[2]).asin(), mat[1].atan2(mat[0])];
665 }
666
667 let pi = std::f32::consts::PI;
668 let candidates: [[f32; 3]; 8] = [
669 [result[0] + pi, pi - result[1], result[2] + pi],
670 [result[0] + pi, pi - result[1], result[2] - pi],
671 [result[0] + pi, -pi - result[1], result[2] + pi],
672 [result[0] + pi, -pi - result[1], result[2] - pi],
673 [result[0] - pi, pi - result[1], result[2] + pi],
674 [result[0] - pi, pi - result[1], result[2] - pi],
675 [result[0] - pi, -pi - result[1], result[2] + pi],
676 [result[0] - pi, -pi - result[1], result[2] - pi],
677 ];
678 let mut min_error = diff_angle(result[0], before[0]).abs()
679 + diff_angle(result[1], before[1]).abs()
680 + diff_angle(result[2], before[2]).abs();
681 for candidate in &candidates {
682 let error = diff_angle(candidate[0], before[0]).abs()
683 + diff_angle(candidate[1], before[1]).abs()
684 + diff_angle(candidate[2], before[2]).abs();
685 if error < min_error {
686 min_error = error;
687 result = *candidate;
688 }
689 }
690 result
691}
692
693fn diff_angle(a: f32, b: f32) -> f32 {
694 let diff = normalize_angle(a) - normalize_angle(b);
695 if diff > std::f32::consts::PI {
696 diff - std::f32::consts::TAU
697 } else if diff < -std::f32::consts::PI {
698 diff + std::f32::consts::TAU
699 } else {
700 diff
701 }
702}
703
704fn normalize_angle(angle: f32) -> f32 {
705 let mut result = angle;
706 while result >= std::f32::consts::TAU {
707 result -= std::f32::consts::TAU;
708 }
709 while result < 0.0 {
710 result += std::f32::consts::TAU;
711 }
712 result
713}
714
715pub(crate) fn euler_xyz_to_quat(euler: &[f32; 3]) -> Quat {
716 let [x, y, z] = *euler;
717 let c1 = (x / 2.0).cos();
718 let c2 = (y / 2.0).cos();
719 let c3 = (z / 2.0).cos();
720 let s1 = (x / 2.0).sin();
721 let s2 = (y / 2.0).sin();
722 let s3 = (z / 2.0).sin();
723 Quat::from_xyzw(
724 s1 * c2 * c3 + c1 * s2 * s3,
725 c1 * s2 * c3 - s1 * c2 * s3,
726 c1 * c2 * s3 + s1 * s2 * c3,
727 c1 * c2 * c3 - s1 * s2 * s3,
728 )
729}
730
731pub(crate) struct LimitedAxesLinkStepInput<'a> {
732 pub local_effector: &'a Vec3A,
733 pub local_target: &'a Vec3A,
734 pub link_index: usize,
735 pub base_rotations: &'a [Quat],
736 pub ik_rotations: &'a mut [Quat],
737 pub chain_states: &'a mut [ChainLinkState],
738 pub limits: IkAngleLimit,
739 pub limit_angle: f32,
740 pub local_axis_basis: Option<Quat>,
741}
742
743pub(crate) fn solve_limited_axes_link_step(input: LimitedAxesLinkStepInput<'_>) {
744 let state = &mut input.chain_states[input.link_index];
745 let base = input.base_rotations[input.link_index];
746 let current = (input.ik_rotations[input.link_index] * base).normalize();
747 let (q_b, q_b_inv) = match input.local_axis_basis {
750 Some(basis) if basis.is_finite() => (basis.normalize(), basis.normalize().inverse()),
751 _ => (Quat::IDENTITY, Quat::IDENTITY),
752 };
753 let current_la = (q_b_inv * current * q_b).normalize();
754 let current_mat = quat_to_rotation_mat3(current_la);
755 let mut total_euler = decompose_euler_xyz(¤t_mat, &state.previous_euler);
756 let mut working_effector = q_b_inv.mul_vec3a(*input.local_effector);
757 let target = q_b_inv.mul_vec3a(*input.local_target).normalize();
758
759 for axis_index in [2usize, 1, 0] {
760 let (lower, upper) = limit_axis_bounds(input.limits, axis_index);
761 if lower == 0.0 && upper == 0.0 {
762 let next = total_euler[axis_index].clamp(lower, upper);
763 let applied = next - total_euler[axis_index];
764 total_euler[axis_index] = next;
765 if applied.abs() > 0.0 {
766 working_effector = Quat::from_axis_angle(axis_vec(axis_index).into(), applied)
767 .mul_vec3a(working_effector);
768 }
769 continue;
770 }
771
772 let axis = axis_vec(axis_index);
773 let signed_angle = signed_projected_angle(working_effector, target, axis);
774 if signed_angle.abs() <= 1.0e-6 {
775 continue;
776 }
777 let step = if input.limit_angle > 0.0 {
778 signed_angle.clamp(-input.limit_angle, input.limit_angle)
779 } else {
780 signed_angle
781 };
782 let next = (total_euler[axis_index] + step).clamp(lower, upper);
783 let applied = next - total_euler[axis_index];
784 total_euler[axis_index] = next;
785 if applied.abs() > 0.0 {
786 working_effector =
787 Quat::from_axis_angle(axis.into(), applied).mul_vec3a(working_effector);
788 }
789 }
790
791 state.previous_euler = total_euler;
792 let chain_rotation_la = euler_xyz_to_quat(&total_euler).normalize();
793 let chain_rotation = (q_b * chain_rotation_la * q_b_inv).normalize();
794 input.ik_rotations[input.link_index] = (chain_rotation * base.inverse()).normalize();
795}
796
797pub(crate) fn limit_axis_bounds(limits: IkAngleLimit, axis_index: usize) -> (f32, f32) {
798 match axis_index {
799 0 => (limits.min.x, limits.max.x),
800 1 => (limits.min.y, limits.max.y),
801 _ => (limits.min.z, limits.max.z),
802 }
803}
804
805pub(crate) fn axis_vec(axis_index: usize) -> Vec3A {
806 match axis_index {
807 0 => Vec3A::new(1.0, 0.0, 0.0),
808 1 => Vec3A::new(0.0, 1.0, 0.0),
809 _ => Vec3A::new(0.0, 0.0, 1.0),
810 }
811}
812
813pub(crate) fn signed_projected_angle(from: Vec3A, to: Vec3A, axis: Vec3A) -> f32 {
814 let projected_from = from - axis * from.dot(axis);
815 let projected_to = to - axis * to.dot(axis);
816 if projected_from.length_squared() <= f32::EPSILON
817 || projected_to.length_squared() <= f32::EPSILON
818 {
819 return 0.0;
820 }
821 let from_n = projected_from.normalize();
822 let to_n = projected_to.normalize();
823 let dot = from_n.dot(to_n).clamp(-1.0, 1.0);
824 let angle = dot.acos();
825 let sign = axis.dot(from_n.cross(to_n)).signum();
826 angle * if sign == 0.0 { 1.0 } else { sign }
827}
828
829pub(crate) struct PlaneLinkStepInput<'a> {
830 pub local_effector: &'a Vec3A,
831 pub local_target: &'a Vec3A,
832 pub link_index: usize,
833 pub base_rotations: &'a [Quat],
834 pub ik_rotations: &'a mut [Quat],
835 pub chain_states: &'a mut [ChainLinkState],
836 pub axis_index: usize,
837 pub limits: IkAngleLimit,
838 pub iteration: usize,
839 pub limit_angle: f32,
840 pub local_axis_basis: Option<Quat>,
841}
842
843pub(crate) fn solve_plane_link_step(input: PlaneLinkStepInput<'_>) {
844 let rotate_axis_la = match input.axis_index {
845 0 => Vec3A::new(1.0, 0.0, 0.0),
846 1 => Vec3A::new(0.0, 1.0, 0.0),
847 _ => Vec3A::new(0.0, 0.0, 1.0),
848 };
849 let (q_b, q_b_inv) = match input.local_axis_basis {
850 Some(basis) if basis.is_finite() => (basis.normalize(), basis.normalize().inverse()),
851 _ => (Quat::IDENTITY, Quat::IDENTITY),
852 };
853 let local_eff_n = q_b_inv.mul_vec3a(*input.local_effector).normalize();
855 let local_tgt_n = q_b_inv.mul_vec3a(*input.local_target).normalize();
856
857 let dot = local_eff_n.dot(local_tgt_n).clamp(-1.0, 1.0);
858 let raw_angle = dot.acos();
859 let capped_angle = if input.limit_angle > 0.0 {
860 raw_angle.min(input.limit_angle)
861 } else {
862 raw_angle
863 };
864
865 let target_vec1 =
866 Quat::from_axis_angle(rotate_axis_la.into(), capped_angle).mul_vec3a(local_eff_n);
867 let target_vec2 =
868 Quat::from_axis_angle(rotate_axis_la.into(), -capped_angle).mul_vec3a(local_eff_n);
869 let signed_angle = if target_vec1.dot(local_tgt_n) > target_vec2.dot(local_tgt_n) {
870 capped_angle
871 } else {
872 -capped_angle
873 };
874
875 let state = &mut input.chain_states[input.link_index];
876 let mut next_angle = state.plane_mode_angle + signed_angle;
877 let (lower, upper) = match input.axis_index {
878 0 => (input.limits.min.x, input.limits.max.x),
879 1 => (input.limits.min.y, input.limits.max.y),
880 _ => (input.limits.min.z, input.limits.max.z),
881 };
882 let base = input.base_rotations[input.link_index];
883
884 if input.iteration == 0 && (next_angle < lower || next_angle > upper) {
885 if -next_angle > lower && -next_angle < upper {
886 next_angle = -next_angle;
887 } else {
888 let half = (lower + upper) * 0.5;
889 if (half - next_angle).abs() > (half + next_angle).abs() {
890 next_angle = -next_angle;
891 }
892 }
893 }
894
895 state.plane_mode_angle = next_angle.clamp(lower, upper);
896 let chain_rotation_la = Quat::from_axis_angle(rotate_axis_la.into(), state.plane_mode_angle);
897 let chain_rotation = (q_b * chain_rotation_la * q_b_inv).normalize();
898 input.ik_rotations[input.link_index] = (chain_rotation * base.inverse()).normalize();
899}
900
901#[cfg(test)]
902mod tests {
903 use std::sync::Arc;
904
905 use super::*;
906 use crate::{BoneIndex, BoneInit, IkLinkInit, IkSolverInit, ModelArena, RuntimeInstance};
907
908 fn assert_vec3a_near(actual: Vec3A, expected: Vec3A) {
909 let delta = (actual - expected).abs();
910 assert!(
911 delta.x < 1.0e-5 && delta.y < 1.0e-5 && delta.z < 1.0e-5,
912 "actual={actual:?} expected={expected:?} delta={delta:?}"
913 );
914 }
915
916 fn assert_quat_near(actual: Quat, expected: Quat) {
917 let actual = actual.to_array();
918 let expected = expected.to_array();
919 let delta = [
920 (actual[0] - expected[0]).abs(),
921 (actual[1] - expected[1]).abs(),
922 (actual[2] - expected[2]).abs(),
923 (actual[3] - expected[3]).abs(),
924 ];
925 assert!(
926 delta[0] < 1.0e-5 && delta[1] < 1.0e-5 && delta[2] < 1.0e-5 && delta[3] < 1.0e-5,
927 "actual={actual:?} expected={expected:?} delta={delta:?}"
928 );
929 }
930
931 fn one_link_definition(angle_limit: Option<IkAngleLimit>) -> IkChainDefinition {
932 IkChainDefinition {
933 parent_slots: vec![None, Some(0)],
934 rest_positions: vec![Vec3A::ZERO, Vec3A::X],
935 fixed_axes: vec![None, None],
936 target_slot: 1,
937 links: vec![IkChainLinkDefinition {
938 bone_slot: 0,
939 angle_limit,
940 }],
941 iteration_count: 1,
942 limit_angle: 0.0,
943 }
944 }
945
946 #[test]
947 fn mini_chain_world_update_uses_identity_when_parent_world_is_unspecified() {
948 let definition = one_link_definition(None);
949 let mut world = vec![Mat4::IDENTITY; 2];
950 update_mini_chain_world_matrices(
951 &definition,
952 Mat4::IDENTITY,
953 &[Vec3A::ZERO, Vec3A::new(0.0, 2.0, 0.0)],
954 &[
955 Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
956 Quat::IDENTITY,
957 ],
958 &mut world,
959 );
960
961 assert_vec3a_near(translation(world[1]), Vec3A::new(-2.0, 1.0, 0.0));
962 }
963
964 #[test]
965 fn primitive_matches_full_runtime_for_unconstrained_chain() {
966 let model = Arc::new(
967 ModelArena::new_with_ik(
968 vec![
969 BoneInit::new(None, Vec3A::ZERO),
970 BoneInit::new(Some(BoneIndex(0)), Vec3A::X),
971 BoneInit::new(None, Vec3A::Y),
972 ],
973 vec![IkSolverInit {
974 ik_bone: BoneIndex(2),
975 target_bone: BoneIndex(1),
976 links: vec![IkLinkInit::new(BoneIndex(0))],
977 iteration_count: 1,
978 limit_angle: 0.0,
979 }],
980 )
981 .unwrap(),
982 );
983 let mut runtime = RuntimeInstance::new(model);
984 runtime.evaluate_current_pose();
985
986 let mut solver = IkChainSolver::new(one_link_definition(None));
987 let local_position_offsets = [Vec3A::ZERO; 2];
988 let local_rotations = [Quat::IDENTITY; 2];
989 let output = solver.solve(IkChainPoseInput {
990 parent_world_matrix: None,
991 local_position_offsets: &local_position_offsets,
992 local_rotations: &local_rotations,
993 goal_position: Vec3A::Y,
994 tolerance: 1.0e-2,
995 max_iterations_cap: None,
996 });
997
998 assert_quat_near(
999 output.solved_link_rotations[0],
1000 runtime.pose().local_rotation(BoneIndex(0)),
1001 );
1002 }
1003
1004 #[test]
1005 fn primitive_matches_full_runtime_for_two_link_unconstrained_chain() {
1006 let model = Arc::new(
1007 ModelArena::new_with_ik(
1008 vec![
1009 BoneInit::new(None, Vec3A::ZERO),
1010 BoneInit::new(Some(BoneIndex(0)), Vec3A::X),
1011 BoneInit::new(Some(BoneIndex(1)), Vec3A::X),
1012 BoneInit::new(None, Vec3A::new(1.0, 1.0, 0.0)),
1013 ],
1014 vec![IkSolverInit {
1015 ik_bone: BoneIndex(3),
1016 target_bone: BoneIndex(2),
1017 links: vec![IkLinkInit::new(BoneIndex(1)), IkLinkInit::new(BoneIndex(0))],
1018 iteration_count: 4,
1019 limit_angle: 0.0,
1020 }],
1021 )
1022 .unwrap(),
1023 );
1024 let mut runtime = RuntimeInstance::new(model);
1025 runtime.evaluate_current_pose();
1026
1027 let definition = IkChainDefinition {
1028 parent_slots: vec![None, Some(0), Some(1)],
1029 rest_positions: vec![Vec3A::ZERO, Vec3A::X, Vec3A::X],
1030 fixed_axes: vec![None, None, None],
1031 target_slot: 2,
1032 links: vec![
1033 IkChainLinkDefinition {
1034 bone_slot: 1,
1035 angle_limit: None,
1036 },
1037 IkChainLinkDefinition {
1038 bone_slot: 0,
1039 angle_limit: None,
1040 },
1041 ],
1042 iteration_count: 4,
1043 limit_angle: 0.0,
1044 };
1045 let mut solver = IkChainSolver::new(definition);
1046 let local_position_offsets = [Vec3A::ZERO; 3];
1047 let local_rotations = [Quat::IDENTITY; 3];
1048 let output = solver.solve(IkChainPoseInput {
1049 parent_world_matrix: None,
1050 local_position_offsets: &local_position_offsets,
1051 local_rotations: &local_rotations,
1052 goal_position: Vec3A::new(1.0, 1.0, 0.0),
1053 tolerance: 1.0e-2,
1054 max_iterations_cap: None,
1055 });
1056
1057 assert_quat_near(
1058 output.solved_link_rotations[0],
1059 runtime.pose().local_rotation(BoneIndex(1)),
1060 );
1061 assert_quat_near(
1062 output.solved_link_rotations[1],
1063 runtime.pose().local_rotation(BoneIndex(0)),
1064 );
1065 }
1066
1067 #[test]
1068 fn deform_order_characterization_keeps_known_ik_delta_bounded() {
1069 fn solve_one_pass(
1070 definition: &IkChainDefinition,
1071 goal_position: Vec3A,
1072 strict: bool,
1073 ) -> Vec<Quat> {
1074 let local_position_offsets = vec![Vec3A::ZERO; definition.rest_positions.len()];
1075 let mut local_rotations = vec![Quat::IDENTITY; definition.rest_positions.len()];
1076 let base_rotations = vec![Quat::IDENTITY; definition.links.len()];
1077 let mut ik_rotations = vec![Quat::IDENTITY; definition.links.len()];
1078 let mut chain_states = vec![ChainLinkState::default(); definition.links.len()];
1079 let mut world_matrices = vec![Mat4::IDENTITY; definition.rest_positions.len()];
1080
1081 update_mini_chain_world_matrices(
1082 definition,
1083 Mat4::IDENTITY,
1084 &local_position_offsets,
1085 &local_rotations,
1086 &mut world_matrices,
1087 );
1088
1089 for link_index in 0..definition.links.len() {
1090 let link = &definition.links[link_index];
1091 let link_slot = link.bone_slot;
1092 let link_world = world_matrices[link_slot];
1093 let link_pos = translation(link_world);
1094 let eff_pos = translation(world_matrices[definition.target_slot]);
1095 let link_world_rot = rotation(link_world);
1096 let local_effector = link_world_rot.inverse().mul_vec3a(eff_pos - link_pos);
1097 let local_target = link_world_rot.inverse().mul_vec3a(goal_position - link_pos);
1098
1099 let fixed_axis = definition.fixed_axes.get(link_slot).copied().flatten();
1100 solve_link_step(LinkStepInput {
1101 local_effector: &local_effector,
1102 local_target: &local_target,
1103 link_index,
1104 base_rotations: &base_rotations,
1105 ik_rotations: &mut ik_rotations,
1106 chain_states: &mut chain_states,
1107 angle_limit: link.angle_limit,
1108 iteration: 0,
1109 limit_angle: definition.limit_angle,
1110 local_axis_basis: None,
1111 fixed_axis,
1112 });
1113
1114 if strict {
1115 local_rotations[link_slot] = ik_rotations[link_index].normalize();
1116 update_mini_chain_world_matrices(
1117 definition,
1118 Mat4::IDENTITY,
1119 &local_position_offsets,
1120 &local_rotations,
1121 &mut world_matrices,
1122 );
1123 }
1124 }
1125
1126 if !strict {
1127 for (link_index, link) in definition.links.iter().enumerate() {
1128 local_rotations[link.bone_slot] = ik_rotations[link_index].normalize();
1129 }
1130 update_mini_chain_world_matrices(
1131 definition,
1132 Mat4::IDENTITY,
1133 &local_position_offsets,
1134 &local_rotations,
1135 &mut world_matrices,
1136 );
1137 }
1138
1139 definition
1140 .links
1141 .iter()
1142 .map(|link| local_rotations[link.bone_slot])
1143 .collect()
1144 }
1145
1146 let definition = IkChainDefinition {
1147 parent_slots: vec![None, Some(0), Some(1)],
1148 rest_positions: vec![Vec3A::ZERO, Vec3A::X, Vec3A::X],
1149 fixed_axes: vec![None, None, None],
1150 target_slot: 2,
1151 links: vec![
1152 IkChainLinkDefinition {
1153 bone_slot: 1,
1154 angle_limit: None,
1155 },
1156 IkChainLinkDefinition {
1157 bone_slot: 0,
1158 angle_limit: None,
1159 },
1160 ],
1161 iteration_count: 1,
1162 limit_angle: 0.0,
1163 };
1164 let goal_position = Vec3A::new(1.0, 1.0, 0.0);
1165
1166 let correct_order = solve_one_pass(&definition, goal_position, true);
1167 let dependency_order = solve_one_pass(&definition, goal_position, false);
1168 let max_angular_delta = correct_order
1169 .iter()
1170 .zip(&dependency_order)
1171 .map(|(correct, dependency)| correct.angle_between(*dependency))
1172 .fold(0.0f32, f32::max);
1173
1174 assert!(
1175 max_angular_delta > 0.0,
1176 "fixture must characterize a non-zero strict-order vs dependency-order IK delta"
1177 );
1178 assert!(
1179 max_angular_delta <= 0.79,
1180 "characterization budget widened unexpectedly: max_angular_delta={max_angular_delta}"
1181 );
1182 }
1183
1184 #[test]
1185 fn primitive_matches_full_runtime_for_knee_plane_limit() {
1186 let limit = IkAngleLimit::new(
1187 Vec3A::new(0.0, 0.0, 0.0),
1188 Vec3A::new(0.0, 0.0, std::f32::consts::FRAC_PI_4),
1189 );
1190 let model = Arc::new(
1191 ModelArena::new_with_ik(
1192 vec![
1193 BoneInit::new(None, Vec3A::ZERO),
1194 BoneInit::new(Some(BoneIndex(0)), Vec3A::X),
1195 BoneInit::new(None, Vec3A::Y),
1196 ],
1197 vec![IkSolverInit {
1198 ik_bone: BoneIndex(2),
1199 target_bone: BoneIndex(1),
1200 links: vec![IkLinkInit::new(BoneIndex(0)).with_angle_limit(limit)],
1201 iteration_count: 1,
1202 limit_angle: 0.0,
1203 }],
1204 )
1205 .unwrap(),
1206 );
1207 let mut runtime = RuntimeInstance::new(model);
1208 runtime.evaluate_current_pose();
1209
1210 let mut solver = IkChainSolver::new(one_link_definition(Some(limit)));
1211 let local_position_offsets = [Vec3A::ZERO; 2];
1212 let local_rotations = [Quat::IDENTITY; 2];
1213 let output = solver.solve(IkChainPoseInput {
1214 parent_world_matrix: None,
1215 local_position_offsets: &local_position_offsets,
1216 local_rotations: &local_rotations,
1217 goal_position: Vec3A::Y,
1218 tolerance: 1.0e-2,
1219 max_iterations_cap: None,
1220 });
1221
1222 assert_quat_near(
1223 output.solved_link_rotations[0],
1224 runtime.pose().local_rotation(BoneIndex(0)),
1225 );
1226 }
1227
1228 #[test]
1229 fn primitive_matches_full_runtime_for_limited_axes_chain() {
1230 let limit = IkAngleLimit::new(Vec3A::new(0.0, -0.6, -0.6), Vec3A::new(0.0, 0.6, 0.6));
1231 let goal = Vec3A::new(0.25, 0.55, 0.80).normalize();
1232 let model = Arc::new(
1233 ModelArena::new_with_ik(
1234 vec![
1235 BoneInit::new(None, Vec3A::ZERO),
1236 BoneInit::new(Some(BoneIndex(0)), Vec3A::X),
1237 BoneInit::new(None, goal),
1238 ],
1239 vec![IkSolverInit {
1240 ik_bone: BoneIndex(2),
1241 target_bone: BoneIndex(1),
1242 links: vec![IkLinkInit::new(BoneIndex(0)).with_angle_limit(limit)],
1243 iteration_count: 1,
1244 limit_angle: 0.0,
1245 }],
1246 )
1247 .unwrap(),
1248 );
1249 let mut runtime = RuntimeInstance::new(model);
1250 runtime.evaluate_current_pose();
1251
1252 let mut solver = IkChainSolver::new(one_link_definition(Some(limit)));
1253 let local_position_offsets = [Vec3A::ZERO; 2];
1254 let local_rotations = [Quat::IDENTITY; 2];
1255 let output = solver.solve(IkChainPoseInput {
1256 parent_world_matrix: None,
1257 local_position_offsets: &local_position_offsets,
1258 local_rotations: &local_rotations,
1259 goal_position: goal,
1260 tolerance: 1.0e-2,
1261 max_iterations_cap: None,
1262 });
1263
1264 assert_quat_near(
1265 output.solved_link_rotations[0],
1266 runtime.pose().local_rotation(BoneIndex(0)),
1267 );
1268 }
1269
1270 #[test]
1271 fn primitive_is_bit_deterministic_in_current_process_profile() {
1272 let definition = IkChainDefinition {
1275 parent_slots: vec![None, Some(0), Some(1)],
1276 rest_positions: vec![Vec3A::ZERO, Vec3A::X, Vec3A::X],
1277 fixed_axes: vec![None, None, None],
1278 target_slot: 2,
1279 links: vec![
1280 IkChainLinkDefinition {
1281 bone_slot: 1,
1282 angle_limit: None,
1283 },
1284 IkChainLinkDefinition {
1285 bone_slot: 0,
1286 angle_limit: None,
1287 },
1288 ],
1289 iteration_count: 4,
1290 limit_angle: 0.0,
1291 };
1292 let local_position_offsets = [Vec3A::ZERO; 3];
1293 let local_rotations = [Quat::IDENTITY; 3];
1294 let input = IkChainPoseInput {
1295 parent_world_matrix: None,
1296 local_position_offsets: &local_position_offsets,
1297 local_rotations: &local_rotations,
1298 goal_position: Vec3A::new(1.0, 1.0, 0.0),
1299 tolerance: 1.0e-2,
1300 max_iterations_cap: None,
1301 };
1302 let mut baseline_solver = IkChainSolver::new(definition.clone());
1303 let expected = baseline_solver.solve(input);
1304
1305 for _ in 0..32 {
1306 let mut solver = IkChainSolver::new(definition.clone());
1307 let actual = solver.solve(input);
1308 assert_eq!(
1309 actual.final_distance.to_bits(),
1310 expected.final_distance.to_bits()
1311 );
1312 assert_eq!(actual.executed_iterations, expected.executed_iterations);
1313 assert_eq!(actual.link_steps, expected.link_steps);
1314 let actual_bits: Vec<_> = actual
1315 .solved_link_rotations
1316 .iter()
1317 .map(|q| q.to_array().map(f32::to_bits))
1318 .collect();
1319 let expected_bits: Vec<_> = expected
1320 .solved_link_rotations
1321 .iter()
1322 .map(|q| q.to_array().map(f32::to_bits))
1323 .collect();
1324 assert_eq!(actual_bits, expected_bits);
1325 }
1326 }
1327
1328 #[test]
1329 fn existing_ik_chain_definition_struct_literal_has_no_local_axes() {
1330 let definition = IkChainDefinition {
1333 parent_slots: vec![None, Some(0)],
1334 rest_positions: vec![Vec3A::ZERO, Vec3A::X],
1335 fixed_axes: vec![None, None],
1336 target_slot: 1,
1337 links: vec![IkChainLinkDefinition {
1338 bone_slot: 0,
1339 angle_limit: None,
1340 }],
1341 iteration_count: 1,
1342 limit_angle: 0.0,
1343 };
1344 let mut solver = IkChainSolver::new(definition);
1345 let output = solver.solve(IkChainPoseInput {
1346 parent_world_matrix: None,
1347 local_position_offsets: &[Vec3A::ZERO; 2],
1348 local_rotations: &[Quat::IDENTITY; 2],
1349 goal_position: Vec3A::Y,
1350 tolerance: 0.0,
1351 max_iterations_cap: None,
1352 });
1353 assert_eq!(output.solved_link_rotations.len(), 1);
1354 }
1355
1356 #[test]
1357 fn fixed_axis_composes_with_single_axis_angle_limit() {
1358 let limit = IkAngleLimit::new(
1361 Vec3A::new(0.0, 0.0, -std::f32::consts::FRAC_PI_2),
1362 Vec3A::new(0.0, 0.0, std::f32::consts::FRAC_PI_2),
1363 );
1364 let definition = IkChainDefinition {
1365 parent_slots: vec![None, Some(0)],
1366 rest_positions: vec![Vec3A::ZERO, Vec3A::X],
1367 fixed_axes: vec![Some(Vec3A::Y), None],
1368 target_slot: 1,
1369 links: vec![IkChainLinkDefinition {
1370 bone_slot: 0,
1371 angle_limit: Some(limit),
1372 }],
1373 iteration_count: 4,
1374 limit_angle: 0.0,
1375 };
1376 let mut solver = IkChainSolver::new(definition);
1377 let output = solver.solve(IkChainPoseInput {
1378 parent_world_matrix: None,
1379 local_position_offsets: &[Vec3A::ZERO; 2],
1380 local_rotations: &[Quat::IDENTITY; 2],
1381 goal_position: Vec3A::Y,
1382 tolerance: 0.0,
1383 max_iterations_cap: None,
1384 });
1385 let child = output.solved_link_rotations[0].mul_vec3a(Vec3A::X);
1386 assert!(
1387 child.y.abs() < 1.0e-3,
1388 "fixed Y + Z-plane limit must not lift child off XZ; child={child:?}"
1389 );
1390 assert!(
1393 child.x > 0.9,
1394 "unreachable +Y goal under Y fixed-axis keeps child near +X; child={child:?}"
1395 );
1396 }
1397
1398 #[test]
1399 fn fixed_axis_composes_with_multi_axis_angle_limit() {
1400 let limit = IkAngleLimit::new(Vec3A::new(-1.0, -1.0, 0.0), Vec3A::new(1.0, 1.0, 0.0));
1401 let definition = IkChainDefinition {
1402 parent_slots: vec![None, Some(0)],
1403 rest_positions: vec![Vec3A::ZERO, Vec3A::X],
1404 fixed_axes: vec![Some(Vec3A::Y), None],
1405 target_slot: 1,
1406 links: vec![IkChainLinkDefinition {
1407 bone_slot: 0,
1408 angle_limit: Some(limit),
1409 }],
1410 iteration_count: 4,
1411 limit_angle: 0.0,
1412 };
1413 let mut solver = IkChainSolver::new(definition);
1414 let output = solver.solve(IkChainPoseInput {
1415 parent_world_matrix: None,
1416 local_position_offsets: &[Vec3A::ZERO; 2],
1417 local_rotations: &[Quat::IDENTITY; 2],
1418 goal_position: Vec3A::new(0.7, 0.0, 0.7),
1421 tolerance: 0.0,
1422 max_iterations_cap: None,
1423 });
1424 let solved = output.solved_link_rotations[0];
1425 let child = solved.mul_vec3a(Vec3A::X);
1426 assert!(
1427 child.y.abs() < 1.0e-3,
1428 "fixed Y + multi-axis limit must keep child on XZ; child={child:?}"
1429 );
1430 let rot_vec = Vec3A::new(solved.x, solved.y, solved.z);
1432 if rot_vec.length_squared() > 1.0e-8 {
1433 let axis = rot_vec.normalize();
1434 assert!(
1435 axis.x.abs() < 1.0e-3 && axis.z.abs() < 1.0e-3,
1436 "IK delta must remain pure Y twist; axis={axis:?} quat={solved:?}"
1437 );
1438 }
1439 assert!(
1440 child.z > 0.2,
1441 "fixed-axis multi-limit IK should still approach +Z; child={child:?}"
1442 );
1443 }
1444
1445 #[test]
1446 fn fixed_axis_angle_limit_preserves_correction_with_non_identity_base() {
1447 let limit = IkAngleLimit::new(
1448 Vec3A::splat(-std::f32::consts::PI),
1449 Vec3A::splat(std::f32::consts::PI),
1450 );
1451 let definition = IkChainDefinition {
1452 parent_slots: vec![None, Some(0)],
1453 rest_positions: vec![Vec3A::ZERO, Vec3A::X],
1454 fixed_axes: vec![Some(Vec3A::Y), None],
1455 target_slot: 1,
1456 links: vec![IkChainLinkDefinition {
1457 bone_slot: 0,
1458 angle_limit: Some(limit),
1459 }],
1460 iteration_count: 1,
1461 limit_angle: 0.0,
1462 };
1463 let base = Quat::from_rotation_x(std::f32::consts::FRAC_PI_2);
1464 let goal = Vec3A::Y;
1465 let mut solver = IkChainSolver::new(definition);
1466 let output = solver.solve(IkChainPoseInput {
1467 parent_world_matrix: None,
1468 local_position_offsets: &[Vec3A::ZERO; 2],
1469 local_rotations: &[base, Quat::IDENTITY],
1470 goal_position: goal,
1471 tolerance: 0.0,
1472 max_iterations_cap: None,
1473 });
1474
1475 let solved = output.solved_link_rotations[0];
1476 let child = solved.mul_vec3a(Vec3A::X).normalize();
1477 assert!(
1478 child.dot(goal) > 0.99,
1479 "fixed-axis correction must survive projection in the base-rotated frame; solved={solved:?} child={child:?}"
1480 );
1481 }
1482
1483 #[test]
1484 fn fixed_axis_projection_uses_base_rotated_correction_axis() {
1485 let base = Quat::from_rotation_x(0.7);
1486 let correction_axis = base.mul_vec3a(Vec3A::Y).normalize();
1487 let mut ik_rotations = [(Quat::from_axis_angle(correction_axis.into(), 0.6)
1488 * Quat::from_rotation_x(0.2))
1489 .normalize()];
1490
1491 project_link_rotation_onto_fixed_axis(0, &[base], &mut ik_rotations, Vec3A::Y);
1492
1493 let vector = Vec3A::new(ik_rotations[0].x, ik_rotations[0].y, ik_rotations[0].z);
1494 assert!(
1495 vector.length_squared() > 1.0e-4,
1496 "projection must preserve a non-trivial fixed-axis correction"
1497 );
1498 let projected_axis = vector.normalize();
1499 assert!(
1500 projected_axis.dot(correction_axis).abs() > 0.999,
1501 "projection axis must follow base * fixedAxis; projected={projected_axis:?} expected={correction_axis:?}"
1502 );
1503 assert!(
1504 projected_axis.dot(Vec3A::Y).abs() < 0.95,
1505 "non-identity base must not project the IK correction onto raw fixedAxis"
1506 );
1507 }
1508
1509 #[test]
1510 fn additive_local_axis_bases_change_limited_solve() {
1511 let limits = IkAngleLimit::new(
1512 Vec3A::new(-std::f32::consts::FRAC_PI_2, 0.0, 0.0),
1513 Vec3A::new(std::f32::consts::FRAC_PI_2, 0.0, 0.0),
1514 );
1515 let definition = one_link_definition(Some(limits));
1516 let local_position_offsets = [Vec3A::ZERO; 2];
1517 let local_rotations = [Quat::IDENTITY; 2];
1518 let input = IkChainPoseInput {
1519 parent_world_matrix: None,
1520 local_position_offsets: &local_position_offsets,
1521 local_rotations: &local_rotations,
1522 goal_position: Vec3A::Y,
1523 tolerance: 0.0,
1524 max_iterations_cap: None,
1525 };
1526
1527 let mut unit = IkChainSolver::new(definition.clone());
1528 let unit_out = unit.solve(input);
1529
1530 let basis = Quat::from_rotation_y(-std::f32::consts::FRAC_PI_2);
1531 let mut la = IkChainSolver::new_with_local_axis_bases(definition, vec![Some(basis), None]);
1532 let la_out = la.solve(input);
1533
1534 let unit_dir = unit_out.solved_link_rotations[0]
1535 .mul_vec3a(Vec3A::X)
1536 .normalize();
1537 let la_dir = la_out.solved_link_rotations[0]
1538 .mul_vec3a(Vec3A::X)
1539 .normalize();
1540 assert!(
1541 (unit_dir - la_dir).length() > 0.2,
1542 "additive local-axis bases must change limited solve; unit={unit_dir:?} la={la_dir:?}"
1543 );
1544 }
1545}