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 let mut best_distance = f32::MAX;
305
306 self.apply_ik_link_rotations(&links, &base_rotations, &ik_rotations);
307 self.update_world_matrices();
308
309 let mut broke_early = false;
310 let mut final_distance = f32::MAX;
311 for _iteration in 0..iteration_count {
312 let eff_pos = translation(self.pose.world_matrices()[target_bone.as_usize()]);
314 let ik_pos = translation(self.pose.world_matrices()[ik_bone.as_usize()]);
315 final_distance = (eff_pos - ik_pos).length();
316 if final_distance <= tolerance {
317 self.ik_stats[ik_index].tolerance_precheck_breaks += 1;
318 broke_early = true;
319 break;
320 }
321 self.ik_stats[ik_index].executed_iterations += 1;
322
323 for link_index in 0..link_count {
324 let link = &links[link_index];
325 let link_bone = link.bone;
326 self.ik_stats[ik_index].link_visits += 1;
327
328 if link_bone == target_bone {
329 continue;
330 }
331
332 let link_world = self.pose.world_matrices()[link_bone.as_usize()];
333 let link_pos = translation(link_world);
334 let eff_pos = translation(self.pose.world_matrices()[target_bone.as_usize()]);
335 let ik_pos = translation(self.pose.world_matrices()[ik_bone.as_usize()]);
336
337 let link_world_rot = rotation(link_world);
339 let local_effector = link_world_rot.inverse().mul_vec3a(eff_pos - link_pos);
340 let local_target = link_world_rot.inverse().mul_vec3a(ik_pos - link_pos);
341
342 if local_effector.length_squared() <= f32::EPSILON
343 || local_target.length_squared() <= f32::EPSILON
344 {
345 continue;
346 }
347
348 let single_axis = get_single_axis_limit(link.angle_limit);
349
350 if let (Some(angle_limit), Some(axis_index)) = (link.angle_limit, single_axis) {
351 solve_plane_link_step(PlaneLinkStepInput {
352 local_effector: &local_effector,
353 local_target: &local_target,
354 link_index,
355 base_rotations: &base_rotations,
356 ik_rotations: &mut ik_rotations,
357 chain_states: &mut chain_states,
358 axis_index,
359 limits: angle_limit,
360 iteration: _iteration,
361 limit_angle,
362 });
363 } else {
364 let local_eff_n = local_effector.normalize();
365 let local_tgt_n = local_target.normalize();
366 let dot = local_eff_n.dot(local_tgt_n).clamp(-1.0, 1.0);
367 let mut angle = dot.acos();
368
369 let tiny_angle = 1e-3 * std::f32::consts::PI / 180.0;
370 if angle < tiny_angle {
371 continue;
372 }
373
374 if limit_angle > 0.0 {
375 angle = angle.min(limit_angle);
376 }
377
378 let axis = local_eff_n.cross(local_tgt_n);
379 let axis_vec = if axis.length() < 1e-5 {
380 if dot > -1.0 + 1e-5 {
381 continue;
382 }
383 let basis = if local_eff_n.x.abs() < 0.9 {
384 Vec3A::new(1.0, 0.0, 0.0)
385 } else {
386 Vec3A::new(0.0, 1.0, 0.0)
387 };
388 local_eff_n.cross(basis).normalize()
389 } else {
390 axis.normalize()
391 };
392
393 let delta = Quat::from_axis_angle(axis_vec.into(), angle);
394 let base = base_rotations[link_index];
395 let ik = ik_rotations[link_index];
396 let mut chain_rotation = (ik * base * delta).normalize();
397
398 if let Some(angle_limit) = link.angle_limit {
399 chain_rotation = clamp_limited_rotation(
400 chain_rotation,
401 angle_limit,
402 &mut chain_states[link_index],
403 limit_angle,
404 );
405 }
406
407 ik_rotations[link_index] = (chain_rotation * base.inverse()).normalize();
408 }
409
410 self.apply_ik_link_rotations(&links, &base_rotations, &ik_rotations);
411 self.update_world_matrices_from_bone(link_bone);
412 self.ik_stats[ik_index].link_steps += 1;
413 }
414
415 let current_distance = {
417 let eff = translation(self.pose.world_matrices()[target_bone.as_usize()]);
418 let ik = translation(self.pose.world_matrices()[ik_bone.as_usize()]);
419 (eff - ik).length()
420 };
421 final_distance = current_distance;
422
423 if current_distance < best_distance {
424 best_distance = current_distance;
425 best_ik_rotations.copy_from_slice(&ik_rotations);
426 if current_distance <= tolerance {
427 self.ik_stats[ik_index].tolerance_post_iteration_breaks += 1;
428 broke_early = true;
429 break;
430 }
431 } else {
432 self.ik_stats[ik_index].rollback_breaks += 1;
433 ik_rotations.copy_from_slice(&best_ik_rotations);
434 self.apply_ik_link_rotations(&links, &base_rotations, &ik_rotations);
435 if let Some(start_position) = self.min_link_eval_order_position(&links) {
436 self.update_world_matrices_from_eval_order_position(start_position);
437 }
438 broke_early = true;
439 break;
440 }
441 }
442 self.ik_stats[ik_index].final_distance_sum += f64::from(final_distance);
443 self.ik_stats[ik_index].final_distance_max = self.ik_stats[ik_index]
444 .final_distance_max
445 .max(final_distance);
446 if !broke_early {
447 self.ik_stats[ik_index].max_iteration_exhaustions += 1;
448 self.ik_stats[ik_index].exhausted_final_distance_sum += f64::from(final_distance);
449 self.ik_stats[ik_index].exhausted_final_distance_max = self.ik_stats[ik_index]
450 .exhausted_final_distance_max
451 .max(final_distance);
452 }
453
454 self.apply_ik_link_rotations(&links, &base_rotations, &best_ik_rotations);
456 if let Some(start_position) = self.min_link_eval_order_position(&links) {
457 self.update_world_matrices_from_eval_order_position(start_position);
458 }
459 }
460
461 self.ik_scratch.links = links;
462 self.ik_scratch.base_rotations = base_rotations;
463 self.ik_scratch.ik_rotations = ik_rotations;
464 self.ik_scratch.best_ik_rotations = best_ik_rotations;
465 self.ik_scratch.chain_states = chain_states;
466 }
467
468 fn apply_ik_link_rotations(
469 &mut self,
470 links: &[crate::IkLink],
471 base_rotations: &[Quat],
472 ik_rotations: &[Quat],
473 ) {
474 for (i, link) in links.iter().enumerate() {
475 let effective = (ik_rotations[i] * base_rotations[i]).normalize();
476 self.pose.set_local_rotation(link.bone, effective);
477 }
478 }
479
480 pub fn evaluate_rest_pose(&mut self) {
481 self.pose.reset_local_pose();
482 self.evaluate_current_pose();
483 }
484
485 pub fn evaluate_clip_frame(&mut self, clip: &AnimationClip, frame: f32) {
486 clip.apply_to_pose(frame, &mut self.pose);
487 self.expand_morphs();
488 self.evaluate_current_pose();
489 }
490
491 pub fn evaluate_clip_frame_with_ik_options(
492 &mut self,
493 clip: &AnimationClip,
494 frame: f32,
495 options: IkSolveOptions,
496 ) {
497 clip.apply_to_pose(frame, &mut self.pose);
498 self.expand_morphs();
499 self.evaluate_current_pose_with_ik_options(options);
500 }
501
502 pub fn evaluate_clip_frame_without_ik(&mut self, clip: &AnimationClip, frame: f32) {
507 clip.apply_to_pose(frame, &mut self.pose);
508 self.expand_morphs();
509 self.update_world_matrices();
510 }
511
512 pub fn expand_morphs(&mut self) {
518 self.expand_group_morphs();
519 self.apply_bone_morphs();
520 }
521
522 fn expand_group_morphs(&mut self) {
527 let spans = self.model.group_morph_spans();
528 let offsets = self.model.group_morph_offsets();
529 if spans.is_empty() || offsets.is_empty() {
530 return;
531 }
532 let mc = self.model.morph_count() as usize;
533 self.morph_scratch.expanded_weights.clear();
534 self.morph_scratch
535 .expanded_weights
536 .extend_from_slice(&self.pose.morph_weights()[..mc]);
537
538 for (morph_idx, &w) in self.pose.morph_weights()[..mc].iter().enumerate() {
539 if w == 0.0 {
540 continue;
541 }
542 expand_group_morph_weight(
543 morph_idx,
544 w,
545 spans,
546 offsets,
547 &mut self.morph_scratch.expanded_weights,
548 );
549 }
550 for (i, &w) in self.morph_scratch.expanded_weights.iter().enumerate() {
551 self.pose.set_morph_weight(MorphIndex(i as u32), w);
552 }
553 }
554
555 fn apply_bone_morphs(&mut self) {
558 let spans = self.model.bone_morph_spans();
559 let offsets = self.model.bone_morph_offsets();
560 if spans.is_empty() || offsets.is_empty() {
561 return;
562 }
563 for (morph_idx, span) in spans.iter().enumerate() {
564 let weight = self.pose.morph_weight(MorphIndex(morph_idx as u32));
565 if weight == 0.0 {
566 continue;
567 }
568 for i in span.start..span.start + span.count {
569 let off = &offsets[i as usize];
570 let pos = self.pose.local_position_offset(off.target_bone);
571 self.pose
572 .set_local_position_offset(off.target_bone, pos + off.position_offset * weight);
573 let rot = self.pose.local_rotation(off.target_bone);
574 let scaled = Quat::IDENTITY.slerp(off.rotation_offset, weight);
575 self.pose
576 .set_local_rotation(off.target_bone, (rot * scaled).normalize());
577 }
578 }
579 }
580
581 #[inline]
582 pub fn world_matrices(&self) -> &[Mat4] {
583 self.pose.world_matrices()
584 }
585
586 #[cfg(test)]
587 fn reset_world_matrix_bone_update_count(&mut self) {
588 self.world_matrix_bone_update_count = 0;
589 }
590
591 #[cfg(test)]
592 fn world_matrix_bone_update_count(&self) -> usize {
593 self.world_matrix_bone_update_count
594 }
595
596 #[inline]
597 pub fn skinning_matrices(&self) -> &[Mat4] {
598 self.pose.skinning_matrices()
599 }
600
601 #[inline]
602 pub fn morph_weights(&self) -> &[f32] {
603 self.pose.morph_weights()
604 }
605
606 pub fn reset_ik_runtime_stats(&mut self) {
607 for stats in &mut self.ik_stats {
608 stats.reset();
609 }
610 }
611
612 pub fn ik_runtime_stats(&self) -> &[IkSolverRuntimeStats] {
613 &self.ik_stats
614 }
615
616 #[inline]
617 pub fn ik_enabled(&self) -> &[u8] {
618 self.pose.ik_enabled()
619 }
620}
621
622fn expand_group_morph_weight(
623 morph_idx: usize,
624 weight: f32,
625 spans: &[crate::MorphOffsetSpan],
626 offsets: &[crate::GroupMorphOffset],
627 expanded_weights: &mut [f32],
628) {
629 let span = spans[morph_idx];
630 for i in span.start..span.start + span.count {
631 let off = &offsets[i as usize];
632 let child = off.child_morph.as_usize();
633 let contribution = weight * off.ratio;
634 expanded_weights[child] += contribution;
635 if spans[child].count > 0 {
636 expand_group_morph_weight(child, contribution, spans, offsets, expanded_weights);
637 }
638 }
639}
640
641fn translation(matrix: Mat4) -> Vec3A {
642 Vec3A::from_vec4(matrix.w_axis)
643}
644
645fn rotation(matrix: Mat4) -> Quat {
646 matrix.to_scale_rotation_translation().1
647}
648
649fn constrain_rotation_to_axis(rotation: Quat, axis: Vec3A) -> Quat {
650 let axis = axis.normalize();
651 let vector = Vec3A::new(rotation.x, rotation.y, rotation.z);
652 let projected = axis * vector.dot(axis);
653 let twist = Quat::from_xyzw(projected.x, projected.y, projected.z, rotation.w);
654 if twist.length_squared() <= f32::EPSILON {
655 Quat::IDENTITY
656 } else {
657 twist.normalize()
658 }
659}
660
661#[derive(Debug)]
662struct ChainLinkState {
663 previous_euler: [f32; 3],
664 plane_mode_angle: f32,
665}
666
667fn get_single_axis_limit(limit: Option<crate::IkAngleLimit>) -> Option<usize> {
668 let limit = limit?;
669 let has = [
670 limit.min.x != 0.0 || limit.max.x != 0.0,
671 limit.min.y != 0.0 || limit.max.y != 0.0,
672 limit.min.z != 0.0 || limit.max.z != 0.0,
673 ];
674 if has[0]
675 && limit.min.y == 0.0
676 && limit.max.y == 0.0
677 && limit.min.z == 0.0
678 && limit.max.z == 0.0
679 {
680 return Some(0);
681 }
682 if has[1]
683 && limit.min.x == 0.0
684 && limit.max.x == 0.0
685 && limit.min.z == 0.0
686 && limit.max.z == 0.0
687 {
688 return Some(1);
689 }
690 if has[2]
691 && limit.min.x == 0.0
692 && limit.max.x == 0.0
693 && limit.min.y == 0.0
694 && limit.max.y == 0.0
695 {
696 return Some(2);
697 }
698 None
699}
700
701fn clamp_limited_rotation(
702 rotation: Quat,
703 limits: crate::IkAngleLimit,
704 state: &mut ChainLinkState,
705 limit_angle: f32,
706) -> Quat {
707 let mat = quat_to_rotation_mat3(rotation);
708 let euler = decompose_euler_xyz(&mat, &state.previous_euler);
709 let clamped: [f32; 3] = [
710 euler[0].clamp(limits.min.x, limits.max.x),
711 euler[1].clamp(limits.min.y, limits.max.y),
712 euler[2].clamp(limits.min.z, limits.max.z),
713 ];
714 let mut limited_step: [f32; 3] = [0.0; 3];
715 for i in 0..3 {
716 let delta = clamped[i] - state.previous_euler[i];
717 limited_step[i] = if limit_angle > 0.0 {
718 delta.clamp(-limit_angle, limit_angle) + state.previous_euler[i]
719 } else {
720 clamped[i]
721 };
722 }
723 state.previous_euler = limited_step;
724 euler_xyz_to_quat(&limited_step)
725}
726
727fn quat_to_rotation_mat3(rotation: Quat) -> [f32; 9] {
728 let [x, y, z, w] = rotation.normalize().to_array();
729 let x2 = x + x;
730 let y2 = y + y;
731 let z2 = z + z;
732 let xx = x * x2;
733 let xy = x * y2;
734 let xz = x * z2;
735 let yy = y * y2;
736 let yz = y * z2;
737 let zz = z * z2;
738 let wx = w * x2;
739 let wy = w * y2;
740 let wz = w * z2;
741 [
742 1.0 - (yy + zz),
743 xy + wz,
744 xz - wy,
745 xy - wz,
746 1.0 - (xx + zz),
747 yz + wx,
748 xz + wy,
749 yz - wx,
750 1.0 - (xx + yy),
751 ]
752}
753
754fn decompose_euler_xyz(mat: &[f32; 9], before: &[f32; 3]) -> [f32; 3] {
755 let sy = -mat[2];
756 let mut result: [f32; 3];
757 if 1.0 - sy.abs() < 1e-6 {
758 let y = sy.asin();
759 let sx = before[0].sin();
760 let sz = before[2].sin();
761 if sx.abs() < sz.abs() {
762 let cx = before[0].cos();
763 result = if cx > 0.0 {
764 [0.0, y, (-mat[3]).asin()]
765 } else {
766 [std::f32::consts::PI, y, mat[3].asin()]
767 };
768 } else {
769 let cz = before[2].cos();
770 result = if cz > 0.0 {
771 [(-mat[7]).asin(), y, 0.0]
772 } else {
773 [mat[7].asin(), y, std::f32::consts::PI]
774 };
775 }
776 } else {
777 result = [mat[5].atan2(mat[8]), (-mat[2]).asin(), mat[1].atan2(mat[0])];
778 }
779
780 let pi = std::f32::consts::PI;
781 let candidates: [[f32; 3]; 8] = [
782 [result[0] + pi, pi - result[1], result[2] + pi],
783 [result[0] + pi, pi - result[1], result[2] - pi],
784 [result[0] + pi, -pi - result[1], result[2] + pi],
785 [result[0] + pi, -pi - result[1], result[2] - pi],
786 [result[0] - pi, pi - result[1], result[2] + pi],
787 [result[0] - pi, pi - result[1], result[2] - pi],
788 [result[0] - pi, -pi - result[1], result[2] + pi],
789 [result[0] - pi, -pi - result[1], result[2] - pi],
790 ];
791 let mut min_error = diff_angle(result[0], before[0]).abs()
792 + diff_angle(result[1], before[1]).abs()
793 + diff_angle(result[2], before[2]).abs();
794 for candidate in &candidates {
795 let error = diff_angle(candidate[0], before[0]).abs()
796 + diff_angle(candidate[1], before[1]).abs()
797 + diff_angle(candidate[2], before[2]).abs();
798 if error < min_error {
799 min_error = error;
800 result = *candidate;
801 }
802 }
803 result
804}
805
806fn diff_angle(a: f32, b: f32) -> f32 {
807 let diff = normalize_angle(a) - normalize_angle(b);
808 if diff > std::f32::consts::PI {
809 diff - std::f32::consts::TAU
810 } else if diff < -std::f32::consts::PI {
811 diff + std::f32::consts::TAU
812 } else {
813 diff
814 }
815}
816
817fn normalize_angle(angle: f32) -> f32 {
818 let mut result = angle;
819 while result >= std::f32::consts::TAU {
820 result -= std::f32::consts::TAU;
821 }
822 while result < 0.0 {
823 result += std::f32::consts::TAU;
824 }
825 result
826}
827
828fn euler_xyz_to_quat(euler: &[f32; 3]) -> Quat {
829 let [x, y, z] = *euler;
830 let c1 = (x / 2.0).cos();
831 let c2 = (y / 2.0).cos();
832 let c3 = (z / 2.0).cos();
833 let s1 = (x / 2.0).sin();
834 let s2 = (y / 2.0).sin();
835 let s3 = (z / 2.0).sin();
836 Quat::from_xyzw(
837 s1 * c2 * c3 + c1 * s2 * s3,
838 c1 * s2 * c3 - s1 * c2 * s3,
839 c1 * c2 * s3 + s1 * s2 * c3,
840 c1 * c2 * c3 - s1 * s2 * s3,
841 )
842}
843
844struct PlaneLinkStepInput<'a> {
845 local_effector: &'a Vec3A,
846 local_target: &'a Vec3A,
847 link_index: usize,
848 base_rotations: &'a [Quat],
849 ik_rotations: &'a mut [Quat],
850 chain_states: &'a mut [ChainLinkState],
851 axis_index: usize,
852 limits: crate::IkAngleLimit,
853 iteration: usize,
854 limit_angle: f32,
855}
856
857fn solve_plane_link_step(input: PlaneLinkStepInput<'_>) {
858 let rotate_axis = match input.axis_index {
859 0 => Vec3A::new(1.0, 0.0, 0.0),
860 1 => Vec3A::new(0.0, 1.0, 0.0),
861 _ => Vec3A::new(0.0, 0.0, 1.0),
862 };
863 let local_eff_n = input.local_effector.normalize();
864 let local_tgt_n = input.local_target.normalize();
865
866 let dot = local_eff_n.dot(local_tgt_n).clamp(-1.0, 1.0);
867 let raw_angle = dot.acos();
868 let capped_angle = if input.limit_angle > 0.0 {
869 raw_angle.min(input.limit_angle)
870 } else {
871 raw_angle
872 };
873
874 let target_vec1 =
875 Quat::from_axis_angle(rotate_axis.into(), capped_angle).mul_vec3a(local_eff_n);
876 let target_vec2 =
877 Quat::from_axis_angle(rotate_axis.into(), -capped_angle).mul_vec3a(local_eff_n);
878 let signed_angle = if target_vec1.dot(local_tgt_n) > target_vec2.dot(local_tgt_n) {
879 capped_angle
880 } else {
881 -capped_angle
882 };
883
884 let state = &mut input.chain_states[input.link_index];
885 let mut next_angle = state.plane_mode_angle + signed_angle;
886 let (lower, upper) = match input.axis_index {
887 0 => (input.limits.min.x, input.limits.max.x),
888 1 => (input.limits.min.y, input.limits.max.y),
889 _ => (input.limits.min.z, input.limits.max.z),
890 };
891 let base = input.base_rotations[input.link_index];
892 let base_mat = quat_to_rotation_mat3(base);
896 let base_euler = decompose_euler_xyz(&base_mat, &[0.0; 3]);
897 let base_axis_angle = base_euler[input.axis_index];
898 let effective_min = lower - base_axis_angle;
899 let effective_max = upper - base_axis_angle;
900
901 if input.iteration == 0 && (next_angle < effective_min || next_angle > effective_max) {
902 if -next_angle > effective_min && -next_angle < effective_max {
903 next_angle = -next_angle;
904 } else {
905 let half = (effective_min + effective_max) * 0.5;
906 if (half - next_angle).abs() > (half + next_angle).abs() {
907 next_angle = -next_angle;
908 }
909 }
910 }
911
912 state.plane_mode_angle = next_angle.clamp(effective_min, effective_max);
913 let ik_adj = Quat::from_axis_angle(rotate_axis.into(), state.plane_mode_angle);
916 input.ik_rotations[input.link_index] = (base * ik_adj * base.inverse()).normalize();
917}
918
919#[cfg(test)]
920mod tests {
921 use std::sync::Arc;
922
923 use glam::{Quat, Vec3A};
924
925 use crate::{
926 AnimationClip, AppendTransformInit, BoneAnimationBinding, BoneIndex, BoneInit,
927 IkAngleLimit, IkLinkInit, IkSolverInit, ModelArena, MovableBoneKeyframe, MovableBoneTrack,
928 RuntimeInstance,
929 };
930
931 fn translation(matrix: glam::Mat4) -> Vec3A {
932 Vec3A::from_vec4(matrix.w_axis)
933 }
934
935 fn assert_vec3a_near(actual: Vec3A, expected: Vec3A) {
936 let delta = (actual - expected).abs();
937 assert!(
938 delta.x < 1.0e-5 && delta.y < 1.0e-5 && delta.z < 1.0e-5,
939 "actual={actual:?} expected={expected:?} delta={delta:?}"
940 );
941 }
942
943 #[test]
944 fn evaluates_rest_pose_world_matrices() {
945 let model = Arc::new(
946 ModelArena::new(vec![
947 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
948 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
949 ])
950 .unwrap(),
951 );
952 let mut runtime = RuntimeInstance::new(model);
953
954 runtime.evaluate_rest_pose();
955
956 assert_vec3a_near(
957 translation(runtime.world_matrices()[0]),
958 Vec3A::new(1.0, 0.0, 0.0),
959 );
960 assert_vec3a_near(
961 translation(runtime.world_matrices()[1]),
962 Vec3A::new(1.0, 2.0, 0.0),
963 );
964 }
965
966 #[test]
967 fn evaluates_current_pose_with_parent_rotation() {
968 let model = Arc::new(
969 ModelArena::new(vec![
970 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
971 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
972 ])
973 .unwrap(),
974 );
975 let mut runtime = RuntimeInstance::new(model);
976
977 runtime.pose_mut().set_local_rotation(
978 BoneIndex(0),
979 Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
980 );
981 runtime.evaluate_current_pose();
982
983 assert_vec3a_near(
984 translation(runtime.world_matrices()[1]),
985 Vec3A::new(-1.0, 0.0, 0.0),
986 );
987 }
988
989 #[test]
990 fn fixed_axis_bone_rotation_keeps_only_axis_twist() {
991 let model = Arc::new(
992 ModelArena::new(vec![
993 BoneInit::new(None, Vec3A::ZERO).with_fixed_axis(Vec3A::Y),
994 BoneInit::new(Some(BoneIndex(0)), Vec3A::X),
995 ])
996 .unwrap(),
997 );
998 let mut runtime = RuntimeInstance::new(model);
999
1000 runtime.pose_mut().set_local_rotation(
1001 BoneIndex(0),
1002 (Quat::from_rotation_y(std::f32::consts::FRAC_PI_2)
1003 * Quat::from_rotation_x(std::f32::consts::FRAC_PI_2))
1004 .normalize(),
1005 );
1006 runtime.evaluate_current_pose();
1007
1008 assert_vec3a_near(
1009 translation(runtime.world_matrices()[1]),
1010 Vec3A::new(0.0, 0.0, -1.0),
1011 );
1012 }
1013
1014 #[test]
1015 fn evaluates_current_pose_with_local_position_offset() {
1016 let model = Arc::new(
1017 ModelArena::new(vec![
1018 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1019 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1020 ])
1021 .unwrap(),
1022 );
1023 let mut runtime = RuntimeInstance::new(model);
1024
1025 runtime
1026 .pose_mut()
1027 .set_local_position_offset(BoneIndex(1), Vec3A::new(0.0, 0.0, 3.0));
1028 runtime.evaluate_current_pose();
1029
1030 assert_vec3a_near(
1031 translation(runtime.world_matrices()[1]),
1032 Vec3A::new(1.0, 2.0, 3.0),
1033 );
1034 }
1035
1036 #[test]
1037 fn evaluates_clip_frame_into_world_matrices() {
1038 let model = Arc::new(
1039 ModelArena::new(vec![
1040 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1041 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1042 ])
1043 .unwrap(),
1044 );
1045 let clip = AnimationClip::new(vec![BoneAnimationBinding {
1046 bone: BoneIndex(1),
1047 track: MovableBoneTrack::from_keyframes(vec![
1048 MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
1049 MovableBoneKeyframe::new(10, Vec3A::new(0.0, 0.0, 4.0), Quat::IDENTITY),
1050 ]),
1051 }]);
1052 let mut runtime = RuntimeInstance::new(model);
1053
1054 runtime.evaluate_clip_frame(&clip, 5.0);
1055
1056 assert_vec3a_near(
1057 translation(runtime.world_matrices()[1]),
1058 Vec3A::new(1.0, 2.0, 2.0),
1059 );
1060 }
1061
1062 #[test]
1063 fn applies_append_rotation_before_world_matrix_output() {
1064 let model = Arc::new(
1065 ModelArena::new_full(
1066 vec![
1067 BoneInit::new(None, Vec3A::ZERO),
1068 BoneInit::new(None, Vec3A::ZERO),
1069 BoneInit::new(Some(BoneIndex(1)), Vec3A::new(1.0, 0.0, 0.0)),
1070 ],
1071 Vec::new(),
1072 vec![AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 1.0).with_rotation()],
1073 )
1074 .unwrap(),
1075 );
1076 let mut runtime = RuntimeInstance::new(model);
1077
1078 runtime.pose_mut().set_local_rotation(
1079 BoneIndex(0),
1080 Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
1081 );
1082 runtime.evaluate_current_pose();
1083
1084 assert_vec3a_near(
1085 translation(runtime.world_matrices()[2]),
1086 Vec3A::new(0.0, 1.0, 0.0),
1087 );
1088 }
1089
1090 #[test]
1091 fn applies_append_translation_before_world_matrix_output() {
1092 let model = Arc::new(
1093 ModelArena::new_full(
1094 vec![
1095 BoneInit::new(None, Vec3A::ZERO),
1096 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1097 ],
1098 Vec::new(),
1099 vec![AppendTransformInit::new(BoneIndex(1), BoneIndex(0), 0.5).with_translation()],
1100 )
1101 .unwrap(),
1102 );
1103 let mut runtime = RuntimeInstance::new(model);
1104
1105 runtime
1106 .pose_mut()
1107 .set_local_position_offset(BoneIndex(0), Vec3A::new(2.0, 0.0, 0.0));
1108 runtime.evaluate_current_pose();
1109
1110 assert_vec3a_near(
1111 translation(runtime.world_matrices()[1]),
1112 Vec3A::new(1.0, 1.0, 0.0),
1113 );
1114 }
1115
1116 #[test]
1117 fn initializes_ik_enabled_from_model_solvers() {
1118 let model = Arc::new(
1119 ModelArena::new_with_ik(
1120 vec![
1121 BoneInit::new(None, Vec3A::ZERO),
1122 BoneInit::new(Some(BoneIndex(0)), Vec3A::ZERO),
1123 ],
1124 vec![IkSolverInit::new(
1125 BoneIndex(1),
1126 BoneIndex(0),
1127 vec![IkLinkInit::new(BoneIndex(0))],
1128 )],
1129 )
1130 .unwrap(),
1131 );
1132
1133 let runtime = RuntimeInstance::new(model);
1134
1135 assert_eq!(runtime.ik_enabled(), &[1]);
1136 }
1137
1138 #[test]
1139 fn solves_one_link_ik_toward_controller_bone() {
1140 let model = Arc::new(
1141 ModelArena::new_with_ik(
1142 vec![
1143 BoneInit::new(None, Vec3A::ZERO),
1144 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1145 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1146 ],
1147 vec![IkSolverInit {
1148 ik_bone: BoneIndex(2),
1149 target_bone: BoneIndex(1),
1150 links: vec![IkLinkInit::new(BoneIndex(0))],
1151 iteration_count: 1,
1152 limit_angle: 0.0,
1153 }],
1154 )
1155 .unwrap(),
1156 );
1157 let mut runtime = RuntimeInstance::new(model);
1158
1159 runtime.evaluate_current_pose();
1160
1161 assert_vec3a_near(
1162 translation(runtime.world_matrices()[1]),
1163 Vec3A::new(0.0, 1.0, 0.0),
1164 );
1165 }
1166
1167 #[test]
1168 fn skips_disabled_ik_solver() {
1169 let model = Arc::new(
1170 ModelArena::new_with_ik(
1171 vec![
1172 BoneInit::new(None, Vec3A::ZERO),
1173 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1174 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1175 ],
1176 vec![IkSolverInit {
1177 ik_bone: BoneIndex(2),
1178 target_bone: BoneIndex(1),
1179 links: vec![IkLinkInit::new(BoneIndex(0))],
1180 iteration_count: 1,
1181 limit_angle: 0.0,
1182 }],
1183 )
1184 .unwrap(),
1185 );
1186 let mut runtime = RuntimeInstance::new(model);
1187
1188 runtime.pose_mut().set_ik_enabled(0, false);
1189 runtime.evaluate_current_pose();
1190
1191 assert_vec3a_near(
1192 translation(runtime.world_matrices()[1]),
1193 Vec3A::new(1.0, 0.0, 0.0),
1194 );
1195 }
1196
1197 #[test]
1198 fn solves_two_link_ik_chain_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(Some(BoneIndex(1)), Vec3A::new(1.0, 0.0, 0.0)),
1205 BoneInit::new(None, Vec3A::new(1.0, 1.0, 0.0)),
1206 ],
1207 vec![IkSolverInit {
1208 ik_bone: BoneIndex(3),
1209 target_bone: BoneIndex(2),
1210 links: vec![IkLinkInit::new(BoneIndex(1)), IkLinkInit::new(BoneIndex(0))],
1211 iteration_count: 4,
1212 limit_angle: 0.0,
1213 }],
1214 )
1215 .unwrap(),
1216 );
1217 let mut runtime = RuntimeInstance::new(model);
1218
1219 runtime.evaluate_current_pose();
1220
1221 assert_vec3a_near(
1222 translation(runtime.world_matrices()[2]),
1223 Vec3A::new(1.0, 1.0, 0.0),
1224 );
1225 }
1226
1227 #[test]
1228 fn ik_updates_only_affected_eval_suffix_for_late_chain() {
1229 let unrelated_count = 96usize;
1230 let chain_root = BoneIndex(unrelated_count as u32);
1231 let chain_mid = BoneIndex(unrelated_count as u32 + 1);
1232 let chain_tip = BoneIndex(unrelated_count as u32 + 2);
1233 let controller = BoneIndex(unrelated_count as u32 + 3);
1234
1235 let mut bones = Vec::new();
1236 for i in 0..unrelated_count {
1237 bones.push(BoneInit::new(None, Vec3A::new(i as f32 * 10.0, -10.0, 0.0)));
1238 }
1239 bones.push(BoneInit::new(None, Vec3A::ZERO));
1240 bones.push(BoneInit::new(Some(chain_root), Vec3A::new(1.0, 0.0, 0.0)));
1241 bones.push(BoneInit::new(Some(chain_mid), Vec3A::new(1.0, 0.0, 0.0)));
1242 bones.push(BoneInit::new(None, Vec3A::new(1.0, 1.0, 0.0)));
1243
1244 let model = Arc::new(
1245 ModelArena::new_with_ik(
1246 bones,
1247 vec![IkSolverInit {
1248 ik_bone: controller,
1249 target_bone: chain_tip,
1250 links: vec![IkLinkInit::new(chain_mid), IkLinkInit::new(chain_root)],
1251 iteration_count: 4,
1252 limit_angle: 0.0,
1253 }],
1254 )
1255 .unwrap(),
1256 );
1257 let mut runtime = RuntimeInstance::new(model);
1258
1259 runtime.reset_world_matrix_bone_update_count();
1260 runtime.evaluate_current_pose();
1261
1262 assert_vec3a_near(
1263 translation(runtime.world_matrices()[chain_tip.as_usize()]),
1264 Vec3A::new(1.0, 1.0, 0.0),
1265 );
1266 assert!(
1267 runtime.world_matrix_bone_update_count() < 250,
1268 "IK should not recompute unrelated prefix bones repeatedly; updated {} bones",
1269 runtime.world_matrix_bone_update_count()
1270 );
1271 }
1272
1273 #[test]
1274 fn clamps_ik_rotation_by_solver_limit_angle() {
1275 let model = Arc::new(
1276 ModelArena::new_with_ik(
1277 vec![
1278 BoneInit::new(None, Vec3A::ZERO),
1279 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1280 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1281 ],
1282 vec![IkSolverInit {
1283 ik_bone: BoneIndex(2),
1284 target_bone: BoneIndex(1),
1285 links: vec![IkLinkInit::new(BoneIndex(0))],
1286 iteration_count: 1,
1287 limit_angle: std::f32::consts::FRAC_PI_4,
1288 }],
1289 )
1290 .unwrap(),
1291 );
1292 let mut runtime = RuntimeInstance::new(model);
1293
1294 runtime.evaluate_current_pose();
1295
1296 let expected = Vec3A::new(
1297 std::f32::consts::FRAC_1_SQRT_2,
1298 std::f32::consts::FRAC_1_SQRT_2,
1299 0.0,
1300 );
1301 assert_vec3a_near(translation(runtime.world_matrices()[1]), expected);
1302 }
1303
1304 #[test]
1305 fn applies_constant_limit_angle_per_iteration() {
1306 let model = Arc::new(
1307 ModelArena::new_with_ik(
1308 vec![
1309 BoneInit::new(None, Vec3A::ZERO),
1310 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1311 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1312 ],
1313 vec![IkSolverInit {
1314 ik_bone: BoneIndex(2),
1315 target_bone: BoneIndex(1),
1316 links: vec![IkLinkInit::new(BoneIndex(1)), IkLinkInit::new(BoneIndex(0))],
1317 iteration_count: 1,
1318 limit_angle: std::f32::consts::FRAC_PI_4,
1319 }],
1320 )
1321 .unwrap(),
1322 );
1323 let mut runtime = RuntimeInstance::new(model);
1324
1325 runtime.evaluate_current_pose();
1326
1327 let expected = Vec3A::new(
1331 std::f32::consts::FRAC_1_SQRT_2,
1332 std::f32::consts::FRAC_1_SQRT_2,
1333 0.0,
1334 );
1335 assert_vec3a_near(translation(runtime.world_matrices()[1]), expected);
1336 }
1337
1338 #[test]
1339 fn clip_frame_produces_deterministic_world_translations() {
1340 let model = Arc::new(
1341 ModelArena::new(vec![
1342 BoneInit::new(None, Vec3A::new(1.0, 0.0, 0.0)),
1343 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 2.0, 0.0)),
1344 ])
1345 .unwrap(),
1346 );
1347 let clip = AnimationClip::new(vec![BoneAnimationBinding {
1348 bone: BoneIndex(1),
1349 track: MovableBoneTrack::from_keyframes(vec![
1350 MovableBoneKeyframe::new(0, Vec3A::ZERO, Quat::IDENTITY),
1351 MovableBoneKeyframe::new(10, Vec3A::new(0.0, 0.0, 4.0), Quat::IDENTITY),
1352 ]),
1353 }]);
1354 let mut runtime = RuntimeInstance::new(model);
1355
1356 runtime.evaluate_clip_frame(&clip, 5.0);
1357
1358 let matrices = runtime.world_matrices();
1359 assert_eq!(matrices.len(), 2);
1360 assert_vec3a_near(translation(matrices[0]), Vec3A::new(1.0, 0.0, 0.0));
1361 assert_vec3a_near(translation(matrices[1]), Vec3A::new(1.0, 2.0, 2.0));
1362 }
1363
1364 #[test]
1365 fn evaluate_clip_frame_without_ik_leaves_ik_unsolved() {
1366 let model = Arc::new(
1367 ModelArena::new_with_ik(
1368 vec![
1369 BoneInit::new(None, Vec3A::ZERO),
1370 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1371 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1372 ],
1373 vec![IkSolverInit {
1374 ik_bone: BoneIndex(2),
1375 target_bone: BoneIndex(1),
1376 links: vec![IkLinkInit::new(BoneIndex(0))],
1377 iteration_count: 1,
1378 limit_angle: 0.0,
1379 }],
1380 )
1381 .unwrap(),
1382 );
1383 let clip = AnimationClip::new(vec![]);
1384
1385 let mut without_ik = RuntimeInstance::new(Arc::clone(&model));
1386 let mut with_ik = RuntimeInstance::new(model);
1387
1388 without_ik.evaluate_clip_frame_without_ik(&clip, 0.0);
1389 with_ik.evaluate_clip_frame(&clip, 0.0);
1390
1391 assert_vec3a_near(
1393 translation(without_ik.world_matrices()[1]),
1394 Vec3A::new(1.0, 0.0, 0.0),
1395 );
1396 assert_vec3a_near(
1398 translation(with_ik.world_matrices()[1]),
1399 Vec3A::new(0.0, 1.0, 0.0),
1400 );
1401 }
1402
1403 #[test]
1404 fn ik_options_cap_configured_iterations() {
1405 let model = Arc::new(
1406 ModelArena::new_with_ik(
1407 vec![
1408 BoneInit::new(None, Vec3A::ZERO),
1409 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1410 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1411 ],
1412 vec![IkSolverInit {
1413 ik_bone: BoneIndex(2),
1414 target_bone: BoneIndex(1),
1415 links: vec![IkLinkInit::new(BoneIndex(0))],
1416 iteration_count: 100,
1417 limit_angle: 0.0,
1418 }],
1419 )
1420 .unwrap(),
1421 );
1422 let mut runtime = RuntimeInstance::new(model);
1423
1424 runtime.reset_ik_runtime_stats();
1425 runtime.evaluate_current_pose_with_ik_options(super::IkSolveOptions {
1426 tolerance: 0.0,
1427 max_iterations_cap: Some(5),
1428 });
1429
1430 assert_eq!(runtime.ik_runtime_stats()[0].configured_iterations, 5);
1431 }
1432
1433 fn assert_near(actual: f32, expected: f32) {
1436 let delta = (actual - expected).abs();
1437 assert!(
1438 delta < 1.0e-5,
1439 "actual={actual:?} expected={expected:?} delta={delta:?}"
1440 );
1441 }
1442
1443 #[test]
1444 fn bone_morph_position_offset_drives_world_position() {
1445 let model = Arc::new(
1446 ModelArena::new_with_morphs(
1447 vec![
1448 BoneInit::new(None, Vec3A::ZERO),
1449 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1450 ],
1451 Vec::new(),
1452 Vec::new(),
1453 crate::MorphInit {
1454 morph_count: 1,
1455 bone_offsets: vec![crate::BoneMorphOffset {
1456 target_bone: BoneIndex(1),
1457 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1458 rotation_offset: Quat::IDENTITY,
1459 }],
1460 bone_spans: vec![crate::MorphOffsetSpan { start: 0, count: 1 }],
1461 group_offsets: vec![],
1462 group_spans: vec![crate::MorphOffsetSpan::default()],
1463 ..crate::MorphInit::default()
1464 },
1465 )
1466 .unwrap(),
1467 );
1468 let clip = AnimationClip::new_with_morphs(
1469 Vec::new(),
1470 vec![crate::MorphAnimationBinding {
1471 morph: crate::MorphIndex(0),
1472 track: crate::MorphTrack::from_keyframes(vec![
1473 crate::MorphKeyframe::new(0, 0.0),
1474 crate::MorphKeyframe::new(10, 1.0),
1475 ]),
1476 }],
1477 );
1478 let mut runtime = RuntimeInstance::new_with_morph_count(model, 1);
1479
1480 runtime.evaluate_clip_frame(&clip, 5.0);
1481
1482 assert_vec3a_near(
1484 translation(runtime.world_matrices()[1]),
1485 Vec3A::new(0.0, 1.0, 1.0),
1486 );
1487 assert_near(runtime.morph_weights()[0], 0.5);
1488 }
1489
1490 #[test]
1491 fn bone_morph_rotation_offset_affects_child_position() {
1492 let model = Arc::new(
1493 ModelArena::new_with_morphs(
1494 vec![
1495 BoneInit::new(None, Vec3A::ZERO),
1496 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1497 BoneInit::new(Some(BoneIndex(1)), Vec3A::new(1.0, 0.0, 0.0)),
1498 ],
1499 Vec::new(),
1500 Vec::new(),
1501 crate::MorphInit {
1502 morph_count: 1,
1503 bone_offsets: vec![crate::BoneMorphOffset {
1504 target_bone: BoneIndex(1),
1505 position_offset: Vec3A::ZERO,
1506 rotation_offset: Quat::from_rotation_z(std::f32::consts::FRAC_PI_2),
1507 }],
1508 bone_spans: vec![crate::MorphOffsetSpan { start: 0, count: 1 }],
1509 group_offsets: vec![],
1510 group_spans: vec![crate::MorphOffsetSpan::default()],
1511 ..crate::MorphInit::default()
1512 },
1513 )
1514 .unwrap(),
1515 );
1516 let clip = AnimationClip::new_with_morphs(
1517 Vec::new(),
1518 vec![crate::MorphAnimationBinding {
1519 morph: crate::MorphIndex(0),
1520 track: crate::MorphTrack::from_keyframes(vec![
1521 crate::MorphKeyframe::new(0, 0.0),
1522 crate::MorphKeyframe::new(10, 1.0),
1523 ]),
1524 }],
1525 );
1526 let mut runtime = RuntimeInstance::new_with_morph_count(model, 1);
1527
1528 runtime.evaluate_clip_frame(&clip, 10.0);
1529
1530 assert_vec3a_near(
1533 translation(runtime.world_matrices()[2]),
1534 Vec3A::new(1.0, 1.0, 0.0),
1535 );
1536 }
1537
1538 #[test]
1539 fn group_morph_contributes_to_bone_morph_weight() {
1540 let model = Arc::new(
1543 ModelArena::new_with_morphs(
1544 vec![
1545 BoneInit::new(None, Vec3A::ZERO),
1546 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1547 ],
1548 Vec::new(),
1549 Vec::new(),
1550 crate::MorphInit {
1551 morph_count: 2,
1552 bone_offsets: vec![crate::BoneMorphOffset {
1553 target_bone: BoneIndex(1),
1554 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1555 rotation_offset: Quat::IDENTITY,
1556 }],
1557 bone_spans: vec![
1558 crate::MorphOffsetSpan { start: 0, count: 1 },
1559 crate::MorphOffsetSpan::default(),
1560 ],
1561 group_offsets: vec![crate::GroupMorphOffset {
1562 child_morph: crate::MorphIndex(0),
1563 ratio: 0.5,
1564 }],
1565 group_spans: vec![
1566 crate::MorphOffsetSpan::default(),
1567 crate::MorphOffsetSpan { start: 0, count: 1 },
1568 ],
1569 ..crate::MorphInit::default()
1570 },
1571 )
1572 .unwrap(),
1573 );
1574 let clip = AnimationClip::new_with_morphs(
1576 Vec::new(),
1577 vec![crate::MorphAnimationBinding {
1578 morph: crate::MorphIndex(1),
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, 2);
1586
1587 runtime.evaluate_clip_frame(&clip, 10.0);
1588
1589 assert_near(runtime.morph_weights()[0], 0.5);
1592 assert_near(runtime.morph_weights()[1], 1.0);
1593 assert_vec3a_near(
1594 translation(runtime.world_matrices()[1]),
1595 Vec3A::new(0.0, 1.0, 1.0),
1596 );
1597 }
1598
1599 #[test]
1600 fn group_morph_can_reference_later_child_morph() {
1601 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::default(),
1618 crate::MorphOffsetSpan { start: 0, count: 1 },
1619 ],
1620 group_offsets: vec![crate::GroupMorphOffset {
1621 child_morph: crate::MorphIndex(1),
1622 ratio: 0.5,
1623 }],
1624 group_spans: vec![
1625 crate::MorphOffsetSpan { start: 0, count: 1 },
1626 crate::MorphOffsetSpan::default(),
1627 ],
1628 ..crate::MorphInit::default()
1629 },
1630 )
1631 .unwrap(),
1632 );
1633 let clip = AnimationClip::new_with_morphs(
1634 Vec::new(),
1635 vec![crate::MorphAnimationBinding {
1636 morph: crate::MorphIndex(0),
1637 track: crate::MorphTrack::from_keyframes(vec![crate::MorphKeyframe::new(0, 1.0)]),
1638 }],
1639 );
1640 let mut runtime = RuntimeInstance::new(model);
1641
1642 runtime.evaluate_clip_frame(&clip, 0.0);
1643
1644 assert_near(runtime.morph_weights()[0], 1.0);
1645 assert_near(runtime.morph_weights()[1], 0.5);
1646 assert_vec3a_near(
1647 translation(runtime.world_matrices()[1]),
1648 Vec3A::new(0.0, 1.0, 1.0),
1649 );
1650 }
1651
1652 #[test]
1653 fn chained_group_morphs_descend_to_bone_morph_weight() {
1654 let model = Arc::new(
1655 ModelArena::new_with_morphs(
1656 vec![
1657 BoneInit::new(None, Vec3A::ZERO),
1658 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1659 ],
1660 Vec::new(),
1661 Vec::new(),
1662 crate::MorphInit {
1663 morph_count: 3,
1664 bone_offsets: vec![crate::BoneMorphOffset {
1665 target_bone: BoneIndex(1),
1666 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1667 rotation_offset: Quat::IDENTITY,
1668 }],
1669 bone_spans: vec![
1670 crate::MorphOffsetSpan { start: 0, count: 1 },
1671 crate::MorphOffsetSpan::default(),
1672 crate::MorphOffsetSpan::default(),
1673 ],
1674 group_offsets: vec![
1675 crate::GroupMorphOffset {
1676 child_morph: crate::MorphIndex(0),
1677 ratio: 0.25,
1678 },
1679 crate::GroupMorphOffset {
1680 child_morph: crate::MorphIndex(1),
1681 ratio: 0.5,
1682 },
1683 ],
1684 group_spans: vec![
1685 crate::MorphOffsetSpan::default(),
1686 crate::MorphOffsetSpan { start: 0, count: 1 },
1687 crate::MorphOffsetSpan { start: 1, count: 1 },
1688 ],
1689 ..crate::MorphInit::default()
1690 },
1691 )
1692 .unwrap(),
1693 );
1694 let clip = AnimationClip::new_with_morphs(
1695 Vec::new(),
1696 vec![crate::MorphAnimationBinding {
1697 morph: crate::MorphIndex(2),
1698 track: crate::MorphTrack::from_keyframes(vec![crate::MorphKeyframe::new(0, 1.0)]),
1699 }],
1700 );
1701 let mut runtime = RuntimeInstance::new(model);
1702
1703 runtime.evaluate_clip_frame(&clip, 0.0);
1704
1705 assert_near(runtime.morph_weights()[2], 1.0);
1706 assert_near(runtime.morph_weights()[1], 0.5);
1707 assert_near(runtime.morph_weights()[0], 0.125);
1708 assert_vec3a_near(
1709 translation(runtime.world_matrices()[1]),
1710 Vec3A::new(0.0, 1.0, 0.25),
1711 );
1712 }
1713
1714 #[test]
1715 fn expand_morphs_noop_when_no_morph_defs() {
1716 let model = Arc::new(ModelArena::new(vec![BoneInit::new(None, Vec3A::ZERO)]).unwrap());
1717 let mut runtime = RuntimeInstance::new_with_morph_count(model, 1);
1718 runtime
1719 .pose_mut()
1720 .set_morph_weight(crate::MorphIndex(0), 1.0);
1721 runtime.expand_morphs();
1722 assert_near(runtime.morph_weights()[0], 1.0);
1724 }
1725
1726 #[test]
1727 fn clamps_link_local_rotation_to_angle_limit() {
1728 let model = Arc::new(
1729 ModelArena::new_with_ik(
1730 vec![
1731 BoneInit::new(None, Vec3A::ZERO),
1732 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1733 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1734 ],
1735 vec![IkSolverInit {
1736 ik_bone: BoneIndex(2),
1737 target_bone: BoneIndex(1),
1738 links: vec![
1739 IkLinkInit::new(BoneIndex(0)).with_angle_limit(IkAngleLimit::new(
1740 Vec3A::new(0.0, 0.0, 0.0),
1741 Vec3A::new(0.0, 0.0, std::f32::consts::FRAC_PI_4),
1742 )),
1743 ],
1744 iteration_count: 1,
1745 limit_angle: 0.0,
1746 }],
1747 )
1748 .unwrap(),
1749 );
1750 let mut runtime = RuntimeInstance::new(model);
1751
1752 runtime.evaluate_current_pose();
1753
1754 let expected = Vec3A::new(
1755 std::f32::consts::FRAC_1_SQRT_2,
1756 std::f32::consts::FRAC_1_SQRT_2,
1757 0.0,
1758 );
1759 assert_vec3a_near(translation(runtime.world_matrices()[1]), expected);
1760 }
1761
1762 #[test]
1763 fn plane_link_step_preserves_base_rotation() {
1764 let base = Quat::from_rotation_x(0.3);
1765 let base_rotations = vec![base];
1766 let mut ik_rotations = vec![Quat::IDENTITY];
1767 let mut chain_states = vec![super::ChainLinkState {
1768 previous_euler: [0.0; 3],
1769 plane_mode_angle: 0.0,
1770 }];
1771 let local_effector = Vec3A::X;
1772 let local_target = Vec3A::Y;
1773
1774 super::solve_plane_link_step(super::PlaneLinkStepInput {
1775 local_effector: &local_effector,
1776 local_target: &local_target,
1777 link_index: 0,
1778 base_rotations: &base_rotations,
1779 ik_rotations: &mut ik_rotations,
1780 chain_states: &mut chain_states,
1781 axis_index: 2,
1782 limits: IkAngleLimit::new(
1783 Vec3A::new(-std::f32::consts::PI, 0.0, -std::f32::consts::PI),
1784 Vec3A::new(std::f32::consts::PI, 0.0, std::f32::consts::PI),
1785 ),
1786 iteration: 0,
1787 limit_angle: 0.0,
1788 });
1789
1790 let effective = (ik_rotations[0] * base_rotations[0]).normalize();
1791 assert_vec3a_near(effective.mul_vec3a(Vec3A::Z), base.mul_vec3a(Vec3A::Z));
1792 }
1793
1794 #[test]
1795 fn append_rotation_propagates_post_ik_link_rotation() {
1796 let model = Arc::new(
1797 ModelArena::new_full(
1798 vec![
1799 BoneInit::new(None, Vec3A::ZERO),
1800 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1801 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1802 BoneInit::new(None, Vec3A::ZERO),
1803 BoneInit::new(Some(BoneIndex(3)), Vec3A::new(1.0, 0.0, 0.0)),
1804 ],
1805 vec![IkSolverInit {
1806 ik_bone: BoneIndex(2),
1807 target_bone: BoneIndex(1),
1808 links: vec![IkLinkInit::new(BoneIndex(0))],
1809 iteration_count: 1,
1810 limit_angle: 0.0,
1811 }],
1812 vec![AppendTransformInit::new(BoneIndex(3), BoneIndex(0), 1.0).with_rotation()],
1813 )
1814 .unwrap(),
1815 );
1816 let mut runtime = RuntimeInstance::new(model);
1817
1818 runtime.evaluate_current_pose();
1819
1820 assert_vec3a_near(
1821 translation(runtime.world_matrices()[4]),
1822 Vec3A::new(0.0, 1.0, 0.0),
1823 );
1824 }
1825
1826 #[test]
1827 fn scratch_ik_capacities_stable_after_repeated_evaluate() {
1828 let model = Arc::new(
1829 ModelArena::new_with_ik(
1830 vec![
1831 BoneInit::new(None, Vec3A::ZERO),
1832 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(1.0, 0.0, 0.0)),
1833 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1834 BoneInit::new(None, Vec3A::ZERO),
1835 BoneInit::new(Some(BoneIndex(3)), Vec3A::new(1.0, 0.0, 0.0)),
1836 BoneInit::new(Some(BoneIndex(4)), Vec3A::new(1.0, 0.0, 0.0)),
1837 BoneInit::new(None, Vec3A::new(0.0, 1.0, 0.0)),
1838 ],
1839 vec![
1840 IkSolverInit {
1841 ik_bone: BoneIndex(2),
1842 target_bone: BoneIndex(1),
1843 links: vec![IkLinkInit::new(BoneIndex(0))],
1844 iteration_count: 1,
1845 limit_angle: 0.0,
1846 },
1847 IkSolverInit {
1848 ik_bone: BoneIndex(6),
1849 target_bone: BoneIndex(5),
1850 links: vec![IkLinkInit::new(BoneIndex(3)), IkLinkInit::new(BoneIndex(4))],
1851 iteration_count: 1,
1852 limit_angle: 0.0,
1853 },
1854 ],
1855 )
1856 .unwrap(),
1857 );
1858 let mut runtime = RuntimeInstance::new(model);
1859
1860 runtime.evaluate_current_pose();
1861
1862 let cap_links = runtime.ik_scratch.links.capacity();
1863 let cap_base = runtime.ik_scratch.base_rotations.capacity();
1864 let cap_ik = runtime.ik_scratch.ik_rotations.capacity();
1865 let cap_best = runtime.ik_scratch.best_ik_rotations.capacity();
1866 let cap_chain = runtime.ik_scratch.chain_states.capacity();
1867
1868 for _ in 0..10 {
1869 runtime.evaluate_current_pose();
1870 }
1871
1872 assert_eq!(runtime.ik_scratch.links.capacity(), cap_links);
1873 assert_eq!(runtime.ik_scratch.base_rotations.capacity(), cap_base);
1874 assert_eq!(runtime.ik_scratch.ik_rotations.capacity(), cap_ik);
1875 assert_eq!(runtime.ik_scratch.best_ik_rotations.capacity(), cap_best);
1876 assert_eq!(runtime.ik_scratch.chain_states.capacity(), cap_chain);
1877 }
1878
1879 #[test]
1880 fn scratch_morph_capacity_stable_after_repeated_clip_frame() {
1881 let model = Arc::new(
1882 ModelArena::new_with_morphs(
1883 vec![
1884 BoneInit::new(None, Vec3A::ZERO),
1885 BoneInit::new(Some(BoneIndex(0)), Vec3A::new(0.0, 1.0, 0.0)),
1886 ],
1887 Vec::new(),
1888 Vec::new(),
1889 crate::MorphInit {
1890 morph_count: 2,
1891 bone_offsets: vec![crate::BoneMorphOffset {
1892 target_bone: BoneIndex(1),
1893 position_offset: Vec3A::new(0.0, 0.0, 2.0),
1894 rotation_offset: Quat::IDENTITY,
1895 }],
1896 bone_spans: vec![
1897 crate::MorphOffsetSpan { start: 0, count: 1 },
1898 crate::MorphOffsetSpan::default(),
1899 ],
1900 group_offsets: vec![crate::GroupMorphOffset {
1901 child_morph: crate::MorphIndex(0),
1902 ratio: 0.5,
1903 }],
1904 group_spans: vec![
1905 crate::MorphOffsetSpan::default(),
1906 crate::MorphOffsetSpan { start: 0, count: 1 },
1907 ],
1908 ..crate::MorphInit::default()
1909 },
1910 )
1911 .unwrap(),
1912 );
1913 let clip = AnimationClip::new_with_morphs(
1914 Vec::new(),
1915 vec![crate::MorphAnimationBinding {
1916 morph: crate::MorphIndex(1),
1917 track: crate::MorphTrack::from_keyframes(vec![
1918 crate::MorphKeyframe::new(0, 0.0),
1919 crate::MorphKeyframe::new(10, 1.0),
1920 ]),
1921 }],
1922 );
1923 let mut runtime = RuntimeInstance::new_with_morph_count(model, 2);
1924
1925 runtime.evaluate_clip_frame(&clip, 5.0);
1926
1927 let cap_expanded = runtime.morph_scratch.expanded_weights.capacity();
1928
1929 for _ in 0..10 {
1930 runtime.evaluate_clip_frame(&clip, 5.0);
1931 }
1932
1933 assert_eq!(
1934 runtime.morph_scratch.expanded_weights.capacity(),
1935 cap_expanded
1936 );
1937 }
1938}