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