Skip to main content

mmd_anim_runtime/reduce/
mod.rs

1use std::cmp::Ordering;
2use std::time::Duration;
3#[cfg(not(target_family = "wasm"))]
4use std::time::Instant;
5
6use glam::{EulerRot, Mat3, Mat4, Quat, Vec3, Vec3A};
7#[cfg(not(target_family = "wasm"))]
8use rayon::prelude::*;
9#[cfg(not(target_family = "wasm"))]
10use rayon::{ThreadPool, ThreadPoolBuilder};
11use thiserror::Error;
12
13use crate::{BoneIndex, InterpolationScalar, ModelArena};
14
15const AFFINE_EPSILON: f32 = 1.0e-4;
16// Below this point the one-time prefit costs more than the global passes it removes.
17const DCC_LOCAL_PREFIT_MIN_FRAMES: usize = 90;
18
19#[cfg(not(target_family = "wasm"))]
20fn reduction_timer_start() -> Instant {
21    Instant::now()
22}
23
24#[cfg(target_family = "wasm")]
25fn reduction_timer_start() {}
26
27#[cfg(not(target_family = "wasm"))]
28fn reduction_timer_elapsed(started: Instant) -> Duration {
29    started.elapsed()
30}
31
32#[cfg(target_family = "wasm")]
33fn reduction_timer_elapsed(_: ()) -> Duration {
34    Duration::ZERO
35}
36
37#[derive(Debug, Clone, Copy)]
38pub struct DensePoseSequenceView<'a> {
39    world_matrices: &'a [Mat4],
40    morph_weights: &'a [f32],
41    frame_count: usize,
42    bone_count: usize,
43    morph_count: usize,
44    start_frame: f32,
45    frame_step: f32,
46}
47
48impl<'a> DensePoseSequenceView<'a> {
49    pub fn new(
50        world_matrices: &'a [Mat4],
51        morph_weights: &'a [f32],
52        frame_count: usize,
53        bone_count: usize,
54        morph_count: usize,
55        start_frame: f32,
56        frame_step: f32,
57    ) -> Result<Self, PoseReductionError> {
58        if frame_count == 0 {
59            return Err(PoseReductionError::EmptySequence);
60        }
61        if bone_count == 0 {
62            return Err(PoseReductionError::EmptySkeleton);
63        }
64        if !start_frame.is_finite() || !frame_step.is_finite() || frame_step <= 0.0 {
65            return Err(PoseReductionError::InvalidTimeBase);
66        }
67        let mut previous_frame = start_frame;
68        for sample_index in 1..frame_count {
69            let frame = start_frame + sample_index as f32 * frame_step;
70            if !frame.is_finite() || frame <= previous_frame {
71                return Err(PoseReductionError::InvalidTimeBase);
72            }
73            previous_frame = frame;
74        }
75        let expected_world = frame_count
76            .checked_mul(bone_count)
77            .ok_or(PoseReductionError::LengthOverflow)?;
78        let expected_morph = frame_count
79            .checked_mul(morph_count)
80            .ok_or(PoseReductionError::LengthOverflow)?;
81        if world_matrices.len() != expected_world {
82            return Err(PoseReductionError::InvalidWorldMatrixCount {
83                actual: world_matrices.len(),
84                expected: expected_world,
85            });
86        }
87        if morph_weights.len() != expected_morph {
88            return Err(PoseReductionError::InvalidMorphWeightCount {
89                actual: morph_weights.len(),
90                expected: expected_morph,
91            });
92        }
93        Ok(Self {
94            world_matrices,
95            morph_weights,
96            frame_count,
97            bone_count,
98            morph_count,
99            start_frame,
100            frame_step,
101        })
102    }
103
104    pub fn frame_count(&self) -> usize {
105        self.frame_count
106    }
107    pub fn bone_count(&self) -> usize {
108        self.bone_count
109    }
110    pub fn morph_count(&self) -> usize {
111        self.morph_count
112    }
113    pub fn start_frame(&self) -> f32 {
114        self.start_frame
115    }
116    pub fn frame_step(&self) -> f32 {
117        self.frame_step
118    }
119
120    fn world_matrix(&self, frame: usize, bone: usize) -> Mat4 {
121        self.world_matrices[frame * self.bone_count + bone]
122    }
123
124    fn morph_weight(&self, frame: usize, morph: usize) -> f32 {
125        self.morph_weights[frame * self.morph_count + morph]
126    }
127
128    fn sample_frame(&self, sample_index: usize) -> f32 {
129        self.start_frame + sample_index as f32 * self.frame_step
130    }
131}
132
133#[derive(Debug, Clone, PartialEq)]
134pub struct SkeletonSnapshot {
135    parent_indices: Box<[i32]>,
136    rest_local_translations: Box<[Vec3A]>,
137    rest_local_rotations: Box<[Quat]>,
138    evaluation_order: Box<[usize]>,
139    morph_count: usize,
140    model_identity: u64,
141}
142
143impl SkeletonSnapshot {
144    pub fn new(
145        parent_indices: Vec<i32>,
146        rest_local_translations: Vec<Vec3A>,
147        rest_local_rotations: Vec<Quat>,
148        morph_count: usize,
149        model_identity: u64,
150    ) -> Result<Self, PoseReductionError> {
151        if parent_indices.is_empty() {
152            return Err(PoseReductionError::EmptySkeleton);
153        }
154        if rest_local_translations.len() != parent_indices.len()
155            || rest_local_rotations.len() != parent_indices.len()
156        {
157            return Err(PoseReductionError::InvalidSkeletonLengths);
158        }
159        for (bone, &parent) in parent_indices.iter().enumerate() {
160            if parent < -1 || parent >= parent_indices.len() as i32 || parent == bone as i32 {
161                return Err(PoseReductionError::InvalidParent { bone, parent });
162            }
163            if !rest_local_translations[bone].is_finite()
164                || !quat_is_finite(rest_local_rotations[bone])
165            {
166                return Err(PoseReductionError::NonFiniteSkeleton { bone });
167            }
168        }
169        let evaluation_order = build_evaluation_order(&parent_indices)?;
170        Ok(Self {
171            parent_indices: parent_indices.into_boxed_slice(),
172            rest_local_translations: rest_local_translations.into_boxed_slice(),
173            rest_local_rotations: rest_local_rotations
174                .into_iter()
175                .map(normalize_quat)
176                .collect::<Vec<_>>()
177                .into_boxed_slice(),
178            evaluation_order: evaluation_order.into_boxed_slice(),
179            morph_count,
180            model_identity,
181        })
182    }
183
184    pub fn from_model(model: &ModelArena, model_identity: u64) -> Result<Self, PoseReductionError> {
185        Self::from_model_with_morph_count(model, model_identity, model.morph_count() as usize)
186    }
187
188    pub fn from_model_with_morph_count(
189        model: &ModelArena,
190        model_identity: u64,
191        morph_count: usize,
192    ) -> Result<Self, PoseReductionError> {
193        let mut parents = Vec::with_capacity(model.bone_count());
194        let mut translations = Vec::with_capacity(model.bone_count());
195        for bone in 0..model.bone_count() {
196            let bone_index = BoneIndex(bone as u32);
197            let parent = model.parent_index(bone_index);
198            parents.push(parent.map_or(-1, |value| value.0 as i32));
199            let parent_position = parent.map_or(Vec3A::ZERO, |value| model.rest_position(value));
200            translations.push(model.rest_position(bone_index) - parent_position);
201        }
202        Self::new(
203            parents,
204            translations,
205            vec![Quat::IDENTITY; model.bone_count()],
206            morph_count,
207            model_identity,
208        )
209    }
210
211    pub fn bone_count(&self) -> usize {
212        self.parent_indices.len()
213    }
214    pub fn morph_count(&self) -> usize {
215        self.morph_count
216    }
217    pub fn model_identity(&self) -> u64 {
218        self.model_identity
219    }
220    pub fn parent_indices(&self) -> &[i32] {
221        &self.parent_indices
222    }
223    pub fn rest_local_translations(&self) -> &[Vec3A] {
224        &self.rest_local_translations
225    }
226    pub fn rest_local_rotations(&self) -> &[Quat] {
227        &self.rest_local_rotations
228    }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq)]
232pub enum ReductionTarget {
233    LinearSlerp,
234    VmdBezier,
235    DccCubic,
236}
237
238#[derive(Debug, Clone, Copy, Default, PartialEq)]
239pub struct DccCubicSegment {
240    pub translation_out_tangent: Vec3A,
241    pub translation_in_tangent: Vec3A,
242    pub rotation_start_euler_xyz: Vec3A,
243    pub rotation_end_euler_xyz: Vec3A,
244    pub rotation_out_tangent: Vec3A,
245    pub rotation_in_tangent: Vec3A,
246}
247
248#[derive(Debug, Clone, Copy, Default, PartialEq)]
249pub struct DccScalarSegment {
250    pub out_tangent: f32,
251    pub in_tangent: f32,
252}
253
254#[derive(Debug, Clone, Copy, PartialEq, Eq)]
255pub struct QuantizedBezier {
256    pub x1: u8,
257    pub y1: u8,
258    pub x2: u8,
259    pub y2: u8,
260}
261
262impl QuantizedBezier {
263    pub const LINEAR: Self = Self {
264        x1: 20,
265        y1: 20,
266        x2: 107,
267        y2: 107,
268    };
269
270    pub fn evaluate(self, time: f32) -> f32 {
271        InterpolationScalar {
272            x1: self.x1.min(127),
273            y1: self.y1.min(127),
274            x2: self.x2.min(127),
275            y2: self.y2.min(127),
276        }
277        .evaluate(time)
278    }
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq)]
282pub struct VmdBoneInterpolation {
283    pub translation: [QuantizedBezier; 3],
284    pub rotation: QuantizedBezier,
285}
286
287impl VmdBoneInterpolation {
288    pub const LINEAR: Self = Self {
289        translation: [QuantizedBezier::LINEAR; 3],
290        rotation: QuantizedBezier::LINEAR,
291    };
292}
293
294#[derive(Debug, Clone, Copy, PartialEq)]
295pub struct ReductionTolerances {
296    pub local_position: f32,
297    pub local_rotation_radians: f32,
298    pub world_position: f32,
299    pub world_rotation_radians: f32,
300    pub morph_weight: f32,
301}
302
303impl ReductionTolerances {
304    fn validate(self) -> Result<Self, PoseReductionError> {
305        let values = [
306            self.local_position,
307            self.local_rotation_radians,
308            self.world_position,
309            self.world_rotation_radians,
310            self.morph_weight,
311        ];
312        if values
313            .iter()
314            .any(|value| !value.is_finite() || *value < 0.0)
315        {
316            return Err(PoseReductionError::InvalidTolerance);
317        }
318        Ok(self)
319    }
320}
321
322impl Default for ReductionTolerances {
323    fn default() -> Self {
324        Self {
325            local_position: 1.0e-4,
326            local_rotation_radians: 1.0e-4,
327            world_position: 1.0e-4,
328            world_rotation_radians: 1.0e-4,
329            morph_weight: 1.0e-4,
330        }
331    }
332}
333
334#[derive(Debug, Clone, Copy, PartialEq)]
335pub struct ReducedBoneKey {
336    pub sample_index: usize,
337    pub translation: Vec3A,
338    pub rotation: Quat,
339    pub vmd_interpolation: VmdBoneInterpolation,
340    pub dcc_segment: DccCubicSegment,
341}
342
343#[derive(Debug, Clone, PartialEq)]
344pub struct ReducedBoneTrack {
345    keys: Box<[ReducedBoneKey]>,
346}
347
348impl ReducedBoneTrack {
349    pub fn keys(&self) -> &[ReducedBoneKey] {
350        &self.keys
351    }
352}
353
354#[derive(Debug, Clone, Copy, PartialEq)]
355pub struct ReducedMorphKey {
356    pub sample_index: usize,
357    pub weight: f32,
358    pub dcc_segment: DccScalarSegment,
359}
360
361#[derive(Debug, Clone, PartialEq)]
362pub struct ReducedMorphTrack {
363    keys: Box<[ReducedMorphKey]>,
364}
365
366impl ReducedMorphTrack {
367    pub fn keys(&self) -> &[ReducedMorphKey] {
368        &self.keys
369    }
370}
371
372#[derive(Debug, Clone, Copy, Default, PartialEq)]
373pub struct PoseReductionReport {
374    pub source_bone_key_count: usize,
375    pub reduced_bone_key_count: usize,
376    pub source_morph_key_count: usize,
377    pub reduced_morph_key_count: usize,
378    pub max_local_position_error: f32,
379    pub max_local_rotation_error_radians: f32,
380    pub max_world_position_error: f32,
381    pub max_world_rotation_error_radians: f32,
382    pub max_morph_weight_error: f32,
383}
384
385#[derive(Debug, Clone, Default, PartialEq, Eq)]
386pub struct ReductionWorkStats {
387    pub global_validation_passes: usize,
388    pub candidate_rebuilds: usize,
389    pub candidate_bone_track_rebuilds: usize,
390    pub candidate_morph_track_rebuilds: usize,
391    pub local_prefit_bone_segment_fits: usize,
392    pub local_prefit_morph_segment_fits: usize,
393    pub local_prefit_bone_key_additions: usize,
394    pub local_prefit_morph_key_additions: usize,
395    pub local_prefit_bone_samples: usize,
396    pub local_prefit_morph_samples: usize,
397    pub dcc_bone_segment_fits: usize,
398    pub dcc_morph_segment_fits: usize,
399    pub bone_samples: usize,
400    pub morph_samples: usize,
401    pub world_rebuilds: usize,
402    /// Number of cached candidate world transforms recomputed during validation.
403    pub world_bone_recomputes: usize,
404    pub world_rotation_decompositions: usize,
405    pub normal_key_additions: usize,
406    pub ancestor_key_additions: usize,
407    pub added_keys_per_pass: Vec<usize>,
408}
409
410#[derive(Debug, Clone, Copy, PartialEq, Eq)]
411enum ValidationMode {
412    Incremental,
413    FullScan,
414}
415
416#[derive(Debug, Clone)]
417struct DirtyRanges {
418    bone_local: Vec<Vec<std::ops::RangeInclusive<usize>>>,
419    morph: Vec<Vec<std::ops::RangeInclusive<usize>>>,
420}
421
422impl DirtyRanges {
423    fn full(frame_count: usize, bone_count: usize, morph_count: usize) -> Self {
424        let range = || vec![0..=frame_count.saturating_sub(1)];
425        Self {
426            bone_local: (0..bone_count).map(|_| range()).collect(),
427            morph: (0..morph_count).map(|_| range()).collect(),
428        }
429    }
430
431    fn empty(bone_count: usize, morph_count: usize) -> Self {
432        Self {
433            bone_local: vec![Vec::new(); bone_count],
434            morph: vec![Vec::new(); morph_count],
435        }
436    }
437
438    fn mark_bone(&mut self, bone: usize, range: std::ops::RangeInclusive<usize>) {
439        insert_dirty_range(&mut self.bone_local[bone], range);
440    }
441
442    fn mark_morph(&mut self, morph: usize, range: std::ops::RangeInclusive<usize>) {
443        insert_dirty_range(&mut self.morph[morph], range);
444    }
445}
446
447fn insert_dirty_range(
448    target: &mut Vec<std::ops::RangeInclusive<usize>>,
449    range: std::ops::RangeInclusive<usize>,
450) {
451    target.push(range);
452    target.sort_by_key(|value| *value.start());
453    let mut merged: Vec<std::ops::RangeInclusive<usize>> = Vec::with_capacity(target.len());
454    for current in target.drain(..) {
455        if let Some(previous) = merged.last_mut()
456            && *current.start() <= previous.end().saturating_add(1)
457        {
458            let start = *previous.start();
459            *previous = start..=(*previous.end()).max(*current.end());
460        } else {
461            merged.push(current);
462        }
463    }
464    *target = merged;
465}
466
467#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
468pub struct ReductionTimings {
469    pub local_prefit: Duration,
470    pub candidate_build: Duration,
471    pub error_measure: Duration,
472    pub dcc_fit: Duration,
473}
474
475#[cfg(not(target_family = "wasm"))]
476type ReductionThreadPool = ThreadPool;
477
478#[cfg(target_family = "wasm")]
479struct ReductionThreadPool;
480
481#[derive(Debug, Clone)]
482pub struct ReducedPoseSequence {
483    snapshot: SkeletonSnapshot,
484    target: ReductionTarget,
485    start_frame: f32,
486    frame_step: f32,
487    frame_count: usize,
488    sample_frames: Box<[f32]>,
489    bone_tracks: Box<[ReducedBoneTrack]>,
490    morph_tracks: Box<[ReducedMorphTrack]>,
491    report: PoseReductionReport,
492    work_stats: ReductionWorkStats,
493    timings: ReductionTimings,
494}
495
496impl PartialEq for ReducedPoseSequence {
497    fn eq(&self, other: &Self) -> bool {
498        self.snapshot == other.snapshot
499            && self.target == other.target
500            && self.start_frame == other.start_frame
501            && self.frame_step == other.frame_step
502            && self.frame_count == other.frame_count
503            && self.sample_frames == other.sample_frames
504            && self.bone_tracks == other.bone_tracks
505            && self.morph_tracks == other.morph_tracks
506            && self.report == other.report
507            && self.work_stats == other.work_stats
508    }
509}
510
511impl ReducedPoseSequence {
512    pub fn snapshot(&self) -> &SkeletonSnapshot {
513        &self.snapshot
514    }
515    pub fn target(&self) -> ReductionTarget {
516        self.target
517    }
518    pub fn start_frame(&self) -> f32 {
519        self.start_frame
520    }
521    pub fn frame_step(&self) -> f32 {
522        self.frame_step
523    }
524    pub fn frame_count(&self) -> usize {
525        self.frame_count
526    }
527    pub fn sample_frames(&self) -> &[f32] {
528        &self.sample_frames
529    }
530    pub fn bone_tracks(&self) -> &[ReducedBoneTrack] {
531        &self.bone_tracks
532    }
533    pub fn morph_tracks(&self) -> &[ReducedMorphTrack] {
534        &self.morph_tracks
535    }
536    pub fn report(&self) -> PoseReductionReport {
537        self.report
538    }
539    pub fn work_stats(&self) -> &ReductionWorkStats {
540        &self.work_stats
541    }
542    pub fn timings(&self) -> ReductionTimings {
543        self.timings
544    }
545
546    pub fn sample(&self, frame: f32) -> Result<ReducedPoseSample, PoseReductionError> {
547        let mut scratch = ReducedPoseScratch::default();
548        self.sample_into(frame, &mut scratch)?;
549        Ok(ReducedPoseSample {
550            local_translations: scratch.local_translations,
551            local_rotations: scratch.local_rotations,
552            world_matrices: scratch.world_matrices,
553            morph_weights: scratch.morph_weights,
554        })
555    }
556
557    fn sample_into(
558        &self,
559        frame: f32,
560        scratch: &mut ReducedPoseScratch,
561    ) -> Result<(), PoseReductionError> {
562        if !frame.is_finite() {
563            return Err(PoseReductionError::InvalidSampleTime);
564        }
565        scratch.prepare(self.snapshot.bone_count(), self.snapshot.morph_count());
566        for (bone, track) in self.bone_tracks.iter().enumerate() {
567            let (translation, rotation) =
568                sample_bone_track(track, &self.sample_frames, frame, self.target);
569            scratch.local_translations[bone] = translation;
570            scratch.local_rotations[bone] = rotation;
571        }
572        for (morph, track) in self.morph_tracks.iter().enumerate() {
573            scratch.morph_weights[morph] =
574                sample_morph_track(track, &self.sample_frames, frame, self.target);
575        }
576        build_world_matrices_into(
577            &self.snapshot,
578            &scratch.local_translations,
579            &scratch.local_rotations,
580            &mut scratch.world_matrices,
581        );
582        Ok(())
583    }
584
585    pub fn validate_model(
586        &self,
587        model_identity: u64,
588        bone_count: usize,
589        morph_count: usize,
590    ) -> bool {
591        self.snapshot.model_identity == model_identity
592            && self.snapshot.bone_count() == bone_count
593            && self.snapshot.morph_count == morph_count
594    }
595}
596
597#[derive(Debug, Default)]
598struct ReducedPoseScratch {
599    local_translations: Vec<Vec3A>,
600    local_rotations: Vec<Quat>,
601    world_matrices: Vec<Mat4>,
602    morph_weights: Vec<f32>,
603}
604
605impl ReducedPoseScratch {
606    fn prepare(&mut self, bone_count: usize, morph_count: usize) {
607        self.local_translations.resize(bone_count, Vec3A::ZERO);
608        self.local_rotations.resize(bone_count, Quat::IDENTITY);
609        self.world_matrices.resize(bone_count, Mat4::IDENTITY);
610        self.morph_weights.resize(morph_count, 0.0);
611    }
612}
613
614#[derive(Debug, Clone, PartialEq)]
615pub struct ReducedPoseSample {
616    pub local_translations: Vec<Vec3A>,
617    pub local_rotations: Vec<Quat>,
618    pub world_matrices: Vec<Mat4>,
619    pub morph_weights: Vec<f32>,
620}
621
622#[derive(Debug, Error, Clone, PartialEq)]
623pub enum PoseReductionError {
624    #[error("dense pose sequence must contain at least one frame")]
625    EmptySequence,
626    #[error("skeleton must contain at least one bone")]
627    EmptySkeleton,
628    #[error("invalid or non-positive dense pose time base")]
629    InvalidTimeBase,
630    #[error("dense pose buffer length overflow")]
631    LengthOverflow,
632    #[error("world matrix count {actual} does not match expected {expected}")]
633    InvalidWorldMatrixCount { actual: usize, expected: usize },
634    #[error("morph weight count {actual} does not match expected {expected}")]
635    InvalidMorphWeightCount { actual: usize, expected: usize },
636    #[error("skeleton arrays have inconsistent lengths")]
637    InvalidSkeletonLengths,
638    #[error("bone {bone} has invalid parent index {parent}")]
639    InvalidParent { bone: usize, parent: i32 },
640    #[error("skeleton hierarchy contains a cycle at bone {bone}")]
641    SkeletonCycle { bone: usize },
642    #[error("bone {bone} has non-finite rest data")]
643    NonFiniteSkeleton { bone: usize },
644    #[error("dense pose skeleton counts do not match snapshot")]
645    SnapshotMismatch,
646    #[error("reduction tolerance must be finite and non-negative")]
647    InvalidTolerance,
648    #[error("frame {frame}, bone {bone} contains a non-finite matrix")]
649    NonFiniteMatrix { frame: usize, bone: usize },
650    #[error("frame {frame}, bone {bone} contains scale or shear")]
651    ScaleOrShear { frame: usize, bone: usize },
652    #[error("frame {frame}, bone {bone} has a singular parent transform")]
653    SingularParent { frame: usize, bone: usize },
654    #[error("frame {frame}, morph {morph} contains a non-finite weight")]
655    NonFiniteMorph { frame: usize, morph: usize },
656    #[error("sample time must be finite")]
657    InvalidSampleTime,
658    #[error("requested tolerance cannot be attained at source frame {frame}")]
659    ToleranceUnattainable { frame: usize },
660    #[error("failed to create pose reduction worker pool")]
661    WorkerPool,
662}
663
664pub fn reduce_dense_pose_sequence(
665    input: DensePoseSequenceView<'_>,
666    snapshot: SkeletonSnapshot,
667    tolerances: ReductionTolerances,
668    target: ReductionTarget,
669) -> Result<ReducedPoseSequence, PoseReductionError> {
670    reduce_dense_pose_sequence_with_worker_count(input, snapshot, tolerances, target, 0)
671}
672
673pub fn reduce_dense_pose_sequence_with_worker_count(
674    input: DensePoseSequenceView<'_>,
675    snapshot: SkeletonSnapshot,
676    tolerances: ReductionTolerances,
677    target: ReductionTarget,
678    worker_count: usize,
679) -> Result<ReducedPoseSequence, PoseReductionError> {
680    reduce_dense_pose_sequence_internal(
681        input,
682        snapshot,
683        tolerances,
684        target,
685        worker_count,
686        ValidationMode::Incremental,
687    )
688}
689
690fn reduce_dense_pose_sequence_internal(
691    input: DensePoseSequenceView<'_>,
692    snapshot: SkeletonSnapshot,
693    tolerances: ReductionTolerances,
694    target: ReductionTarget,
695    worker_count: usize,
696    validation_mode: ValidationMode,
697) -> Result<ReducedPoseSequence, PoseReductionError> {
698    let tolerances = tolerances.validate()?;
699    if input.bone_count != snapshot.bone_count() || input.morph_count != snapshot.morph_count() {
700        return Err(PoseReductionError::SnapshotMismatch);
701    }
702
703    let (local_translations, local_rotations) = decompose_dense_pose(input, &snapshot)?;
704    let (world_positions, world_rotations) = cache_dense_world_components(input)?;
705    let mut work_stats = ReductionWorkStats {
706        world_rotation_decompositions: input.frame_count * input.bone_count,
707        ..Default::default()
708    };
709    let mut timings = ReductionTimings::default();
710    let worker_count = resolve_reduction_worker_count(worker_count, input.frame_count);
711    let worker_pool = build_reduction_worker_pool(worker_count)?;
712    let local_euler_xyz = unwrap_euler_xyz(&local_rotations, input.frame_count, input.bone_count);
713    for frame in 0..input.frame_count {
714        for morph in 0..input.morph_count {
715            if !input.morph_weight(frame, morph).is_finite() {
716                return Err(PoseReductionError::NonFiniteMorph { frame, morph });
717            }
718        }
719    }
720
721    let mut bone_key_indices = vec![endpoint_indices(input.frame_count); input.bone_count];
722    let mut morph_key_indices = vec![endpoint_indices(input.frame_count); input.morph_count];
723
724    if target == ReductionTarget::DccCubic && input.frame_count >= DCC_LOCAL_PREFIT_MIN_FRAMES {
725        let dcc_prefit_started = reduction_timer_start();
726        for (bone, keys) in bone_key_indices.iter_mut().enumerate() {
727            let prefit = split_dcc_bone_track(
728                keys,
729                input,
730                bone,
731                &local_translations,
732                &local_rotations,
733                &local_euler_xyz,
734                tolerances,
735            );
736            work_stats.local_prefit_bone_segment_fits += prefit.segment_fits;
737            work_stats.local_prefit_bone_key_additions += prefit.key_additions;
738            work_stats.local_prefit_bone_samples += prefit.samples;
739        }
740        for (morph, keys) in morph_key_indices.iter_mut().enumerate() {
741            let prefit = split_dcc_morph_track(keys, input, morph, tolerances.morph_weight);
742            work_stats.local_prefit_morph_segment_fits += prefit.segment_fits;
743            work_stats.local_prefit_morph_key_additions += prefit.key_additions;
744            work_stats.local_prefit_morph_samples += prefit.samples;
745        }
746        timings.local_prefit += reduction_timer_elapsed(dcc_prefit_started);
747    } else if target == ReductionTarget::LinearSlerp {
748        for (bone, keys) in bone_key_indices.iter_mut().enumerate() {
749            split_bone_track(
750                keys,
751                input,
752                bone,
753                &local_translations,
754                &local_rotations,
755                tolerances,
756            );
757        }
758    }
759    if target != ReductionTarget::DccCubic {
760        for (morph, keys) in morph_key_indices.iter_mut().enumerate() {
761            split_morph_track(keys, input, morph, tolerances.morph_weight);
762        }
763    }
764
765    let mut candidate: Option<ReducedPoseSequence> = None;
766    let mut dirty = DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count);
767    let mut validation_cache =
768        ValidationCache::new(input.frame_count, input.bone_count, input.morph_count);
769    loop {
770        work_stats.global_validation_passes += 1;
771        work_stats.candidate_rebuilds += 1;
772        if candidate.is_some() && matches!(validation_mode, ValidationMode::FullScan) {
773            dirty = DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count);
774        }
775        let candidate_started = reduction_timer_start();
776        let local_pose = DenseLocalPose {
777            translations: &local_translations,
778            rotations: &local_rotations,
779            euler_xyz: &local_euler_xyz,
780        };
781        if let Some(sequence) = candidate.as_mut() {
782            rebuild_dirty_tracks(
783                sequence,
784                target,
785                input,
786                local_pose,
787                &bone_key_indices,
788                &morph_key_indices,
789                &dirty,
790                ReductionInstrumentation {
791                    work_stats: &mut work_stats,
792                    timings: &mut timings,
793                },
794            );
795        } else {
796            candidate = Some(build_sequence(
797                &snapshot,
798                target,
799                input,
800                local_pose,
801                &bone_key_indices,
802                &morph_key_indices,
803                ReductionInstrumentation {
804                    work_stats: &mut work_stats,
805                    timings: &mut timings,
806                },
807            ));
808        }
809        timings.candidate_build += reduction_timer_elapsed(candidate_started);
810        let error_measure_started = reduction_timer_start();
811        let (report, worst_by_track) = measure_error_cached(
812            candidate.as_ref().expect("candidate initialized"),
813            input,
814            DenseValidationPose {
815                translations: &local_translations,
816                rotations: &local_rotations,
817                world_positions: &world_positions,
818                world_rotations: &world_rotations,
819            },
820            tolerances,
821            &mut work_stats,
822            &dirty,
823            &mut validation_cache,
824            worker_pool.as_ref(),
825            worker_count,
826        )?;
827        timings.error_measure += reduction_timer_elapsed(error_measure_started);
828        let mut failing = worst_by_track
829            .into_iter()
830            .flatten()
831            .filter(|worst| worst.normalized_error > 1.0)
832            .collect::<Vec<_>>();
833        failing.sort_by(compare_worst_errors);
834        if failing.is_empty() {
835            work_stats.added_keys_per_pass.push(0);
836            let mut result = candidate.expect("candidate initialized");
837            result.report = PoseReductionReport {
838                source_bone_key_count: input.frame_count * input.bone_count,
839                reduced_bone_key_count: result
840                    .bone_tracks
841                    .iter()
842                    .map(|track| track.keys.len())
843                    .sum(),
844                source_morph_key_count: input.frame_count * input.morph_count,
845                reduced_morph_key_count: result
846                    .morph_tracks
847                    .iter()
848                    .map(|track| track.keys.len())
849                    .sum(),
850                ..report
851            };
852            result.work_stats = work_stats;
853            result.timings = timings;
854            return Ok(result);
855        }
856        let mut inserted = false;
857        let mut added_this_pass = 0;
858        let mut next_dirty = DirtyRanges::empty(input.bone_count, input.morph_count);
859        let first_failure_frame = failing[0].frame;
860        for worst in failing {
861            match worst.track {
862                ErrorTrack::Bone(bone) => {
863                    let mut cursor = Some(bone);
864                    let mut is_origin = true;
865                    while let Some(index) = cursor {
866                        if let Some(range) = insert_key_with_affected_range(
867                            &mut bone_key_indices[index],
868                            worst.frame,
869                        ) {
870                            inserted = true;
871                            added_this_pass += 1;
872                            next_dirty.mark_bone(index, range);
873                            if is_origin {
874                                work_stats.normal_key_additions += 1;
875                            } else {
876                                work_stats.ancestor_key_additions += 1;
877                            }
878                        }
879                        let parent = snapshot.parent_indices[index];
880                        cursor = (parent >= 0).then_some(parent as usize);
881                        is_origin = false;
882                    }
883                }
884                ErrorTrack::Morph(morph) => {
885                    if let Some(range) =
886                        insert_key_with_affected_range(&mut morph_key_indices[morph], worst.frame)
887                    {
888                        inserted = true;
889                        added_this_pass += 1;
890                        next_dirty.mark_morph(morph, range);
891                        work_stats.normal_key_additions += 1;
892                    }
893                }
894            }
895        }
896        work_stats.added_keys_per_pass.push(added_this_pass);
897        if !inserted {
898            return Err(PoseReductionError::ToleranceUnattainable {
899                frame: first_failure_frame,
900            });
901        }
902        dirty = next_dirty;
903    }
904}
905
906fn endpoint_indices(frame_count: usize) -> Vec<usize> {
907    if frame_count == 1 {
908        vec![0]
909    } else {
910        vec![0, frame_count - 1]
911    }
912}
913
914fn decompose_dense_pose(
915    input: DensePoseSequenceView<'_>,
916    snapshot: &SkeletonSnapshot,
917) -> Result<(Vec<Vec3A>, Vec<Quat>), PoseReductionError> {
918    let mut translations = vec![Vec3A::ZERO; input.frame_count * input.bone_count];
919    let mut rotations = vec![Quat::IDENTITY; input.frame_count * input.bone_count];
920    for frame in 0..input.frame_count {
921        for &bone in snapshot.evaluation_order.iter() {
922            let world = input.world_matrix(frame, bone);
923            validate_finite_matrix(world, frame, bone)?;
924            let local = if snapshot.parent_indices[bone] < 0 {
925                world
926            } else {
927                let parent = snapshot.parent_indices[bone] as usize;
928                let parent_world = input.world_matrix(frame, parent);
929                validate_finite_matrix(parent_world, frame, parent)?;
930                let determinant = Mat3::from_mat4(parent_world).determinant();
931                if !determinant.is_finite() || determinant.abs() <= f32::EPSILON {
932                    return Err(PoseReductionError::SingularParent { frame, bone });
933                }
934                parent_world.inverse() * world
935            };
936            let (translation, rotation) = decompose_rigid(local, frame, bone)?;
937            let index = frame * input.bone_count + bone;
938            translations[index] = translation;
939            rotations[index] = if frame > 0 {
940                let previous = rotations[index - input.bone_count];
941                if previous.dot(rotation) < 0.0 {
942                    -rotation
943                } else {
944                    rotation
945                }
946            } else {
947                rotation
948            };
949        }
950    }
951    Ok((translations, rotations))
952}
953
954fn cache_dense_world_components(
955    input: DensePoseSequenceView<'_>,
956) -> Result<(Vec<Vec3A>, Vec<Quat>), PoseReductionError> {
957    let count = input.frame_count * input.bone_count;
958    let mut positions = Vec::with_capacity(count);
959    let mut rotations = Vec::with_capacity(count);
960    for frame in 0..input.frame_count {
961        for bone in 0..input.bone_count {
962            let world = input.world_matrix(frame, bone);
963            positions.push(Vec3A::from(world.w_axis.truncate()));
964            rotations.push(decompose_rigid(world, frame, bone)?.1);
965        }
966    }
967    Ok((positions, rotations))
968}
969
970fn decompose_rigid(
971    matrix: Mat4,
972    frame: usize,
973    bone: usize,
974) -> Result<(Vec3A, Quat), PoseReductionError> {
975    validate_finite_matrix(matrix, frame, bone)?;
976    let x = matrix.x_axis.truncate();
977    let y = matrix.y_axis.truncate();
978    let z = matrix.z_axis.truncate();
979    let unit = |v: Vec3| (v.length_squared() - 1.0).abs() <= AFFINE_EPSILON;
980    if !unit(x)
981        || !unit(y)
982        || !unit(z)
983        || x.dot(y).abs() > AFFINE_EPSILON
984        || x.dot(z).abs() > AFFINE_EPSILON
985        || y.dot(z).abs() > AFFINE_EPSILON
986        || Mat3::from_cols(x, y, z).determinant() < 0.0
987        || matrix.x_axis.w.abs() > AFFINE_EPSILON
988        || matrix.y_axis.w.abs() > AFFINE_EPSILON
989        || matrix.z_axis.w.abs() > AFFINE_EPSILON
990        || (matrix.w_axis.w - 1.0).abs() > AFFINE_EPSILON
991    {
992        return Err(PoseReductionError::ScaleOrShear { frame, bone });
993    }
994    let rotation = normalize_quat(Quat::from_mat3(&Mat3::from_cols(x, y, z)));
995    Ok((Vec3A::from(matrix.w_axis.truncate()), rotation))
996}
997
998fn validate_finite_matrix(
999    matrix: Mat4,
1000    frame: usize,
1001    bone: usize,
1002) -> Result<(), PoseReductionError> {
1003    if matrix
1004        .to_cols_array()
1005        .iter()
1006        .any(|value| !value.is_finite())
1007    {
1008        Err(PoseReductionError::NonFiniteMatrix { frame, bone })
1009    } else {
1010        Ok(())
1011    }
1012}
1013
1014fn split_bone_track(
1015    keys: &mut Vec<usize>,
1016    input: DensePoseSequenceView<'_>,
1017    bone: usize,
1018    translations: &[Vec3A],
1019    rotations: &[Quat],
1020    tolerances: ReductionTolerances,
1021) {
1022    if input.frame_count <= 2 {
1023        return;
1024    }
1025    let bone_count = translations.len() / input.frame_count;
1026    let mut stack = vec![(0usize, input.frame_count - 1)];
1027    while let Some((start, end)) = stack.pop() {
1028        if end <= start + 1 {
1029            continue;
1030        }
1031        let start_index = start * bone_count + bone;
1032        let end_index = end * bone_count + bone;
1033        let mut worst: Option<(f32, usize)> = None;
1034        for frame in start + 1..end {
1035            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1036            let index = frame * bone_count + bone;
1037            let position_error = translations[index]
1038                .distance(translations[start_index].lerp(translations[end_index], amount));
1039            let rotation_error = quat_angle(
1040                rotations[index],
1041                rotations[start_index].slerp(rotations[end_index], amount),
1042            );
1043            let normalized = normalized_error(position_error, tolerances.local_position).max(
1044                normalized_error(rotation_error, tolerances.local_rotation_radians),
1045            );
1046            if is_worse(worst, normalized, frame) {
1047                worst = Some((normalized, frame));
1048            }
1049        }
1050        if let Some((normalized, frame)) = worst.filter(|value| value.0 > 1.0) {
1051            let _ = normalized;
1052            insert_key(keys, frame);
1053            stack.push((frame, end));
1054            stack.push((start, frame));
1055        }
1056    }
1057}
1058
1059#[derive(Default)]
1060struct LocalPrefitStats {
1061    segment_fits: usize,
1062    key_additions: usize,
1063    samples: usize,
1064}
1065
1066fn split_dcc_bone_track(
1067    keys: &mut Vec<usize>,
1068    input: DensePoseSequenceView<'_>,
1069    bone: usize,
1070    translations: &[Vec3A],
1071    rotations: &[Quat],
1072    euler_xyz: &[Vec3A],
1073    tolerances: ReductionTolerances,
1074) -> LocalPrefitStats {
1075    if input.frame_count <= 2 {
1076        return LocalPrefitStats::default();
1077    }
1078    let bone_count = input.bone_count;
1079    let mut stats = LocalPrefitStats::default();
1080    let mut stack = vec![(0usize, input.frame_count - 1)];
1081    while let Some((start, end)) = stack.pop() {
1082        if end <= start + 1 {
1083            continue;
1084        }
1085        let segment = fit_dcc_bone_segment(input, bone, start, end, translations, euler_xyz);
1086        stats.segment_fits += 1;
1087        let start_index = start * bone_count + bone;
1088        let end_index = end * bone_count + bone;
1089        let duration = input.sample_frame(end) - input.sample_frame(start);
1090        let mut worst: Option<(f32, usize)> = None;
1091        for frame in start + 1..end {
1092            stats.samples += 1;
1093            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1094            let translation = sample_dcc_vec3(
1095                translations[start_index],
1096                translations[end_index],
1097                segment.translation_out_tangent,
1098                segment.translation_in_tangent,
1099                duration,
1100                amount,
1101            );
1102            let euler = sample_dcc_vec3(
1103                segment.rotation_start_euler_xyz,
1104                segment.rotation_end_euler_xyz,
1105                segment.rotation_out_tangent,
1106                segment.rotation_in_tangent,
1107                duration,
1108                amount,
1109            );
1110            let rotation =
1111                normalize_quat(Quat::from_euler(EulerRot::XYZ, euler.x, euler.y, euler.z));
1112            let index = frame * bone_count + bone;
1113            let normalized = normalized_error(
1114                translations[index].distance(translation),
1115                tolerances.local_position,
1116            )
1117            .max(normalized_error(
1118                quat_angle(rotations[index], rotation),
1119                tolerances.local_rotation_radians,
1120            ));
1121            if is_worse(worst, normalized, frame) {
1122                worst = Some((normalized, frame));
1123            }
1124        }
1125        if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1126            stats.key_additions += usize::from(insert_key(keys, frame));
1127            stack.push((frame, end));
1128            stack.push((start, frame));
1129        }
1130    }
1131    stats
1132}
1133
1134fn split_dcc_morph_track(
1135    keys: &mut Vec<usize>,
1136    input: DensePoseSequenceView<'_>,
1137    morph: usize,
1138    tolerance: f32,
1139) -> LocalPrefitStats {
1140    if input.frame_count <= 2 {
1141        return LocalPrefitStats::default();
1142    }
1143    let mut stats = LocalPrefitStats::default();
1144    let mut stack = vec![(0usize, input.frame_count - 1)];
1145    while let Some((start, end)) = stack.pop() {
1146        if end <= start + 1 {
1147            continue;
1148        }
1149        let segment = fit_dcc_scalar_segment(
1150            start,
1151            end,
1152            |sample| input.morph_weight(sample, morph),
1153            |sample| input.sample_frame(sample),
1154        );
1155        stats.segment_fits += 1;
1156        let duration = input.sample_frame(end) - input.sample_frame(start);
1157        let mut worst: Option<(f32, usize)> = None;
1158        for frame in start + 1..end {
1159            stats.samples += 1;
1160            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1161            let expected = sample_hermite(
1162                input.morph_weight(start, morph),
1163                input.morph_weight(end, morph),
1164                segment.out_tangent,
1165                segment.in_tangent,
1166                duration,
1167                amount,
1168            );
1169            let normalized = normalized_error(
1170                (input.morph_weight(frame, morph) - expected).abs(),
1171                tolerance,
1172            );
1173            if is_worse(worst, normalized, frame) {
1174                worst = Some((normalized, frame));
1175            }
1176        }
1177        if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1178            stats.key_additions += usize::from(insert_key(keys, frame));
1179            stack.push((frame, end));
1180            stack.push((start, frame));
1181        }
1182    }
1183    stats
1184}
1185
1186fn split_morph_track(
1187    keys: &mut Vec<usize>,
1188    input: DensePoseSequenceView<'_>,
1189    morph: usize,
1190    tolerance: f32,
1191) {
1192    if input.frame_count <= 2 {
1193        return;
1194    }
1195    let mut stack = vec![(0usize, input.frame_count - 1)];
1196    while let Some((start, end)) = stack.pop() {
1197        if end <= start + 1 {
1198            continue;
1199        }
1200        let mut worst: Option<(f32, usize)> = None;
1201        for frame in start + 1..end {
1202            let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1203            let expected = input.morph_weight(start, morph)
1204                + (input.morph_weight(end, morph) - input.morph_weight(start, morph)) * amount;
1205            let normalized = normalized_error(
1206                (input.morph_weight(frame, morph) - expected).abs(),
1207                tolerance,
1208            );
1209            if is_worse(worst, normalized, frame) {
1210                worst = Some((normalized, frame));
1211            }
1212        }
1213        if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1214            insert_key(keys, frame);
1215            stack.push((frame, end));
1216            stack.push((start, frame));
1217        }
1218    }
1219}
1220
1221fn insert_key(keys: &mut Vec<usize>, frame: usize) -> bool {
1222    if let Err(position) = keys.binary_search(&frame) {
1223        keys.insert(position, frame);
1224        true
1225    } else {
1226        false
1227    }
1228}
1229
1230fn insert_key_with_affected_range(
1231    keys: &mut Vec<usize>,
1232    frame: usize,
1233) -> Option<std::ops::RangeInclusive<usize>> {
1234    let position = keys.binary_search(&frame).err()?;
1235    let start = keys[position.saturating_sub(1)];
1236    let end = keys[position.min(keys.len() - 1)];
1237    keys.insert(position, frame);
1238    Some(start..=end)
1239}
1240
1241#[derive(Clone, Copy)]
1242struct DenseLocalPose<'a> {
1243    translations: &'a [Vec3A],
1244    rotations: &'a [Quat],
1245    euler_xyz: &'a [Vec3A],
1246}
1247
1248struct ReductionInstrumentation<'a> {
1249    work_stats: &'a mut ReductionWorkStats,
1250    timings: &'a mut ReductionTimings,
1251}
1252
1253fn build_sequence(
1254    snapshot: &SkeletonSnapshot,
1255    target: ReductionTarget,
1256    input: DensePoseSequenceView<'_>,
1257    local_pose: DenseLocalPose<'_>,
1258    bone_key_indices: &[Vec<usize>],
1259    morph_key_indices: &[Vec<usize>],
1260    instrumentation: ReductionInstrumentation<'_>,
1261) -> ReducedPoseSequence {
1262    let ReductionInstrumentation {
1263        work_stats,
1264        timings,
1265    } = instrumentation;
1266    if target == ReductionTarget::DccCubic {
1267        work_stats.dcc_bone_segment_fits += bone_key_indices
1268            .iter()
1269            .map(|indices| indices.len().saturating_sub(1))
1270            .sum::<usize>();
1271        work_stats.dcc_morph_segment_fits += morph_key_indices
1272            .iter()
1273            .map(|indices| indices.len().saturating_sub(1))
1274            .sum::<usize>();
1275    }
1276    work_stats.candidate_bone_track_rebuilds += bone_key_indices.len();
1277    work_stats.candidate_morph_track_rebuilds += morph_key_indices.len();
1278    let dcc_bone_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1279    let bone_tracks = bone_key_indices
1280        .iter()
1281        .enumerate()
1282        .map(|(bone, indices)| build_bone_track(target, input, local_pose, bone, indices))
1283        .collect::<Vec<_>>()
1284        .into_boxed_slice();
1285    if let Some(started) = dcc_bone_started {
1286        timings.dcc_fit += reduction_timer_elapsed(started);
1287    }
1288    let dcc_morph_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1289    let morph_tracks = morph_key_indices
1290        .iter()
1291        .enumerate()
1292        .map(|(morph, indices)| build_morph_track(target, input, morph, indices))
1293        .collect::<Vec<_>>()
1294        .into_boxed_slice();
1295    if let Some(started) = dcc_morph_started {
1296        timings.dcc_fit += reduction_timer_elapsed(started);
1297    }
1298    ReducedPoseSequence {
1299        snapshot: snapshot.clone(),
1300        target,
1301        start_frame: input.start_frame,
1302        frame_step: input.frame_step,
1303        frame_count: input.frame_count,
1304        sample_frames: (0..input.frame_count)
1305            .map(|sample| input.sample_frame(sample))
1306            .collect::<Vec<_>>()
1307            .into_boxed_slice(),
1308        bone_tracks,
1309        morph_tracks,
1310        report: PoseReductionReport::default(),
1311        work_stats: ReductionWorkStats::default(),
1312        timings: ReductionTimings::default(),
1313    }
1314}
1315
1316fn build_bone_track(
1317    target: ReductionTarget,
1318    input: DensePoseSequenceView<'_>,
1319    local_pose: DenseLocalPose<'_>,
1320    bone: usize,
1321    indices: &[usize],
1322) -> ReducedBoneTrack {
1323    ReducedBoneTrack {
1324        keys: indices
1325            .iter()
1326            .enumerate()
1327            .map(|(key_position, &frame)| {
1328                let index = frame * input.bone_count + bone;
1329                ReducedBoneKey {
1330                    sample_index: frame,
1331                    translation: local_pose.translations[index],
1332                    rotation: local_pose.rotations[index],
1333                    vmd_interpolation: if target == ReductionTarget::VmdBezier && key_position > 0 {
1334                        fit_vmd_bone_interpolation(
1335                            input,
1336                            bone,
1337                            indices[key_position - 1],
1338                            frame,
1339                            local_pose.translations,
1340                            local_pose.rotations,
1341                        )
1342                    } else {
1343                        VmdBoneInterpolation::LINEAR
1344                    },
1345                    dcc_segment: if target == ReductionTarget::DccCubic && key_position > 0 {
1346                        fit_dcc_bone_segment(
1347                            input,
1348                            bone,
1349                            indices[key_position - 1],
1350                            frame,
1351                            local_pose.translations,
1352                            local_pose.euler_xyz,
1353                        )
1354                    } else {
1355                        DccCubicSegment::default()
1356                    },
1357                }
1358            })
1359            .collect::<Vec<_>>()
1360            .into_boxed_slice(),
1361    }
1362}
1363
1364fn build_morph_track(
1365    target: ReductionTarget,
1366    input: DensePoseSequenceView<'_>,
1367    morph: usize,
1368    indices: &[usize],
1369) -> ReducedMorphTrack {
1370    ReducedMorphTrack {
1371        keys: indices
1372            .iter()
1373            .enumerate()
1374            .map(|(key_position, &frame)| ReducedMorphKey {
1375                sample_index: frame,
1376                weight: input.morph_weight(frame, morph),
1377                dcc_segment: if target == ReductionTarget::DccCubic && key_position > 0 {
1378                    fit_dcc_scalar_segment(
1379                        indices[key_position - 1],
1380                        frame,
1381                        |sample| input.morph_weight(sample, morph),
1382                        |sample| input.sample_frame(sample),
1383                    )
1384                } else {
1385                    DccScalarSegment::default()
1386                },
1387            })
1388            .collect::<Vec<_>>()
1389            .into_boxed_slice(),
1390    }
1391}
1392
1393#[allow(clippy::too_many_arguments)]
1394fn rebuild_dirty_tracks(
1395    sequence: &mut ReducedPoseSequence,
1396    target: ReductionTarget,
1397    input: DensePoseSequenceView<'_>,
1398    local_pose: DenseLocalPose<'_>,
1399    bone_key_indices: &[Vec<usize>],
1400    morph_key_indices: &[Vec<usize>],
1401    dirty: &DirtyRanges,
1402    instrumentation: ReductionInstrumentation<'_>,
1403) {
1404    let ReductionInstrumentation {
1405        work_stats,
1406        timings,
1407    } = instrumentation;
1408    let dcc_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1409    for (bone, range) in dirty.bone_local.iter().enumerate() {
1410        if range.is_empty() {
1411            continue;
1412        }
1413        let indices = &bone_key_indices[bone];
1414        work_stats.candidate_bone_track_rebuilds += 1;
1415        if target == ReductionTarget::DccCubic {
1416            work_stats.dcc_bone_segment_fits += indices.len().saturating_sub(1);
1417        }
1418        sequence.bone_tracks[bone] = build_bone_track(target, input, local_pose, bone, indices);
1419    }
1420    for (morph, range) in dirty.morph.iter().enumerate() {
1421        if range.is_empty() {
1422            continue;
1423        }
1424        let indices = &morph_key_indices[morph];
1425        work_stats.candidate_morph_track_rebuilds += 1;
1426        if target == ReductionTarget::DccCubic {
1427            work_stats.dcc_morph_segment_fits += indices.len().saturating_sub(1);
1428        }
1429        sequence.morph_tracks[morph] = build_morph_track(target, input, morph, indices);
1430    }
1431    if let Some(started) = dcc_started {
1432        timings.dcc_fit += reduction_timer_elapsed(started);
1433    }
1434}
1435
1436#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1437enum ErrorTrack {
1438    Bone(usize),
1439    Morph(usize),
1440}
1441
1442#[derive(Clone, Copy)]
1443struct WorstError {
1444    normalized_error: f32,
1445    frame: usize,
1446    track: ErrorTrack,
1447}
1448
1449#[derive(Clone, Copy)]
1450struct DenseValidationPose<'a> {
1451    translations: &'a [Vec3A],
1452    rotations: &'a [Quat],
1453    world_positions: &'a [Vec3A],
1454    world_rotations: &'a [Quat],
1455}
1456
1457#[derive(Clone, Copy, Default)]
1458struct BoneErrorCell {
1459    local_position: f32,
1460    local_rotation: f32,
1461    world_position: f32,
1462    world_rotation: f32,
1463}
1464
1465struct ValidationCache {
1466    local_translations: Vec<Vec3A>,
1467    local_rotations: Vec<Quat>,
1468    world_matrices: Vec<Mat4>,
1469    world_rotations: Vec<Quat>,
1470    morph_weights: Vec<f32>,
1471    bone_errors: Vec<BoneErrorCell>,
1472    morph_errors: Vec<f32>,
1473}
1474
1475#[cfg(not(target_family = "wasm"))]
1476struct ValidationCacheChunk {
1477    start_frame: usize,
1478    local_translations: Vec<Vec3A>,
1479    local_rotations: Vec<Quat>,
1480    world_matrices: Vec<Mat4>,
1481    world_rotations: Vec<Quat>,
1482    morph_weights: Vec<f32>,
1483    bone_errors: Vec<BoneErrorCell>,
1484    morph_errors: Vec<f32>,
1485}
1486
1487impl ValidationCache {
1488    fn new(frame_count: usize, bone_count: usize, morph_count: usize) -> Self {
1489        Self {
1490            local_translations: vec![Vec3A::ZERO; frame_count * bone_count],
1491            local_rotations: vec![Quat::IDENTITY; frame_count * bone_count],
1492            world_matrices: vec![Mat4::IDENTITY; frame_count * bone_count],
1493            world_rotations: vec![Quat::IDENTITY; frame_count * bone_count],
1494            morph_weights: vec![0.0; frame_count * morph_count],
1495            bone_errors: vec![BoneErrorCell::default(); frame_count * bone_count],
1496            morph_errors: vec![0.0; frame_count * morph_count],
1497        }
1498    }
1499}
1500
1501#[cfg(not(target_family = "wasm"))]
1502#[allow(clippy::too_many_arguments)]
1503fn refresh_full_validation_cache_parallel(
1504    sequence: &ReducedPoseSequence,
1505    input: DensePoseSequenceView<'_>,
1506    dense: DenseValidationPose<'_>,
1507    cache: &mut ValidationCache,
1508    work_stats: &mut ReductionWorkStats,
1509    worker_pool: &ReductionThreadPool,
1510    worker_count: usize,
1511) -> Result<(), PoseReductionError> {
1512    let chunk_size = input.frame_count.div_ceil(worker_count);
1513    let ranges = (0..worker_count)
1514        .map(|worker| {
1515            let start = worker * chunk_size;
1516            start..(start + chunk_size).min(input.frame_count)
1517        })
1518        .filter(|range| !range.is_empty())
1519        .collect::<Vec<_>>();
1520    let chunks = worker_pool.install(|| {
1521        ranges
1522            .par_iter()
1523            .map(|range| {
1524                let frame_count = range.end - range.start;
1525                let mut chunk = ValidationCacheChunk {
1526                    start_frame: range.start,
1527                    local_translations: Vec::with_capacity(frame_count * input.bone_count),
1528                    local_rotations: Vec::with_capacity(frame_count * input.bone_count),
1529                    world_matrices: Vec::with_capacity(frame_count * input.bone_count),
1530                    world_rotations: Vec::with_capacity(frame_count * input.bone_count),
1531                    morph_weights: Vec::with_capacity(frame_count * input.morph_count),
1532                    bone_errors: Vec::with_capacity(frame_count * input.bone_count),
1533                    morph_errors: Vec::with_capacity(frame_count * input.morph_count),
1534                };
1535                let mut scratch = ReducedPoseScratch::default();
1536                scratch.prepare(input.bone_count, input.morph_count);
1537                for frame in range.clone() {
1538                    sequence.sample_into(input.sample_frame(frame), &mut scratch)?;
1539                    chunk
1540                        .local_translations
1541                        .extend_from_slice(&scratch.local_translations);
1542                    chunk
1543                        .local_rotations
1544                        .extend_from_slice(&scratch.local_rotations);
1545                    chunk
1546                        .world_matrices
1547                        .extend_from_slice(&scratch.world_matrices);
1548                    for bone in 0..input.bone_count {
1549                        let index = frame * input.bone_count + bone;
1550                        let world_rotation =
1551                            decompose_rigid(scratch.world_matrices[bone], frame, bone)?.1;
1552                        chunk.world_rotations.push(world_rotation);
1553                        chunk.bone_errors.push(BoneErrorCell {
1554                            local_position: dense.translations[index]
1555                                .distance(scratch.local_translations[bone]),
1556                            local_rotation: quat_angle(
1557                                dense.rotations[index],
1558                                scratch.local_rotations[bone],
1559                            ),
1560                            world_position: dense.world_positions[index].distance(Vec3A::from(
1561                                scratch.world_matrices[bone].w_axis.truncate(),
1562                            )),
1563                            world_rotation: quat_angle(
1564                                dense.world_rotations[index],
1565                                world_rotation,
1566                            ),
1567                        });
1568                    }
1569                    chunk
1570                        .morph_weights
1571                        .extend_from_slice(&scratch.morph_weights);
1572                    for morph in 0..input.morph_count {
1573                        chunk.morph_errors.push(
1574                            (input.morph_weight(frame, morph) - scratch.morph_weights[morph]).abs(),
1575                        );
1576                    }
1577                }
1578                Ok(chunk)
1579            })
1580            .collect::<Result<Vec<_>, PoseReductionError>>()
1581    })?;
1582
1583    for chunk in chunks {
1584        let bone_start = chunk.start_frame * input.bone_count;
1585        let bone_end = bone_start + chunk.local_translations.len();
1586        cache.local_translations[bone_start..bone_end].copy_from_slice(&chunk.local_translations);
1587        cache.local_rotations[bone_start..bone_end].copy_from_slice(&chunk.local_rotations);
1588        cache.world_matrices[bone_start..bone_end].copy_from_slice(&chunk.world_matrices);
1589        cache.world_rotations[bone_start..bone_end].copy_from_slice(&chunk.world_rotations);
1590        cache.bone_errors[bone_start..bone_end].copy_from_slice(&chunk.bone_errors);
1591        let morph_start = chunk.start_frame * input.morph_count;
1592        let morph_end = morph_start + chunk.morph_weights.len();
1593        cache.morph_weights[morph_start..morph_end].copy_from_slice(&chunk.morph_weights);
1594        cache.morph_errors[morph_start..morph_end].copy_from_slice(&chunk.morph_errors);
1595    }
1596    work_stats.bone_samples += input.frame_count * input.bone_count;
1597    work_stats.morph_samples += input.frame_count * input.morph_count;
1598    work_stats.world_rebuilds += input.frame_count;
1599    work_stats.world_bone_recomputes += input.frame_count * input.bone_count;
1600    work_stats.world_rotation_decompositions += input.frame_count * input.bone_count;
1601    Ok(())
1602}
1603
1604#[allow(clippy::too_many_arguments)]
1605fn measure_error_cached(
1606    sequence: &ReducedPoseSequence,
1607    input: DensePoseSequenceView<'_>,
1608    dense: DenseValidationPose<'_>,
1609    tolerances: ReductionTolerances,
1610    work_stats: &mut ReductionWorkStats,
1611    dirty: &DirtyRanges,
1612    cache: &mut ValidationCache,
1613    worker_pool: Option<&ReductionThreadPool>,
1614    worker_count: usize,
1615) -> Result<(PoseReductionReport, Vec<Option<WorstError>>), PoseReductionError> {
1616    let full_dirty = dirty_ranges_are_full(
1617        dirty,
1618        input.frame_count,
1619        input.bone_count,
1620        input.morph_count,
1621    );
1622    #[cfg(not(target_family = "wasm"))]
1623    let refreshed_in_parallel = if full_dirty && worker_count > 1 {
1624        refresh_full_validation_cache_parallel(
1625            sequence,
1626            input,
1627            dense,
1628            cache,
1629            work_stats,
1630            worker_pool.expect("multi-worker reduction has a pool"),
1631            worker_count,
1632        )?;
1633        true
1634    } else {
1635        false
1636    };
1637    #[cfg(target_family = "wasm")]
1638    let refreshed_in_parallel = {
1639        let _ = (full_dirty, worker_pool, worker_count);
1640        false
1641    };
1642
1643    let mut world_dirty = dirty.bone_local.clone();
1644    for &bone in sequence.snapshot.evaluation_order.iter() {
1645        let parent = sequence.snapshot.parent_indices[bone];
1646        if parent >= 0 {
1647            for range in world_dirty[parent as usize].clone() {
1648                insert_dirty_range(&mut world_dirty[bone], range);
1649            }
1650        }
1651    }
1652
1653    if !refreshed_in_parallel {
1654        for (bone, ranges) in dirty.bone_local.iter().enumerate() {
1655            for range in ranges {
1656                for frame in range.clone() {
1657                    let index = frame * input.bone_count + bone;
1658                    let (translation, rotation) = sample_bone_track(
1659                        &sequence.bone_tracks[bone],
1660                        &sequence.sample_frames,
1661                        input.sample_frame(frame),
1662                        sequence.target,
1663                    );
1664                    cache.local_translations[index] = translation;
1665                    cache.local_rotations[index] = rotation;
1666                    let cell = &mut cache.bone_errors[index];
1667                    cell.local_position = dense.translations[index].distance(translation);
1668                    cell.local_rotation = quat_angle(dense.rotations[index], rotation);
1669                    work_stats.bone_samples += 1;
1670                }
1671            }
1672        }
1673        for (morph, ranges) in dirty.morph.iter().enumerate() {
1674            for range in ranges {
1675                for frame in range.clone() {
1676                    let index = frame * input.morph_count + morph;
1677                    let sampled = sample_morph_track(
1678                        &sequence.morph_tracks[morph],
1679                        &sequence.sample_frames,
1680                        input.sample_frame(frame),
1681                        sequence.target,
1682                    );
1683                    cache.morph_weights[index] = sampled;
1684                    cache.morph_errors[index] = (input.morph_weight(frame, morph) - sampled).abs();
1685                    work_stats.morph_samples += 1;
1686                }
1687            }
1688        }
1689
1690        let mut rebuilt_frames = vec![false; input.frame_count];
1691        for &bone in sequence.snapshot.evaluation_order.iter() {
1692            for range in &world_dirty[bone] {
1693                for frame in range.clone() {
1694                    rebuilt_frames[frame] = true;
1695                    let index = frame * input.bone_count + bone;
1696                    let local = Mat4::from_rotation_translation(
1697                        cache.local_rotations[index],
1698                        cache.local_translations[index].into(),
1699                    );
1700                    let parent = sequence.snapshot.parent_indices[bone];
1701                    cache.world_matrices[index] = if parent < 0 {
1702                        local
1703                    } else {
1704                        cache.world_matrices[frame * input.bone_count + parent as usize] * local
1705                    };
1706                    cache.world_rotations[index] =
1707                        decompose_rigid(cache.world_matrices[index], frame, bone)?.1;
1708                    let cell = &mut cache.bone_errors[index];
1709                    cell.world_position = dense.world_positions[index]
1710                        .distance(Vec3A::from(cache.world_matrices[index].w_axis.truncate()));
1711                    cell.world_rotation =
1712                        quat_angle(dense.world_rotations[index], cache.world_rotations[index]);
1713                    work_stats.world_bone_recomputes += 1;
1714                    work_stats.world_rotation_decompositions += 1;
1715                }
1716            }
1717        }
1718        work_stats.world_rebuilds += rebuilt_frames
1719            .into_iter()
1720            .filter(|rebuilt| *rebuilt)
1721            .count();
1722    }
1723
1724    let mut report = PoseReductionReport::default();
1725    let mut worst = vec![None; input.bone_count + input.morph_count];
1726    for frame in 0..input.frame_count {
1727        let (bone_worst, morph_worst) = worst.split_at_mut(input.bone_count);
1728        for (bone, worst) in bone_worst.iter_mut().enumerate() {
1729            let index = frame * input.bone_count + bone;
1730            let cell = cache.bone_errors[index];
1731            report.max_local_position_error =
1732                report.max_local_position_error.max(cell.local_position);
1733            report.max_local_rotation_error_radians = report
1734                .max_local_rotation_error_radians
1735                .max(cell.local_rotation);
1736            report.max_world_position_error =
1737                report.max_world_position_error.max(cell.world_position);
1738            report.max_world_rotation_error_radians = report
1739                .max_world_rotation_error_radians
1740                .max(cell.world_rotation);
1741            for normalized in [
1742                normalized_error(cell.local_position, tolerances.local_position),
1743                normalized_error(cell.local_rotation, tolerances.local_rotation_radians),
1744                normalized_error(cell.world_position, tolerances.world_position),
1745                normalized_error(cell.world_rotation, tolerances.world_rotation_radians),
1746            ] {
1747                update_worst(worst, normalized, frame, ErrorTrack::Bone(bone));
1748            }
1749        }
1750        for (morph, morph_worst) in morph_worst.iter_mut().enumerate() {
1751            let error = cache.morph_errors[frame * input.morph_count + morph];
1752            report.max_morph_weight_error = report.max_morph_weight_error.max(error);
1753            update_worst(
1754                morph_worst,
1755                normalized_error(error, tolerances.morph_weight),
1756                frame,
1757                ErrorTrack::Morph(morph),
1758            );
1759        }
1760    }
1761    Ok((report, worst))
1762}
1763
1764fn dirty_ranges_are_full(
1765    dirty: &DirtyRanges,
1766    frame_count: usize,
1767    bone_count: usize,
1768    morph_count: usize,
1769) -> bool {
1770    let is_full = |ranges: &[std::ops::RangeInclusive<usize>]| {
1771        ranges.len() == 1
1772            && *ranges[0].start() == 0
1773            && *ranges[0].end() == frame_count.saturating_sub(1)
1774    };
1775    dirty.bone_local.len() == bone_count
1776        && dirty.morph.len() == morph_count
1777        && dirty.bone_local.iter().all(|ranges| is_full(ranges))
1778        && dirty.morph.iter().all(|ranges| is_full(ranges))
1779}
1780
1781fn update_worst(worst: &mut Option<WorstError>, normalized: f32, frame: usize, track: ErrorTrack) {
1782    update_worst_candidate(
1783        worst,
1784        WorstError {
1785            normalized_error: normalized,
1786            frame,
1787            track,
1788        },
1789    );
1790}
1791
1792fn update_worst_candidate(worst: &mut Option<WorstError>, candidate: WorstError) {
1793    if worst.is_none_or(|current| compare_worst_errors(&candidate, &current) == Ordering::Less) {
1794        *worst = Some(candidate);
1795    }
1796}
1797
1798fn compare_worst_errors(a: &WorstError, b: &WorstError) -> Ordering {
1799    b.normalized_error
1800        .total_cmp(&a.normalized_error)
1801        .then_with(|| a.frame.cmp(&b.frame))
1802        .then_with(|| error_track_sort_key(a.track).cmp(&error_track_sort_key(b.track)))
1803}
1804
1805fn error_track_sort_key(track: ErrorTrack) -> (u8, usize) {
1806    match track {
1807        ErrorTrack::Bone(index) => (0, index),
1808        ErrorTrack::Morph(index) => (1, index),
1809    }
1810}
1811
1812fn resolve_reduction_worker_count(requested: usize, frame_count: usize) -> usize {
1813    #[cfg(target_family = "wasm")]
1814    {
1815        let _ = (requested, frame_count);
1816        1
1817    }
1818    #[cfg(not(target_family = "wasm"))]
1819    {
1820        let workers = if requested == 0 {
1821            std::thread::available_parallelism()
1822                .map(usize::from)
1823                .unwrap_or(1)
1824        } else {
1825            requested
1826        };
1827        workers.clamp(1, frame_count.max(1))
1828    }
1829}
1830
1831#[cfg(not(target_family = "wasm"))]
1832fn build_reduction_worker_pool(
1833    worker_count: usize,
1834) -> Result<Option<ReductionThreadPool>, PoseReductionError> {
1835    (worker_count > 1)
1836        .then(|| {
1837            ThreadPoolBuilder::new()
1838                .num_threads(worker_count)
1839                .build()
1840                .map_err(|_| PoseReductionError::WorkerPool)
1841        })
1842        .transpose()
1843}
1844
1845#[cfg(target_family = "wasm")]
1846fn build_reduction_worker_pool(
1847    _worker_count: usize,
1848) -> Result<Option<ReductionThreadPool>, PoseReductionError> {
1849    Ok(None)
1850}
1851
1852fn fit_vmd_bone_interpolation(
1853    input: DensePoseSequenceView<'_>,
1854    bone: usize,
1855    start: usize,
1856    end: usize,
1857    translations: &[Vec3A],
1858    rotations: &[Quat],
1859) -> VmdBoneInterpolation {
1860    let bone_count = input.bone_count;
1861    let start_index = start * bone_count + bone;
1862    let end_index = end * bone_count + bone;
1863    let start_translation = translations[start_index];
1864    let end_translation = translations[end_index];
1865    let translation = std::array::from_fn(|axis| {
1866        let start_value = start_translation.to_array()[axis];
1867        let end_value = end_translation.to_array()[axis];
1868        fit_quantized_bezier(
1869            start,
1870            end,
1871            |sample| {
1872                let value = translations[sample * bone_count + bone].to_array()[axis];
1873                normalized_channel_value(start_value, end_value, value)
1874            },
1875            |sample| input.sample_frame(sample),
1876        )
1877    });
1878    let start_rotation = rotations[start_index];
1879    let end_rotation = rotations[end_index];
1880    let total_angle = quat_angle(start_rotation, end_rotation);
1881    let rotation = fit_quantized_bezier(
1882        start,
1883        end,
1884        |sample| {
1885            if total_angle <= f32::EPSILON {
1886                0.0
1887            } else {
1888                (quat_angle(start_rotation, rotations[sample * bone_count + bone]) / total_angle)
1889                    .clamp(0.0, 1.0)
1890            }
1891        },
1892        |sample| input.sample_frame(sample),
1893    );
1894    VmdBoneInterpolation {
1895        translation,
1896        rotation,
1897    }
1898}
1899
1900fn fit_quantized_bezier(
1901    start: usize,
1902    end: usize,
1903    value_at: impl Fn(usize) -> f32,
1904    frame_at: impl Fn(usize) -> f32,
1905) -> QuantizedBezier {
1906    if end <= start + 1 {
1907        return QuantizedBezier::LINEAR;
1908    }
1909    let mut best = QuantizedBezier::LINEAR;
1910    let score = |curve: QuantizedBezier| -> f32 {
1911        (start + 1..end)
1912            .map(|sample| {
1913                let time = segment_amount(start, end, sample, &frame_at);
1914                (curve.evaluate(time) - value_at(sample)).abs()
1915            })
1916            .fold(0.0f32, f32::max)
1917    };
1918    let mut best_score = score(best);
1919    const COARSE: [u8; 9] = [0, 16, 32, 48, 64, 80, 96, 112, 127];
1920    for &x1 in &COARSE {
1921        for &x2 in &COARSE {
1922            if x1 > x2 {
1923                continue;
1924            }
1925            for &y1 in &COARSE {
1926                for &y2 in &COARSE {
1927                    let candidate = QuantizedBezier { x1, y1, x2, y2 };
1928                    let candidate_score = score(candidate);
1929                    if candidate_score.total_cmp(&best_score) == Ordering::Less {
1930                        best = candidate;
1931                        best_score = candidate_score;
1932                    }
1933                }
1934            }
1935        }
1936    }
1937    for step in [32i16, 16, 8, 4, 2, 1] {
1938        loop {
1939            let origin = [best.x1, best.y1, best.x2, best.y2];
1940            let mut next_best = best;
1941            let mut next_score = best_score;
1942            for d0 in [-step, 0, step] {
1943                for d1 in [-step, 0, step] {
1944                    for d2 in [-step, 0, step] {
1945                        for d3 in [-step, 0, step] {
1946                            let offsets = [d0, d1, d2, d3];
1947                            let mut values = [0u8; 4];
1948                            let mut valid = true;
1949                            for coordinate in 0..4 {
1950                                let value = origin[coordinate] as i16 + offsets[coordinate];
1951                                if !(0..=127).contains(&value) {
1952                                    valid = false;
1953                                    break;
1954                                }
1955                                values[coordinate] = value as u8;
1956                            }
1957                            if !valid || values[0] > values[2] {
1958                                continue;
1959                            }
1960                            let candidate = QuantizedBezier {
1961                                x1: values[0],
1962                                y1: values[1],
1963                                x2: values[2],
1964                                y2: values[3],
1965                            };
1966                            let candidate_score = score(candidate);
1967                            if candidate_score.total_cmp(&next_score) == Ordering::Less {
1968                                next_best = candidate;
1969                                next_score = candidate_score;
1970                            }
1971                        }
1972                    }
1973                }
1974            }
1975            if next_best == best {
1976                break;
1977            }
1978            best = next_best;
1979            best_score = next_score;
1980        }
1981    }
1982    best
1983}
1984
1985fn normalized_channel_value(start: f32, end: f32, value: f32) -> f32 {
1986    let range = end - start;
1987    if range.abs() <= f32::EPSILON {
1988        0.0
1989    } else {
1990        ((value - start) / range).clamp(0.0, 1.0)
1991    }
1992}
1993
1994fn unwrap_euler_xyz(rotations: &[Quat], frame_count: usize, bone_count: usize) -> Vec<Vec3A> {
1995    let mut result = vec![Vec3A::ZERO; rotations.len()];
1996    for bone in 0..bone_count {
1997        for frame in 0..frame_count {
1998            let index = frame * bone_count + bone;
1999            let (x, y, z) = rotations[index].to_euler(EulerRot::XYZ);
2000            let mut value = Vec3A::new(x, y, z);
2001            if frame > 0 {
2002                let previous = result[index - bone_count];
2003                value.x = unwrap_angle(previous.x, value.x);
2004                value.y = unwrap_angle(previous.y, value.y);
2005                value.z = unwrap_angle(previous.z, value.z);
2006            }
2007            result[index] = value;
2008        }
2009    }
2010    result
2011}
2012
2013fn unwrap_angle(previous: f32, value: f32) -> f32 {
2014    let turns = ((previous - value) / std::f32::consts::TAU).round();
2015    value + turns * std::f32::consts::TAU
2016}
2017
2018fn fit_dcc_bone_segment(
2019    input: DensePoseSequenceView<'_>,
2020    bone: usize,
2021    start: usize,
2022    end: usize,
2023    translations: &[Vec3A],
2024    euler_xyz: &[Vec3A],
2025) -> DccCubicSegment {
2026    let bone_count = input.bone_count;
2027    let translation = std::array::from_fn::<_, 3, _>(|axis| {
2028        fit_dcc_scalar_segment(
2029            start,
2030            end,
2031            |sample| translations[sample * bone_count + bone].to_array()[axis],
2032            |sample| input.sample_frame(sample),
2033        )
2034    });
2035    let rotation = std::array::from_fn::<_, 3, _>(|axis| {
2036        fit_dcc_scalar_segment(
2037            start,
2038            end,
2039            |sample| euler_xyz[sample * bone_count + bone].to_array()[axis],
2040            |sample| input.sample_frame(sample),
2041        )
2042    });
2043    DccCubicSegment {
2044        translation_out_tangent: Vec3A::new(
2045            translation[0].out_tangent,
2046            translation[1].out_tangent,
2047            translation[2].out_tangent,
2048        ),
2049        translation_in_tangent: Vec3A::new(
2050            translation[0].in_tangent,
2051            translation[1].in_tangent,
2052            translation[2].in_tangent,
2053        ),
2054        rotation_start_euler_xyz: euler_xyz[start * bone_count + bone],
2055        rotation_end_euler_xyz: euler_xyz[end * bone_count + bone],
2056        rotation_out_tangent: Vec3A::new(
2057            rotation[0].out_tangent,
2058            rotation[1].out_tangent,
2059            rotation[2].out_tangent,
2060        ),
2061        rotation_in_tangent: Vec3A::new(
2062            rotation[0].in_tangent,
2063            rotation[1].in_tangent,
2064            rotation[2].in_tangent,
2065        ),
2066    }
2067}
2068
2069fn fit_dcc_scalar_segment(
2070    start: usize,
2071    end: usize,
2072    value_at: impl Fn(usize) -> f32,
2073    frame_at: impl Fn(usize) -> f32,
2074) -> DccScalarSegment {
2075    let duration = frame_at(end) - frame_at(start);
2076    let start_value = value_at(start);
2077    let end_value = value_at(end);
2078    let slope = (end_value - start_value) / duration;
2079    if end <= start + 1 {
2080        return DccScalarSegment {
2081            out_tangent: slope,
2082            in_tangent: slope,
2083        };
2084    }
2085
2086    let mut aa = 0.0;
2087    let mut ab = 0.0;
2088    let mut bb = 0.0;
2089    let mut ar = 0.0;
2090    let mut br = 0.0;
2091    for sample in start + 1..end {
2092        let t = segment_amount(start, end, sample, &frame_at);
2093        let (h00, h10, h01, h11) = hermite_basis(t);
2094        let a = h10 * duration;
2095        let b = h11 * duration;
2096        let residual = value_at(sample) - h00 * start_value - h01 * end_value;
2097        aa += a * a;
2098        ab += a * b;
2099        bb += b * b;
2100        ar += a * residual;
2101        br += b * residual;
2102    }
2103    let determinant = aa * bb - ab * ab;
2104    let (mut out_tangent, mut in_tangent) = if determinant.abs() > f32::EPSILON {
2105        (
2106            (ar * bb - br * ab) / determinant,
2107            (br * aa - ar * ab) / determinant,
2108        )
2109    } else {
2110        (slope, slope)
2111    };
2112    clamp_monotonic_tangents(slope, &mut out_tangent, &mut in_tangent);
2113    DccScalarSegment {
2114        out_tangent,
2115        in_tangent,
2116    }
2117}
2118
2119fn clamp_monotonic_tangents(slope: f32, out_tangent: &mut f32, in_tangent: &mut f32) {
2120    if slope.abs() <= f32::EPSILON {
2121        *out_tangent = 0.0;
2122        *in_tangent = 0.0;
2123        return;
2124    }
2125    if *out_tangent * slope < 0.0 {
2126        *out_tangent = 0.0;
2127    }
2128    if *in_tangent * slope < 0.0 {
2129        *in_tangent = 0.0;
2130    }
2131    let alpha = *out_tangent / slope;
2132    let beta = *in_tangent / slope;
2133    let length = alpha.hypot(beta);
2134    if length > 3.0 {
2135        let scale = 3.0 / length;
2136        *out_tangent = scale * alpha * slope;
2137        *in_tangent = scale * beta * slope;
2138    }
2139}
2140
2141fn hermite_basis(t: f32) -> (f32, f32, f32, f32) {
2142    let t2 = t * t;
2143    let t3 = t2 * t;
2144    (
2145        2.0 * t3 - 3.0 * t2 + 1.0,
2146        t3 - 2.0 * t2 + t,
2147        -2.0 * t3 + 3.0 * t2,
2148        t3 - t2,
2149    )
2150}
2151
2152fn sample_hermite(
2153    start: f32,
2154    end: f32,
2155    out_tangent: f32,
2156    in_tangent: f32,
2157    duration: f32,
2158    t: f32,
2159) -> f32 {
2160    let (h00, h10, h01, h11) = hermite_basis(t);
2161    h00 * start + h10 * duration * out_tangent + h01 * end + h11 * duration * in_tangent
2162}
2163
2164fn sample_dcc_vec3(
2165    start: Vec3A,
2166    end: Vec3A,
2167    out_tangent: Vec3A,
2168    in_tangent: Vec3A,
2169    duration: f32,
2170    t: f32,
2171) -> Vec3A {
2172    Vec3A::new(
2173        sample_hermite(start.x, end.x, out_tangent.x, in_tangent.x, duration, t),
2174        sample_hermite(start.y, end.y, out_tangent.y, in_tangent.y, duration, t),
2175        sample_hermite(start.z, end.z, out_tangent.z, in_tangent.z, duration, t),
2176    )
2177}
2178
2179fn sample_bone_track(
2180    track: &ReducedBoneTrack,
2181    sample_frames: &[f32],
2182    frame: f32,
2183    target: ReductionTarget,
2184) -> (Vec3A, Quat) {
2185    let upper = track
2186        .keys
2187        .partition_point(|key| sample_frames[key.sample_index] <= frame);
2188    if upper == 0 {
2189        return (track.keys[0].translation, track.keys[0].rotation);
2190    }
2191    if upper == track.keys.len() {
2192        let key = track.keys[track.keys.len() - 1];
2193        return (key.translation, key.rotation);
2194    }
2195    let left = track.keys[upper - 1];
2196    let right = track.keys[upper];
2197    let amount = ((frame - sample_frames[left.sample_index])
2198        / (sample_frames[right.sample_index] - sample_frames[left.sample_index]))
2199        .clamp(0.0, 1.0);
2200    if target == ReductionTarget::DccCubic {
2201        let duration = sample_frames[right.sample_index] - sample_frames[left.sample_index];
2202        let segment = right.dcc_segment;
2203        let translation = Vec3A::new(
2204            sample_hermite(
2205                left.translation.x,
2206                right.translation.x,
2207                segment.translation_out_tangent.x,
2208                segment.translation_in_tangent.x,
2209                duration,
2210                amount,
2211            ),
2212            sample_hermite(
2213                left.translation.y,
2214                right.translation.y,
2215                segment.translation_out_tangent.y,
2216                segment.translation_in_tangent.y,
2217                duration,
2218                amount,
2219            ),
2220            sample_hermite(
2221                left.translation.z,
2222                right.translation.z,
2223                segment.translation_out_tangent.z,
2224                segment.translation_in_tangent.z,
2225                duration,
2226                amount,
2227            ),
2228        );
2229        let euler = Vec3A::new(
2230            sample_hermite(
2231                segment.rotation_start_euler_xyz.x,
2232                segment.rotation_end_euler_xyz.x,
2233                segment.rotation_out_tangent.x,
2234                segment.rotation_in_tangent.x,
2235                duration,
2236                amount,
2237            ),
2238            sample_hermite(
2239                segment.rotation_start_euler_xyz.y,
2240                segment.rotation_end_euler_xyz.y,
2241                segment.rotation_out_tangent.y,
2242                segment.rotation_in_tangent.y,
2243                duration,
2244                amount,
2245            ),
2246            sample_hermite(
2247                segment.rotation_start_euler_xyz.z,
2248                segment.rotation_end_euler_xyz.z,
2249                segment.rotation_out_tangent.z,
2250                segment.rotation_in_tangent.z,
2251                duration,
2252                amount,
2253            ),
2254        );
2255        return (
2256            translation,
2257            normalize_quat(Quat::from_euler(EulerRot::XYZ, euler.x, euler.y, euler.z)),
2258        );
2259    }
2260    let translation_amount = if target == ReductionTarget::VmdBezier {
2261        Vec3A::new(
2262            right.vmd_interpolation.translation[0].evaluate(amount),
2263            right.vmd_interpolation.translation[1].evaluate(amount),
2264            right.vmd_interpolation.translation[2].evaluate(amount),
2265        )
2266    } else {
2267        Vec3A::splat(amount)
2268    };
2269    let rotation_amount = if target == ReductionTarget::VmdBezier {
2270        right.vmd_interpolation.rotation.evaluate(amount)
2271    } else {
2272        amount
2273    };
2274    (
2275        Vec3A::new(
2276            left.translation.x + (right.translation.x - left.translation.x) * translation_amount.x,
2277            left.translation.y + (right.translation.y - left.translation.y) * translation_amount.y,
2278            left.translation.z + (right.translation.z - left.translation.z) * translation_amount.z,
2279        ),
2280        normalize_quat(left.rotation.slerp(right.rotation, rotation_amount)),
2281    )
2282}
2283
2284fn sample_morph_track(
2285    track: &ReducedMorphTrack,
2286    sample_frames: &[f32],
2287    frame: f32,
2288    target: ReductionTarget,
2289) -> f32 {
2290    let upper = track
2291        .keys
2292        .partition_point(|key| sample_frames[key.sample_index] <= frame);
2293    if upper == 0 {
2294        return track.keys[0].weight;
2295    }
2296    if upper == track.keys.len() {
2297        return track.keys[track.keys.len() - 1].weight;
2298    }
2299    let left = track.keys[upper - 1];
2300    let right = track.keys[upper];
2301    let amount = ((frame - sample_frames[left.sample_index])
2302        / (sample_frames[right.sample_index] - sample_frames[left.sample_index]))
2303        .clamp(0.0, 1.0);
2304    if target == ReductionTarget::DccCubic {
2305        let duration = sample_frames[right.sample_index] - sample_frames[left.sample_index];
2306        return sample_hermite(
2307            left.weight,
2308            right.weight,
2309            right.dcc_segment.out_tangent,
2310            right.dcc_segment.in_tangent,
2311            duration,
2312            amount,
2313        );
2314    }
2315    left.weight + (right.weight - left.weight) * amount
2316}
2317
2318fn build_world_matrices_into(
2319    snapshot: &SkeletonSnapshot,
2320    translations: &[Vec3A],
2321    rotations: &[Quat],
2322    result: &mut Vec<Mat4>,
2323) {
2324    result.resize(snapshot.bone_count(), Mat4::IDENTITY);
2325    for &bone in snapshot.evaluation_order.iter() {
2326        let local = Mat4::from_rotation_translation(rotations[bone], translations[bone].into());
2327        result[bone] = if snapshot.parent_indices[bone] < 0 {
2328            local
2329        } else {
2330            result[snapshot.parent_indices[bone] as usize] * local
2331        };
2332    }
2333}
2334
2335fn build_evaluation_order(parents: &[i32]) -> Result<Vec<usize>, PoseReductionError> {
2336    fn visit(
2337        bone: usize,
2338        parents: &[i32],
2339        state: &mut [u8],
2340        order: &mut Vec<usize>,
2341    ) -> Result<(), PoseReductionError> {
2342        match state[bone] {
2343            1 => return Err(PoseReductionError::SkeletonCycle { bone }),
2344            2 => return Ok(()),
2345            _ => {}
2346        }
2347        state[bone] = 1;
2348        if parents[bone] >= 0 {
2349            visit(parents[bone] as usize, parents, state, order)?;
2350        }
2351        state[bone] = 2;
2352        order.push(bone);
2353        Ok(())
2354    }
2355    let mut state = vec![0u8; parents.len()];
2356    let mut order = Vec::with_capacity(parents.len());
2357    for bone in 0..parents.len() {
2358        visit(bone, parents, &mut state, &mut order)?;
2359    }
2360    Ok(order)
2361}
2362
2363fn normalize_quat(value: Quat) -> Quat {
2364    if value.length_squared() <= f32::EPSILON {
2365        Quat::IDENTITY
2366    } else {
2367        value.normalize()
2368    }
2369}
2370
2371fn quat_is_finite(value: Quat) -> bool {
2372    value.to_array().iter().all(|value| value.is_finite()) && value.length_squared() > f32::EPSILON
2373}
2374
2375fn quat_angle(a: Quat, b: Quat) -> f32 {
2376    let a = normalize_quat(a);
2377    let mut b = normalize_quat(b);
2378    if a.dot(b) < 0.0 {
2379        b = -b;
2380    }
2381    // For unit quaternions, the hemisphere-aligned chord length is
2382    // `2 * sin(theta / 4)`. Unlike `acos(dot)`, this remains stable for tiny
2383    // angles and returns exactly zero for identical f32 quaternions.
2384    let half_chord = ((a - b).length() * 0.5).clamp(0.0, 1.0);
2385    4.0 * half_chord.asin()
2386}
2387
2388fn normalized_error(error: f32, tolerance: f32) -> f32 {
2389    if tolerance == 0.0 {
2390        if error == 0.0 { 0.0 } else { f32::INFINITY }
2391    } else {
2392        error / tolerance
2393    }
2394}
2395
2396fn segment_amount(start: usize, end: usize, sample: usize, frame_at: impl Fn(usize) -> f32) -> f32 {
2397    let start_frame = frame_at(start);
2398    (frame_at(sample) - start_frame) / (frame_at(end) - start_frame)
2399}
2400
2401fn is_worse(current: Option<(f32, usize)>, error: f32, frame: usize) -> bool {
2402    current.is_none_or(|(best, best_frame)| error > best || (error == best && frame < best_frame))
2403}
2404
2405#[cfg(test)]
2406mod tests;