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