Skip to main content

mmd_anim_runtime/
runtime.rs

1use std::sync::Arc;
2
3use glam::Quat;
4
5use crate::ik_primitive::ChainLinkState;
6use crate::{AnimationClip, ModelArena, PoseArena};
7
8mod ik;
9mod morph;
10mod world;
11
12#[cfg(test)]
13use crate::ik_primitive::{
14    LimitedAxesLinkStepInput, PlaneLinkStepInput, axis_vec, decompose_euler_xyz, euler_xyz_to_quat,
15    limit_axis_bounds, quat_to_rotation_mat3, signed_projected_angle, solve_limited_axes_link_step,
16    solve_plane_link_step,
17};
18
19#[derive(Debug)]
20struct IkScratch {
21    links: Vec<crate::IkLink>,
22    base_rotations: Vec<Quat>,
23    base_ik_rotations: Vec<Quat>,
24    ik_rotations: Vec<Quat>,
25    best_ik_rotations: Vec<Quat>,
26    chain_states: Vec<ChainLinkState>,
27}
28
29impl IkScratch {
30    fn new(model: &ModelArena) -> Self {
31        let max_links = model
32            .ik_solvers()
33            .iter()
34            .map(|s| s.links.len())
35            .max()
36            .unwrap_or(0);
37        IkScratch {
38            links: Vec::with_capacity(max_links),
39            base_rotations: Vec::with_capacity(max_links),
40            base_ik_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: 0.0,
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.pose.reset_ik_rotations();
155        self.evaluate_current_pose_ordered(IkSolveOptions::default());
156    }
157
158    pub fn evaluate_current_pose_with_ik_options(&mut self, options: IkSolveOptions) {
159        self.pose.reset_ik_rotations();
160        self.evaluate_current_pose_ordered(options);
161    }
162
163    /// Evaluate the current pose by updating world matrices only, without
164    /// running any IK solver. This is useful for diagnostics that need to
165    /// inspect clip/VMD state before IK is applied.
166    pub fn evaluate_current_pose_without_ik(&mut self) {
167        self.pose.reset_ik_rotations();
168        self.update_world_matrices();
169    }
170
171    fn evaluate_current_pose_ordered(&mut self, options: IkSolveOptions) {
172        self.pose.reset_append_transforms();
173        self.update_world_matrices_using_current_append_from_eval_order_position(0);
174
175        for after_physics in [false, true] {
176            for position in 0..self.model.eval_order().len() {
177                let bone = self.model.eval_order()[position];
178                if self.model.transform_after_physics(bone) != after_physics {
179                    continue;
180                }
181
182                if self.model.append_transform_index(bone).is_some() {
183                    self.pose.reset_append_transform(bone);
184                    self.update_append_transform_for_bone(bone);
185                }
186                self.update_world_matrix_for_bone(bone);
187
188                for ik_index in 0..self.model.ik_count() {
189                    if self.model.ik_solvers()[ik_index].ik_bone == bone {
190                        self.solve_ik_solver(ik_index, options, after_physics);
191                    }
192                }
193            }
194        }
195        self.update_world_matrices_using_current_append_from_eval_order_position(0);
196    }
197
198    pub fn evaluate_rest_pose(&mut self) {
199        self.pose.reset_local_pose();
200        self.evaluate_current_pose();
201    }
202
203    pub fn evaluate_clip_frame(&mut self, clip: &AnimationClip, frame: f32) {
204        clip.apply_to_pose(frame, &mut self.pose);
205        self.expand_morphs();
206        self.evaluate_current_pose();
207    }
208
209    pub fn evaluate_clip_frame_with_ik_options(
210        &mut self,
211        clip: &AnimationClip,
212        frame: f32,
213        options: IkSolveOptions,
214    ) {
215        clip.apply_to_pose(frame, &mut self.pose);
216        self.expand_morphs();
217        self.evaluate_current_pose_with_ik_options(options);
218    }
219
220    /// Evaluate a clip frame but stop before solving IK. Applies the clip to
221    /// the pose, expands morphs, and updates world matrices - the same setup
222    /// as [`Self::evaluate_clip_frame`] but without calling `solve_enabled_ik`.
223    /// Useful for diagnostics that need to inspect pre-IK runtime state.
224    pub fn evaluate_clip_frame_without_ik(&mut self, clip: &AnimationClip, frame: f32) {
225        clip.apply_to_pose(frame, &mut self.pose);
226        self.expand_morphs();
227        self.pose.reset_ik_rotations();
228        self.update_world_matrices();
229    }
230
231    pub fn reset_ik_runtime_stats(&mut self) {
232        for stats in &mut self.ik_stats {
233            stats.reset();
234        }
235    }
236
237    pub fn ik_runtime_stats(&self) -> &[IkSolverRuntimeStats] {
238        &self.ik_stats
239    }
240
241    #[inline]
242    pub fn ik_enabled(&self) -> &[u8] {
243        self.pose.ik_enabled()
244    }
245}
246
247#[cfg(test)]
248mod tests;