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, 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 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, ¤t) == 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 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;