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