1use std::sync::Arc;
2
3use glam::{Mat4, Quat, Vec3A};
4
5use crate::{AnimationClip, ModelArena, MorphIndex, PoseArena};
6
7#[derive(Debug)]
8struct IkScratch {
9 links: Vec<crate::IkLink>,
10 base_rotations: Vec<Quat>,
11 ik_rotations: Vec<Quat>,
12 best_ik_rotations: Vec<Quat>,
13 chain_states: Vec<ChainLinkState>,
14}
15
16impl IkScratch {
17 fn new(model: &ModelArena) -> Self {
18 let max_links = model
19 .ik_solvers()
20 .iter()
21 .map(|s| s.links.len())
22 .max()
23 .unwrap_or(0);
24 IkScratch {
25 links: Vec::with_capacity(max_links),
26 base_rotations: Vec::with_capacity(max_links),
27 ik_rotations: Vec::with_capacity(max_links),
28 best_ik_rotations: Vec::with_capacity(max_links),
29 chain_states: Vec::with_capacity(max_links),
30 }
31 }
32}
33
34#[derive(Debug)]
35struct MorphScratch {
36 expanded_weights: Vec<f32>,
37}
38
39impl MorphScratch {
40 fn new(morph_count: usize) -> Self {
41 Self {
42 expanded_weights: vec![0.0; morph_count],
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, Default, PartialEq)]
48pub struct IkSolverRuntimeStats {
49 pub solver_evaluations: u64,
50 pub configured_iterations: u64,
51 pub executed_iterations: u64,
52 pub tolerance_precheck_breaks: u64,
53 pub tolerance_post_iteration_breaks: u64,
54 pub rollback_breaks: u64,
55 pub max_iteration_exhaustions: u64,
56 pub link_visits: u64,
57 pub link_steps: u64,
58 pub final_distance_sum: f64,
59 pub final_distance_max: f32,
60 pub exhausted_final_distance_sum: f64,
61 pub exhausted_final_distance_max: f32,
62}
63
64impl IkSolverRuntimeStats {
65 fn reset(&mut self) {
66 *self = Self::default();
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq)]
71pub struct IkSolveOptions {
72 pub tolerance: f32,
73 pub max_iterations_cap: Option<u32>,
74}
75
76impl Default for IkSolveOptions {
77 fn default() -> Self {
78 Self {
79 tolerance: 1.0e-2,
80 max_iterations_cap: None,
81 }
82 }
83}
84
85#[derive(Debug)]
86pub struct RuntimeInstance {
87 model: Arc<ModelArena>,
88 pose: PoseArena,
89 ik_scratch: IkScratch,
90 morph_scratch: MorphScratch,
91 ik_stats: Vec<IkSolverRuntimeStats>,
92 #[cfg(test)]
93 world_matrix_bone_update_count: usize,
94}
95
96impl RuntimeInstance {
97 pub fn new(model: Arc<ModelArena>) -> Self {
98 let morph_count = model.morph_count() as usize;
99 Self::new_with_morph_count(model, morph_count)
100 }
101
102 pub fn new_with_morph_count(model: Arc<ModelArena>, morph_count: usize) -> Self {
103 let ik_count = model.ik_count();
104 Self::new_with_counts(model, morph_count, ik_count)
105 }
106
107 pub fn new_with_counts(model: Arc<ModelArena>, morph_count: usize, ik_count: usize) -> Self {
108 let morph_count = morph_count.max(model.morph_count() as usize);
109 let pose = PoseArena::new_with_counts(model.bone_count(), morph_count, ik_count);
110 let ik_scratch = IkScratch::new(&model);
111 let morph_scratch = MorphScratch::new(morph_count);
112 let ik_stats = vec![IkSolverRuntimeStats::default(); model.ik_count()];
113 Self {
114 model,
115 pose,
116 ik_scratch,
117 morph_scratch,
118 ik_stats,
119 #[cfg(test)]
120 world_matrix_bone_update_count: 0,
121 }
122 }
123
124 #[inline]
125 pub fn model(&self) -> &ModelArena {
126 &self.model
127 }
128
129 #[inline]
130 pub fn pose(&self) -> &PoseArena {
131 &self.pose
132 }
133
134 #[inline]
135 pub fn pose_mut(&mut self) -> &mut PoseArena {
136 &mut self.pose
137 }
138
139 pub fn evaluate_current_pose(&mut self) {
140 self.update_world_matrices();
141 self.solve_enabled_ik(IkSolveOptions::default());
142 }
143
144 pub fn evaluate_current_pose_with_ik_options(&mut self, options: IkSolveOptions) {
145 self.update_world_matrices();
146 self.solve_enabled_ik(options);
147 }
148
149 pub fn evaluate_current_pose_without_ik(&mut self) {
153 self.update_world_matrices();
154 }
155
156 fn update_world_matrices(&mut self) {
157 self.update_world_matrices_from_eval_order_position(0);
158 }
159
160 fn update_world_matrices_from_bone(&mut self, bone: crate::BoneIndex) {
161 self.update_world_matrices_from_eval_order_position(self.model.eval_order_position(bone));
162 }
163
164 fn update_world_matrices_from_eval_order_position(&mut self, start_position: usize) {
165 let start_position = self.expand_update_start_for_append_dependencies(start_position);
166 for bone in &self.model.eval_order()[start_position..] {
167 self.pose.reset_append_transform(*bone);
168 }
169 for bone in &self.model.eval_order()[start_position..] {
170 #[cfg(test)]
171 {
172 self.world_matrix_bone_update_count += 1;
173 }
174 let mut local_position =
175 self.model.rest_position(*bone) + self.pose.local_position_offset(*bone);
176 let mut local_rotation = self.pose.local_rotation(*bone);
177 let local_scale = self.pose.local_scale(*bone);
178
179 if let Some(append_index) = self.model.append_transform_index(*bone) {
180 let append = self.model.append_transform(append_index);
181 if append.affect_rotation {
182 let source_rotation = if !append.local
183 && self
184 .model
185 .append_transform_index(append.source_bone)
186 .is_some()
187 {
188 self.pose.append_rotation(append.source_bone)
189 } else {
190 self.pose.local_rotation(append.source_bone)
191 };
192 let append_rotation = Quat::IDENTITY
193 .slerp(source_rotation, append.ratio)
194 .normalize();
195 self.pose.set_append_rotation(*bone, append_rotation);
196 local_rotation = (local_rotation * append_rotation).normalize();
197 }
198
199 if append.affect_translation {
200 let source_position_offset = if !append.local
201 && self
202 .model
203 .append_transform_index(append.source_bone)
204 .is_some()
205 {
206 self.pose.append_position_offset(append.source_bone)
207 } else {
208 self.pose.local_position_offset(append.source_bone)
209 };
210 let append_position_offset = source_position_offset * append.ratio;
211 self.pose
212 .set_append_position_offset(*bone, append_position_offset);
213 local_position += append_position_offset;
214 }
215 }
216
217 if let Some(axis) = self.model.fixed_axis(*bone) {
218 local_rotation = constrain_rotation_to_axis(local_rotation, axis);
219 }
220
221 let local_matrix = Mat4::from_scale_rotation_translation(
222 local_scale.into(),
223 local_rotation,
224 local_position.into(),
225 );
226
227 let world_matrix = match self.model.parent_index(*bone) {
228 Some(parent) => self.pose.world_matrices()[parent.as_usize()] * local_matrix,
229 None => local_matrix,
230 };
231
232 self.pose.set_world_matrix(*bone, world_matrix);
233 self.pose
234 .set_skinning_matrix(*bone, world_matrix * self.model.inverse_bind_matrix(*bone));
235 }
236 }
237
238 fn expand_update_start_for_append_dependencies(&self, start_position: usize) -> usize {
239 let mut start = start_position;
240 loop {
241 let mut changed = false;
242 for append in self.model.append_transforms() {
243 let source_position = self.model.eval_order_position(append.source_bone);
244 let target_position = self.model.eval_order_position(append.target_bone);
245 if source_position >= start && target_position < start {
246 start = target_position;
247 changed = true;
248 }
249 }
250 if !changed {
251 return start;
252 }
253 }
254 }
255
256 fn min_link_eval_order_position(&self, links: &[crate::IkLink]) -> Option<usize> {
257 links
258 .iter()
259 .map(|link| self.model.eval_order_position(link.bone))
260 .min()
261 }
262
263 fn solve_enabled_ik(&mut self, options: IkSolveOptions) {
264 let tolerance = options.tolerance.max(0.0);
265 let mut links = std::mem::take(&mut self.ik_scratch.links);
266 let mut base_rotations = std::mem::take(&mut self.ik_scratch.base_rotations);
267 let mut ik_rotations = std::mem::take(&mut self.ik_scratch.ik_rotations);
268 let mut best_ik_rotations = std::mem::take(&mut self.ik_scratch.best_ik_rotations);
269 let mut chain_states = std::mem::take(&mut self.ik_scratch.chain_states);
270
271 for ik_index in 0..self.model.ik_count() {
272 if self.pose.ik_enabled()[ik_index] == 0 {
273 continue;
274 }
275
276 let solver = &self.model.ik_solvers()[ik_index];
277 let ik_bone = solver.ik_bone;
278 let target_bone = solver.target_bone;
279 let iteration_count = options
280 .max_iterations_cap
281 .map(|cap| solver.iteration_count.min(cap))
282 .unwrap_or(solver.iteration_count)
283 .max(1) as usize;
284 let limit_angle = solver.limit_angle.max(0.0);
285 let link_count = solver.links.len();
286
287 links.clear();
288 links.extend(solver.links.iter().cloned());
289 self.ik_stats[ik_index].solver_evaluations += 1;
290 self.ik_stats[ik_index].configured_iterations += iteration_count as u64;
291
292 base_rotations.clear();
293 base_rotations.extend(links.iter().map(|l| self.pose.local_rotation(l.bone)));
294 ik_rotations.clear();
295 ik_rotations.resize(link_count, Quat::IDENTITY);
296 best_ik_rotations.clear();
297 best_ik_rotations.resize(link_count, Quat::IDENTITY);
298 chain_states.clear();
299 chain_states.resize_with(link_count, || ChainLinkState {
300 previous_euler: [0.0; 3],
301 plane_mode_angle: 0.0,
302 });
303
304 self.apply_ik_link_rotations(&links, &base_rotations, &ik_rotations);
306 if let Some(start_position) = self.min_link_eval_order_position(&links) {
307 self.update_world_matrices_from_eval_order_position(start_position);
308 } else {
309 self.update_world_matrices();
310 }
311
312 let mut broke_early = false;
313 let mut final_distance = f32::MAX;
314 let mut best_distance = f32::MAX;
315 for _iteration in 0..iteration_count {
316 let eff_pos = translation(self.pose.world_matrices()[target_bone.as_usize()]);
318 let ik_pos = translation(self.pose.world_matrices()[ik_bone.as_usize()]);
319 final_distance = (eff_pos - ik_pos).length();
320 if final_distance <= tolerance {
321 self.ik_stats[ik_index].tolerance_precheck_breaks += 1;
322 broke_early = true;
323 break;
324 }
325 self.ik_stats[ik_index].executed_iterations += 1;
326
327 for link_index in 0..link_count {
328 let link = &links[link_index];
329 let link_bone = link.bone;
330 self.ik_stats[ik_index].link_visits += 1;
331
332 if link_bone == target_bone {
333 continue;
334 }
335
336 let link_world = self.pose.world_matrices()[link_bone.as_usize()];
337 let link_pos = translation(link_world);
338 let eff_pos = translation(self.pose.world_matrices()[target_bone.as_usize()]);
339 let ik_pos = translation(self.pose.world_matrices()[ik_bone.as_usize()]);
340
341 let link_world_rot = rotation(link_world);
343 let local_effector = link_world_rot.inverse().mul_vec3a(eff_pos - link_pos);
344 let local_target = link_world_rot.inverse().mul_vec3a(ik_pos - link_pos);
345
346 if local_effector.length_squared() <= f32::EPSILON
347 || local_target.length_squared() <= f32::EPSILON
348 {
349 continue;
350 }
351
352 let single_axis = get_single_axis_limit(link.angle_limit);
353
354 if let (Some(angle_limit), Some(axis_index)) = (link.angle_limit, single_axis) {
355 solve_plane_link_step(PlaneLinkStepInput {
356 local_effector: &local_effector,
357 local_target: &local_target,
358 link_index,
359 base_rotations: &base_rotations,
360 ik_rotations: &mut ik_rotations,
361 chain_states: &mut chain_states,
362 axis_index,
363 limits: angle_limit,
364 iteration: _iteration,
365 limit_angle,
366 });
367 } else if let Some(angle_limit) = link.angle_limit {
368 solve_limited_axes_link_step(LimitedAxesLinkStepInput {
369 local_effector: &local_effector,
370 local_target: &local_target,
371 link_index,
372 base_rotations: &base_rotations,
373 ik_rotations: &mut ik_rotations,
374 chain_states: &mut chain_states,
375 limits: angle_limit,
376 limit_angle,
377 });
378 } else {
379 let local_eff_n = local_effector.normalize();
380 let local_tgt_n = local_target.normalize();
381 let dot = local_eff_n.dot(local_tgt_n).clamp(-1.0, 1.0);
382 let mut angle = dot.acos();
383
384 let tiny_angle = 1e-3 * std::f32::consts::PI / 180.0;
385 if angle < tiny_angle {
386 continue;
387 }
388
389 if limit_angle > 0.0 {
390 angle = angle.min(limit_angle);
391 }
392
393 let axis = local_eff_n.cross(local_tgt_n);
394 let axis_vec = if axis.length() < 1e-5 {
395 if dot > -1.0 + 1e-5 {
396 continue;
397 }
398 let basis = if local_eff_n.x.abs() < 0.9 {
399 Vec3A::new(1.0, 0.0, 0.0)
400 } else {
401 Vec3A::new(0.0, 1.0, 0.0)
402 };
403 local_eff_n.cross(basis).normalize()
404 } else {
405 axis.normalize()
406 };
407
408 let delta = Quat::from_axis_angle(axis_vec.into(), angle);
409 let base = base_rotations[link_index];
410 let ik = ik_rotations[link_index];
411 let chain_rotation = (ik * base * delta).normalize();
412
413 ik_rotations[link_index] = (chain_rotation * base.inverse()).normalize();
414 }
415
416 self.apply_ik_link_rotations(&links, &base_rotations, &ik_rotations);
417 self.update_world_matrices_from_bone(link_bone);
418 self.ik_stats[ik_index].link_steps += 1;
419 }
420
421 let current_distance = {
423 let eff = translation(self.pose.world_matrices()[target_bone.as_usize()]);
424 let ik = translation(self.pose.world_matrices()[ik_bone.as_usize()]);
425 (eff - ik).length()
426 };
427 final_distance = current_distance;
428
429 if current_distance < best_distance {
430 best_distance = current_distance;
431 best_ik_rotations.copy_from_slice(&ik_rotations);
432 if current_distance <= tolerance {
433 self.ik_stats[ik_index].tolerance_post_iteration_breaks += 1;
434 broke_early = true;
435 break;
436 }
437 } else {
438 self.ik_stats[ik_index].rollback_breaks += 1;
439 ik_rotations.copy_from_slice(&best_ik_rotations);
440 self.apply_ik_link_rotations(&links, &base_rotations, &ik_rotations);
441 if let Some(start_position) = self.min_link_eval_order_position(&links) {
442 self.update_world_matrices_from_eval_order_position(start_position);
443 }
444 broke_early = true;
445 break;
446 }
447 }
448 self.ik_stats[ik_index].final_distance_sum += f64::from(final_distance);
449 self.ik_stats[ik_index].final_distance_max = self.ik_stats[ik_index]
450 .final_distance_max
451 .max(final_distance);
452 if !broke_early {
453 self.ik_stats[ik_index].max_iteration_exhaustions += 1;
454 self.ik_stats[ik_index].exhausted_final_distance_sum += f64::from(final_distance);
455 self.ik_stats[ik_index].exhausted_final_distance_max = self.ik_stats[ik_index]
456 .exhausted_final_distance_max
457 .max(final_distance);
458 }
459
460 self.apply_ik_link_rotations(&links, &base_rotations, &best_ik_rotations);
462 if let Some(start_position) = self.min_link_eval_order_position(&links) {
463 self.update_world_matrices_from_eval_order_position(start_position);
464 }
465 } self.ik_scratch.links = links;
468 self.ik_scratch.base_rotations = base_rotations;
469 self.ik_scratch.ik_rotations = ik_rotations;
470 self.ik_scratch.best_ik_rotations = best_ik_rotations;
471 self.ik_scratch.chain_states = chain_states;
472 }
473
474 fn apply_ik_link_rotations(
475 &mut self,
476 links: &[crate::IkLink],
477 base_rotations: &[Quat],
478 ik_rotations: &[Quat],
479 ) {
480 for (i, link) in links.iter().enumerate() {
481 let effective = (ik_rotations[i] * base_rotations[i]).normalize();
482 self.pose.set_local_rotation(link.bone, effective);
483 }
484 }
485
486 pub fn evaluate_rest_pose(&mut self) {
487 self.pose.reset_local_pose();
488 self.evaluate_current_pose();
489 }
490
491 pub fn evaluate_clip_frame(&mut self, clip: &AnimationClip, frame: f32) {
492 clip.apply_to_pose(frame, &mut self.pose);
493 self.expand_morphs();
494 self.evaluate_current_pose();
495 }
496
497 pub fn evaluate_clip_frame_with_ik_options(
498 &mut self,
499 clip: &AnimationClip,
500 frame: f32,
501 options: IkSolveOptions,
502 ) {
503 clip.apply_to_pose(frame, &mut self.pose);
504 self.expand_morphs();
505 self.evaluate_current_pose_with_ik_options(options);
506 }
507
508 pub fn evaluate_clip_frame_without_ik(&mut self, clip: &AnimationClip, frame: f32) {
513 clip.apply_to_pose(frame, &mut self.pose);
514 self.expand_morphs();
515 self.update_world_matrices();
516 }
517
518 pub fn expand_morphs(&mut self) {
524 self.expand_group_morphs();
525 self.apply_bone_morphs();
526 }
527
528 fn expand_group_morphs(&mut self) {
533 let spans = self.model.group_morph_spans();
534 let offsets = self.model.group_morph_offsets();
535 if spans.is_empty() || offsets.is_empty() {
536 return;
537 }
538 let mc = self.model.morph_count() as usize;
539 self.morph_scratch.expanded_weights.clear();
540 self.morph_scratch
541 .expanded_weights
542 .extend_from_slice(&self.pose.morph_weights()[..mc]);
543
544 for (morph_idx, &w) in self.pose.morph_weights()[..mc].iter().enumerate() {
545 if w == 0.0 {
546 continue;
547 }
548 expand_group_morph_weight(
549 morph_idx,
550 w,
551 spans,
552 offsets,
553 &mut self.morph_scratch.expanded_weights,
554 );
555 }
556 for (i, &w) in self.morph_scratch.expanded_weights.iter().enumerate() {
557 self.pose.set_morph_weight(MorphIndex(i as u32), w);
558 }
559 }
560
561 fn apply_bone_morphs(&mut self) {
564 let spans = self.model.bone_morph_spans();
565 let offsets = self.model.bone_morph_offsets();
566 if spans.is_empty() || offsets.is_empty() {
567 return;
568 }
569 for (morph_idx, span) in spans.iter().enumerate() {
570 let weight = self.pose.morph_weight(MorphIndex(morph_idx as u32));
571 if weight == 0.0 {
572 continue;
573 }
574 for i in span.start..span.start + span.count {
575 let off = &offsets[i as usize];
576 let pos = self.pose.local_position_offset(off.target_bone);
577 self.pose
578 .set_local_position_offset(off.target_bone, pos + off.position_offset * weight);
579 let rot = self.pose.local_rotation(off.target_bone);
580 let scaled = Quat::IDENTITY.slerp(off.rotation_offset, weight);
581 self.pose
582 .set_local_rotation(off.target_bone, (rot * scaled).normalize());
583 }
584 }
585 }
586
587 #[inline]
588 pub fn world_matrices(&self) -> &[Mat4] {
589 self.pose.world_matrices()
590 }
591
592 #[cfg(test)]
593 fn reset_world_matrix_bone_update_count(&mut self) {
594 self.world_matrix_bone_update_count = 0;
595 }
596
597 #[cfg(test)]
598 fn world_matrix_bone_update_count(&self) -> usize {
599 self.world_matrix_bone_update_count
600 }
601
602 #[inline]
603 pub fn skinning_matrices(&self) -> &[Mat4] {
604 self.pose.skinning_matrices()
605 }
606
607 #[inline]
608 pub fn morph_weights(&self) -> &[f32] {
609 self.pose.morph_weights()
610 }
611
612 pub fn reset_ik_runtime_stats(&mut self) {
613 for stats in &mut self.ik_stats {
614 stats.reset();
615 }
616 }
617
618 pub fn ik_runtime_stats(&self) -> &[IkSolverRuntimeStats] {
619 &self.ik_stats
620 }
621
622 #[inline]
623 pub fn ik_enabled(&self) -> &[u8] {
624 self.pose.ik_enabled()
625 }
626}
627
628fn expand_group_morph_weight(
629 morph_idx: usize,
630 weight: f32,
631 spans: &[crate::MorphOffsetSpan],
632 offsets: &[crate::GroupMorphOffset],
633 expanded_weights: &mut [f32],
634) {
635 let span = spans[morph_idx];
636 for i in span.start..span.start + span.count {
637 let off = &offsets[i as usize];
638 let child = off.child_morph.as_usize();
639 let contribution = weight * off.ratio;
640 expanded_weights[child] += contribution;
641 if spans[child].count > 0 {
642 expand_group_morph_weight(child, contribution, spans, offsets, expanded_weights);
643 }
644 }
645}
646
647fn translation(matrix: Mat4) -> Vec3A {
648 Vec3A::from_vec4(matrix.w_axis)
649}
650
651fn rotation(matrix: Mat4) -> Quat {
652 matrix.to_scale_rotation_translation().1
653}
654
655fn constrain_rotation_to_axis(rotation: Quat, axis: Vec3A) -> Quat {
656 let axis = axis.normalize();
657 let vector = Vec3A::new(rotation.x, rotation.y, rotation.z);
658 let projected = axis * vector.dot(axis);
659 let twist = Quat::from_xyzw(projected.x, projected.y, projected.z, rotation.w);
660 if twist.length_squared() <= f32::EPSILON {
661 Quat::IDENTITY
662 } else {
663 twist.normalize()
664 }
665}
666
667#[derive(Debug)]
668struct ChainLinkState {
669 previous_euler: [f32; 3],
670 plane_mode_angle: f32,
671}
672
673fn get_single_axis_limit(limit: Option<crate::IkAngleLimit>) -> Option<usize> {
674 let limit = limit?;
675 let has = [
676 limit.min.x != 0.0 || limit.max.x != 0.0,
677 limit.min.y != 0.0 || limit.max.y != 0.0,
678 limit.min.z != 0.0 || limit.max.z != 0.0,
679 ];
680 if has[0]
681 && limit.min.y == 0.0
682 && limit.max.y == 0.0
683 && limit.min.z == 0.0
684 && limit.max.z == 0.0
685 {
686 return Some(0);
687 }
688 if has[1]
689 && limit.min.x == 0.0
690 && limit.max.x == 0.0
691 && limit.min.z == 0.0
692 && limit.max.z == 0.0
693 {
694 return Some(1);
695 }
696 if has[2]
697 && limit.min.x == 0.0
698 && limit.max.x == 0.0
699 && limit.min.y == 0.0
700 && limit.max.y == 0.0
701 {
702 return Some(2);
703 }
704 None
705}
706
707fn quat_to_rotation_mat3(rotation: Quat) -> [f32; 9] {
708 let [x, y, z, w] = rotation.normalize().to_array();
709 let x2 = x + x;
710 let y2 = y + y;
711 let z2 = z + z;
712 let xx = x * x2;
713 let xy = x * y2;
714 let xz = x * z2;
715 let yy = y * y2;
716 let yz = y * z2;
717 let zz = z * z2;
718 let wx = w * x2;
719 let wy = w * y2;
720 let wz = w * z2;
721 [
722 1.0 - (yy + zz),
723 xy + wz,
724 xz - wy,
725 xy - wz,
726 1.0 - (xx + zz),
727 yz + wx,
728 xz + wy,
729 yz - wx,
730 1.0 - (xx + yy),
731 ]
732}
733
734fn decompose_euler_xyz(mat: &[f32; 9], before: &[f32; 3]) -> [f32; 3] {
735 let sy = -mat[2];
736 let mut result: [f32; 3];
737 if 1.0 - sy.abs() < 1e-6 {
738 let y = sy.asin();
739 let sx = before[0].sin();
740 let sz = before[2].sin();
741 if sx.abs() < sz.abs() {
742 let cx = before[0].cos();
743 result = if cx > 0.0 {
744 [0.0, y, (-mat[3]).asin()]
745 } else {
746 [std::f32::consts::PI, y, mat[3].asin()]
747 };
748 } else {
749 let cz = before[2].cos();
750 result = if cz > 0.0 {
751 [(-mat[7]).asin(), y, 0.0]
752 } else {
753 [mat[7].asin(), y, std::f32::consts::PI]
754 };
755 }
756 } else {
757 result = [mat[5].atan2(mat[8]), (-mat[2]).asin(), mat[1].atan2(mat[0])];
758 }
759
760 let pi = std::f32::consts::PI;
761 let candidates: [[f32; 3]; 8] = [
762 [result[0] + pi, pi - result[1], result[2] + pi],
763 [result[0] + pi, pi - result[1], result[2] - pi],
764 [result[0] + pi, -pi - result[1], result[2] + pi],
765 [result[0] + pi, -pi - result[1], result[2] - pi],
766 [result[0] - pi, pi - result[1], result[2] + pi],
767 [result[0] - pi, pi - result[1], result[2] - pi],
768 [result[0] - pi, -pi - result[1], result[2] + pi],
769 [result[0] - pi, -pi - result[1], result[2] - pi],
770 ];
771 let mut min_error = diff_angle(result[0], before[0]).abs()
772 + diff_angle(result[1], before[1]).abs()
773 + diff_angle(result[2], before[2]).abs();
774 for candidate in &candidates {
775 let error = diff_angle(candidate[0], before[0]).abs()
776 + diff_angle(candidate[1], before[1]).abs()
777 + diff_angle(candidate[2], before[2]).abs();
778 if error < min_error {
779 min_error = error;
780 result = *candidate;
781 }
782 }
783 result
784}
785
786fn diff_angle(a: f32, b: f32) -> f32 {
787 let diff = normalize_angle(a) - normalize_angle(b);
788 if diff > std::f32::consts::PI {
789 diff - std::f32::consts::TAU
790 } else if diff < -std::f32::consts::PI {
791 diff + std::f32::consts::TAU
792 } else {
793 diff
794 }
795}
796
797fn normalize_angle(angle: f32) -> f32 {
798 let mut result = angle;
799 while result >= std::f32::consts::TAU {
800 result -= std::f32::consts::TAU;
801 }
802 while result < 0.0 {
803 result += std::f32::consts::TAU;
804 }
805 result
806}
807
808fn euler_xyz_to_quat(euler: &[f32; 3]) -> Quat {
809 let [x, y, z] = *euler;
810 let c1 = (x / 2.0).cos();
811 let c2 = (y / 2.0).cos();
812 let c3 = (z / 2.0).cos();
813 let s1 = (x / 2.0).sin();
814 let s2 = (y / 2.0).sin();
815 let s3 = (z / 2.0).sin();
816 Quat::from_xyzw(
817 s1 * c2 * c3 + c1 * s2 * s3,
818 c1 * s2 * c3 - s1 * c2 * s3,
819 c1 * c2 * s3 + s1 * s2 * c3,
820 c1 * c2 * c3 - s1 * s2 * s3,
821 )
822}
823
824struct LimitedAxesLinkStepInput<'a> {
825 local_effector: &'a Vec3A,
826 local_target: &'a Vec3A,
827 link_index: usize,
828 base_rotations: &'a [Quat],
829 ik_rotations: &'a mut [Quat],
830 chain_states: &'a mut [ChainLinkState],
831 limits: crate::IkAngleLimit,
832 limit_angle: f32,
833}
834
835fn solve_limited_axes_link_step(input: LimitedAxesLinkStepInput<'_>) {
836 let state = &mut input.chain_states[input.link_index];
837 let base = input.base_rotations[input.link_index];
838 let current = (input.ik_rotations[input.link_index] * base).normalize();
839 let current_mat = quat_to_rotation_mat3(current);
840 let mut total_euler = decompose_euler_xyz(¤t_mat, &state.previous_euler);
841 let mut working_effector = *input.local_effector;
842 let target = input.local_target.normalize();
843
844 for axis_index in [2usize, 1, 0] {
845 let (lower, upper) = limit_axis_bounds(input.limits, axis_index);
846 if lower == 0.0 && upper == 0.0 {
847 let next = total_euler[axis_index].clamp(lower, upper);
848 let applied = next - total_euler[axis_index];
849 total_euler[axis_index] = next;
850 if applied.abs() > 0.0 {
851 working_effector = Quat::from_axis_angle(axis_vec(axis_index).into(), applied)
852 .mul_vec3a(working_effector);
853 }
854 continue;
855 }
856
857 let axis = axis_vec(axis_index);
858 let signed_angle = signed_projected_angle(working_effector, target, axis);
859 if signed_angle.abs() <= 1.0e-6 {
860 continue;
861 }
862 let step = if input.limit_angle > 0.0 {
863 signed_angle.clamp(-input.limit_angle, input.limit_angle)
864 } else {
865 signed_angle
866 };
867 let next = (total_euler[axis_index] + step).clamp(lower, upper);
868 let applied = next - total_euler[axis_index];
869 total_euler[axis_index] = next;
870 if applied.abs() > 0.0 {
871 working_effector =
872 Quat::from_axis_angle(axis.into(), applied).mul_vec3a(working_effector);
873 }
874 }
875
876 state.previous_euler = total_euler;
877 let chain_rotation = euler_xyz_to_quat(&total_euler).normalize();
878 input.ik_rotations[input.link_index] = (chain_rotation * base.inverse()).normalize();
879}
880
881fn limit_axis_bounds(limits: crate::IkAngleLimit, axis_index: usize) -> (f32, f32) {
882 match axis_index {
883 0 => (limits.min.x, limits.max.x),
884 1 => (limits.min.y, limits.max.y),
885 _ => (limits.min.z, limits.max.z),
886 }
887}
888
889fn axis_vec(axis_index: usize) -> Vec3A {
890 match axis_index {
891 0 => Vec3A::new(1.0, 0.0, 0.0),
892 1 => Vec3A::new(0.0, 1.0, 0.0),
893 _ => Vec3A::new(0.0, 0.0, 1.0),
894 }
895}
896
897fn signed_projected_angle(from: Vec3A, to: Vec3A, axis: Vec3A) -> f32 {
898 let projected_from = from - axis * from.dot(axis);
899 let projected_to = to - axis * to.dot(axis);
900 if projected_from.length_squared() <= f32::EPSILON
901 || projected_to.length_squared() <= f32::EPSILON
902 {
903 return 0.0;
904 }
905 let from_n = projected_from.normalize();
906 let to_n = projected_to.normalize();
907 let dot = from_n.dot(to_n).clamp(-1.0, 1.0);
908 let angle = dot.acos();
909 let sign = axis.dot(from_n.cross(to_n)).signum();
910 angle * if sign == 0.0 { 1.0 } else { sign }
911}
912
913struct PlaneLinkStepInput<'a> {
914 local_effector: &'a Vec3A,
915 local_target: &'a Vec3A,
916 link_index: usize,
917 base_rotations: &'a [Quat],
918 ik_rotations: &'a mut [Quat],
919 chain_states: &'a mut [ChainLinkState],
920 axis_index: usize,
921 limits: crate::IkAngleLimit,
922 iteration: usize,
923 limit_angle: f32,
924}
925
926fn solve_plane_link_step(input: PlaneLinkStepInput<'_>) {
927 let rotate_axis = match input.axis_index {
928 0 => Vec3A::new(1.0, 0.0, 0.0),
929 1 => Vec3A::new(0.0, 1.0, 0.0),
930 _ => Vec3A::new(0.0, 0.0, 1.0),
931 };
932 let local_eff_n = input.local_effector.normalize();
933 let local_tgt_n = input.local_target.normalize();
934
935 let dot = local_eff_n.dot(local_tgt_n).clamp(-1.0, 1.0);
936 let raw_angle = dot.acos();
937 let capped_angle = if input.limit_angle > 0.0 {
938 raw_angle.min(input.limit_angle)
939 } else {
940 raw_angle
941 };
942
943 let target_vec1 =
944 Quat::from_axis_angle(rotate_axis.into(), capped_angle).mul_vec3a(local_eff_n);
945 let target_vec2 =
946 Quat::from_axis_angle(rotate_axis.into(), -capped_angle).mul_vec3a(local_eff_n);
947 let signed_angle = if target_vec1.dot(local_tgt_n) > target_vec2.dot(local_tgt_n) {
948 capped_angle
949 } else {
950 -capped_angle
951 };
952
953 let state = &mut input.chain_states[input.link_index];
954 let mut next_angle = state.plane_mode_angle + signed_angle;
955 let (lower, upper) = match input.axis_index {
956 0 => (input.limits.min.x, input.limits.max.x),
957 1 => (input.limits.min.y, input.limits.max.y),
958 _ => (input.limits.min.z, input.limits.max.z),
959 };
960 let base = input.base_rotations[input.link_index];
961
962 if input.iteration == 0 && (next_angle < lower || next_angle > upper) {
963 if -next_angle > lower && -next_angle < upper {
964 next_angle = -next_angle;
965 } else {
966 let half = (lower + upper) * 0.5;
967 if (half - next_angle).abs() > (half + next_angle).abs() {
968 next_angle = -next_angle;
969 }
970 }
971 }
972
973 state.plane_mode_angle = next_angle.clamp(lower, upper);
974 let chain_rotation = Quat::from_axis_angle(rotate_axis.into(), state.plane_mode_angle);
975 input.ik_rotations[input.link_index] = (chain_rotation * base.inverse()).normalize();
976}
977
978#[cfg(test)]
979mod tests {
980 use std::sync::Arc;
981
982 use glam::{Quat, Vec3A};
983
984 use crate::{
985 AnimationClip, AppendTransformInit, BoneAnimationBinding, BoneIndex, BoneInit,
986 IkAngleLimit, IkLinkInit, IkSolverInit, ModelArena, MovableBoneKeyframe, MovableBoneTrack,
987 RuntimeInstance,
988 };
989
990 fn translation(matrix: glam::Mat4) -> Vec3A {
991 Vec3A::from_vec4(matrix.w_axis)
992 }
993
994 fn assert_vec3a_near(actual: Vec3A, expected: Vec3A) {
995 let delta = (actual - expected).abs();
996 assert!(
997 delta.x < 1.0e-5 && delta.y < 1.0e-5 && delta.z < 1.0e-5,
998 "actual={actual:?} expected={expected:?} delta={delta:?}"
999 );
1000 }
1001
1002 #[test]
1003 fn evaluates_rest_pose_world_matrices() {
1004 let model = Arc::new(
1005 ModelArena::new(vec![
1006 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1007 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1008 ])
1009 .unwrap(),
1010 );
1011 let mut runtime = RuntimeInstance::new(model);
1012
1013 runtime.evaluate_rest_pose();
1014
1015 assert_vec3a_near(
1016 translation(runtime.world_matrices()[0]),
1017 Vec3A::new(1.0, 0.0, 0.0),
1018 );
1019 assert_vec3a_near(
1020 translation(runtime.world_matrices()[1]),
1021 Vec3A::new(1.0, 2.0, 0.0),
1022 );
1023 }
1024
1025 #[test]
1026 fn evaluates_current_pose_with_parent_rotation() {
1027 let model = Arc::new(
1028 ModelArena::new(vec![
1029 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1030 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1031 ])
1032 .unwrap(),
1033 );
1034 let mut runtime = RuntimeInstance::new(model);
1035
1036 runtime.pose_mut().set_local_rotation(
1037 BoneIndex(0),
1038 Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
1039 );
1040 runtime.evaluate_current_pose();
1041
1042 assert_vec3a_near(
1043 translation(runtime.world_matrices()[1]),
1044 Vec3A::new(-1.0, 0.0, 0.0),
1045 );
1046 }
1047
1048 #[test]
1049 fn fixed_axis_bone_rotation_keeps_only_axis_twist() {
1050 let model = Arc::new(
1051 ModelArena::new(vec![
1052 BoneInit::new(None, Vec3A::ZERO).with_fixed_axis(Vec3A::Y),
1053 BoneInit::new(Some(BoneIndex(0)), Vec3A::X),
1054 ])
1055 .unwrap(),
1056 );
1057 let mut runtime = RuntimeInstance::new(model);
1058
1059 runtime.pose_mut().set_local_rotation(
1060 BoneIndex(0),
1061 (Quat::from_rotation_y(std::f32::consts::FRAC_PI_2)
1062 * Quat::from_rotation_x(std::f32::consts::FRAC_PI_2))
1063 .normalize(),
1064 );
1065 runtime.evaluate_current_pose();
1066
1067 assert_vec3a_near(
1068 translation(runtime.world_matrices()[1]),
1069 Vec3A::new(0.0, 0.0, -1.0),
1070 );
1071 }
1072
1073 #[test]
1074 fn evaluates_current_pose_with_local_position_offset() {
1075 let model = Arc::new(
1076 ModelArena::new(vec![
1077 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1078 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1079 ])
1080 .unwrap(),
1081 );
1082 let mut runtime = RuntimeInstance::new(model);
1083
1084 runtime
1085 .pose_mut()
1086 .set_local_position_offset(BoneIndex(1), Vec3A::new(0.0, 0.0, 3.0));
1087 runtime.evaluate_current_pose();
1088
1089 assert_vec3a_near(
1090 translation(runtime.world_matrices()[1]),
1091 Vec3A::new(1.0, 2.0, 3.0),
1092 );
1093 }
1094
1095 #[test]
1096 fn evaluates_clip_frame_into_world_matrices() {
1097 let model = Arc::new(
1098 ModelArena::new(vec![
1099 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1100 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1101 ])
1102 .unwrap(),
1103 );
1104 let clip = AnimationClip::new(vec![BoneAnimationBinding {
1105 bone: BoneIndex(1),
1106 track: MovableBoneTrack::from_keyframes(vec![
1107 MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
1108 MovableBoneKeyframe::new(10, Vec3A::new(0.0, 0.0, 4.0), Quat::IDENTITY),
1109 ]),
1110 }]);
1111 let mut runtime = RuntimeInstance::new(model);
1112
1113 runtime.evaluate_clip_frame(&clip, 5.0);
1114
1115 assert_vec3a_near(
1116 translation(runtime.world_matrices()[1]),
1117 Vec3A::new(1.0, 2.0, 2.0),
1118 );
1119 }
1120
1121 #[test]
1122 fn applies_append_rotation_before_world_matrix_output() {
1123 let model = Arc::new(
1124 ModelArena::new_full(
1125 vec![
1126 BoneInit::new(None, Vec3A::ZERO),
1127 BoneInit::new(None, Vec3A::ZERO),
1128 BoneInit::new(Some(BoneIndex(1)), Vec3A::new(1.0, 0.0, 0.0)),
1129 ],
1130 Vec::new(),
1131 vec![AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 1.0).with_rotation()],
1132 )
1133 .unwrap(),
1134 );
1135 let mut runtime = RuntimeInstance::new(model);
1136
1137 runtime.pose_mut().set_local_rotation(
1138 BoneIndex(0),
1139 Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
1140 );
1141 runtime.evaluate_current_pose();
1142
1143 assert_vec3a_near(
1144 translation(runtime.world_matrices()[2]),
1145 Vec3A::new(0.0, 1.0, 0.0),
1146 );
1147 }
1148
1149 #[test]
1150 fn applies_append_translation_before_world_matrix_output() {
1151 let model = Arc::new(
1152 ModelArena::new_full(
1153 vec![
1154 BoneInit::new(None, Vec3A::ZERO),
1155 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1156 ],
1157 Vec::new(),
1158 vec![AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 0.5).with_translation()],
1159 )
1160 .unwrap(),
1161 );
1162 let mut runtime = RuntimeInstance::new(model);
1163
1164 runtime
1165 .pose_mut()
1166 .set_local_position_offset(BoneIndex(0), Vec3A::new(2.0, 0.0, 0.0));
1167 runtime.evaluate_current_pose();
1168
1169 assert_vec3a_near(
1170 translation(runtime.world_matrices()[1]),
1171 Vec3A::new(1.0, 1.0, 0.0),
1172 );
1173 }
1174
1175 #[test]
1176 fn initializes_ik_enabled_from_model_solvers() {
1177 let model = Arc::new(
1178 ModelArena::new_with_ik(
1179 vec![
1180 BoneInit::new(None, Vec3A::ZERO),
1181 BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1182 ],
1183 vec![IkSolverInit::new(
1184 BoneIndex(1),
1185 BoneIndex(0),
1186 vec![IkLinkInit::new(BoneIndex(0))],
1187 )],
1188 )
1189 .unwrap(),
1190 );
1191
1192 let runtime = RuntimeInstance::new(model);
1193
1194 assert_eq!(runtime.ik_enabled(), &[1]);
1195 }
1196
1197 #[test]
1198 fn solves_one_link_ik_toward_controller_bone() {
1199 let model = Arc::new(
1200 ModelArena::new_with_ik(
1201 vec![
1202 BoneInit::new(None, Vec3A::ZERO),
1203 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1204 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1205 ],
1206 vec![IkSolverInit {
1207 ik_bone: BoneIndex(2),
1208 target_bone: BoneIndex(1),
1209 links: vec![IkLinkInit::new(BoneIndex(0))],
1210 iteration_count: 1,
1211 limit_angle: 0.0,
1212 }],
1213 )
1214 .unwrap(),
1215 );
1216 let mut runtime = RuntimeInstance::new(model);
1217
1218 runtime.evaluate_current_pose();
1219
1220 assert_vec3a_near(
1221 translation(runtime.world_matrices()[1]),
1222 Vec3A::new(0.0, 1.0, 0.0),
1223 );
1224 }
1225
1226 #[test]
1227 fn skips_disabled_ik_solver() {
1228 let model = Arc::new(
1229 ModelArena::new_with_ik(
1230 vec![
1231 BoneInit::new(None, Vec3A::ZERO),
1232 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1233 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1234 ],
1235 vec![IkSolverInit {
1236 ik_bone: BoneIndex(2),
1237 target_bone: BoneIndex(1),
1238 links: vec![IkLinkInit::new(BoneIndex(0))],
1239 iteration_count: 1,
1240 limit_angle: 0.0,
1241 }],
1242 )
1243 .unwrap(),
1244 );
1245 let mut runtime = RuntimeInstance::new(model);
1246
1247 runtime.pose_mut().set_ik_enabled(0, false);
1248 runtime.evaluate_current_pose();
1249
1250 assert_vec3a_near(
1251 translation(runtime.world_matrices()[1]),
1252 Vec3A::new(1.0, 0.0, 0.0),
1253 );
1254 }
1255
1256 #[test]
1257 fn solves_two_link_ik_chain_toward_controller_bone() {
1258 let model = Arc::new(
1259 ModelArena::new_with_ik(
1260 vec![
1261 BoneInit::new(None, Vec3A::ZERO),
1262 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1263 BoneInit::new(Some(BoneIndex(1)), Vec3A::new(1.0, 0.0, 0.0)),
1264 BoneInit::new(None, Vec3A::new(1.0, 1.0, 0.0)),
1265 ],
1266 vec![IkSolverInit {
1267 ik_bone: BoneIndex(3),
1268 target_bone: BoneIndex(2),
1269 links: vec![IkLinkInit::new(BoneIndex(1)), IkLinkInit::new(BoneIndex(0))],
1270 iteration_count: 4,
1271 limit_angle: 0.0,
1272 }],
1273 )
1274 .unwrap(),
1275 );
1276 let mut runtime = RuntimeInstance::new(model);
1277
1278 runtime.evaluate_current_pose();
1279
1280 assert_vec3a_near(
1281 translation(runtime.world_matrices()[2]),
1282 Vec3A::new(1.0, 1.0, 0.0),
1283 );
1284 }
1285
1286 #[test]
1287 fn ik_updates_only_affected_eval_suffix_for_late_chain() {
1288 let unrelated_count = 96usize;
1289 let chain_root = BoneIndex(unrelated_count as u32);
1290 let chain_mid = BoneIndex(unrelated_count as u32 + 1);
1291 let chain_tip = BoneIndex(unrelated_count as u32 + 2);
1292 let controller = BoneIndex(unrelated_count as u32 + 3);
1293
1294 let mut bones = Vec::new();
1295 for i in 0..unrelated_count {
1296 bones.push(BoneInit::new(None, Vec3A::new(i as f32 * 10.0, -10.0, 0.0)));
1297 }
1298 bones.push(BoneInit::new(None, Vec3A::ZERO));
1299 bones.push(BoneInit::new(Some(chain_root), Vec3A::new(1.0, 0.0, 0.0)));
1300 bones.push(BoneInit::new(Some(chain_mid), Vec3A::new(1.0, 0.0, 0.0)));
1301 bones.push(BoneInit::new(None, Vec3A::new(1.0, 1.0, 0.0)));
1302
1303 let model = Arc::new(
1304 ModelArena::new_with_ik(
1305 bones,
1306 vec![IkSolverInit {
1307 ik_bone: controller,
1308 target_bone: chain_tip,
1309 links: vec![IkLinkInit::new(chain_mid), IkLinkInit::new(chain_root)],
1310 iteration_count: 4,
1311 limit_angle: 0.0,
1312 }],
1313 )
1314 .unwrap(),
1315 );
1316 let mut runtime = RuntimeInstance::new(model);
1317
1318 runtime.reset_world_matrix_bone_update_count();
1319 runtime.evaluate_current_pose();
1320
1321 assert_vec3a_near(
1322 translation(runtime.world_matrices()[chain_tip.as_usize()]),
1323 Vec3A::new(1.0, 1.0, 0.0),
1324 );
1325 assert!(
1326 runtime.world_matrix_bone_update_count() < 250,
1327 "IK should not recompute unrelated prefix bones repeatedly; updated {} bones",
1328 runtime.world_matrix_bone_update_count()
1329 );
1330 }
1331
1332 #[test]
1333 fn clamps_ik_rotation_by_solver_limit_angle() {
1334 let model = Arc::new(
1335 ModelArena::new_with_ik(
1336 vec![
1337 BoneInit::new(None, Vec3A::ZERO),
1338 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1339 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1340 ],
1341 vec![IkSolverInit {
1342 ik_bone: BoneIndex(2),
1343 target_bone: BoneIndex(1),
1344 links: vec![IkLinkInit::new(BoneIndex(0))],
1345 iteration_count: 1,
1346 limit_angle: std::f32::consts::FRAC_PI_4,
1347 }],
1348 )
1349 .unwrap(),
1350 );
1351 let mut runtime = RuntimeInstance::new(model);
1352
1353 runtime.evaluate_current_pose();
1354
1355 let expected = Vec3A::new(
1356 std::f32::consts::FRAC_1_SQRT_2,
1357 std::f32::consts::FRAC_1_SQRT_2,
1358 0.0,
1359 );
1360 assert_vec3a_near(translation(runtime.world_matrices()[1]), expected);
1361 }
1362
1363 #[test]
1364 fn applies_constant_limit_angle_per_iteration() {
1365 let model = Arc::new(
1366 ModelArena::new_with_ik(
1367 vec![
1368 BoneInit::new(None, Vec3A::ZERO),
1369 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1370 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1371 ],
1372 vec![IkSolverInit {
1373 ik_bone: BoneIndex(2),
1374 target_bone: BoneIndex(1),
1375 links: vec![IkLinkInit::new(BoneIndex(1)), IkLinkInit::new(BoneIndex(0))],
1376 iteration_count: 1,
1377 limit_angle: std::f32::consts::FRAC_PI_4,
1378 }],
1379 )
1380 .unwrap(),
1381 );
1382 let mut runtime = RuntimeInstance::new(model);
1383
1384 runtime.evaluate_current_pose();
1385
1386 let expected = Vec3A::new(
1390 std::f32::consts::FRAC_1_SQRT_2,
1391 std::f32::consts::FRAC_1_SQRT_2,
1392 0.0,
1393 );
1394 assert_vec3a_near(translation(runtime.world_matrices()[1]), expected);
1395 }
1396
1397 #[test]
1398 fn clip_frame_produces_deterministic_world_translations() {
1399 let model = Arc::new(
1400 ModelArena::new(vec![
1401 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1402 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1403 ])
1404 .unwrap(),
1405 );
1406 let clip = AnimationClip::new(vec![BoneAnimationBinding {
1407 bone: BoneIndex(1),
1408 track: MovableBoneTrack::from_keyframes(vec![
1409 MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
1410 MovableBoneKeyframe::new(10, Vec3A::new(0.0, 0.0, 4.0), Quat::IDENTITY),
1411 ]),
1412 }]);
1413 let mut runtime = RuntimeInstance::new(model);
1414
1415 runtime.evaluate_clip_frame(&clip, 5.0);
1416
1417 let matrices = runtime.world_matrices();
1418 assert_eq!(matrices.len(), 2);
1419 assert_vec3a_near(translation(matrices[0]), Vec3A::new(1.0, 0.0, 0.0));
1420 assert_vec3a_near(translation(matrices[1]), Vec3A::new(1.0, 2.0, 2.0));
1421 }
1422
1423 #[test]
1424 fn evaluate_clip_frame_without_ik_leaves_ik_unsolved() {
1425 let model = Arc::new(
1426 ModelArena::new_with_ik(
1427 vec![
1428 BoneInit::new(None, Vec3A::ZERO),
1429 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1430 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1431 ],
1432 vec![IkSolverInit {
1433 ik_bone: BoneIndex(2),
1434 target_bone: BoneIndex(1),
1435 links: vec![IkLinkInit::new(BoneIndex(0))],
1436 iteration_count: 1,
1437 limit_angle: 0.0,
1438 }],
1439 )
1440 .unwrap(),
1441 );
1442 let clip = AnimationClip::new(vec![]);
1443
1444 let mut without_ik = RuntimeInstance::new(Arc::clone(&model));
1445 let mut with_ik = RuntimeInstance::new(model);
1446
1447 without_ik.evaluate_clip_frame_without_ik(&clip, 0.0);
1448 with_ik.evaluate_clip_frame(&clip, 0.0);
1449
1450 assert_vec3a_near(
1452 translation(without_ik.world_matrices()[1]),
1453 Vec3A::new(1.0, 0.0, 0.0),
1454 );
1455 assert_vec3a_near(
1457 translation(with_ik.world_matrices()[1]),
1458 Vec3A::new(0.0, 1.0, 0.0),
1459 );
1460 }
1461
1462 #[test]
1463 fn ik_options_cap_configured_iterations() {
1464 let model = Arc::new(
1465 ModelArena::new_with_ik(
1466 vec![
1467 BoneInit::new(None, Vec3A::ZERO),
1468 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1469 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1470 ],
1471 vec![IkSolverInit {
1472 ik_bone: BoneIndex(2),
1473 target_bone: BoneIndex(1),
1474 links: vec![IkLinkInit::new(BoneIndex(0))],
1475 iteration_count: 100,
1476 limit_angle: 0.0,
1477 }],
1478 )
1479 .unwrap(),
1480 );
1481 let mut runtime = RuntimeInstance::new(model);
1482
1483 runtime.reset_ik_runtime_stats();
1484 runtime.evaluate_current_pose_with_ik_options(super::IkSolveOptions {
1485 tolerance: 0.0,
1486 max_iterations_cap: Some(5),
1487 });
1488
1489 assert_eq!(runtime.ik_runtime_stats()[0].configured_iterations, 5);
1490 }
1491
1492 fn assert_near(actual: f32, expected: f32) {
1495 let delta = (actual - expected).abs();
1496 assert!(
1497 delta < 1.0e-5,
1498 "actual={actual:?} expected={expected:?} delta={delta:?}"
1499 );
1500 }
1501
1502 #[test]
1503 fn bone_morph_position_offset_drives_world_position() {
1504 let model = Arc::new(
1505 ModelArena::new_with_morphs(
1506 vec![
1507 BoneInit::new(None, Vec3A::ZERO),
1508 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1509 ],
1510 Vec::new(),
1511 Vec::new(),
1512 crate::MorphInit {
1513 morph_count: 1,
1514 bone_offsets: vec![crate::BoneMorphOffset {
1515 target_bone: BoneIndex(1),
1516 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1517 rotation_offset: Quat::IDENTITY,
1518 }],
1519 bone_spans: vec![crate::MorphOffsetSpan { start: 0, count: 1 }],
1520 group_offsets: vec![],
1521 group_spans: vec![crate::MorphOffsetSpan::default()],
1522 ..crate::MorphInit::default()
1523 },
1524 )
1525 .unwrap(),
1526 );
1527 let clip = AnimationClip::new_with_morphs(
1528 Vec::new(),
1529 vec![crate::MorphAnimationBinding {
1530 morph: crate::MorphIndex(0),
1531 track: crate::MorphTrack::from_keyframes(vec![
1532 crate::MorphKeyframe::new(0, 0.0),
1533 crate::MorphKeyframe::new(10, 1.0),
1534 ]),
1535 }],
1536 );
1537 let mut runtime = RuntimeInstance::new_with_morph_count(model, 1);
1538
1539 runtime.evaluate_clip_frame(&clip, 5.0);
1540
1541 assert_vec3a_near(
1543 translation(runtime.world_matrices()[1]),
1544 Vec3A::new(0.0, 1.0, 1.0),
1545 );
1546 assert_near(runtime.morph_weights()[0], 0.5);
1547 }
1548
1549 #[test]
1550 fn bone_morph_rotation_offset_affects_child_position() {
1551 let model = Arc::new(
1552 ModelArena::new_with_morphs(
1553 vec![
1554 BoneInit::new(None, Vec3A::ZERO),
1555 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1556 BoneInit::new(Some(BoneIndex(1)), Vec3A::new(1.0, 0.0, 0.0)),
1557 ],
1558 Vec::new(),
1559 Vec::new(),
1560 crate::MorphInit {
1561 morph_count: 1,
1562 bone_offsets: vec![crate::BoneMorphOffset {
1563 target_bone: BoneIndex(1),
1564 position_offset: Vec3A::ZERO,
1565 rotation_offset: Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
1566 }],
1567 bone_spans: vec![crate::MorphOffsetSpan { start: 0, count: 1 }],
1568 group_offsets: vec![],
1569 group_spans: vec![crate::MorphOffsetSpan::default()],
1570 ..crate::MorphInit::default()
1571 },
1572 )
1573 .unwrap(),
1574 );
1575 let clip = AnimationClip::new_with_morphs(
1576 Vec::new(),
1577 vec![crate::MorphAnimationBinding {
1578 morph: crate::MorphIndex(0),
1579 track: crate::MorphTrack::from_keyframes(vec![
1580 crate::MorphKeyframe::new(0, 0.0),
1581 crate::MorphKeyframe::new(10, 1.0),
1582 ]),
1583 }],
1584 );
1585 let mut runtime = RuntimeInstance::new_with_morph_count(model, 1);
1586
1587 runtime.evaluate_clip_frame(&clip, 10.0);
1588
1589 assert_vec3a_near(
1592 translation(runtime.world_matrices()[2]),
1593 Vec3A::new(1.0, 1.0, 0.0),
1594 );
1595 }
1596
1597 #[test]
1598 fn group_morph_contributes_to_bone_morph_weight() {
1599 let model = Arc::new(
1602 ModelArena::new_with_morphs(
1603 vec![
1604 BoneInit::new(None, Vec3A::ZERO),
1605 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1606 ],
1607 Vec::new(),
1608 Vec::new(),
1609 crate::MorphInit {
1610 morph_count: 2,
1611 bone_offsets: vec![crate::BoneMorphOffset {
1612 target_bone: BoneIndex(1),
1613 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1614 rotation_offset: Quat::IDENTITY,
1615 }],
1616 bone_spans: vec![
1617 crate::MorphOffsetSpan { start: 0, count: 1 },
1618 crate::MorphOffsetSpan::default(),
1619 ],
1620 group_offsets: vec![crate::GroupMorphOffset {
1621 child_morph: crate::MorphIndex(0),
1622 ratio: 0.5,
1623 }],
1624 group_spans: vec![
1625 crate::MorphOffsetSpan::default(),
1626 crate::MorphOffsetSpan { start: 0, count: 1 },
1627 ],
1628 ..crate::MorphInit::default()
1629 },
1630 )
1631 .unwrap(),
1632 );
1633 let clip = AnimationClip::new_with_morphs(
1635 Vec::new(),
1636 vec![crate::MorphAnimationBinding {
1637 morph: crate::MorphIndex(1),
1638 track: crate::MorphTrack::from_keyframes(vec![
1639 crate::MorphKeyframe::new(0, 0.0),
1640 crate::MorphKeyframe::new(10, 1.0),
1641 ]),
1642 }],
1643 );
1644 let mut runtime = RuntimeInstance::new_with_morph_count(model, 2);
1645
1646 runtime.evaluate_clip_frame(&clip, 10.0);
1647
1648 assert_near(runtime.morph_weights()[0], 0.5);
1651 assert_near(runtime.morph_weights()[1], 1.0);
1652 assert_vec3a_near(
1653 translation(runtime.world_matrices()[1]),
1654 Vec3A::new(0.0, 1.0, 1.0),
1655 );
1656 }
1657
1658 #[test]
1659 fn group_morph_can_reference_later_child_morph() {
1660 let model = Arc::new(
1661 ModelArena::new_with_morphs(
1662 vec![
1663 BoneInit::new(None, Vec3A::ZERO),
1664 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1665 ],
1666 Vec::new(),
1667 Vec::new(),
1668 crate::MorphInit {
1669 morph_count: 2,
1670 bone_offsets: vec![crate::BoneMorphOffset {
1671 target_bone: BoneIndex(1),
1672 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1673 rotation_offset: Quat::IDENTITY,
1674 }],
1675 bone_spans: vec![
1676 crate::MorphOffsetSpan::default(),
1677 crate::MorphOffsetSpan { start: 0, count: 1 },
1678 ],
1679 group_offsets: vec![crate::GroupMorphOffset {
1680 child_morph: crate::MorphIndex(1),
1681 ratio: 0.5,
1682 }],
1683 group_spans: vec![
1684 crate::MorphOffsetSpan { start: 0, count: 1 },
1685 crate::MorphOffsetSpan::default(),
1686 ],
1687 ..crate::MorphInit::default()
1688 },
1689 )
1690 .unwrap(),
1691 );
1692 let clip = AnimationClip::new_with_morphs(
1693 Vec::new(),
1694 vec![crate::MorphAnimationBinding {
1695 morph: crate::MorphIndex(0),
1696 track: crate::MorphTrack::from_keyframes(vec![crate::MorphKeyframe::new(0, 1.0)]),
1697 }],
1698 );
1699 let mut runtime = RuntimeInstance::new(model);
1700
1701 runtime.evaluate_clip_frame(&clip, 0.0);
1702
1703 assert_near(runtime.morph_weights()[0], 1.0);
1704 assert_near(runtime.morph_weights()[1], 0.5);
1705 assert_vec3a_near(
1706 translation(runtime.world_matrices()[1]),
1707 Vec3A::new(0.0, 1.0, 1.0),
1708 );
1709 }
1710
1711 #[test]
1712 fn chained_group_morphs_descend_to_bone_morph_weight() {
1713 let model = Arc::new(
1714 ModelArena::new_with_morphs(
1715 vec![
1716 BoneInit::new(None, Vec3A::ZERO),
1717 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1718 ],
1719 Vec::new(),
1720 Vec::new(),
1721 crate::MorphInit {
1722 morph_count: 3,
1723 bone_offsets: vec![crate::BoneMorphOffset {
1724 target_bone: BoneIndex(1),
1725 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1726 rotation_offset: Quat::IDENTITY,
1727 }],
1728 bone_spans: vec![
1729 crate::MorphOffsetSpan { start: 0, count: 1 },
1730 crate::MorphOffsetSpan::default(),
1731 crate::MorphOffsetSpan::default(),
1732 ],
1733 group_offsets: vec![
1734 crate::GroupMorphOffset {
1735 child_morph: crate::MorphIndex(0),
1736 ratio: 0.25,
1737 },
1738 crate::GroupMorphOffset {
1739 child_morph: crate::MorphIndex(1),
1740 ratio: 0.5,
1741 },
1742 ],
1743 group_spans: vec![
1744 crate::MorphOffsetSpan::default(),
1745 crate::MorphOffsetSpan { start: 0, count: 1 },
1746 crate::MorphOffsetSpan { start: 1, count: 1 },
1747 ],
1748 ..crate::MorphInit::default()
1749 },
1750 )
1751 .unwrap(),
1752 );
1753 let clip = AnimationClip::new_with_morphs(
1754 Vec::new(),
1755 vec![crate::MorphAnimationBinding {
1756 morph: crate::MorphIndex(2),
1757 track: crate::MorphTrack::from_keyframes(vec![crate::MorphKeyframe::new(0, 1.0)]),
1758 }],
1759 );
1760 let mut runtime = RuntimeInstance::new(model);
1761
1762 runtime.evaluate_clip_frame(&clip, 0.0);
1763
1764 assert_near(runtime.morph_weights()[2], 1.0);
1765 assert_near(runtime.morph_weights()[1], 0.5);
1766 assert_near(runtime.morph_weights()[0], 0.125);
1767 assert_vec3a_near(
1768 translation(runtime.world_matrices()[1]),
1769 Vec3A::new(0.0, 1.0, 0.25),
1770 );
1771 }
1772
1773 #[test]
1774 fn expand_morphs_noop_when_no_morph_defs() {
1775 let model = Arc::new(ModelArena::new(vec![BoneInit::new(None, Vec3A::ZERO)]).unwrap());
1776 let mut runtime = RuntimeInstance::new_with_morph_count(model, 1);
1777 runtime
1778 .pose_mut()
1779 .set_morph_weight(crate::MorphIndex(0), 1.0);
1780 runtime.expand_morphs();
1781 assert_near(runtime.morph_weights()[0], 1.0);
1783 }
1784
1785 #[test]
1786 fn clamps_link_local_rotation_to_angle_limit() {
1787 let model = Arc::new(
1788 ModelArena::new_with_ik(
1789 vec![
1790 BoneInit::new(None, Vec3A::ZERO),
1791 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1792 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1793 ],
1794 vec![IkSolverInit {
1795 ik_bone: BoneIndex(2),
1796 target_bone: BoneIndex(1),
1797 links: vec![
1798 IkLinkInit::new(BoneIndex(0)).with_angle_limit(IkAngleLimit::new(
1799 Vec3A::new(0.0, 0.0, 0.0),
1800 Vec3A::new(0.0, 0.0, std::f32::consts::FRAC_PI_4),
1801 )),
1802 ],
1803 iteration_count: 1,
1804 limit_angle: 0.0,
1805 }],
1806 )
1807 .unwrap(),
1808 );
1809 let mut runtime = RuntimeInstance::new(model);
1810
1811 runtime.evaluate_current_pose();
1812
1813 let expected = Vec3A::new(
1814 std::f32::consts::FRAC_1_SQRT_2,
1815 std::f32::consts::FRAC_1_SQRT_2,
1816 0.0,
1817 );
1818 assert_vec3a_near(translation(runtime.world_matrices()[1]), expected);
1819 }
1820
1821 #[test]
1822 fn multi_axis_limited_link_solves_before_clamping() {
1823 let local_effector = Vec3A::X;
1824 let local_target = Vec3A::new(0.25, 0.55, 0.80).normalize();
1825 let limits = IkAngleLimit::new(Vec3A::new(0.0, -1.0, -1.0), Vec3A::new(0.0, 1.0, 1.0));
1826 let base_rotations = vec![Quat::IDENTITY];
1827 let mut ik_rotations = vec![Quat::IDENTITY];
1828 let mut chain_states = vec![super::ChainLinkState {
1829 previous_euler: [0.0; 3],
1830 plane_mode_angle: 0.0,
1831 }];
1832
1833 super::solve_limited_axes_link_step(super::LimitedAxesLinkStepInput {
1834 local_effector: &local_effector,
1835 local_target: &local_target,
1836 link_index: 0,
1837 base_rotations: &base_rotations,
1838 ik_rotations: &mut ik_rotations,
1839 chain_states: &mut chain_states,
1840 limits,
1841 limit_angle: 0.0,
1842 });
1843
1844 let current_direction = ik_rotations[0].mul_vec3a(local_effector).normalize();
1845 let legacy_direction =
1846 legacy_clamp_only_limited_direction(local_effector, local_target, limits);
1847 let current_error = (current_direction - local_target).length();
1848 let legacy_error = (legacy_direction - local_target).length();
1849
1850 assert!(
1851 current_error < legacy_error - 0.015,
1852 "current_error={current_error:.6} legacy_error={legacy_error:.6} current={current_direction:?} legacy={legacy_direction:?} target={local_target:?}"
1853 );
1854 assert!(
1855 chain_states[0].previous_euler[1].abs() > 0.1
1856 && chain_states[0].previous_euler[2].abs() > 0.1,
1857 "multi-axis limited IK should use both Y and Z axes; euler={:?}",
1858 chain_states[0].previous_euler
1859 );
1860 }
1861
1862 #[test]
1863 fn multi_axis_limited_link_applies_limits_to_total_rotation() {
1864 let local_effector = Vec3A::new(0.25, 0.45, 0.85).normalize();
1865 let local_target = Vec3A::new(0.55, 0.15, 0.80).normalize();
1866 let limits = IkAngleLimit::new(Vec3A::new(-1.0, -1.0, 0.0), Vec3A::new(1.0, 1.0, 0.0));
1867 let base_rotations = vec![Quat::from_rotation_z(0.45)];
1868 let mut ik_rotations = vec![Quat::IDENTITY];
1869 let mut chain_states = vec![super::ChainLinkState {
1870 previous_euler: [0.0; 3],
1871 plane_mode_angle: 0.0,
1872 }];
1873
1874 super::solve_limited_axes_link_step(super::LimitedAxesLinkStepInput {
1875 local_effector: &local_effector,
1876 local_target: &local_target,
1877 link_index: 0,
1878 base_rotations: &base_rotations,
1879 ik_rotations: &mut ik_rotations,
1880 chain_states: &mut chain_states,
1881 limits,
1882 limit_angle: 0.0,
1883 });
1884
1885 let base_direction = base_rotations[0].mul_vec3a(local_effector).normalize();
1886 let effective = (ik_rotations[0] * base_rotations[0]).normalize();
1887 let stale_direction = limited_direction_without_fixed_axis_working_update(
1888 local_effector,
1889 local_target,
1890 base_rotations[0],
1891 limits,
1892 );
1893 let solved_direction = effective.mul_vec3a(local_effector).normalize();
1894 assert_near(chain_states[0].previous_euler[2], 0.0);
1895 assert!(
1896 (solved_direction - stale_direction).length() > 0.05,
1897 "fixed axis clamp should affect later axis solve; solved={solved_direction:?} stale={stale_direction:?}"
1898 );
1899 assert!(
1900 (solved_direction - local_target).length() < (base_direction - local_target).length(),
1901 "non-identity base should still solve toward target; base={base_direction:?} solved={solved_direction:?} target={local_target:?}"
1902 );
1903 }
1904
1905 fn limited_direction_without_fixed_axis_working_update(
1906 local_effector: Vec3A,
1907 local_target: Vec3A,
1908 base: Quat,
1909 limits: IkAngleLimit,
1910 ) -> Vec3A {
1911 let mut total_euler =
1912 super::decompose_euler_xyz(&super::quat_to_rotation_mat3(base), &[0.0; 3]);
1913 let mut working_effector = local_effector;
1914 let target = local_target.normalize();
1915
1916 for axis_index in [2usize, 1, 0] {
1917 let (lower, upper) = super::limit_axis_bounds(limits, axis_index);
1918 if lower == 0.0 && upper == 0.0 {
1919 total_euler[axis_index] = total_euler[axis_index].clamp(lower, upper);
1920 continue;
1921 }
1922
1923 let axis = super::axis_vec(axis_index);
1924 let signed_angle = super::signed_projected_angle(working_effector, target, axis);
1925 if signed_angle.abs() <= 1.0e-6 {
1926 continue;
1927 }
1928 let next = (total_euler[axis_index] + signed_angle).clamp(lower, upper);
1929 let applied = next - total_euler[axis_index];
1930 total_euler[axis_index] = next;
1931 if applied.abs() > 0.0 {
1932 working_effector =
1933 Quat::from_axis_angle(axis.into(), applied).mul_vec3a(working_effector);
1934 }
1935 }
1936
1937 super::euler_xyz_to_quat(&total_euler)
1938 .normalize()
1939 .mul_vec3a(local_effector)
1940 .normalize()
1941 }
1942
1943 fn legacy_clamp_only_limited_direction(
1944 local_effector: Vec3A,
1945 local_target: Vec3A,
1946 limits: IkAngleLimit,
1947 ) -> Vec3A {
1948 let local_eff_n = local_effector.normalize();
1949 let local_tgt_n = local_target.normalize();
1950 let dot = local_eff_n.dot(local_tgt_n).clamp(-1.0, 1.0);
1951 let angle = dot.acos();
1952 let axis = local_eff_n.cross(local_tgt_n);
1953 let axis_vec = if axis.length() < 1e-5 {
1954 if dot > -1.0 + 1e-5 {
1955 return local_eff_n;
1956 }
1957 let basis = if local_eff_n.x.abs() < 0.9 {
1958 Vec3A::new(1.0, 0.0, 0.0)
1959 } else {
1960 Vec3A::new(0.0, 1.0, 0.0)
1961 };
1962 local_eff_n.cross(basis).normalize()
1963 } else {
1964 axis.normalize()
1965 };
1966 let rotation = Quat::from_axis_angle(axis_vec.into(), angle).normalize();
1967 let euler = super::decompose_euler_xyz(&super::quat_to_rotation_mat3(rotation), &[0.0; 3]);
1968 let clamped = [
1969 euler[0].clamp(limits.min.x, limits.max.x),
1970 euler[1].clamp(limits.min.y, limits.max.y),
1971 euler[2].clamp(limits.min.z, limits.max.z),
1972 ];
1973 super::euler_xyz_to_quat(&clamped)
1974 .normalize()
1975 .mul_vec3a(local_effector)
1976 .normalize()
1977 }
1978
1979 #[test]
1980 fn plane_link_step_matches_saba_total_axis_rotation() {
1981 let base = Quat::from_rotation_x(0.3);
1982 let base_rotations = vec![base];
1983 let mut ik_rotations = vec![Quat::IDENTITY];
1984 let mut chain_states = vec![super::ChainLinkState {
1985 previous_euler: [0.0; 3],
1986 plane_mode_angle: 0.0,
1987 }];
1988 let local_effector = Vec3A::X;
1989 let local_target = Vec3A::Y;
1990
1991 super::solve_plane_link_step(super::PlaneLinkStepInput {
1992 local_effector: &local_effector,
1993 local_target: &local_target,
1994 link_index: 0,
1995 base_rotations: &base_rotations,
1996 ik_rotations: &mut ik_rotations,
1997 chain_states: &mut chain_states,
1998 axis_index: 2,
1999 limits: IkAngleLimit::new(
2000 Vec3A::new(-std::f32::consts::PI, 0.0, -std::f32::consts::PI),
2001 Vec3A::new(std::f32::consts::PI, 0.0, std::f32::consts::PI),
2002 ),
2003 iteration: 0,
2004 limit_angle: 0.0,
2005 });
2006
2007 let effective = (ik_rotations[0] * base_rotations[0]).normalize();
2008 assert_near(
2009 chain_states[0].plane_mode_angle,
2010 std::f32::consts::FRAC_PI_2,
2011 );
2012 assert_vec3a_near(
2013 effective.mul_vec3a(Vec3A::X),
2014 Quat::from_rotation_z(std::f32::consts::FRAC_PI_2).mul_vec3a(Vec3A::X),
2015 );
2016 assert_vec3a_near(effective.mul_vec3a(Vec3A::Z), Vec3A::Z);
2017 }
2018
2019 #[test]
2020 fn append_rotation_propagates_post_ik_link_rotation() {
2021 let model = Arc::new(
2022 ModelArena::new_full(
2023 vec![
2024 BoneInit::new(None, Vec3A::ZERO),
2025 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
2026 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
2027 BoneInit::new(None, Vec3A::ZERO),
2028 BoneInit::new(Some(BoneIndex(3)), Vec3A::new(1.0, 0.0, 0.0)),
2029 ],
2030 vec![IkSolverInit {
2031 ik_bone: BoneIndex(2),
2032 target_bone: BoneIndex(1),
2033 links: vec![IkLinkInit::new(BoneIndex(0))],
2034 iteration_count: 1,
2035 limit_angle: 0.0,
2036 }],
2037 vec![AppendTransformInit::new(BoneIndex(3), BoneIndex(0), 1.0).with_rotation()],
2038 )
2039 .unwrap(),
2040 );
2041 let mut runtime = RuntimeInstance::new(model);
2042
2043 runtime.evaluate_current_pose();
2044
2045 assert_vec3a_near(
2046 translation(runtime.world_matrices()[4]),
2047 Vec3A::new(0.0, 1.0, 0.0),
2048 );
2049 }
2050
2051 #[test]
2052 fn scratch_ik_capacities_stable_after_repeated_evaluate() {
2053 let model = Arc::new(
2054 ModelArena::new_with_ik(
2055 vec![
2056 BoneInit::new(None, Vec3A::ZERO),
2057 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
2058 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
2059 BoneInit::new(None, Vec3A::ZERO),
2060 BoneInit::new(Some(BoneIndex(3)), Vec3A::new(1.0, 0.0, 0.0)),
2061 BoneInit::new(Some(BoneIndex(4)), Vec3A::new(1.0, 0.0, 0.0)),
2062 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
2063 ],
2064 vec![
2065 IkSolverInit {
2066 ik_bone: BoneIndex(2),
2067 target_bone: BoneIndex(1),
2068 links: vec![IkLinkInit::new(BoneIndex(0))],
2069 iteration_count: 1,
2070 limit_angle: 0.0,
2071 },
2072 IkSolverInit {
2073 ik_bone: BoneIndex(6),
2074 target_bone: BoneIndex(5),
2075 links: vec![IkLinkInit::new(BoneIndex(3)), IkLinkInit::new(BoneIndex(4))],
2076 iteration_count: 1,
2077 limit_angle: 0.0,
2078 },
2079 ],
2080 )
2081 .unwrap(),
2082 );
2083 let mut runtime = RuntimeInstance::new(model);
2084
2085 runtime.evaluate_current_pose();
2086
2087 let cap_links = runtime.ik_scratch.links.capacity();
2088 let cap_base = runtime.ik_scratch.base_rotations.capacity();
2089 let cap_ik = runtime.ik_scratch.ik_rotations.capacity();
2090 let cap_best = runtime.ik_scratch.best_ik_rotations.capacity();
2091 let cap_chain = runtime.ik_scratch.chain_states.capacity();
2092
2093 for _ in 0..10 {
2094 runtime.evaluate_current_pose();
2095 }
2096
2097 assert_eq!(runtime.ik_scratch.links.capacity(), cap_links);
2098 assert_eq!(runtime.ik_scratch.base_rotations.capacity(), cap_base);
2099 assert_eq!(runtime.ik_scratch.ik_rotations.capacity(), cap_ik);
2100 assert_eq!(runtime.ik_scratch.best_ik_rotations.capacity(), cap_best);
2101 assert_eq!(runtime.ik_scratch.chain_states.capacity(), cap_chain);
2102 }
2103
2104 #[test]
2105 fn scratch_morph_capacity_stable_after_repeated_clip_frame() {
2106 let model = Arc::new(
2107 ModelArena::new_with_morphs(
2108 vec![
2109 BoneInit::new(None, Vec3A::ZERO),
2110 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
2111 ],
2112 Vec::new(),
2113 Vec::new(),
2114 crate::MorphInit {
2115 morph_count: 2,
2116 bone_offsets: vec![crate::BoneMorphOffset {
2117 target_bone: BoneIndex(1),
2118 position_offset: Vec3A::new(0.0, 0.0, 2.0),
2119 rotation_offset: Quat::IDENTITY,
2120 }],
2121 bone_spans: vec![
2122 crate::MorphOffsetSpan { start: 0, count: 1 },
2123 crate::MorphOffsetSpan::default(),
2124 ],
2125 group_offsets: vec![crate::GroupMorphOffset {
2126 child_morph: crate::MorphIndex(0),
2127 ratio: 0.5,
2128 }],
2129 group_spans: vec![
2130 crate::MorphOffsetSpan::default(),
2131 crate::MorphOffsetSpan { start: 0, count: 1 },
2132 ],
2133 ..crate::MorphInit::default()
2134 },
2135 )
2136 .unwrap(),
2137 );
2138 let clip = AnimationClip::new_with_morphs(
2139 Vec::new(),
2140 vec![crate::MorphAnimationBinding {
2141 morph: crate::MorphIndex(1),
2142 track: crate::MorphTrack::from_keyframes(vec![
2143 crate::MorphKeyframe::new(0, 0.0),
2144 crate::MorphKeyframe::new(10, 1.0),
2145 ]),
2146 }],
2147 );
2148 let mut runtime = RuntimeInstance::new_with_morph_count(model, 2);
2149
2150 runtime.evaluate_clip_frame(&clip, 5.0);
2151
2152 let cap_expanded = runtime.morph_scratch.expanded_weights.capacity();
2153
2154 for _ in 0..10 {
2155 runtime.evaluate_clip_frame(&clip, 5.0);
2156 }
2157
2158 assert_eq!(
2159 runtime.morph_scratch.expanded_weights.capacity(),
2160 cap_expanded
2161 );
2162 }
2163}