Skip to main content

mmd_anim_runtime/
runtime.rs

1use std::sync::Arc;
2
3use glam::{Quat, Vec3A};
4
5use crate::ik_primitive::ChainLinkState;
6use crate::{AnimationClip, ModelArena, PoseArena};
7
8mod ik;
9mod morph;
10mod physics;
11mod world;
12
13#[cfg(test)]
14use crate::ik_primitive::{
15    LimitedAxesLinkStepInput, PlaneLinkStepInput, axis_vec, decompose_euler_xyz, euler_xyz_to_quat,
16    limit_axis_bounds, quat_to_rotation_mat3, signed_projected_angle, solve_limited_axes_link_step,
17    solve_plane_link_step,
18};
19
20#[derive(Debug)]
21struct IkScratch {
22    links: Vec<crate::IkLink>,
23    base_rotations: Vec<Quat>,
24    base_ik_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            base_ik_rotations: Vec::with_capacity(max_links),
42            ik_rotations: Vec::with_capacity(max_links),
43            best_ik_rotations: Vec::with_capacity(max_links),
44            chain_states: Vec::with_capacity(max_links),
45        }
46    }
47}
48
49#[derive(Debug)]
50struct GroupMorphFrame {
51    morph_idx: usize,
52    weight: f32,
53    next_offset: u32,
54}
55
56#[derive(Debug)]
57struct MorphScratch {
58    expanded_weights: Vec<f32>,
59    group_stack: Vec<GroupMorphFrame>,
60}
61
62impl MorphScratch {
63    fn new(morph_count: usize) -> Self {
64        Self {
65            expanded_weights: vec![0.0; morph_count],
66            // Grow to the graph depth on first use, then reuse that allocation.
67            // Reserving morph_count here would penalize models with many
68            // non-group morphs and every additional runtime instance.
69            group_stack: Vec::new(),
70        }
71    }
72}
73
74#[derive(Clone, Copy, Debug, Default, PartialEq)]
75pub struct IkSolverRuntimeStats {
76    pub solver_evaluations: u64,
77    pub configured_iterations: u64,
78    pub executed_iterations: u64,
79    pub tolerance_precheck_breaks: u64,
80    pub tolerance_post_iteration_breaks: u64,
81    pub rollback_breaks: u64,
82    pub max_iteration_exhaustions: u64,
83    pub link_visits: u64,
84    pub link_steps: u64,
85    pub final_distance_sum: f64,
86    pub final_distance_max: f32,
87    pub exhausted_final_distance_sum: f64,
88    pub exhausted_final_distance_max: f32,
89}
90
91impl IkSolverRuntimeStats {
92    fn reset(&mut self) {
93        *self = Self::default();
94    }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct IkSolveOptions {
99    /// Goal distance at or below which the solver stops. Defaults to `1e-4` model units.
100    pub tolerance: f32,
101    /// Optional per-chain iteration cap. Defaults to the iteration count stored in the model.
102    pub max_iterations_cap: Option<u32>,
103}
104
105pub use physics::{PhysicsMode, PhysicsStepStats, PhysicsTickConfig};
106
107impl Default for IkSolveOptions {
108    fn default() -> Self {
109        Self {
110            // A zero tolerance effectively disables convergence breaks for ordinary floating-point
111            // poses. This matches the TypeScript runtime while leaving slow-converging chains free
112            // to use the full iteration count authored in the model.
113            tolerance: 1.0e-4,
114            max_iterations_cap: None,
115        }
116    }
117}
118
119#[cfg(test)]
120#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
121pub(super) enum WorldMatrixBoneUpdateCategory {
122    LeadingBookend,
123    PhaseLoop,
124    TrailingBookend,
125    IkLinkChange,
126    #[default]
127    Other,
128}
129
130#[derive(Clone, Copy, Debug)]
131pub struct HostPoseView<'a> {
132    pub local_position_offsets: &'a [Vec3A],
133    pub local_rotations: &'a [Quat],
134    pub local_scales: &'a [Vec3A],
135    pub morph_weights: &'a [f32],
136    pub ik_enabled: &'a [u8],
137}
138
139#[derive(Clone, Debug, PartialEq, Eq)]
140pub enum HostPoseError {
141    BoneCountMismatch { expected: usize, got: usize },
142    MorphCountMismatch { expected: usize, got: usize },
143    IkCountMismatch { expected: usize, got: usize },
144    NonFiniteValue { field: &'static str, index: usize },
145    NonNormalizedQuaternion { index: usize },
146}
147
148impl std::fmt::Display for HostPoseError {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self {
151            HostPoseError::BoneCountMismatch { expected, got } => {
152                write!(f, "bone count mismatch: expected {expected}, got {got}")
153            }
154            HostPoseError::MorphCountMismatch { expected, got } => {
155                write!(f, "morph count mismatch: expected {expected}, got {got}")
156            }
157            HostPoseError::IkCountMismatch { expected, got } => {
158                write!(f, "ik count mismatch: expected {expected}, got {got}")
159            }
160            HostPoseError::NonFiniteValue { field, index } => {
161                write!(f, "non-finite value in {field} at index {index}")
162            }
163            Self::NonNormalizedQuaternion { index } => {
164                write!(f, "non-normalized quaternion at local_rotations[{index}]")
165            }
166        }
167    }
168}
169
170impl std::error::Error for HostPoseError {}
171
172#[derive(Debug)]
173pub struct RuntimeInstance {
174    model: Arc<ModelArena>,
175    pose: PoseArena,
176    physics_mode: PhysicsMode,
177    physics_tick_config: PhysicsTickConfig,
178    physics_accumulator_seconds: f32,
179    ik_scratch: IkScratch,
180    morph_scratch: MorphScratch,
181    ik_stats: Vec<IkSolverRuntimeStats>,
182    ik_link_change_update_bones: Vec<Option<Vec<crate::BoneIndex>>>,
183    #[cfg(test)]
184    world_matrix_bone_update_count: usize,
185    #[cfg(test)]
186    world_matrix_bone_update_category: WorldMatrixBoneUpdateCategory,
187    #[cfg(test)]
188    world_matrix_bone_update_leading_bookend_count: usize,
189    #[cfg(test)]
190    world_matrix_bone_update_phase_loop_count: usize,
191    #[cfg(test)]
192    world_matrix_bone_update_trailing_bookend_count: usize,
193    #[cfg(test)]
194    world_matrix_bone_update_ik_link_change_count: usize,
195    #[cfg(test)]
196    world_matrix_bone_update_other_count: usize,
197}
198
199impl RuntimeInstance {
200    pub fn new(model: Arc<ModelArena>) -> Self {
201        let morph_count = model.morph_count() as usize;
202        Self::new_with_morph_count(model, morph_count)
203    }
204
205    pub fn new_with_morph_count(model: Arc<ModelArena>, morph_count: usize) -> Self {
206        let ik_count = model.ik_count();
207        Self::new_with_counts(model, morph_count, ik_count)
208    }
209
210    pub fn new_with_counts(model: Arc<ModelArena>, morph_count: usize, ik_count: usize) -> Self {
211        let morph_count = morph_count.max(model.morph_count() as usize);
212        let ik_count = ik_count.max(model.ik_count());
213        let pose = PoseArena::new_with_counts(model.bone_count(), morph_count, ik_count);
214        let ik_scratch = IkScratch::new(&model);
215        let morph_scratch = MorphScratch::new(morph_count);
216        let ik_stats = vec![IkSolverRuntimeStats::default(); model.ik_count()];
217        let ik_link_change_update_bones = vec![None; model.ik_count()];
218        Self {
219            model,
220            pose,
221            physics_mode: PhysicsMode::default(),
222            physics_tick_config: PhysicsTickConfig::default(),
223            physics_accumulator_seconds: 0.0,
224            ik_scratch,
225            morph_scratch,
226            ik_stats,
227            ik_link_change_update_bones,
228            #[cfg(test)]
229            world_matrix_bone_update_count: 0,
230            #[cfg(test)]
231            world_matrix_bone_update_category: WorldMatrixBoneUpdateCategory::default(),
232            #[cfg(test)]
233            world_matrix_bone_update_leading_bookend_count: 0,
234            #[cfg(test)]
235            world_matrix_bone_update_phase_loop_count: 0,
236            #[cfg(test)]
237            world_matrix_bone_update_trailing_bookend_count: 0,
238            #[cfg(test)]
239            world_matrix_bone_update_ik_link_change_count: 0,
240            #[cfg(test)]
241            world_matrix_bone_update_other_count: 0,
242        }
243    }
244
245    #[inline]
246    pub fn model(&self) -> &ModelArena {
247        &self.model
248    }
249
250    #[inline]
251    pub fn pose(&self) -> &PoseArena {
252        &self.pose
253    }
254
255    #[inline]
256    pub fn pose_mut(&mut self) -> &mut PoseArena {
257        &mut self.pose
258    }
259
260    pub fn evaluate_current_pose(&mut self) {
261        self.pose.reset_ik_rotations();
262        self.evaluate_current_pose_ordered(IkSolveOptions::default());
263    }
264
265    pub fn evaluate_current_pose_with_ik_options(&mut self, options: IkSolveOptions) {
266        self.pose.reset_ik_rotations();
267        self.evaluate_current_pose_ordered(options);
268    }
269
270    /// Evaluate the current pose by updating world matrices only, without
271    /// running any IK solver. This is useful for diagnostics that need to
272    /// inspect clip/VMD state before IK is applied.
273    pub fn evaluate_current_pose_without_ik(&mut self) {
274        self.pose.reset_ik_rotations();
275        self.update_world_matrices();
276    }
277
278    fn evaluate_current_pose_ordered(&mut self, options: IkSolveOptions) {
279        self.begin_current_pose_evaluation();
280        let mut earliest_after_physics_eval_order_position = None;
281        self.evaluate_current_pose_phase(
282            false,
283            options,
284            &mut earliest_after_physics_eval_order_position,
285        );
286        self.evaluate_current_pose_phase(
287            true,
288            options,
289            &mut earliest_after_physics_eval_order_position,
290        );
291        self.finish_current_pose_evaluation(earliest_after_physics_eval_order_position);
292    }
293
294    pub fn evaluate_current_pose_before_physics(&mut self) {
295        self.evaluate_current_pose_before_physics_with_ik_options(IkSolveOptions::default());
296    }
297
298    pub fn evaluate_current_pose_before_physics_with_ik_options(
299        &mut self,
300        options: IkSolveOptions,
301    ) {
302        self.pose.reset_ik_rotations();
303        self.begin_current_pose_evaluation();
304        let mut earliest_after_physics_eval_order_position = None;
305        self.evaluate_current_pose_phase(
306            false,
307            options,
308            &mut earliest_after_physics_eval_order_position,
309        );
310    }
311
312    pub fn evaluate_current_pose_after_physics(&mut self) {
313        self.evaluate_current_pose_after_physics_with_ik_options(IkSolveOptions::default());
314    }
315
316    pub fn evaluate_current_pose_after_physics_with_ik_options(&mut self, options: IkSolveOptions) {
317        let mut earliest_after_physics_eval_order_position = None;
318        self.evaluate_current_pose_phase(
319            true,
320            options,
321            &mut earliest_after_physics_eval_order_position,
322        );
323        self.finish_current_pose_evaluation(earliest_after_physics_eval_order_position);
324    }
325
326    fn begin_current_pose_evaluation(&mut self) {
327        self.pose.reset_append_transforms();
328        #[cfg(test)]
329        self.set_world_matrix_bone_update_category(WorldMatrixBoneUpdateCategory::LeadingBookend);
330        self.update_world_matrices_using_current_append_from_eval_order_position(0);
331    }
332
333    fn evaluate_current_pose_phase(
334        &mut self,
335        after_physics: bool,
336        options: IkSolveOptions,
337        earliest_after_physics_eval_order_position: &mut Option<usize>,
338    ) {
339        let phase_bone_count = self.model.eval_order_for_phase(after_physics).len();
340        for phase_index in 0..phase_bone_count {
341            let bone = self.model.eval_order_for_phase(after_physics)[phase_index];
342            if after_physics {
343                let position = self.model.eval_order_position(bone);
344                *earliest_after_physics_eval_order_position = Some(
345                    (*earliest_after_physics_eval_order_position)
346                        .map_or(position, |earliest| earliest.min(position)),
347                );
348            }
349            if self.model.append_transform_index(bone).is_some() {
350                self.pose.reset_append_transform(bone);
351                self.update_append_transform_for_bone(bone);
352            }
353            #[cfg(test)]
354            self.set_world_matrix_bone_update_category(WorldMatrixBoneUpdateCategory::PhaseLoop);
355            self.update_world_matrix_for_bone(bone);
356
357            let ik_solver_count = self.model.ik_solver_count_for_bone(bone);
358            for local_index in 0..ik_solver_count {
359                let ik_index = self.model.ik_solver_index_for_bone(bone, local_index);
360                self.solve_ik_solver(ik_index, options, after_physics);
361            }
362        }
363    }
364
365    fn finish_current_pose_evaluation(
366        &mut self,
367        earliest_after_physics_eval_order_position: Option<usize>,
368    ) {
369        let mut trailing_refresh_start = earliest_after_physics_eval_order_position;
370        for append in self.model.append_transforms() {
371            let source_position = self.model.eval_order_position(append.source_bone);
372            let target_position = self.model.eval_order_position(append.target_bone);
373            if target_position < source_position {
374                trailing_refresh_start = Some(
375                    trailing_refresh_start
376                        .map_or(target_position, |start| start.min(target_position)),
377                );
378            }
379        }
380
381        if let Some(start_position) = trailing_refresh_start {
382            let start_position =
383                self.expand_update_start_for_append_dependencies(start_position, None);
384            #[cfg(test)]
385            self.set_world_matrix_bone_update_category(
386                WorldMatrixBoneUpdateCategory::TrailingBookend,
387            );
388            self.update_world_matrices_from_eval_order_position(start_position);
389        }
390    }
391
392    pub fn evaluate_rest_pose(&mut self) {
393        self.pose.reset_local_pose();
394        self.evaluate_current_pose();
395    }
396
397    /// Apply a complete host-owned pose and expand its morphs.
398    ///
399    /// The supplied local offsets are the base pose. Group morph weights are
400    /// expanded and bone-morph offsets are applied after all input validation
401    /// and pose slices have been copied, matching clip evaluation semantics.
402    pub fn apply_host_pose(&mut self, view: &HostPoseView) -> Result<(), HostPoseError> {
403        let bone_count = self.model.bone_count();
404        let morph_count = self.pose.morph_weights().len();
405        let ik_count = self.pose.ik_enabled().len();
406
407        if view.local_position_offsets.len() != bone_count {
408            return Err(HostPoseError::BoneCountMismatch {
409                expected: bone_count,
410                got: view.local_position_offsets.len(),
411            });
412        }
413        if view.local_rotations.len() != bone_count {
414            return Err(HostPoseError::BoneCountMismatch {
415                expected: bone_count,
416                got: view.local_rotations.len(),
417            });
418        }
419        if view.local_scales.len() != bone_count {
420            return Err(HostPoseError::BoneCountMismatch {
421                expected: bone_count,
422                got: view.local_scales.len(),
423            });
424        }
425        if view.morph_weights.len() != morph_count {
426            return Err(HostPoseError::MorphCountMismatch {
427                expected: morph_count,
428                got: view.morph_weights.len(),
429            });
430        }
431        if view.ik_enabled.len() != ik_count {
432            return Err(HostPoseError::IkCountMismatch {
433                expected: ik_count,
434                got: view.ik_enabled.len(),
435            });
436        }
437
438        for (i, v) in view.local_position_offsets.iter().enumerate() {
439            if !v.is_finite() {
440                return Err(HostPoseError::NonFiniteValue {
441                    field: "local_position_offsets",
442                    index: i,
443                });
444            }
445        }
446        for (i, q) in view.local_rotations.iter().enumerate() {
447            if !q.is_finite() {
448                return Err(HostPoseError::NonFiniteValue {
449                    field: "local_rotations",
450                    index: i,
451                });
452            }
453            if (q.length_squared() - 1.0).abs() > 1e-3 {
454                return Err(HostPoseError::NonNormalizedQuaternion { index: i });
455            }
456        }
457        for (i, v) in view.local_scales.iter().enumerate() {
458            if !v.is_finite() {
459                return Err(HostPoseError::NonFiniteValue {
460                    field: "local_scales",
461                    index: i,
462                });
463            }
464        }
465        for (i, w) in view.morph_weights.iter().enumerate() {
466            if !w.is_finite() {
467                return Err(HostPoseError::NonFiniteValue {
468                    field: "morph_weights",
469                    index: i,
470                });
471            }
472        }
473
474        self.pose
475            .set_local_position_offsets_from_slice(view.local_position_offsets);
476        self.pose
477            .set_local_rotations_from_slice(view.local_rotations);
478        self.pose.set_local_scales_from_slice(view.local_scales);
479        self.pose.set_morph_weights_from_slice(view.morph_weights);
480        self.pose.set_ik_enabled_from_slice(view.ik_enabled);
481        self.expand_morphs();
482
483        Ok(())
484    }
485
486    pub fn evaluate_clip_frame(&mut self, clip: &AnimationClip, frame: f32) {
487        clip.apply_to_pose(frame, &mut self.pose);
488        self.expand_morphs();
489        self.evaluate_current_pose();
490    }
491
492    pub fn evaluate_clip_frame_with_ik_options(
493        &mut self,
494        clip: &AnimationClip,
495        frame: f32,
496        options: IkSolveOptions,
497    ) {
498        clip.apply_to_pose(frame, &mut self.pose);
499        self.expand_morphs();
500        self.evaluate_current_pose_with_ik_options(options);
501    }
502
503    pub fn evaluate_clip_frame_before_physics(&mut self, clip: &AnimationClip, frame: f32) {
504        self.evaluate_clip_frame_before_physics_with_ik_options(
505            clip,
506            frame,
507            IkSolveOptions::default(),
508        );
509    }
510
511    pub fn evaluate_clip_frame_before_physics_with_ik_options(
512        &mut self,
513        clip: &AnimationClip,
514        frame: f32,
515        options: IkSolveOptions,
516    ) {
517        clip.apply_to_pose(frame, &mut self.pose);
518        self.expand_morphs();
519        self.evaluate_current_pose_before_physics_with_ik_options(options);
520    }
521
522    /// Evaluate a clip frame but stop before solving IK. Applies the clip to
523    /// the pose, expands morphs, and updates world matrices - the same setup
524    /// as [`Self::evaluate_clip_frame`] but without calling `solve_enabled_ik`.
525    /// Useful for diagnostics that need to inspect pre-IK runtime state.
526    pub fn evaluate_clip_frame_without_ik(&mut self, clip: &AnimationClip, frame: f32) {
527        clip.apply_to_pose(frame, &mut self.pose);
528        self.expand_morphs();
529        self.pose.reset_ik_rotations();
530        self.update_world_matrices();
531    }
532
533    pub fn reset_ik_runtime_stats(&mut self) {
534        for stats in &mut self.ik_stats {
535            stats.reset();
536        }
537    }
538
539    pub fn ik_runtime_stats(&self) -> &[IkSolverRuntimeStats] {
540        &self.ik_stats
541    }
542
543    #[inline]
544    pub fn append_position_offset(&self, bone: crate::BoneIndex) -> glam::Vec3A {
545        self.pose.append_position_offset(bone)
546    }
547
548    #[inline]
549    pub fn append_rotation(&self, bone: crate::BoneIndex) -> glam::Quat {
550        self.pose.append_rotation(bone)
551    }
552
553    #[inline]
554    pub fn ik_enabled(&self) -> &[u8] {
555        self.pose.ik_enabled()
556    }
557}
558
559#[cfg(test)]
560mod tests;