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 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 MorphScratch {
51    expanded_weights: Vec<f32>,
52}
53
54impl MorphScratch {
55    fn new(morph_count: usize) -> Self {
56        Self {
57            expanded_weights: vec![0.0; morph_count],
58        }
59    }
60}
61
62#[derive(Clone, Copy, Debug, Default, PartialEq)]
63pub struct IkSolverRuntimeStats {
64    pub solver_evaluations: u64,
65    pub configured_iterations: u64,
66    pub executed_iterations: u64,
67    pub tolerance_precheck_breaks: u64,
68    pub tolerance_post_iteration_breaks: u64,
69    pub rollback_breaks: u64,
70    pub max_iteration_exhaustions: u64,
71    pub link_visits: u64,
72    pub link_steps: u64,
73    pub final_distance_sum: f64,
74    pub final_distance_max: f32,
75    pub exhausted_final_distance_sum: f64,
76    pub exhausted_final_distance_max: f32,
77}
78
79impl IkSolverRuntimeStats {
80    fn reset(&mut self) {
81        *self = Self::default();
82    }
83}
84
85#[derive(Clone, Copy, Debug, PartialEq)]
86pub struct IkSolveOptions {
87    pub tolerance: f32,
88    pub max_iterations_cap: Option<u32>,
89}
90
91pub use physics::{PhysicsMode, PhysicsStepStats, PhysicsTickConfig};
92
93impl Default for IkSolveOptions {
94    fn default() -> Self {
95        Self {
96            tolerance: 0.0,
97            max_iterations_cap: None,
98        }
99    }
100}
101
102#[cfg(test)]
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
104pub(super) enum WorldMatrixBoneUpdateCategory {
105    LeadingBookend,
106    PhaseLoop,
107    TrailingBookend,
108    IkLinkChange,
109    #[default]
110    Other,
111}
112
113#[derive(Debug)]
114pub struct RuntimeInstance {
115    model: Arc<ModelArena>,
116    pose: PoseArena,
117    physics_mode: PhysicsMode,
118    physics_tick_config: PhysicsTickConfig,
119    physics_accumulator_seconds: f32,
120    ik_scratch: IkScratch,
121    morph_scratch: MorphScratch,
122    ik_stats: Vec<IkSolverRuntimeStats>,
123    ik_link_change_update_bones: Vec<Option<Vec<crate::BoneIndex>>>,
124    #[cfg(test)]
125    world_matrix_bone_update_count: usize,
126    #[cfg(test)]
127    world_matrix_bone_update_category: WorldMatrixBoneUpdateCategory,
128    #[cfg(test)]
129    world_matrix_bone_update_leading_bookend_count: usize,
130    #[cfg(test)]
131    world_matrix_bone_update_phase_loop_count: usize,
132    #[cfg(test)]
133    world_matrix_bone_update_trailing_bookend_count: usize,
134    #[cfg(test)]
135    world_matrix_bone_update_ik_link_change_count: usize,
136    #[cfg(test)]
137    world_matrix_bone_update_other_count: usize,
138}
139
140impl RuntimeInstance {
141    pub fn new(model: Arc<ModelArena>) -> Self {
142        let morph_count = model.morph_count() as usize;
143        Self::new_with_morph_count(model, morph_count)
144    }
145
146    pub fn new_with_morph_count(model: Arc<ModelArena>, morph_count: usize) -> Self {
147        let ik_count = model.ik_count();
148        Self::new_with_counts(model, morph_count, ik_count)
149    }
150
151    pub fn new_with_counts(model: Arc<ModelArena>, morph_count: usize, ik_count: usize) -> Self {
152        let morph_count = morph_count.max(model.morph_count() as usize);
153        let pose = PoseArena::new_with_counts(model.bone_count(), morph_count, ik_count);
154        let ik_scratch = IkScratch::new(&model);
155        let morph_scratch = MorphScratch::new(morph_count);
156        let ik_stats = vec![IkSolverRuntimeStats::default(); model.ik_count()];
157        let ik_link_change_update_bones = vec![None; model.ik_count()];
158        Self {
159            model,
160            pose,
161            physics_mode: PhysicsMode::default(),
162            physics_tick_config: PhysicsTickConfig::default(),
163            physics_accumulator_seconds: 0.0,
164            ik_scratch,
165            morph_scratch,
166            ik_stats,
167            ik_link_change_update_bones,
168            #[cfg(test)]
169            world_matrix_bone_update_count: 0,
170            #[cfg(test)]
171            world_matrix_bone_update_category: WorldMatrixBoneUpdateCategory::default(),
172            #[cfg(test)]
173            world_matrix_bone_update_leading_bookend_count: 0,
174            #[cfg(test)]
175            world_matrix_bone_update_phase_loop_count: 0,
176            #[cfg(test)]
177            world_matrix_bone_update_trailing_bookend_count: 0,
178            #[cfg(test)]
179            world_matrix_bone_update_ik_link_change_count: 0,
180            #[cfg(test)]
181            world_matrix_bone_update_other_count: 0,
182        }
183    }
184
185    #[inline]
186    pub fn model(&self) -> &ModelArena {
187        &self.model
188    }
189
190    #[inline]
191    pub fn pose(&self) -> &PoseArena {
192        &self.pose
193    }
194
195    #[inline]
196    pub fn pose_mut(&mut self) -> &mut PoseArena {
197        &mut self.pose
198    }
199
200    pub fn evaluate_current_pose(&mut self) {
201        self.pose.reset_ik_rotations();
202        self.evaluate_current_pose_ordered(IkSolveOptions::default());
203    }
204
205    pub fn evaluate_current_pose_with_ik_options(&mut self, options: IkSolveOptions) {
206        self.pose.reset_ik_rotations();
207        self.evaluate_current_pose_ordered(options);
208    }
209
210    /// Evaluate the current pose by updating world matrices only, without
211    /// running any IK solver. This is useful for diagnostics that need to
212    /// inspect clip/VMD state before IK is applied.
213    pub fn evaluate_current_pose_without_ik(&mut self) {
214        self.pose.reset_ik_rotations();
215        self.update_world_matrices();
216    }
217
218    fn evaluate_current_pose_ordered(&mut self, options: IkSolveOptions) {
219        self.begin_current_pose_evaluation();
220        let mut earliest_after_physics_eval_order_position = None;
221        self.evaluate_current_pose_phase(
222            false,
223            options,
224            &mut earliest_after_physics_eval_order_position,
225        );
226        self.evaluate_current_pose_phase(
227            true,
228            options,
229            &mut earliest_after_physics_eval_order_position,
230        );
231        self.finish_current_pose_evaluation(earliest_after_physics_eval_order_position);
232    }
233
234    pub fn evaluate_current_pose_before_physics(&mut self) {
235        self.evaluate_current_pose_before_physics_with_ik_options(IkSolveOptions::default());
236    }
237
238    pub fn evaluate_current_pose_before_physics_with_ik_options(
239        &mut self,
240        options: IkSolveOptions,
241    ) {
242        self.pose.reset_ik_rotations();
243        self.begin_current_pose_evaluation();
244        let mut earliest_after_physics_eval_order_position = None;
245        self.evaluate_current_pose_phase(
246            false,
247            options,
248            &mut earliest_after_physics_eval_order_position,
249        );
250    }
251
252    pub fn evaluate_current_pose_after_physics(&mut self) {
253        self.evaluate_current_pose_after_physics_with_ik_options(IkSolveOptions::default());
254    }
255
256    pub fn evaluate_current_pose_after_physics_with_ik_options(&mut self, options: IkSolveOptions) {
257        let mut earliest_after_physics_eval_order_position = None;
258        self.evaluate_current_pose_phase(
259            true,
260            options,
261            &mut earliest_after_physics_eval_order_position,
262        );
263        self.finish_current_pose_evaluation(earliest_after_physics_eval_order_position);
264    }
265
266    fn begin_current_pose_evaluation(&mut self) {
267        self.pose.reset_append_transforms();
268        #[cfg(test)]
269        self.set_world_matrix_bone_update_category(WorldMatrixBoneUpdateCategory::LeadingBookend);
270        self.update_world_matrices_using_current_append_from_eval_order_position(0);
271    }
272
273    fn evaluate_current_pose_phase(
274        &mut self,
275        after_physics: bool,
276        options: IkSolveOptions,
277        earliest_after_physics_eval_order_position: &mut Option<usize>,
278    ) {
279        let phase_bone_count = self.model.eval_order_for_phase(after_physics).len();
280        for phase_index in 0..phase_bone_count {
281            let bone = self.model.eval_order_for_phase(after_physics)[phase_index];
282            if after_physics {
283                let position = self.model.eval_order_position(bone);
284                *earliest_after_physics_eval_order_position = Some(
285                    (*earliest_after_physics_eval_order_position)
286                        .map_or(position, |earliest| earliest.min(position)),
287                );
288            }
289            if self.model.append_transform_index(bone).is_some() {
290                self.pose.reset_append_transform(bone);
291                self.update_append_transform_for_bone(bone);
292            }
293            #[cfg(test)]
294            self.set_world_matrix_bone_update_category(WorldMatrixBoneUpdateCategory::PhaseLoop);
295            self.update_world_matrix_for_bone(bone);
296
297            let ik_solver_count = self.model.ik_solver_count_for_bone(bone);
298            for local_index in 0..ik_solver_count {
299                let ik_index = self.model.ik_solver_index_for_bone(bone, local_index);
300                self.solve_ik_solver(ik_index, options, after_physics);
301            }
302        }
303    }
304
305    fn finish_current_pose_evaluation(
306        &mut self,
307        earliest_after_physics_eval_order_position: Option<usize>,
308    ) {
309        let mut trailing_refresh_start = earliest_after_physics_eval_order_position;
310        for append in self.model.append_transforms() {
311            let source_position = self.model.eval_order_position(append.source_bone);
312            let target_position = self.model.eval_order_position(append.target_bone);
313            if target_position < source_position {
314                trailing_refresh_start = Some(
315                    trailing_refresh_start
316                        .map_or(target_position, |start| start.min(target_position)),
317                );
318            }
319        }
320
321        if let Some(start_position) = trailing_refresh_start {
322            let start_position =
323                self.expand_update_start_for_append_dependencies(start_position, None);
324            #[cfg(test)]
325            self.set_world_matrix_bone_update_category(
326                WorldMatrixBoneUpdateCategory::TrailingBookend,
327            );
328            self.update_world_matrices_from_eval_order_position(start_position);
329        }
330    }
331
332    pub fn evaluate_rest_pose(&mut self) {
333        self.pose.reset_local_pose();
334        self.evaluate_current_pose();
335    }
336
337    pub fn evaluate_clip_frame(&mut self, clip: &AnimationClip, frame: f32) {
338        clip.apply_to_pose(frame, &mut self.pose);
339        self.expand_morphs();
340        self.evaluate_current_pose();
341    }
342
343    pub fn evaluate_clip_frame_with_ik_options(
344        &mut self,
345        clip: &AnimationClip,
346        frame: f32,
347        options: IkSolveOptions,
348    ) {
349        clip.apply_to_pose(frame, &mut self.pose);
350        self.expand_morphs();
351        self.evaluate_current_pose_with_ik_options(options);
352    }
353
354    pub fn evaluate_clip_frame_before_physics(&mut self, clip: &AnimationClip, frame: f32) {
355        self.evaluate_clip_frame_before_physics_with_ik_options(
356            clip,
357            frame,
358            IkSolveOptions::default(),
359        );
360    }
361
362    pub fn evaluate_clip_frame_before_physics_with_ik_options(
363        &mut self,
364        clip: &AnimationClip,
365        frame: f32,
366        options: IkSolveOptions,
367    ) {
368        clip.apply_to_pose(frame, &mut self.pose);
369        self.expand_morphs();
370        self.evaluate_current_pose_before_physics_with_ik_options(options);
371    }
372
373    /// Evaluate a clip frame but stop before solving IK. Applies the clip to
374    /// the pose, expands morphs, and updates world matrices - the same setup
375    /// as [`Self::evaluate_clip_frame`] but without calling `solve_enabled_ik`.
376    /// Useful for diagnostics that need to inspect pre-IK runtime state.
377    pub fn evaluate_clip_frame_without_ik(&mut self, clip: &AnimationClip, frame: f32) {
378        clip.apply_to_pose(frame, &mut self.pose);
379        self.expand_morphs();
380        self.pose.reset_ik_rotations();
381        self.update_world_matrices();
382    }
383
384    pub fn reset_ik_runtime_stats(&mut self) {
385        for stats in &mut self.ik_stats {
386            stats.reset();
387        }
388    }
389
390    pub fn ik_runtime_stats(&self) -> &[IkSolverRuntimeStats] {
391        &self.ik_stats
392    }
393
394    #[inline]
395    pub fn append_position_offset(&self, bone: crate::BoneIndex) -> glam::Vec3A {
396        self.pose.append_position_offset(bone)
397    }
398
399    #[inline]
400    pub fn append_rotation(&self, bone: crate::BoneIndex) -> glam::Quat {
401        self.pose.append_rotation(bone)
402    }
403
404    #[inline]
405    pub fn ik_enabled(&self) -> &[u8] {
406        self.pose.ik_enabled()
407    }
408}
409
410#[cfg(test)]
411mod tests;