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;
16const 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, Copy, Default, PartialEq, Eq)]
344struct ReductionKeyProvenance {
345 bits: u8,
346}
347
348impl ReductionKeyProvenance {
349 const ENDPOINT: u8 = 1 << 0;
350 const LOCAL_PREFIT: u8 = 1 << 1;
351 const VALIDATION_FAILURE: u8 = 1 << 2;
352 const ANCESTOR_PROPAGATION: u8 = 1 << 3;
353
354 fn endpoint() -> Self {
355 Self {
356 bits: Self::ENDPOINT,
357 }
358 }
359
360 fn local_prefit() -> Self {
361 Self {
362 bits: Self::LOCAL_PREFIT,
363 }
364 }
365
366 fn validation_failure() -> Self {
367 Self {
368 bits: Self::VALIDATION_FAILURE,
369 }
370 }
371
372 fn ancestor_propagation() -> Self {
373 Self {
374 bits: Self::ANCESTOR_PROPAGATION,
375 }
376 }
377
378 fn is_endpoint(self) -> bool {
379 self.bits & Self::ENDPOINT != 0
380 }
381
382 fn is_local_prefit(self) -> bool {
383 self.bits & Self::LOCAL_PREFIT != 0
384 }
385
386 fn is_validation_failure(self) -> bool {
387 self.bits & Self::VALIDATION_FAILURE != 0
388 }
389
390 fn is_ancestor_propagation(self) -> bool {
391 self.bits & Self::ANCESTOR_PROPAGATION != 0
392 }
393}
394
395#[derive(Debug, Clone, PartialEq)]
396pub struct ReducedBoneTrack {
397 keys: Box<[ReducedBoneKey]>,
398}
399
400impl ReducedBoneTrack {
401 pub fn keys(&self) -> &[ReducedBoneKey] {
402 &self.keys
403 }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq)]
407pub struct ReducedMorphKey {
408 pub sample_index: usize,
409 pub weight: f32,
410 pub dcc_segment: DccScalarSegment,
411}
412
413#[derive(Debug, Clone, PartialEq)]
414pub struct ReducedMorphTrack {
415 keys: Box<[ReducedMorphKey]>,
416}
417
418impl ReducedMorphTrack {
419 pub fn keys(&self) -> &[ReducedMorphKey] {
420 &self.keys
421 }
422}
423
424#[derive(Debug, Clone, Copy, Default, PartialEq)]
425pub struct PoseReductionReport {
426 pub source_bone_key_count: usize,
427 pub reduced_bone_key_count: usize,
428 pub source_morph_key_count: usize,
429 pub reduced_morph_key_count: usize,
430 pub max_local_position_error: f32,
431 pub max_local_rotation_error_radians: f32,
432 pub max_world_position_error: f32,
433 pub max_world_rotation_error_radians: f32,
434 pub max_morph_weight_error: f32,
435}
436
437#[derive(Debug, Clone, Default, PartialEq, Eq)]
438pub struct ReductionWorkStats {
439 pub global_validation_passes: usize,
440 pub candidate_rebuilds: usize,
441 pub candidate_bone_track_rebuilds: usize,
442 pub candidate_morph_track_rebuilds: usize,
443 pub local_prefit_bone_segment_fits: usize,
444 pub local_prefit_morph_segment_fits: usize,
445 pub local_prefit_bone_key_additions: usize,
446 pub local_prefit_morph_key_additions: usize,
447 pub local_prefit_bone_samples: usize,
448 pub local_prefit_morph_samples: usize,
449 pub dcc_bone_segment_fits: usize,
450 pub dcc_morph_segment_fits: usize,
451 pub bone_samples: usize,
452 pub morph_samples: usize,
453 pub world_rebuilds: usize,
454 pub world_bone_recomputes: usize,
456 pub world_rotation_decompositions: usize,
457 pub normal_key_additions: usize,
458 pub ancestor_key_additions: usize,
459 pub ancestor_prune_attempts: usize,
461 pub ancestor_pruned_keys: usize,
463 pub added_keys_per_pass: Vec<usize>,
464}
465
466#[derive(Debug, Clone, Copy, PartialEq, Eq)]
467enum ValidationMode {
468 Incremental,
469 FullScan,
470}
471
472#[derive(Debug, Clone)]
473struct DirtyRanges {
474 bone_local: Vec<Vec<std::ops::RangeInclusive<usize>>>,
475 morph: Vec<Vec<std::ops::RangeInclusive<usize>>>,
476}
477
478impl DirtyRanges {
479 fn full(frame_count: usize, bone_count: usize, morph_count: usize) -> Self {
480 let range = || vec![0..=frame_count.saturating_sub(1)];
481 Self {
482 bone_local: (0..bone_count).map(|_| range()).collect(),
483 morph: (0..morph_count).map(|_| range()).collect(),
484 }
485 }
486
487 fn empty(bone_count: usize, morph_count: usize) -> Self {
488 Self {
489 bone_local: vec![Vec::new(); bone_count],
490 morph: vec![Vec::new(); morph_count],
491 }
492 }
493
494 fn mark_bone(&mut self, bone: usize, range: std::ops::RangeInclusive<usize>) {
495 insert_dirty_range(&mut self.bone_local[bone], range);
496 }
497
498 fn mark_morph(&mut self, morph: usize, range: std::ops::RangeInclusive<usize>) {
499 insert_dirty_range(&mut self.morph[morph], range);
500 }
501}
502
503fn insert_dirty_range(
504 target: &mut Vec<std::ops::RangeInclusive<usize>>,
505 range: std::ops::RangeInclusive<usize>,
506) {
507 target.push(range);
508 target.sort_by_key(|value| *value.start());
509 let mut merged: Vec<std::ops::RangeInclusive<usize>> = Vec::with_capacity(target.len());
510 for current in target.drain(..) {
511 if let Some(previous) = merged.last_mut()
512 && *current.start() <= previous.end().saturating_add(1)
513 {
514 let start = *previous.start();
515 *previous = start..=(*previous.end()).max(*current.end());
516 } else {
517 merged.push(current);
518 }
519 }
520 *target = merged;
521}
522
523#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
524pub struct ReductionTimings {
525 pub local_prefit: Duration,
526 pub candidate_build: Duration,
527 pub error_measure: Duration,
528 pub dcc_fit: Duration,
529 pub ancestor_prune: Duration,
531}
532
533#[cfg(not(target_family = "wasm"))]
534type ReductionThreadPool = ThreadPool;
535
536#[cfg(target_family = "wasm")]
537struct ReductionThreadPool;
538
539#[derive(Debug, Clone)]
540pub struct ReducedPoseSequence {
541 snapshot: SkeletonSnapshot,
542 target: ReductionTarget,
543 start_frame: f32,
544 frame_step: f32,
545 frame_count: usize,
546 sample_frames: Box<[f32]>,
547 bone_tracks: Box<[ReducedBoneTrack]>,
548 morph_tracks: Box<[ReducedMorphTrack]>,
549 report: PoseReductionReport,
550 work_stats: ReductionWorkStats,
551 timings: ReductionTimings,
552}
553
554impl PartialEq for ReducedPoseSequence {
555 fn eq(&self, other: &Self) -> bool {
556 self.snapshot == other.snapshot
557 && self.target == other.target
558 && self.start_frame == other.start_frame
559 && self.frame_step == other.frame_step
560 && self.frame_count == other.frame_count
561 && self.sample_frames == other.sample_frames
562 && self.bone_tracks == other.bone_tracks
563 && self.morph_tracks == other.morph_tracks
564 && self.report == other.report
565 && self.work_stats == other.work_stats
566 }
567}
568
569impl ReducedPoseSequence {
570 pub fn snapshot(&self) -> &SkeletonSnapshot {
571 &self.snapshot
572 }
573 pub fn target(&self) -> ReductionTarget {
574 self.target
575 }
576 pub fn start_frame(&self) -> f32 {
577 self.start_frame
578 }
579 pub fn frame_step(&self) -> f32 {
580 self.frame_step
581 }
582 pub fn frame_count(&self) -> usize {
583 self.frame_count
584 }
585 pub fn sample_frames(&self) -> &[f32] {
586 &self.sample_frames
587 }
588 pub fn bone_tracks(&self) -> &[ReducedBoneTrack] {
589 &self.bone_tracks
590 }
591 pub fn morph_tracks(&self) -> &[ReducedMorphTrack] {
592 &self.morph_tracks
593 }
594 pub fn report(&self) -> PoseReductionReport {
595 self.report
596 }
597 pub fn work_stats(&self) -> &ReductionWorkStats {
598 &self.work_stats
599 }
600 pub fn timings(&self) -> ReductionTimings {
601 self.timings
602 }
603
604 pub fn sample(&self, frame: f32) -> Result<ReducedPoseSample, PoseReductionError> {
605 let mut scratch = ReducedPoseScratch::default();
606 self.sample_into(frame, &mut scratch)?;
607 Ok(ReducedPoseSample {
608 local_translations: scratch.local_translations,
609 local_rotations: scratch.local_rotations,
610 world_matrices: scratch.world_matrices,
611 morph_weights: scratch.morph_weights,
612 })
613 }
614
615 fn sample_into(
616 &self,
617 frame: f32,
618 scratch: &mut ReducedPoseScratch,
619 ) -> Result<(), PoseReductionError> {
620 if !frame.is_finite() {
621 return Err(PoseReductionError::InvalidSampleTime);
622 }
623 scratch.prepare(self.snapshot.bone_count(), self.snapshot.morph_count());
624 for (bone, track) in self.bone_tracks.iter().enumerate() {
625 let (translation, rotation) =
626 sample_bone_track(track, &self.sample_frames, frame, self.target);
627 scratch.local_translations[bone] = translation;
628 scratch.local_rotations[bone] = rotation;
629 }
630 for (morph, track) in self.morph_tracks.iter().enumerate() {
631 scratch.morph_weights[morph] =
632 sample_morph_track(track, &self.sample_frames, frame, self.target);
633 }
634 build_world_matrices_into(
635 &self.snapshot,
636 &scratch.local_translations,
637 &scratch.local_rotations,
638 &mut scratch.world_matrices,
639 );
640 Ok(())
641 }
642
643 pub fn validate_model(
644 &self,
645 model_identity: u64,
646 bone_count: usize,
647 morph_count: usize,
648 ) -> bool {
649 self.snapshot.model_identity == model_identity
650 && self.snapshot.bone_count() == bone_count
651 && self.snapshot.morph_count == morph_count
652 }
653}
654
655#[derive(Debug, Default)]
656struct ReducedPoseScratch {
657 local_translations: Vec<Vec3A>,
658 local_rotations: Vec<Quat>,
659 world_matrices: Vec<Mat4>,
660 morph_weights: Vec<f32>,
661}
662
663impl ReducedPoseScratch {
664 fn prepare(&mut self, bone_count: usize, morph_count: usize) {
665 self.local_translations.resize(bone_count, Vec3A::ZERO);
666 self.local_rotations.resize(bone_count, Quat::IDENTITY);
667 self.world_matrices.resize(bone_count, Mat4::IDENTITY);
668 self.morph_weights.resize(morph_count, 0.0);
669 }
670}
671
672#[derive(Debug, Clone, PartialEq)]
673pub struct ReducedPoseSample {
674 pub local_translations: Vec<Vec3A>,
675 pub local_rotations: Vec<Quat>,
676 pub world_matrices: Vec<Mat4>,
677 pub morph_weights: Vec<f32>,
678}
679
680#[derive(Debug, Error, Clone, PartialEq)]
681pub enum PoseReductionError {
682 #[error("dense pose sequence must contain at least one frame")]
683 EmptySequence,
684 #[error("skeleton must contain at least one bone")]
685 EmptySkeleton,
686 #[error("invalid or non-positive dense pose time base")]
687 InvalidTimeBase,
688 #[error("dense pose buffer length overflow")]
689 LengthOverflow,
690 #[error("world matrix count {actual} does not match expected {expected}")]
691 InvalidWorldMatrixCount { actual: usize, expected: usize },
692 #[error("morph weight count {actual} does not match expected {expected}")]
693 InvalidMorphWeightCount { actual: usize, expected: usize },
694 #[error("skeleton arrays have inconsistent lengths")]
695 InvalidSkeletonLengths,
696 #[error("bone {bone} has invalid parent index {parent}")]
697 InvalidParent { bone: usize, parent: i32 },
698 #[error("skeleton hierarchy contains a cycle at bone {bone}")]
699 SkeletonCycle { bone: usize },
700 #[error("bone {bone} has non-finite rest data")]
701 NonFiniteSkeleton { bone: usize },
702 #[error("dense pose skeleton counts do not match snapshot")]
703 SnapshotMismatch,
704 #[error("reduction tolerance must be finite and non-negative")]
705 InvalidTolerance,
706 #[error("frame {frame}, bone {bone} contains a non-finite matrix")]
707 NonFiniteMatrix { frame: usize, bone: usize },
708 #[error("frame {frame}, bone {bone} contains scale or shear")]
709 ScaleOrShear { frame: usize, bone: usize },
710 #[error("frame {frame}, bone {bone} has a singular parent transform")]
711 SingularParent { frame: usize, bone: usize },
712 #[error("frame {frame}, morph {morph} contains a non-finite weight")]
713 NonFiniteMorph { frame: usize, morph: usize },
714 #[error("sample time must be finite")]
715 InvalidSampleTime,
716 #[error("requested tolerance cannot be attained at source frame {frame}")]
717 ToleranceUnattainable { frame: usize },
718 #[error("failed to create pose reduction worker pool")]
719 WorkerPool,
720}
721
722pub fn reduce_dense_pose_sequence(
723 input: DensePoseSequenceView<'_>,
724 snapshot: SkeletonSnapshot,
725 tolerances: ReductionTolerances,
726 target: ReductionTarget,
727) -> Result<ReducedPoseSequence, PoseReductionError> {
728 reduce_dense_pose_sequence_with_worker_count(input, snapshot, tolerances, target, 0)
729}
730
731pub fn reduce_dense_pose_sequence_with_worker_count(
732 input: DensePoseSequenceView<'_>,
733 snapshot: SkeletonSnapshot,
734 tolerances: ReductionTolerances,
735 target: ReductionTarget,
736 worker_count: usize,
737) -> Result<ReducedPoseSequence, PoseReductionError> {
738 reduce_dense_pose_sequence_internal(
739 input,
740 snapshot,
741 tolerances,
742 target,
743 worker_count,
744 ValidationMode::Incremental,
745 )
746}
747
748fn reduce_dense_pose_sequence_internal(
749 input: DensePoseSequenceView<'_>,
750 snapshot: SkeletonSnapshot,
751 tolerances: ReductionTolerances,
752 target: ReductionTarget,
753 worker_count: usize,
754 validation_mode: ValidationMode,
755) -> Result<ReducedPoseSequence, PoseReductionError> {
756 let tolerances = tolerances.validate()?;
757 if input.bone_count != snapshot.bone_count() || input.morph_count != snapshot.morph_count() {
758 return Err(PoseReductionError::SnapshotMismatch);
759 }
760
761 let (local_translations, local_rotations) = decompose_dense_pose(input, &snapshot)?;
762 let (world_positions, world_rotations) = cache_dense_world_components(input)?;
763 let mut work_stats = ReductionWorkStats {
764 world_rotation_decompositions: input.frame_count * input.bone_count,
765 ..Default::default()
766 };
767 let mut timings = ReductionTimings::default();
768 let worker_count = resolve_reduction_worker_count(worker_count, input.frame_count);
769 let worker_pool = build_reduction_worker_pool(worker_count)?;
770 let local_euler_xyz = unwrap_euler_xyz(&local_rotations, input.frame_count, input.bone_count);
771 for frame in 0..input.frame_count {
772 for morph in 0..input.morph_count {
773 if !input.morph_weight(frame, morph).is_finite() {
774 return Err(PoseReductionError::NonFiniteMorph { frame, morph });
775 }
776 }
777 }
778
779 let mut bone_key_indices = vec![endpoint_indices(input.frame_count); input.bone_count];
780 let mut morph_key_indices = vec![endpoint_indices(input.frame_count); input.morph_count];
781
782 if target == ReductionTarget::DccCubic && input.frame_count >= DCC_LOCAL_PREFIT_MIN_FRAMES {
783 let dcc_prefit_started = reduction_timer_start();
784 for (bone, keys) in bone_key_indices.iter_mut().enumerate() {
785 let prefit = split_dcc_bone_track(
786 keys,
787 input,
788 bone,
789 &local_translations,
790 &local_rotations,
791 &local_euler_xyz,
792 tolerances,
793 );
794 work_stats.local_prefit_bone_segment_fits += prefit.segment_fits;
795 work_stats.local_prefit_bone_key_additions += prefit.key_additions;
796 work_stats.local_prefit_bone_samples += prefit.samples;
797 }
798 for (morph, keys) in morph_key_indices.iter_mut().enumerate() {
799 let prefit = split_dcc_morph_track(keys, input, morph, tolerances.morph_weight);
800 work_stats.local_prefit_morph_segment_fits += prefit.segment_fits;
801 work_stats.local_prefit_morph_key_additions += prefit.key_additions;
802 work_stats.local_prefit_morph_samples += prefit.samples;
803 }
804 timings.local_prefit += reduction_timer_elapsed(dcc_prefit_started);
805 } else if target == ReductionTarget::LinearSlerp {
806 for (bone, keys) in bone_key_indices.iter_mut().enumerate() {
807 split_bone_track(
808 keys,
809 input,
810 bone,
811 &local_translations,
812 &local_rotations,
813 tolerances,
814 );
815 }
816 }
817 if target != ReductionTarget::DccCubic {
818 for (morph, keys) in morph_key_indices.iter_mut().enumerate() {
819 split_morph_track(keys, input, morph, tolerances.morph_weight);
820 }
821 }
822 let mut bone_key_provenance = bone_key_indices
823 .iter()
824 .map(|indices| initial_key_provenance(indices, input.frame_count))
825 .collect::<Vec<_>>();
826 let mut morph_key_provenance = morph_key_indices
827 .iter()
828 .map(|indices| initial_key_provenance(indices, input.frame_count))
829 .collect::<Vec<_>>();
830
831 let mut candidate: Option<ReducedPoseSequence> = None;
832 let mut ancestor_insertions = Vec::<(usize, usize)>::new();
835 let mut dirty = DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count);
836 let mut validation_cache =
837 ValidationCache::new(input.frame_count, input.bone_count, input.morph_count);
838 loop {
839 work_stats.global_validation_passes += 1;
840 work_stats.candidate_rebuilds += 1;
841 if candidate.is_some() && matches!(validation_mode, ValidationMode::FullScan) {
842 dirty = DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count);
843 }
844 let candidate_started = reduction_timer_start();
845 let local_pose = DenseLocalPose {
846 translations: &local_translations,
847 rotations: &local_rotations,
848 euler_xyz: &local_euler_xyz,
849 };
850 if let Some(sequence) = candidate.as_mut() {
851 rebuild_dirty_tracks(
852 sequence,
853 target,
854 input,
855 local_pose,
856 &bone_key_indices,
857 &morph_key_indices,
858 &dirty,
859 ReductionInstrumentation {
860 work_stats: &mut work_stats,
861 timings: &mut timings,
862 },
863 );
864 } else {
865 candidate = Some(build_sequence(
866 &snapshot,
867 target,
868 input,
869 local_pose,
870 &bone_key_indices,
871 &morph_key_indices,
872 ReductionInstrumentation {
873 work_stats: &mut work_stats,
874 timings: &mut timings,
875 },
876 ));
877 }
878 timings.candidate_build += reduction_timer_elapsed(candidate_started);
879 let error_measure_started = reduction_timer_start();
880 let (report, worst_by_track) = measure_error_cached(
881 candidate.as_ref().expect("candidate initialized"),
882 input,
883 DenseValidationPose {
884 translations: &local_translations,
885 rotations: &local_rotations,
886 world_positions: &world_positions,
887 world_rotations: &world_rotations,
888 },
889 tolerances,
890 &mut work_stats,
891 &dirty,
892 &mut validation_cache,
893 worker_pool.as_ref(),
894 worker_count,
895 )?;
896 timings.error_measure += reduction_timer_elapsed(error_measure_started);
897 let mut failing = worst_by_track
898 .into_iter()
899 .flatten()
900 .filter(|worst| worst.normalized_error > 1.0)
901 .collect::<Vec<_>>();
902 failing.sort_by(compare_worst_errors);
903 if failing.is_empty() {
904 work_stats.added_keys_per_pass.push(0);
905 let mut result = candidate.expect("candidate initialized");
906 let mut final_report = report;
907 if !ancestor_insertions.is_empty() {
908 let prune_started = reduction_timer_start();
909 let pruned = prune_ancestor_keys(
910 &mut result,
911 input,
912 local_pose,
913 DenseValidationPose {
914 translations: &local_translations,
915 rotations: &local_rotations,
916 world_positions: &world_positions,
917 world_rotations: &world_rotations,
918 },
919 tolerances,
920 &mut bone_key_indices,
921 &mut bone_key_provenance,
922 &ancestor_insertions,
923 &mut validation_cache,
924 &mut work_stats,
925 )?;
926 timings.ancestor_prune += reduction_timer_elapsed(prune_started);
927 if pruned > 0 {
928 let (pruned_report, _) = measure_error_cached(
932 &result,
933 input,
934 DenseValidationPose {
935 translations: &local_translations,
936 rotations: &local_rotations,
937 world_positions: &world_positions,
938 world_rotations: &world_rotations,
939 },
940 tolerances,
941 &mut work_stats,
942 &DirtyRanges::full(input.frame_count, input.bone_count, input.morph_count),
943 &mut validation_cache,
944 worker_pool.as_ref(),
945 worker_count,
946 )?;
947 final_report = pruned_report;
948 }
949 }
950 result.report = PoseReductionReport {
951 source_bone_key_count: input.frame_count * input.bone_count,
952 reduced_bone_key_count: result
953 .bone_tracks
954 .iter()
955 .map(|track| track.keys.len())
956 .sum(),
957 source_morph_key_count: input.frame_count * input.morph_count,
958 reduced_morph_key_count: result
959 .morph_tracks
960 .iter()
961 .map(|track| track.keys.len())
962 .sum(),
963 ..final_report
964 };
965 result.work_stats = work_stats;
966 result.timings = timings;
967 return Ok(result);
968 }
969 let mut inserted = false;
970 let mut added_this_pass = 0;
971 let mut next_dirty = DirtyRanges::empty(input.bone_count, input.morph_count);
972 let first_failure_frame = failing[0].frame;
973 for worst in failing {
974 match worst.track {
975 ErrorTrack::Bone(bone) => {
976 let mut cursor = Some(bone);
977 let mut is_origin = true;
978 while let Some(index) = cursor {
979 if let Some((range, position)) = insert_key_with_affected_range(
980 &mut bone_key_indices[index],
981 worst.frame,
982 ) {
983 inserted = true;
984 added_this_pass += 1;
985 next_dirty.mark_bone(index, range);
986 if is_origin {
987 work_stats.normal_key_additions += 1;
988 bone_key_provenance[index]
989 .insert(position, ReductionKeyProvenance::validation_failure());
990 } else {
991 work_stats.ancestor_key_additions += 1;
992 ancestor_insertions.push((index, worst.frame));
993 bone_key_provenance[index].insert(
994 position,
995 ReductionKeyProvenance::ancestor_propagation(),
996 );
997 }
998 }
999 let parent = snapshot.parent_indices[index];
1000 cursor = (parent >= 0).then_some(parent as usize);
1001 is_origin = false;
1002 }
1003 }
1004 ErrorTrack::Morph(morph) => {
1005 if let Some((range, position)) =
1006 insert_key_with_affected_range(&mut morph_key_indices[morph], worst.frame)
1007 {
1008 inserted = true;
1009 added_this_pass += 1;
1010 next_dirty.mark_morph(morph, range);
1011 work_stats.normal_key_additions += 1;
1012 morph_key_provenance[morph]
1013 .insert(position, ReductionKeyProvenance::validation_failure());
1014 }
1015 }
1016 }
1017 }
1018 work_stats.added_keys_per_pass.push(added_this_pass);
1019 if !inserted {
1020 return Err(PoseReductionError::ToleranceUnattainable {
1021 frame: first_failure_frame,
1022 });
1023 }
1024 dirty = next_dirty;
1025 }
1026}
1027
1028fn endpoint_indices(frame_count: usize) -> Vec<usize> {
1029 if frame_count == 1 {
1030 vec![0]
1031 } else {
1032 vec![0, frame_count - 1]
1033 }
1034}
1035
1036fn initial_key_provenance(indices: &[usize], frame_count: usize) -> Vec<ReductionKeyProvenance> {
1037 indices
1038 .iter()
1039 .map(|&frame| {
1040 if frame == 0 || frame + 1 == frame_count {
1041 ReductionKeyProvenance::endpoint()
1042 } else {
1043 ReductionKeyProvenance::local_prefit()
1044 }
1045 })
1046 .collect()
1047}
1048
1049fn decompose_dense_pose(
1050 input: DensePoseSequenceView<'_>,
1051 snapshot: &SkeletonSnapshot,
1052) -> Result<(Vec<Vec3A>, Vec<Quat>), PoseReductionError> {
1053 let mut translations = vec![Vec3A::ZERO; input.frame_count * input.bone_count];
1054 let mut rotations = vec![Quat::IDENTITY; input.frame_count * input.bone_count];
1055 for frame in 0..input.frame_count {
1056 for &bone in snapshot.evaluation_order.iter() {
1057 let world = input.world_matrix(frame, bone);
1058 validate_finite_matrix(world, frame, bone)?;
1059 let local = if snapshot.parent_indices[bone] < 0 {
1060 world
1061 } else {
1062 let parent = snapshot.parent_indices[bone] as usize;
1063 let parent_world = input.world_matrix(frame, parent);
1064 validate_finite_matrix(parent_world, frame, parent)?;
1065 let determinant = Mat3::from_mat4(parent_world).determinant();
1066 if !determinant.is_finite() || determinant.abs() <= f32::EPSILON {
1067 return Err(PoseReductionError::SingularParent { frame, bone });
1068 }
1069 parent_world.inverse() * world
1070 };
1071 let (translation, rotation) = decompose_rigid(local, frame, bone)?;
1072 let index = frame * input.bone_count + bone;
1073 translations[index] = translation;
1074 rotations[index] = if frame > 0 {
1075 let previous = rotations[index - input.bone_count];
1076 if previous.dot(rotation) < 0.0 {
1077 -rotation
1078 } else {
1079 rotation
1080 }
1081 } else {
1082 rotation
1083 };
1084 }
1085 }
1086 Ok((translations, rotations))
1087}
1088
1089fn cache_dense_world_components(
1090 input: DensePoseSequenceView<'_>,
1091) -> Result<(Vec<Vec3A>, Vec<Quat>), PoseReductionError> {
1092 let count = input.frame_count * input.bone_count;
1093 let mut positions = Vec::with_capacity(count);
1094 let mut rotations = Vec::with_capacity(count);
1095 for frame in 0..input.frame_count {
1096 for bone in 0..input.bone_count {
1097 let world = input.world_matrix(frame, bone);
1098 positions.push(Vec3A::from(world.w_axis.truncate()));
1099 rotations.push(decompose_rigid(world, frame, bone)?.1);
1100 }
1101 }
1102 Ok((positions, rotations))
1103}
1104
1105fn decompose_rigid(
1106 matrix: Mat4,
1107 frame: usize,
1108 bone: usize,
1109) -> Result<(Vec3A, Quat), PoseReductionError> {
1110 validate_finite_matrix(matrix, frame, bone)?;
1111 let x = matrix.x_axis.truncate();
1112 let y = matrix.y_axis.truncate();
1113 let z = matrix.z_axis.truncate();
1114 let unit = |v: Vec3| (v.length_squared() - 1.0).abs() <= AFFINE_EPSILON;
1115 if !unit(x)
1116 || !unit(y)
1117 || !unit(z)
1118 || x.dot(y).abs() > AFFINE_EPSILON
1119 || x.dot(z).abs() > AFFINE_EPSILON
1120 || y.dot(z).abs() > AFFINE_EPSILON
1121 || Mat3::from_cols(x, y, z).determinant() < 0.0
1122 || matrix.x_axis.w.abs() > AFFINE_EPSILON
1123 || matrix.y_axis.w.abs() > AFFINE_EPSILON
1124 || matrix.z_axis.w.abs() > AFFINE_EPSILON
1125 || (matrix.w_axis.w - 1.0).abs() > AFFINE_EPSILON
1126 {
1127 return Err(PoseReductionError::ScaleOrShear { frame, bone });
1128 }
1129 let rotation = normalize_quat(Quat::from_mat3(&Mat3::from_cols(x, y, z)));
1130 Ok((Vec3A::from(matrix.w_axis.truncate()), rotation))
1131}
1132
1133fn validate_finite_matrix(
1134 matrix: Mat4,
1135 frame: usize,
1136 bone: usize,
1137) -> Result<(), PoseReductionError> {
1138 if matrix
1139 .to_cols_array()
1140 .iter()
1141 .any(|value| !value.is_finite())
1142 {
1143 Err(PoseReductionError::NonFiniteMatrix { frame, bone })
1144 } else {
1145 Ok(())
1146 }
1147}
1148
1149fn split_bone_track(
1150 keys: &mut Vec<usize>,
1151 input: DensePoseSequenceView<'_>,
1152 bone: usize,
1153 translations: &[Vec3A],
1154 rotations: &[Quat],
1155 tolerances: ReductionTolerances,
1156) {
1157 if input.frame_count <= 2 {
1158 return;
1159 }
1160 let bone_count = translations.len() / input.frame_count;
1161 let mut stack = vec![(0usize, input.frame_count - 1)];
1162 while let Some((start, end)) = stack.pop() {
1163 if end <= start + 1 {
1164 continue;
1165 }
1166 let start_index = start * bone_count + bone;
1167 let end_index = end * bone_count + bone;
1168 let mut worst: Option<(f32, usize)> = None;
1169 for frame in start + 1..end {
1170 let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1171 let index = frame * bone_count + bone;
1172 let position_error = translations[index]
1173 .distance(translations[start_index].lerp(translations[end_index], amount));
1174 let rotation_error = quat_angle(
1175 rotations[index],
1176 rotations[start_index].slerp(rotations[end_index], amount),
1177 );
1178 let normalized = normalized_error(position_error, tolerances.local_position).max(
1179 normalized_error(rotation_error, tolerances.local_rotation_radians),
1180 );
1181 if is_worse(worst, normalized, frame) {
1182 worst = Some((normalized, frame));
1183 }
1184 }
1185 if let Some((normalized, frame)) = worst.filter(|value| value.0 > 1.0) {
1186 let _ = normalized;
1187 insert_key(keys, frame);
1188 stack.push((frame, end));
1189 stack.push((start, frame));
1190 }
1191 }
1192}
1193
1194#[derive(Default)]
1195struct LocalPrefitStats {
1196 segment_fits: usize,
1197 key_additions: usize,
1198 samples: usize,
1199}
1200
1201fn split_dcc_bone_track(
1202 keys: &mut Vec<usize>,
1203 input: DensePoseSequenceView<'_>,
1204 bone: usize,
1205 translations: &[Vec3A],
1206 rotations: &[Quat],
1207 euler_xyz: &[Vec3A],
1208 tolerances: ReductionTolerances,
1209) -> LocalPrefitStats {
1210 if input.frame_count <= 2 {
1211 return LocalPrefitStats::default();
1212 }
1213 let bone_count = input.bone_count;
1214 let mut stats = LocalPrefitStats::default();
1215 let mut stack = vec![(0usize, input.frame_count - 1)];
1216 while let Some((start, end)) = stack.pop() {
1217 if end <= start + 1 {
1218 continue;
1219 }
1220 let segment = fit_dcc_bone_segment(input, bone, start, end, translations, euler_xyz);
1221 stats.segment_fits += 1;
1222 let start_index = start * bone_count + bone;
1223 let end_index = end * bone_count + bone;
1224 let duration = input.sample_frame(end) - input.sample_frame(start);
1225 let mut worst: Option<(f32, usize)> = None;
1226 for frame in start + 1..end {
1227 stats.samples += 1;
1228 let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1229 let translation = sample_dcc_vec3(
1230 translations[start_index],
1231 translations[end_index],
1232 segment.translation_out_tangent,
1233 segment.translation_in_tangent,
1234 duration,
1235 amount,
1236 );
1237 let euler = sample_dcc_vec3(
1238 segment.rotation_start_euler_xyz,
1239 segment.rotation_end_euler_xyz,
1240 segment.rotation_out_tangent,
1241 segment.rotation_in_tangent,
1242 duration,
1243 amount,
1244 );
1245 let rotation =
1246 normalize_quat(Quat::from_euler(EulerRot::XYZ, euler.x, euler.y, euler.z));
1247 let index = frame * bone_count + bone;
1248 let normalized = normalized_error(
1249 translations[index].distance(translation),
1250 tolerances.local_position,
1251 )
1252 .max(normalized_error(
1253 quat_angle(rotations[index], rotation),
1254 tolerances.local_rotation_radians,
1255 ));
1256 if is_worse(worst, normalized, frame) {
1257 worst = Some((normalized, frame));
1258 }
1259 }
1260 if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1261 stats.key_additions += usize::from(insert_key(keys, frame));
1262 stack.push((frame, end));
1263 stack.push((start, frame));
1264 }
1265 }
1266 stats
1267}
1268
1269fn split_dcc_morph_track(
1270 keys: &mut Vec<usize>,
1271 input: DensePoseSequenceView<'_>,
1272 morph: usize,
1273 tolerance: f32,
1274) -> LocalPrefitStats {
1275 if input.frame_count <= 2 {
1276 return LocalPrefitStats::default();
1277 }
1278 let mut stats = LocalPrefitStats::default();
1279 let mut stack = vec![(0usize, input.frame_count - 1)];
1280 while let Some((start, end)) = stack.pop() {
1281 if end <= start + 1 {
1282 continue;
1283 }
1284 let segment = fit_dcc_scalar_segment(
1285 start,
1286 end,
1287 |sample| input.morph_weight(sample, morph),
1288 |sample| input.sample_frame(sample),
1289 );
1290 stats.segment_fits += 1;
1291 let duration = input.sample_frame(end) - input.sample_frame(start);
1292 let mut worst: Option<(f32, usize)> = None;
1293 for frame in start + 1..end {
1294 stats.samples += 1;
1295 let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1296 let expected = sample_hermite(
1297 input.morph_weight(start, morph),
1298 input.morph_weight(end, morph),
1299 segment.out_tangent,
1300 segment.in_tangent,
1301 duration,
1302 amount,
1303 );
1304 let normalized = normalized_error(
1305 (input.morph_weight(frame, morph) - expected).abs(),
1306 tolerance,
1307 );
1308 if is_worse(worst, normalized, frame) {
1309 worst = Some((normalized, frame));
1310 }
1311 }
1312 if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1313 stats.key_additions += usize::from(insert_key(keys, frame));
1314 stack.push((frame, end));
1315 stack.push((start, frame));
1316 }
1317 }
1318 stats
1319}
1320
1321fn split_morph_track(
1322 keys: &mut Vec<usize>,
1323 input: DensePoseSequenceView<'_>,
1324 morph: usize,
1325 tolerance: f32,
1326) {
1327 if input.frame_count <= 2 {
1328 return;
1329 }
1330 let mut stack = vec![(0usize, input.frame_count - 1)];
1331 while let Some((start, end)) = stack.pop() {
1332 if end <= start + 1 {
1333 continue;
1334 }
1335 let mut worst: Option<(f32, usize)> = None;
1336 for frame in start + 1..end {
1337 let amount = segment_amount(start, end, frame, |sample| input.sample_frame(sample));
1338 let expected = input.morph_weight(start, morph)
1339 + (input.morph_weight(end, morph) - input.morph_weight(start, morph)) * amount;
1340 let normalized = normalized_error(
1341 (input.morph_weight(frame, morph) - expected).abs(),
1342 tolerance,
1343 );
1344 if is_worse(worst, normalized, frame) {
1345 worst = Some((normalized, frame));
1346 }
1347 }
1348 if let Some((_, frame)) = worst.filter(|value| value.0 > 1.0) {
1349 insert_key(keys, frame);
1350 stack.push((frame, end));
1351 stack.push((start, frame));
1352 }
1353 }
1354}
1355
1356fn insert_key(keys: &mut Vec<usize>, frame: usize) -> bool {
1357 if let Err(position) = keys.binary_search(&frame) {
1358 keys.insert(position, frame);
1359 true
1360 } else {
1361 false
1362 }
1363}
1364
1365fn insert_key_with_affected_range(
1366 keys: &mut Vec<usize>,
1367 frame: usize,
1368) -> Option<(std::ops::RangeInclusive<usize>, usize)> {
1369 let position = keys.binary_search(&frame).err()?;
1370 let start = keys[position.saturating_sub(1)];
1371 let end = keys[position.min(keys.len() - 1)];
1372 keys.insert(position, frame);
1373 Some((start..=end, position))
1374}
1375
1376#[derive(Clone, Copy)]
1377struct DenseLocalPose<'a> {
1378 translations: &'a [Vec3A],
1379 rotations: &'a [Quat],
1380 euler_xyz: &'a [Vec3A],
1381}
1382
1383struct ReductionInstrumentation<'a> {
1384 work_stats: &'a mut ReductionWorkStats,
1385 timings: &'a mut ReductionTimings,
1386}
1387
1388#[allow(clippy::too_many_arguments)]
1389fn build_sequence(
1390 snapshot: &SkeletonSnapshot,
1391 target: ReductionTarget,
1392 input: DensePoseSequenceView<'_>,
1393 local_pose: DenseLocalPose<'_>,
1394 bone_key_indices: &[Vec<usize>],
1395 morph_key_indices: &[Vec<usize>],
1396 instrumentation: ReductionInstrumentation<'_>,
1397) -> ReducedPoseSequence {
1398 let ReductionInstrumentation {
1399 work_stats,
1400 timings,
1401 } = instrumentation;
1402 if target == ReductionTarget::DccCubic {
1403 work_stats.dcc_bone_segment_fits += bone_key_indices
1404 .iter()
1405 .map(|indices| indices.len().saturating_sub(1))
1406 .sum::<usize>();
1407 work_stats.dcc_morph_segment_fits += morph_key_indices
1408 .iter()
1409 .map(|indices| indices.len().saturating_sub(1))
1410 .sum::<usize>();
1411 }
1412 work_stats.candidate_bone_track_rebuilds += bone_key_indices.len();
1413 work_stats.candidate_morph_track_rebuilds += morph_key_indices.len();
1414 let dcc_bone_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1415 let bone_tracks = bone_key_indices
1416 .iter()
1417 .enumerate()
1418 .map(|(bone, indices)| build_bone_track(target, input, local_pose, bone, indices))
1419 .collect::<Vec<_>>()
1420 .into_boxed_slice();
1421 if let Some(started) = dcc_bone_started {
1422 timings.dcc_fit += reduction_timer_elapsed(started);
1423 }
1424 let dcc_morph_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1425 let morph_tracks = morph_key_indices
1426 .iter()
1427 .enumerate()
1428 .map(|(morph, indices)| build_morph_track(target, input, morph, indices))
1429 .collect::<Vec<_>>()
1430 .into_boxed_slice();
1431 if let Some(started) = dcc_morph_started {
1432 timings.dcc_fit += reduction_timer_elapsed(started);
1433 }
1434 ReducedPoseSequence {
1435 snapshot: snapshot.clone(),
1436 target,
1437 start_frame: input.start_frame,
1438 frame_step: input.frame_step,
1439 frame_count: input.frame_count,
1440 sample_frames: (0..input.frame_count)
1441 .map(|sample| input.sample_frame(sample))
1442 .collect::<Vec<_>>()
1443 .into_boxed_slice(),
1444 bone_tracks,
1445 morph_tracks,
1446 report: PoseReductionReport::default(),
1447 work_stats: ReductionWorkStats::default(),
1448 timings: ReductionTimings::default(),
1449 }
1450}
1451
1452fn build_bone_track(
1453 target: ReductionTarget,
1454 input: DensePoseSequenceView<'_>,
1455 local_pose: DenseLocalPose<'_>,
1456 bone: usize,
1457 indices: &[usize],
1458) -> ReducedBoneTrack {
1459 ReducedBoneTrack {
1460 keys: indices
1461 .iter()
1462 .enumerate()
1463 .map(|(key_position, &frame)| {
1464 let index = frame * input.bone_count + bone;
1465 ReducedBoneKey {
1466 sample_index: frame,
1467 translation: local_pose.translations[index],
1468 rotation: local_pose.rotations[index],
1469 vmd_interpolation: if target == ReductionTarget::VmdBezier && key_position > 0 {
1470 fit_vmd_bone_interpolation(
1471 input,
1472 bone,
1473 indices[key_position - 1],
1474 frame,
1475 local_pose.translations,
1476 local_pose.rotations,
1477 )
1478 } else {
1479 VmdBoneInterpolation::LINEAR
1480 },
1481 dcc_segment: if target == ReductionTarget::DccCubic && key_position > 0 {
1482 fit_dcc_bone_segment(
1483 input,
1484 bone,
1485 indices[key_position - 1],
1486 frame,
1487 local_pose.translations,
1488 local_pose.euler_xyz,
1489 )
1490 } else {
1491 DccCubicSegment::default()
1492 },
1493 }
1494 })
1495 .collect::<Vec<_>>()
1496 .into_boxed_slice(),
1497 }
1498}
1499
1500fn build_morph_track(
1501 target: ReductionTarget,
1502 input: DensePoseSequenceView<'_>,
1503 morph: usize,
1504 indices: &[usize],
1505) -> ReducedMorphTrack {
1506 ReducedMorphTrack {
1507 keys: indices
1508 .iter()
1509 .enumerate()
1510 .map(|(key_position, &frame)| ReducedMorphKey {
1511 sample_index: frame,
1512 weight: input.morph_weight(frame, morph),
1513 dcc_segment: if target == ReductionTarget::DccCubic && key_position > 0 {
1514 fit_dcc_scalar_segment(
1515 indices[key_position - 1],
1516 frame,
1517 |sample| input.morph_weight(sample, morph),
1518 |sample| input.sample_frame(sample),
1519 )
1520 } else {
1521 DccScalarSegment::default()
1522 },
1523 })
1524 .collect::<Vec<_>>()
1525 .into_boxed_slice(),
1526 }
1527}
1528
1529#[allow(clippy::too_many_arguments)]
1530fn rebuild_dirty_tracks(
1531 sequence: &mut ReducedPoseSequence,
1532 target: ReductionTarget,
1533 input: DensePoseSequenceView<'_>,
1534 local_pose: DenseLocalPose<'_>,
1535 bone_key_indices: &[Vec<usize>],
1536 morph_key_indices: &[Vec<usize>],
1537 dirty: &DirtyRanges,
1538 instrumentation: ReductionInstrumentation<'_>,
1539) {
1540 let ReductionInstrumentation {
1541 work_stats,
1542 timings,
1543 } = instrumentation;
1544 let dcc_started = (target == ReductionTarget::DccCubic).then(reduction_timer_start);
1545 for (bone, range) in dirty.bone_local.iter().enumerate() {
1546 if range.is_empty() {
1547 continue;
1548 }
1549 let indices = &bone_key_indices[bone];
1550 work_stats.candidate_bone_track_rebuilds += 1;
1551 if target == ReductionTarget::DccCubic {
1552 work_stats.dcc_bone_segment_fits += indices.len().saturating_sub(1);
1553 }
1554 sequence.bone_tracks[bone] = build_bone_track(target, input, local_pose, bone, indices);
1555 }
1556 for (morph, range) in dirty.morph.iter().enumerate() {
1557 if range.is_empty() {
1558 continue;
1559 }
1560 let indices = &morph_key_indices[morph];
1561 work_stats.candidate_morph_track_rebuilds += 1;
1562 if target == ReductionTarget::DccCubic {
1563 work_stats.dcc_morph_segment_fits += indices.len().saturating_sub(1);
1564 }
1565 sequence.morph_tracks[morph] = build_morph_track(target, input, morph, indices);
1566 }
1567 if let Some(started) = dcc_started {
1568 timings.dcc_fit += reduction_timer_elapsed(started);
1569 }
1570}
1571
1572#[allow(clippy::too_many_arguments)]
1577fn prune_ancestor_keys(
1578 sequence: &mut ReducedPoseSequence,
1579 input: DensePoseSequenceView<'_>,
1580 local_pose: DenseLocalPose<'_>,
1581 dense: DenseValidationPose<'_>,
1582 tolerances: ReductionTolerances,
1583 bone_key_indices: &mut [Vec<usize>],
1584 bone_key_provenance: &mut [Vec<ReductionKeyProvenance>],
1585 ancestor_insertions: &[(usize, usize)],
1586 cache: &mut ValidationCache,
1587 work_stats: &mut ReductionWorkStats,
1588) -> Result<usize, PoseReductionError> {
1589 let subtrees = build_subtree_lists(&sequence.snapshot);
1590 let mut pruned = 0;
1591 let mut subtree_worlds = vec![Mat4::IDENTITY; sequence.snapshot.bone_count()];
1592
1593 for &(bone, frame) in ancestor_insertions.iter().rev() {
1594 let Some(position) = bone_key_indices[bone].binary_search(&frame).ok() else {
1595 continue;
1596 };
1597 let provenance = bone_key_provenance[bone][position];
1598 if !provenance.is_ancestor_propagation()
1601 || provenance.is_endpoint()
1602 || provenance.is_local_prefit()
1603 || provenance.is_validation_failure()
1604 || position == 0
1605 || position + 1 >= bone_key_indices[bone].len()
1606 {
1607 continue;
1608 }
1609
1610 work_stats.ancestor_prune_attempts += 1;
1611 let start = bone_key_indices[bone][position - 1];
1612 let end = bone_key_indices[bone][position + 1];
1613 let mut replacement_indices = bone_key_indices[bone].clone();
1614 replacement_indices.remove(position);
1615 let mut replacement_provenance = bone_key_provenance[bone].clone();
1616 replacement_provenance.remove(position);
1617 let replacement = build_bone_track_without_key(sequence, input, local_pose, bone, position);
1618 work_stats.candidate_bone_track_rebuilds += 1;
1619 if sequence.target == ReductionTarget::DccCubic {
1620 work_stats.dcc_bone_segment_fits += 1;
1621 }
1622
1623 let interval = start..=end;
1624 if !validate_ancestor_prune_candidate(
1625 sequence,
1626 input,
1627 dense,
1628 tolerances,
1629 bone,
1630 &subtrees[bone],
1631 interval.clone(),
1632 &replacement,
1633 cache,
1634 &mut subtree_worlds,
1635 work_stats,
1636 )? {
1637 continue;
1638 }
1639
1640 bone_key_indices[bone] = replacement_indices;
1641 bone_key_provenance[bone] = replacement_provenance;
1642 sequence.bone_tracks[bone] = replacement;
1643 apply_ancestor_prune_candidate(
1644 sequence,
1645 input,
1646 dense,
1647 bone,
1648 &subtrees[bone],
1649 interval,
1650 cache,
1651 &mut subtree_worlds,
1652 work_stats,
1653 )?;
1654 work_stats.ancestor_pruned_keys += 1;
1655 pruned += 1;
1656 }
1657 Ok(pruned)
1658}
1659
1660fn build_bone_track_without_key(
1661 sequence: &ReducedPoseSequence,
1662 input: DensePoseSequenceView<'_>,
1663 local_pose: DenseLocalPose<'_>,
1664 bone: usize,
1665 position: usize,
1666) -> ReducedBoneTrack {
1667 let mut keys = sequence.bone_tracks[bone].keys.to_vec();
1668 keys.remove(position);
1669 if position < keys.len() {
1670 let start = keys[position - 1].sample_index;
1671 let end = keys[position].sample_index;
1672 keys[position].vmd_interpolation = if sequence.target == ReductionTarget::VmdBezier {
1673 fit_vmd_bone_interpolation(
1674 input,
1675 bone,
1676 start,
1677 end,
1678 local_pose.translations,
1679 local_pose.rotations,
1680 )
1681 } else {
1682 VmdBoneInterpolation::LINEAR
1683 };
1684 keys[position].dcc_segment = if sequence.target == ReductionTarget::DccCubic {
1685 fit_dcc_bone_segment(
1686 input,
1687 bone,
1688 start,
1689 end,
1690 local_pose.translations,
1691 local_pose.euler_xyz,
1692 )
1693 } else {
1694 DccCubicSegment::default()
1695 };
1696 }
1697 ReducedBoneTrack {
1698 keys: keys.into_boxed_slice(),
1699 }
1700}
1701
1702fn build_subtree_lists(snapshot: &SkeletonSnapshot) -> Vec<Vec<usize>> {
1703 (0..snapshot.bone_count())
1704 .map(|root| {
1705 snapshot
1706 .evaluation_order
1707 .iter()
1708 .copied()
1709 .filter(|&bone| is_descendant_or_self(snapshot, bone, root))
1710 .collect()
1711 })
1712 .collect()
1713}
1714
1715fn is_descendant_or_self(snapshot: &SkeletonSnapshot, mut bone: usize, root: usize) -> bool {
1716 loop {
1717 if bone == root {
1718 return true;
1719 }
1720 let parent = snapshot.parent_indices[bone];
1721 if parent < 0 {
1722 return false;
1723 }
1724 bone = parent as usize;
1725 }
1726}
1727
1728#[allow(clippy::too_many_arguments)]
1729fn validate_ancestor_prune_candidate(
1730 sequence: &ReducedPoseSequence,
1731 input: DensePoseSequenceView<'_>,
1732 dense: DenseValidationPose<'_>,
1733 tolerances: ReductionTolerances,
1734 bone: usize,
1735 subtree: &[usize],
1736 interval: std::ops::RangeInclusive<usize>,
1737 replacement: &ReducedBoneTrack,
1738 cache: &ValidationCache,
1739 subtree_worlds: &mut [Mat4],
1740 work_stats: &mut ReductionWorkStats,
1741) -> Result<bool, PoseReductionError> {
1742 for frame in interval {
1743 let (translation, rotation) = sample_bone_track(
1744 replacement,
1745 &sequence.sample_frames,
1746 input.sample_frame(frame),
1747 sequence.target,
1748 );
1749 work_stats.bone_samples += 1;
1750 let index = frame * input.bone_count + bone;
1751 if dense.translations[index].distance(translation) > tolerances.local_position
1752 || quat_angle(dense.rotations[index], rotation) > tolerances.local_rotation_radians
1753 {
1754 return Ok(false);
1755 }
1756
1757 for ¤t in subtree {
1758 let current_index = frame * input.bone_count + current;
1759 let (local_translation, local_rotation) = if current == bone {
1760 (translation, rotation)
1761 } else {
1762 (
1763 cache.local_translations[current_index],
1764 cache.local_rotations[current_index],
1765 )
1766 };
1767 let local = Mat4::from_rotation_translation(local_rotation, local_translation.into());
1768 let parent = sequence.snapshot.parent_indices[current];
1769 let world = if parent < 0 {
1770 local
1771 } else if is_descendant_or_self(&sequence.snapshot, parent as usize, bone) {
1772 subtree_worlds[parent as usize] * local
1773 } else {
1774 cache.world_matrices[frame * input.bone_count + parent as usize] * local
1775 };
1776 subtree_worlds[current] = world;
1777 let world_rotation = decompose_rigid(world, frame, current)?.1;
1778 work_stats.world_bone_recomputes += 1;
1779 work_stats.world_rotation_decompositions += 1;
1780 if dense.world_positions[current_index].distance(Vec3A::from(world.w_axis.truncate()))
1781 > tolerances.world_position
1782 || quat_angle(dense.world_rotations[current_index], world_rotation)
1783 > tolerances.world_rotation_radians
1784 {
1785 return Ok(false);
1786 }
1787 }
1788 work_stats.world_rebuilds += 1;
1789 }
1790 Ok(true)
1791}
1792
1793#[allow(clippy::too_many_arguments)]
1794fn apply_ancestor_prune_candidate(
1795 sequence: &ReducedPoseSequence,
1796 input: DensePoseSequenceView<'_>,
1797 dense: DenseValidationPose<'_>,
1798 bone: usize,
1799 subtree: &[usize],
1800 interval: std::ops::RangeInclusive<usize>,
1801 cache: &mut ValidationCache,
1802 subtree_worlds: &mut [Mat4],
1803 work_stats: &mut ReductionWorkStats,
1804) -> Result<(), PoseReductionError> {
1805 for frame in interval {
1806 let index = frame * input.bone_count + bone;
1807 let (translation, rotation) = sample_bone_track(
1808 &sequence.bone_tracks[bone],
1809 &sequence.sample_frames,
1810 input.sample_frame(frame),
1811 sequence.target,
1812 );
1813 work_stats.bone_samples += 1;
1814 cache.local_translations[index] = translation;
1815 cache.local_rotations[index] = rotation;
1816 cache.bone_errors[index].local_position = dense.translations[index].distance(translation);
1817 cache.bone_errors[index].local_rotation = quat_angle(dense.rotations[index], rotation);
1818
1819 for ¤t in subtree {
1820 let current_index = frame * input.bone_count + current;
1821 let (local_translation, local_rotation) = if current == bone {
1822 (translation, rotation)
1823 } else {
1824 (
1825 cache.local_translations[current_index],
1826 cache.local_rotations[current_index],
1827 )
1828 };
1829 let local = Mat4::from_rotation_translation(local_rotation, local_translation.into());
1830 let parent = sequence.snapshot.parent_indices[current];
1831 let world = if parent < 0 {
1832 local
1833 } else if is_descendant_or_self(&sequence.snapshot, parent as usize, bone) {
1834 subtree_worlds[parent as usize] * local
1835 } else {
1836 cache.world_matrices[frame * input.bone_count + parent as usize] * local
1837 };
1838 subtree_worlds[current] = world;
1839 let world_rotation = decompose_rigid(world, frame, current)?.1;
1840 work_stats.world_bone_recomputes += 1;
1841 work_stats.world_rotation_decompositions += 1;
1842 cache.world_matrices[current_index] = world;
1843 cache.world_rotations[current_index] = world_rotation;
1844 cache.bone_errors[current_index].world_position =
1845 dense.world_positions[current_index].distance(Vec3A::from(world.w_axis.truncate()));
1846 cache.bone_errors[current_index].world_rotation =
1847 quat_angle(dense.world_rotations[current_index], world_rotation);
1848 }
1849 work_stats.world_rebuilds += 1;
1850 }
1851 Ok(())
1852}
1853
1854#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1855enum ErrorTrack {
1856 Bone(usize),
1857 Morph(usize),
1858}
1859
1860#[derive(Clone, Copy)]
1861struct WorstError {
1862 normalized_error: f32,
1863 frame: usize,
1864 track: ErrorTrack,
1865}
1866
1867#[derive(Clone, Copy)]
1868struct DenseValidationPose<'a> {
1869 translations: &'a [Vec3A],
1870 rotations: &'a [Quat],
1871 world_positions: &'a [Vec3A],
1872 world_rotations: &'a [Quat],
1873}
1874
1875#[derive(Clone, Copy, Default)]
1876struct BoneErrorCell {
1877 local_position: f32,
1878 local_rotation: f32,
1879 world_position: f32,
1880 world_rotation: f32,
1881}
1882
1883struct ValidationCache {
1884 local_translations: Vec<Vec3A>,
1885 local_rotations: Vec<Quat>,
1886 world_matrices: Vec<Mat4>,
1887 world_rotations: Vec<Quat>,
1888 morph_weights: Vec<f32>,
1889 bone_errors: Vec<BoneErrorCell>,
1890 morph_errors: Vec<f32>,
1891}
1892
1893#[cfg(not(target_family = "wasm"))]
1894struct ValidationCacheChunk {
1895 start_frame: usize,
1896 local_translations: Vec<Vec3A>,
1897 local_rotations: Vec<Quat>,
1898 world_matrices: Vec<Mat4>,
1899 world_rotations: Vec<Quat>,
1900 morph_weights: Vec<f32>,
1901 bone_errors: Vec<BoneErrorCell>,
1902 morph_errors: Vec<f32>,
1903}
1904
1905impl ValidationCache {
1906 fn new(frame_count: usize, bone_count: usize, morph_count: usize) -> Self {
1907 Self {
1908 local_translations: vec![Vec3A::ZERO; frame_count * bone_count],
1909 local_rotations: vec![Quat::IDENTITY; frame_count * bone_count],
1910 world_matrices: vec![Mat4::IDENTITY; frame_count * bone_count],
1911 world_rotations: vec![Quat::IDENTITY; frame_count * bone_count],
1912 morph_weights: vec![0.0; frame_count * morph_count],
1913 bone_errors: vec![BoneErrorCell::default(); frame_count * bone_count],
1914 morph_errors: vec![0.0; frame_count * morph_count],
1915 }
1916 }
1917}
1918
1919#[cfg(not(target_family = "wasm"))]
1920#[allow(clippy::too_many_arguments)]
1921fn refresh_full_validation_cache_parallel(
1922 sequence: &ReducedPoseSequence,
1923 input: DensePoseSequenceView<'_>,
1924 dense: DenseValidationPose<'_>,
1925 cache: &mut ValidationCache,
1926 work_stats: &mut ReductionWorkStats,
1927 worker_pool: &ReductionThreadPool,
1928 worker_count: usize,
1929) -> Result<(), PoseReductionError> {
1930 let chunk_size = input.frame_count.div_ceil(worker_count);
1931 let ranges = (0..worker_count)
1932 .map(|worker| {
1933 let start = worker * chunk_size;
1934 start..(start + chunk_size).min(input.frame_count)
1935 })
1936 .filter(|range| !range.is_empty())
1937 .collect::<Vec<_>>();
1938 let chunks = worker_pool.install(|| {
1939 ranges
1940 .par_iter()
1941 .map(|range| {
1942 let frame_count = range.end - range.start;
1943 let mut chunk = ValidationCacheChunk {
1944 start_frame: range.start,
1945 local_translations: Vec::with_capacity(frame_count * input.bone_count),
1946 local_rotations: Vec::with_capacity(frame_count * input.bone_count),
1947 world_matrices: Vec::with_capacity(frame_count * input.bone_count),
1948 world_rotations: Vec::with_capacity(frame_count * input.bone_count),
1949 morph_weights: Vec::with_capacity(frame_count * input.morph_count),
1950 bone_errors: Vec::with_capacity(frame_count * input.bone_count),
1951 morph_errors: Vec::with_capacity(frame_count * input.morph_count),
1952 };
1953 let mut scratch = ReducedPoseScratch::default();
1954 scratch.prepare(input.bone_count, input.morph_count);
1955 for frame in range.clone() {
1956 sequence.sample_into(input.sample_frame(frame), &mut scratch)?;
1957 chunk
1958 .local_translations
1959 .extend_from_slice(&scratch.local_translations);
1960 chunk
1961 .local_rotations
1962 .extend_from_slice(&scratch.local_rotations);
1963 chunk
1964 .world_matrices
1965 .extend_from_slice(&scratch.world_matrices);
1966 for bone in 0..input.bone_count {
1967 let index = frame * input.bone_count + bone;
1968 let world_rotation =
1969 decompose_rigid(scratch.world_matrices[bone], frame, bone)?.1;
1970 chunk.world_rotations.push(world_rotation);
1971 chunk.bone_errors.push(BoneErrorCell {
1972 local_position: dense.translations[index]
1973 .distance(scratch.local_translations[bone]),
1974 local_rotation: quat_angle(
1975 dense.rotations[index],
1976 scratch.local_rotations[bone],
1977 ),
1978 world_position: dense.world_positions[index].distance(Vec3A::from(
1979 scratch.world_matrices[bone].w_axis.truncate(),
1980 )),
1981 world_rotation: quat_angle(
1982 dense.world_rotations[index],
1983 world_rotation,
1984 ),
1985 });
1986 }
1987 chunk
1988 .morph_weights
1989 .extend_from_slice(&scratch.morph_weights);
1990 for morph in 0..input.morph_count {
1991 chunk.morph_errors.push(
1992 (input.morph_weight(frame, morph) - scratch.morph_weights[morph]).abs(),
1993 );
1994 }
1995 }
1996 Ok(chunk)
1997 })
1998 .collect::<Result<Vec<_>, PoseReductionError>>()
1999 })?;
2000
2001 for chunk in chunks {
2002 let bone_start = chunk.start_frame * input.bone_count;
2003 let bone_end = bone_start + chunk.local_translations.len();
2004 cache.local_translations[bone_start..bone_end].copy_from_slice(&chunk.local_translations);
2005 cache.local_rotations[bone_start..bone_end].copy_from_slice(&chunk.local_rotations);
2006 cache.world_matrices[bone_start..bone_end].copy_from_slice(&chunk.world_matrices);
2007 cache.world_rotations[bone_start..bone_end].copy_from_slice(&chunk.world_rotations);
2008 cache.bone_errors[bone_start..bone_end].copy_from_slice(&chunk.bone_errors);
2009 let morph_start = chunk.start_frame * input.morph_count;
2010 let morph_end = morph_start + chunk.morph_weights.len();
2011 cache.morph_weights[morph_start..morph_end].copy_from_slice(&chunk.morph_weights);
2012 cache.morph_errors[morph_start..morph_end].copy_from_slice(&chunk.morph_errors);
2013 }
2014 work_stats.bone_samples += input.frame_count * input.bone_count;
2015 work_stats.morph_samples += input.frame_count * input.morph_count;
2016 work_stats.world_rebuilds += input.frame_count;
2017 work_stats.world_bone_recomputes += input.frame_count * input.bone_count;
2018 work_stats.world_rotation_decompositions += input.frame_count * input.bone_count;
2019 Ok(())
2020}
2021
2022#[allow(clippy::too_many_arguments)]
2023fn measure_error_cached(
2024 sequence: &ReducedPoseSequence,
2025 input: DensePoseSequenceView<'_>,
2026 dense: DenseValidationPose<'_>,
2027 tolerances: ReductionTolerances,
2028 work_stats: &mut ReductionWorkStats,
2029 dirty: &DirtyRanges,
2030 cache: &mut ValidationCache,
2031 worker_pool: Option<&ReductionThreadPool>,
2032 worker_count: usize,
2033) -> Result<(PoseReductionReport, Vec<Option<WorstError>>), PoseReductionError> {
2034 let full_dirty = dirty_ranges_are_full(
2035 dirty,
2036 input.frame_count,
2037 input.bone_count,
2038 input.morph_count,
2039 );
2040 #[cfg(not(target_family = "wasm"))]
2041 let refreshed_in_parallel = if full_dirty && worker_count > 1 {
2042 refresh_full_validation_cache_parallel(
2043 sequence,
2044 input,
2045 dense,
2046 cache,
2047 work_stats,
2048 worker_pool.expect("multi-worker reduction has a pool"),
2049 worker_count,
2050 )?;
2051 true
2052 } else {
2053 false
2054 };
2055 #[cfg(target_family = "wasm")]
2056 let refreshed_in_parallel = {
2057 let _ = (full_dirty, worker_pool, worker_count);
2058 false
2059 };
2060
2061 let mut world_dirty = dirty.bone_local.clone();
2062 for &bone in sequence.snapshot.evaluation_order.iter() {
2063 let parent = sequence.snapshot.parent_indices[bone];
2064 if parent >= 0 {
2065 for range in world_dirty[parent as usize].clone() {
2066 insert_dirty_range(&mut world_dirty[bone], range);
2067 }
2068 }
2069 }
2070
2071 if !refreshed_in_parallel {
2072 for (bone, ranges) in dirty.bone_local.iter().enumerate() {
2073 for range in ranges {
2074 for frame in range.clone() {
2075 let index = frame * input.bone_count + bone;
2076 let (translation, rotation) = sample_bone_track(
2077 &sequence.bone_tracks[bone],
2078 &sequence.sample_frames,
2079 input.sample_frame(frame),
2080 sequence.target,
2081 );
2082 cache.local_translations[index] = translation;
2083 cache.local_rotations[index] = rotation;
2084 let cell = &mut cache.bone_errors[index];
2085 cell.local_position = dense.translations[index].distance(translation);
2086 cell.local_rotation = quat_angle(dense.rotations[index], rotation);
2087 work_stats.bone_samples += 1;
2088 }
2089 }
2090 }
2091 for (morph, ranges) in dirty.morph.iter().enumerate() {
2092 for range in ranges {
2093 for frame in range.clone() {
2094 let index = frame * input.morph_count + morph;
2095 let sampled = sample_morph_track(
2096 &sequence.morph_tracks[morph],
2097 &sequence.sample_frames,
2098 input.sample_frame(frame),
2099 sequence.target,
2100 );
2101 cache.morph_weights[index] = sampled;
2102 cache.morph_errors[index] = (input.morph_weight(frame, morph) - sampled).abs();
2103 work_stats.morph_samples += 1;
2104 }
2105 }
2106 }
2107
2108 let mut rebuilt_frames = vec![false; input.frame_count];
2109 for &bone in sequence.snapshot.evaluation_order.iter() {
2110 for range in &world_dirty[bone] {
2111 for frame in range.clone() {
2112 rebuilt_frames[frame] = true;
2113 let index = frame * input.bone_count + bone;
2114 let local = Mat4::from_rotation_translation(
2115 cache.local_rotations[index],
2116 cache.local_translations[index].into(),
2117 );
2118 let parent = sequence.snapshot.parent_indices[bone];
2119 cache.world_matrices[index] = if parent < 0 {
2120 local
2121 } else {
2122 cache.world_matrices[frame * input.bone_count + parent as usize] * local
2123 };
2124 cache.world_rotations[index] =
2125 decompose_rigid(cache.world_matrices[index], frame, bone)?.1;
2126 let cell = &mut cache.bone_errors[index];
2127 cell.world_position = dense.world_positions[index]
2128 .distance(Vec3A::from(cache.world_matrices[index].w_axis.truncate()));
2129 cell.world_rotation =
2130 quat_angle(dense.world_rotations[index], cache.world_rotations[index]);
2131 work_stats.world_bone_recomputes += 1;
2132 work_stats.world_rotation_decompositions += 1;
2133 }
2134 }
2135 }
2136 work_stats.world_rebuilds += rebuilt_frames
2137 .into_iter()
2138 .filter(|rebuilt| *rebuilt)
2139 .count();
2140 }
2141
2142 let mut report = PoseReductionReport::default();
2143 let mut worst = vec![None; input.bone_count + input.morph_count];
2144 for frame in 0..input.frame_count {
2145 let (bone_worst, morph_worst) = worst.split_at_mut(input.bone_count);
2146 for (bone, worst) in bone_worst.iter_mut().enumerate() {
2147 let index = frame * input.bone_count + bone;
2148 let cell = cache.bone_errors[index];
2149 report.max_local_position_error =
2150 report.max_local_position_error.max(cell.local_position);
2151 report.max_local_rotation_error_radians = report
2152 .max_local_rotation_error_radians
2153 .max(cell.local_rotation);
2154 report.max_world_position_error =
2155 report.max_world_position_error.max(cell.world_position);
2156 report.max_world_rotation_error_radians = report
2157 .max_world_rotation_error_radians
2158 .max(cell.world_rotation);
2159 for normalized in [
2160 normalized_error(cell.local_position, tolerances.local_position),
2161 normalized_error(cell.local_rotation, tolerances.local_rotation_radians),
2162 normalized_error(cell.world_position, tolerances.world_position),
2163 normalized_error(cell.world_rotation, tolerances.world_rotation_radians),
2164 ] {
2165 update_worst(worst, normalized, frame, ErrorTrack::Bone(bone));
2166 }
2167 }
2168 for (morph, morph_worst) in morph_worst.iter_mut().enumerate() {
2169 let error = cache.morph_errors[frame * input.morph_count + morph];
2170 report.max_morph_weight_error = report.max_morph_weight_error.max(error);
2171 update_worst(
2172 morph_worst,
2173 normalized_error(error, tolerances.morph_weight),
2174 frame,
2175 ErrorTrack::Morph(morph),
2176 );
2177 }
2178 }
2179 Ok((report, worst))
2180}
2181
2182fn dirty_ranges_are_full(
2183 dirty: &DirtyRanges,
2184 frame_count: usize,
2185 bone_count: usize,
2186 morph_count: usize,
2187) -> bool {
2188 let is_full = |ranges: &[std::ops::RangeInclusive<usize>]| {
2189 ranges.len() == 1
2190 && *ranges[0].start() == 0
2191 && *ranges[0].end() == frame_count.saturating_sub(1)
2192 };
2193 dirty.bone_local.len() == bone_count
2194 && dirty.morph.len() == morph_count
2195 && dirty.bone_local.iter().all(|ranges| is_full(ranges))
2196 && dirty.morph.iter().all(|ranges| is_full(ranges))
2197}
2198
2199fn update_worst(worst: &mut Option<WorstError>, normalized: f32, frame: usize, track: ErrorTrack) {
2200 update_worst_candidate(
2201 worst,
2202 WorstError {
2203 normalized_error: normalized,
2204 frame,
2205 track,
2206 },
2207 );
2208}
2209
2210fn update_worst_candidate(worst: &mut Option<WorstError>, candidate: WorstError) {
2211 if worst.is_none_or(|current| compare_worst_errors(&candidate, ¤t) == Ordering::Less) {
2212 *worst = Some(candidate);
2213 }
2214}
2215
2216fn compare_worst_errors(a: &WorstError, b: &WorstError) -> Ordering {
2217 b.normalized_error
2218 .total_cmp(&a.normalized_error)
2219 .then_with(|| a.frame.cmp(&b.frame))
2220 .then_with(|| error_track_sort_key(a.track).cmp(&error_track_sort_key(b.track)))
2221}
2222
2223fn error_track_sort_key(track: ErrorTrack) -> (u8, usize) {
2224 match track {
2225 ErrorTrack::Bone(index) => (0, index),
2226 ErrorTrack::Morph(index) => (1, index),
2227 }
2228}
2229
2230fn resolve_reduction_worker_count(requested: usize, frame_count: usize) -> usize {
2231 #[cfg(target_family = "wasm")]
2232 {
2233 let _ = (requested, frame_count);
2234 1
2235 }
2236 #[cfg(not(target_family = "wasm"))]
2237 {
2238 let workers = if requested == 0 {
2239 std::thread::available_parallelism()
2240 .map(usize::from)
2241 .unwrap_or(1)
2242 } else {
2243 requested
2244 };
2245 workers.clamp(1, frame_count.max(1))
2246 }
2247}
2248
2249#[cfg(not(target_family = "wasm"))]
2250fn build_reduction_worker_pool(
2251 worker_count: usize,
2252) -> Result<Option<ReductionThreadPool>, PoseReductionError> {
2253 (worker_count > 1)
2254 .then(|| {
2255 ThreadPoolBuilder::new()
2256 .num_threads(worker_count)
2257 .build()
2258 .map_err(|_| PoseReductionError::WorkerPool)
2259 })
2260 .transpose()
2261}
2262
2263#[cfg(target_family = "wasm")]
2264fn build_reduction_worker_pool(
2265 _worker_count: usize,
2266) -> Result<Option<ReductionThreadPool>, PoseReductionError> {
2267 Ok(None)
2268}
2269
2270fn fit_vmd_bone_interpolation(
2271 input: DensePoseSequenceView<'_>,
2272 bone: usize,
2273 start: usize,
2274 end: usize,
2275 translations: &[Vec3A],
2276 rotations: &[Quat],
2277) -> VmdBoneInterpolation {
2278 let bone_count = input.bone_count;
2279 let start_index = start * bone_count + bone;
2280 let end_index = end * bone_count + bone;
2281 let start_translation = translations[start_index];
2282 let end_translation = translations[end_index];
2283 let translation = std::array::from_fn(|axis| {
2284 let start_value = start_translation.to_array()[axis];
2285 let end_value = end_translation.to_array()[axis];
2286 fit_quantized_bezier(
2287 start,
2288 end,
2289 |sample| {
2290 let value = translations[sample * bone_count + bone].to_array()[axis];
2291 normalized_channel_value(start_value, end_value, value)
2292 },
2293 |sample| input.sample_frame(sample),
2294 )
2295 });
2296 let start_rotation = rotations[start_index];
2297 let end_rotation = rotations[end_index];
2298 let total_angle = quat_angle(start_rotation, end_rotation);
2299 let rotation = fit_quantized_bezier(
2300 start,
2301 end,
2302 |sample| {
2303 if total_angle <= f32::EPSILON {
2304 0.0
2305 } else {
2306 (quat_angle(start_rotation, rotations[sample * bone_count + bone]) / total_angle)
2307 .clamp(0.0, 1.0)
2308 }
2309 },
2310 |sample| input.sample_frame(sample),
2311 );
2312 VmdBoneInterpolation {
2313 translation,
2314 rotation,
2315 }
2316}
2317
2318fn fit_quantized_bezier(
2319 start: usize,
2320 end: usize,
2321 value_at: impl Fn(usize) -> f32,
2322 frame_at: impl Fn(usize) -> f32,
2323) -> QuantizedBezier {
2324 if end <= start + 1 {
2325 return QuantizedBezier::LINEAR;
2326 }
2327 let mut best = QuantizedBezier::LINEAR;
2328 let score = |curve: QuantizedBezier| -> f32 {
2329 (start + 1..end)
2330 .map(|sample| {
2331 let time = segment_amount(start, end, sample, &frame_at);
2332 (curve.evaluate(time) - value_at(sample)).abs()
2333 })
2334 .fold(0.0f32, f32::max)
2335 };
2336 let mut best_score = score(best);
2337 const COARSE: [u8; 9] = [0, 16, 32, 48, 64, 80, 96, 112, 127];
2338 for &x1 in &COARSE {
2339 for &x2 in &COARSE {
2340 if x1 > x2 {
2341 continue;
2342 }
2343 for &y1 in &COARSE {
2344 for &y2 in &COARSE {
2345 let candidate = QuantizedBezier { x1, y1, x2, y2 };
2346 let candidate_score = score(candidate);
2347 if candidate_score.total_cmp(&best_score) == Ordering::Less {
2348 best = candidate;
2349 best_score = candidate_score;
2350 }
2351 }
2352 }
2353 }
2354 }
2355 for step in [32i16, 16, 8, 4, 2, 1] {
2356 loop {
2357 let origin = [best.x1, best.y1, best.x2, best.y2];
2358 let mut next_best = best;
2359 let mut next_score = best_score;
2360 for d0 in [-step, 0, step] {
2361 for d1 in [-step, 0, step] {
2362 for d2 in [-step, 0, step] {
2363 for d3 in [-step, 0, step] {
2364 let offsets = [d0, d1, d2, d3];
2365 let mut values = [0u8; 4];
2366 let mut valid = true;
2367 for coordinate in 0..4 {
2368 let value = origin[coordinate] as i16 + offsets[coordinate];
2369 if !(0..=127).contains(&value) {
2370 valid = false;
2371 break;
2372 }
2373 values[coordinate] = value as u8;
2374 }
2375 if !valid || values[0] > values[2] {
2376 continue;
2377 }
2378 let candidate = QuantizedBezier {
2379 x1: values[0],
2380 y1: values[1],
2381 x2: values[2],
2382 y2: values[3],
2383 };
2384 let candidate_score = score(candidate);
2385 if candidate_score.total_cmp(&next_score) == Ordering::Less {
2386 next_best = candidate;
2387 next_score = candidate_score;
2388 }
2389 }
2390 }
2391 }
2392 }
2393 if next_best == best {
2394 break;
2395 }
2396 best = next_best;
2397 best_score = next_score;
2398 }
2399 }
2400 best
2401}
2402
2403fn normalized_channel_value(start: f32, end: f32, value: f32) -> f32 {
2404 let range = end - start;
2405 if range.abs() <= f32::EPSILON {
2406 0.0
2407 } else {
2408 ((value - start) / range).clamp(0.0, 1.0)
2409 }
2410}
2411
2412fn unwrap_euler_xyz(rotations: &[Quat], frame_count: usize, bone_count: usize) -> Vec<Vec3A> {
2413 let mut result = vec![Vec3A::ZERO; rotations.len()];
2414 for bone in 0..bone_count {
2415 for frame in 0..frame_count {
2416 let index = frame * bone_count + bone;
2417 let (x, y, z) = rotations[index].to_euler(EulerRot::XYZ);
2418 let mut value = Vec3A::new(x, y, z);
2419 if frame > 0 {
2420 let previous = result[index - bone_count];
2421 value.x = unwrap_angle(previous.x, value.x);
2422 value.y = unwrap_angle(previous.y, value.y);
2423 value.z = unwrap_angle(previous.z, value.z);
2424 }
2425 result[index] = value;
2426 }
2427 }
2428 result
2429}
2430
2431fn unwrap_angle(previous: f32, value: f32) -> f32 {
2432 let turns = ((previous - value) / std::f32::consts::TAU).round();
2433 value + turns * std::f32::consts::TAU
2434}
2435
2436fn fit_dcc_bone_segment(
2437 input: DensePoseSequenceView<'_>,
2438 bone: usize,
2439 start: usize,
2440 end: usize,
2441 translations: &[Vec3A],
2442 euler_xyz: &[Vec3A],
2443) -> DccCubicSegment {
2444 let bone_count = input.bone_count;
2445 let translation = std::array::from_fn::<_, 3, _>(|axis| {
2446 fit_dcc_scalar_segment(
2447 start,
2448 end,
2449 |sample| translations[sample * bone_count + bone].to_array()[axis],
2450 |sample| input.sample_frame(sample),
2451 )
2452 });
2453 let rotation = std::array::from_fn::<_, 3, _>(|axis| {
2454 fit_dcc_scalar_segment(
2455 start,
2456 end,
2457 |sample| euler_xyz[sample * bone_count + bone].to_array()[axis],
2458 |sample| input.sample_frame(sample),
2459 )
2460 });
2461 DccCubicSegment {
2462 translation_out_tangent: Vec3A::new(
2463 translation[0].out_tangent,
2464 translation[1].out_tangent,
2465 translation[2].out_tangent,
2466 ),
2467 translation_in_tangent: Vec3A::new(
2468 translation[0].in_tangent,
2469 translation[1].in_tangent,
2470 translation[2].in_tangent,
2471 ),
2472 rotation_start_euler_xyz: euler_xyz[start * bone_count + bone],
2473 rotation_end_euler_xyz: euler_xyz[end * bone_count + bone],
2474 rotation_out_tangent: Vec3A::new(
2475 rotation[0].out_tangent,
2476 rotation[1].out_tangent,
2477 rotation[2].out_tangent,
2478 ),
2479 rotation_in_tangent: Vec3A::new(
2480 rotation[0].in_tangent,
2481 rotation[1].in_tangent,
2482 rotation[2].in_tangent,
2483 ),
2484 }
2485}
2486
2487fn fit_dcc_scalar_segment(
2488 start: usize,
2489 end: usize,
2490 value_at: impl Fn(usize) -> f32,
2491 frame_at: impl Fn(usize) -> f32,
2492) -> DccScalarSegment {
2493 let duration = frame_at(end) - frame_at(start);
2494 let start_value = value_at(start);
2495 let end_value = value_at(end);
2496 let slope = (end_value - start_value) / duration;
2497 if end <= start + 1 {
2498 return DccScalarSegment {
2499 out_tangent: slope,
2500 in_tangent: slope,
2501 };
2502 }
2503
2504 let mut aa = 0.0;
2505 let mut ab = 0.0;
2506 let mut bb = 0.0;
2507 let mut ar = 0.0;
2508 let mut br = 0.0;
2509 for sample in start + 1..end {
2510 let t = segment_amount(start, end, sample, &frame_at);
2511 let (h00, h10, h01, h11) = hermite_basis(t);
2512 let a = h10 * duration;
2513 let b = h11 * duration;
2514 let residual = value_at(sample) - h00 * start_value - h01 * end_value;
2515 aa += a * a;
2516 ab += a * b;
2517 bb += b * b;
2518 ar += a * residual;
2519 br += b * residual;
2520 }
2521 let determinant = aa * bb - ab * ab;
2522 let (mut out_tangent, mut in_tangent) = if determinant.abs() > f32::EPSILON {
2523 (
2524 (ar * bb - br * ab) / determinant,
2525 (br * aa - ar * ab) / determinant,
2526 )
2527 } else {
2528 (slope, slope)
2529 };
2530 clamp_monotonic_tangents(slope, &mut out_tangent, &mut in_tangent);
2531 DccScalarSegment {
2532 out_tangent,
2533 in_tangent,
2534 }
2535}
2536
2537fn clamp_monotonic_tangents(slope: f32, out_tangent: &mut f32, in_tangent: &mut f32) {
2538 if slope.abs() <= f32::EPSILON {
2539 *out_tangent = 0.0;
2540 *in_tangent = 0.0;
2541 return;
2542 }
2543 if *out_tangent * slope < 0.0 {
2544 *out_tangent = 0.0;
2545 }
2546 if *in_tangent * slope < 0.0 {
2547 *in_tangent = 0.0;
2548 }
2549 let alpha = *out_tangent / slope;
2550 let beta = *in_tangent / slope;
2551 let length = alpha.hypot(beta);
2552 if length > 3.0 {
2553 let scale = 3.0 / length;
2554 *out_tangent = scale * alpha * slope;
2555 *in_tangent = scale * beta * slope;
2556 }
2557}
2558
2559fn hermite_basis(t: f32) -> (f32, f32, f32, f32) {
2560 let t2 = t * t;
2561 let t3 = t2 * t;
2562 (
2563 2.0 * t3 - 3.0 * t2 + 1.0,
2564 t3 - 2.0 * t2 + t,
2565 -2.0 * t3 + 3.0 * t2,
2566 t3 - t2,
2567 )
2568}
2569
2570fn sample_hermite(
2571 start: f32,
2572 end: f32,
2573 out_tangent: f32,
2574 in_tangent: f32,
2575 duration: f32,
2576 t: f32,
2577) -> f32 {
2578 let (h00, h10, h01, h11) = hermite_basis(t);
2579 h00 * start + h10 * duration * out_tangent + h01 * end + h11 * duration * in_tangent
2580}
2581
2582fn sample_dcc_vec3(
2583 start: Vec3A,
2584 end: Vec3A,
2585 out_tangent: Vec3A,
2586 in_tangent: Vec3A,
2587 duration: f32,
2588 t: f32,
2589) -> Vec3A {
2590 Vec3A::new(
2591 sample_hermite(start.x, end.x, out_tangent.x, in_tangent.x, duration, t),
2592 sample_hermite(start.y, end.y, out_tangent.y, in_tangent.y, duration, t),
2593 sample_hermite(start.z, end.z, out_tangent.z, in_tangent.z, duration, t),
2594 )
2595}
2596
2597fn sample_bone_track(
2598 track: &ReducedBoneTrack,
2599 sample_frames: &[f32],
2600 frame: f32,
2601 target: ReductionTarget,
2602) -> (Vec3A, Quat) {
2603 let upper = track
2604 .keys
2605 .partition_point(|key| sample_frames[key.sample_index] <= frame);
2606 if upper == 0 {
2607 return (track.keys[0].translation, track.keys[0].rotation);
2608 }
2609 if upper == track.keys.len() {
2610 let key = track.keys[track.keys.len() - 1];
2611 return (key.translation, key.rotation);
2612 }
2613 let left = track.keys[upper - 1];
2614 let right = track.keys[upper];
2615 let amount = ((frame - sample_frames[left.sample_index])
2616 / (sample_frames[right.sample_index] - sample_frames[left.sample_index]))
2617 .clamp(0.0, 1.0);
2618 if target == ReductionTarget::DccCubic {
2619 let duration = sample_frames[right.sample_index] - sample_frames[left.sample_index];
2620 let segment = right.dcc_segment;
2621 let translation = Vec3A::new(
2622 sample_hermite(
2623 left.translation.x,
2624 right.translation.x,
2625 segment.translation_out_tangent.x,
2626 segment.translation_in_tangent.x,
2627 duration,
2628 amount,
2629 ),
2630 sample_hermite(
2631 left.translation.y,
2632 right.translation.y,
2633 segment.translation_out_tangent.y,
2634 segment.translation_in_tangent.y,
2635 duration,
2636 amount,
2637 ),
2638 sample_hermite(
2639 left.translation.z,
2640 right.translation.z,
2641 segment.translation_out_tangent.z,
2642 segment.translation_in_tangent.z,
2643 duration,
2644 amount,
2645 ),
2646 );
2647 let euler = Vec3A::new(
2648 sample_hermite(
2649 segment.rotation_start_euler_xyz.x,
2650 segment.rotation_end_euler_xyz.x,
2651 segment.rotation_out_tangent.x,
2652 segment.rotation_in_tangent.x,
2653 duration,
2654 amount,
2655 ),
2656 sample_hermite(
2657 segment.rotation_start_euler_xyz.y,
2658 segment.rotation_end_euler_xyz.y,
2659 segment.rotation_out_tangent.y,
2660 segment.rotation_in_tangent.y,
2661 duration,
2662 amount,
2663 ),
2664 sample_hermite(
2665 segment.rotation_start_euler_xyz.z,
2666 segment.rotation_end_euler_xyz.z,
2667 segment.rotation_out_tangent.z,
2668 segment.rotation_in_tangent.z,
2669 duration,
2670 amount,
2671 ),
2672 );
2673 return (
2674 translation,
2675 normalize_quat(Quat::from_euler(EulerRot::XYZ, euler.x, euler.y, euler.z)),
2676 );
2677 }
2678 let translation_amount = if target == ReductionTarget::VmdBezier {
2679 Vec3A::new(
2680 right.vmd_interpolation.translation[0].evaluate(amount),
2681 right.vmd_interpolation.translation[1].evaluate(amount),
2682 right.vmd_interpolation.translation[2].evaluate(amount),
2683 )
2684 } else {
2685 Vec3A::splat(amount)
2686 };
2687 let rotation_amount = if target == ReductionTarget::VmdBezier {
2688 right.vmd_interpolation.rotation.evaluate(amount)
2689 } else {
2690 amount
2691 };
2692 (
2693 Vec3A::new(
2694 left.translation.x + (right.translation.x - left.translation.x) * translation_amount.x,
2695 left.translation.y + (right.translation.y - left.translation.y) * translation_amount.y,
2696 left.translation.z + (right.translation.z - left.translation.z) * translation_amount.z,
2697 ),
2698 normalize_quat(left.rotation.slerp(right.rotation, rotation_amount)),
2699 )
2700}
2701
2702fn sample_morph_track(
2703 track: &ReducedMorphTrack,
2704 sample_frames: &[f32],
2705 frame: f32,
2706 target: ReductionTarget,
2707) -> f32 {
2708 let upper = track
2709 .keys
2710 .partition_point(|key| sample_frames[key.sample_index] <= frame);
2711 if upper == 0 {
2712 return track.keys[0].weight;
2713 }
2714 if upper == track.keys.len() {
2715 return track.keys[track.keys.len() - 1].weight;
2716 }
2717 let left = track.keys[upper - 1];
2718 let right = track.keys[upper];
2719 let amount = ((frame - sample_frames[left.sample_index])
2720 / (sample_frames[right.sample_index] - sample_frames[left.sample_index]))
2721 .clamp(0.0, 1.0);
2722 if target == ReductionTarget::DccCubic {
2723 let duration = sample_frames[right.sample_index] - sample_frames[left.sample_index];
2724 return sample_hermite(
2725 left.weight,
2726 right.weight,
2727 right.dcc_segment.out_tangent,
2728 right.dcc_segment.in_tangent,
2729 duration,
2730 amount,
2731 );
2732 }
2733 left.weight + (right.weight - left.weight) * amount
2734}
2735
2736fn build_world_matrices_into(
2737 snapshot: &SkeletonSnapshot,
2738 translations: &[Vec3A],
2739 rotations: &[Quat],
2740 result: &mut Vec<Mat4>,
2741) {
2742 result.resize(snapshot.bone_count(), Mat4::IDENTITY);
2743 for &bone in snapshot.evaluation_order.iter() {
2744 let local = Mat4::from_rotation_translation(rotations[bone], translations[bone].into());
2745 result[bone] = if snapshot.parent_indices[bone] < 0 {
2746 local
2747 } else {
2748 result[snapshot.parent_indices[bone] as usize] * local
2749 };
2750 }
2751}
2752
2753fn build_evaluation_order(parents: &[i32]) -> Result<Vec<usize>, PoseReductionError> {
2754 fn visit(
2755 bone: usize,
2756 parents: &[i32],
2757 state: &mut [u8],
2758 order: &mut Vec<usize>,
2759 ) -> Result<(), PoseReductionError> {
2760 match state[bone] {
2761 1 => return Err(PoseReductionError::SkeletonCycle { bone }),
2762 2 => return Ok(()),
2763 _ => {}
2764 }
2765 state[bone] = 1;
2766 if parents[bone] >= 0 {
2767 visit(parents[bone] as usize, parents, state, order)?;
2768 }
2769 state[bone] = 2;
2770 order.push(bone);
2771 Ok(())
2772 }
2773 let mut state = vec![0u8; parents.len()];
2774 let mut order = Vec::with_capacity(parents.len());
2775 for bone in 0..parents.len() {
2776 visit(bone, parents, &mut state, &mut order)?;
2777 }
2778 Ok(order)
2779}
2780
2781fn normalize_quat(value: Quat) -> Quat {
2782 if value.length_squared() <= f32::EPSILON {
2783 Quat::IDENTITY
2784 } else {
2785 value.normalize()
2786 }
2787}
2788
2789fn quat_is_finite(value: Quat) -> bool {
2790 value.to_array().iter().all(|value| value.is_finite()) && value.length_squared() > f32::EPSILON
2791}
2792
2793fn quat_angle(a: Quat, b: Quat) -> f32 {
2794 let a = normalize_quat(a);
2795 let mut b = normalize_quat(b);
2796 if a.dot(b) < 0.0 {
2797 b = -b;
2798 }
2799 let half_chord = ((a - b).length() * 0.5).clamp(0.0, 1.0);
2803 4.0 * half_chord.asin()
2804}
2805
2806fn normalized_error(error: f32, tolerance: f32) -> f32 {
2807 if tolerance == 0.0 {
2808 if error == 0.0 { 0.0 } else { f32::INFINITY }
2809 } else {
2810 error / tolerance
2811 }
2812}
2813
2814fn segment_amount(start: usize, end: usize, sample: usize, frame_at: impl Fn(usize) -> f32) -> f32 {
2815 let start_frame = frame_at(start);
2816 (frame_at(sample) - start_frame) / (frame_at(end) - start_frame)
2817}
2818
2819fn is_worse(current: Option<(f32, usize)>, error: f32, frame: usize) -> bool {
2820 current.is_none_or(|(best, best_frame)| error > best || (error == best && frame < best_frame))
2821}
2822
2823#[cfg(test)]
2824mod tests;