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