1#[allow(dead_code, unused_variables, unused_mut, unused_imports)]
2
3use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::{HashMap, VecDeque, HashSet, BTreeMap};
5
6const MAX_BONES: usize = 256;
16const MAX_KEYFRAMES: usize = 65536;
17const QUANTIZE_POS_BITS: u32 = 16;
18const QUANTIZE_ROT_BITS: u32 = 16;
19const QUANTIZE_SCALE_BITS: u32 = 8;
20const CHUNK_SIZE_FRAMES: usize = 64;
21const MAX_BLEND_TARGETS: usize = 16;
22const MAX_LOD_LEVELS: usize = 4;
23const SMALL3_SCALE: f32 = 0.7071068; #[derive(Debug, Clone, Copy, PartialEq)]
28pub struct Transform {
29 pub position: Vec3,
30 pub rotation: Quat,
31 pub scale: Vec3,
32}
33
34impl Default for Transform {
35 fn default() -> Self {
36 Self {
37 position: Vec3::ZERO,
38 rotation: Quat::IDENTITY,
39 scale: Vec3::ONE,
40 }
41 }
42}
43
44impl Transform {
45 pub fn new(position: Vec3, rotation: Quat, scale: Vec3) -> Self {
46 Self { position, rotation, scale }
47 }
48
49 pub fn identity() -> Self {
50 Self::default()
51 }
52
53 pub fn lerp(&self, other: &Transform, t: f32) -> Transform {
54 Transform {
55 position: self.position.lerp(other.position, t),
56 rotation: self.rotation.slerp(other.rotation, t),
57 scale: self.scale.lerp(other.scale, t),
58 }
59 }
60
61 pub fn to_mat4(&self) -> Mat4 {
62 Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
63 }
64
65 pub fn inverse(&self) -> Transform {
66 let inv_rot = self.rotation.inverse();
67 let inv_scale = Vec3::new(1.0 / self.scale.x, 1.0 / self.scale.y, 1.0 / self.scale.z);
68 let inv_pos = inv_rot * (-self.position * inv_scale);
69 Transform {
70 position: inv_pos,
71 rotation: inv_rot,
72 scale: inv_scale,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq)]
78pub struct Keyframe {
79 pub time: f32,
80 pub transform: Transform,
81}
82
83impl Keyframe {
84 pub fn new(time: f32, transform: Transform) -> Self {
85 Self { time, transform }
86 }
87}
88
89#[derive(Debug, Clone)]
90pub struct BoneTrack {
91 pub bone_index: u32,
92 pub keyframes: Vec<Keyframe>,
93 pub importance: f32, }
95
96impl BoneTrack {
97 pub fn new(bone_index: u32, importance: f32) -> Self {
98 Self {
99 bone_index,
100 keyframes: Vec::new(),
101 importance,
102 }
103 }
104
105 pub fn push(&mut self, kf: Keyframe) {
106 self.keyframes.push(kf);
107 }
108
109 pub fn duration(&self) -> f32 {
110 if self.keyframes.is_empty() {
111 return 0.0;
112 }
113 self.keyframes.last().unwrap().time - self.keyframes.first().unwrap().time
114 }
115}
116
117#[derive(Debug, Clone)]
118pub struct AnimationClip {
119 pub name: String,
120 pub frame_rate: f32,
121 pub duration: f32,
122 pub tracks: Vec<BoneTrack>,
123 pub looping: bool,
124}
125
126impl AnimationClip {
127 pub fn new(name: &str, frame_rate: f32, duration: f32) -> Self {
128 Self {
129 name: name.to_owned(),
130 frame_rate,
131 duration,
132 tracks: Vec::new(),
133 looping: false,
134 }
135 }
136
137 pub fn total_keyframes(&self) -> usize {
138 self.tracks.iter().map(|t| t.keyframes.len()).sum()
139 }
140
141 pub fn sample_at(&self, bone_index: u32, time: f32) -> Option<Transform> {
142 let track = self.tracks.iter().find(|t| t.bone_index == bone_index)?;
143 if track.keyframes.is_empty() {
144 return None;
145 }
146 if track.keyframes.len() == 1 {
147 return Some(track.keyframes[0].transform);
148 }
149 let t = time.clamp(
150 track.keyframes.first().unwrap().time,
151 track.keyframes.last().unwrap().time,
152 );
153 let idx = track.keyframes.partition_point(|kf| kf.time <= t);
155 let idx = idx.min(track.keyframes.len() - 1);
156 if idx == 0 {
157 return Some(track.keyframes[0].transform);
158 }
159 let kf0 = &track.keyframes[idx - 1];
160 let kf1 = &track.keyframes[idx];
161 let dt = kf1.time - kf0.time;
162 let alpha = if dt > 1e-6 { (t - kf0.time) / dt } else { 0.0 };
163 Some(kf0.transform.lerp(&kf1.transform, alpha))
164 }
165}
166
167pub fn rotation_error_geodesic(a: Quat, b: Quat) -> f32 {
171 let d = a.dot(b).abs().min(1.0);
173 2.0 * d.acos()
174}
175
176pub fn position_error_l2(a: Vec3, b: Vec3) -> f32 {
178 (a - b).length()
179}
180
181pub fn scale_error_l2(a: Vec3, b: Vec3) -> f32 {
183 (a - b).length()
184}
185
186pub fn transform_error(a: &Transform, b: &Transform, rot_weight: f32, pos_weight: f32, scale_weight: f32) -> f32 {
188 let re = rotation_error_geodesic(a.rotation, b.rotation) * rot_weight;
189 let pe = position_error_l2(a.position, b.position) * pos_weight;
190 let se = scale_error_l2(a.scale, b.scale) * scale_weight;
191 re + pe + se
192}
193
194pub fn rdp_simplify_positions(keyframes: &[Keyframe], epsilon: f32) -> Vec<usize> {
199 if keyframes.len() <= 2 {
200 return (0..keyframes.len()).collect();
201 }
202 let mut result = Vec::new();
203 rdp_recursive_positions(keyframes, 0, keyframes.len() - 1, epsilon, &mut result);
204 result.sort_unstable();
205 result.dedup();
206 result
207}
208
209fn rdp_recursive_positions(
210 keyframes: &[Keyframe],
211 start: usize,
212 end: usize,
213 epsilon: f32,
214 result: &mut Vec<usize>,
215) {
216 if start >= end {
217 result.push(start);
218 return;
219 }
220 result.push(start);
221 result.push(end);
222 if end - start < 2 {
223 return;
224 }
225
226 let p_start = keyframes[start].transform.position;
227 let p_end = keyframes[end].transform.position;
228 let t_start = keyframes[start].time;
229 let t_end = keyframes[end].time;
230 let dt = t_end - t_start;
231
232 let mut max_dist = 0.0f32;
233 let mut max_idx = start + 1;
234
235 for i in (start + 1)..end {
236 let t = keyframes[i].time;
237 let alpha = if dt > 1e-9 { (t - t_start) / dt } else { 0.0 };
238 let interpolated = p_start.lerp(p_end, alpha);
239 let d = position_error_l2(keyframes[i].transform.position, interpolated);
240 if d > max_dist {
241 max_dist = d;
242 max_idx = i;
243 }
244 }
245
246 if max_dist > epsilon {
247 rdp_recursive_positions(keyframes, start, max_idx, epsilon, result);
248 rdp_recursive_positions(keyframes, max_idx, end, epsilon, result);
249 }
250}
251
252pub fn rdp_simplify_rotations(keyframes: &[Keyframe], epsilon: f32) -> Vec<usize> {
254 if keyframes.len() <= 2 {
255 return (0..keyframes.len()).collect();
256 }
257 let mut result = Vec::new();
258 rdp_recursive_rotations(keyframes, 0, keyframes.len() - 1, epsilon, &mut result);
259 result.sort_unstable();
260 result.dedup();
261 result
262}
263
264fn rdp_recursive_rotations(
265 keyframes: &[Keyframe],
266 start: usize,
267 end: usize,
268 epsilon: f32,
269 result: &mut Vec<usize>,
270) {
271 result.push(start);
272 result.push(end);
273 if end - start < 2 {
274 return;
275 }
276
277 let q_start = keyframes[start].transform.rotation;
278 let q_end = keyframes[end].transform.rotation;
279 let t_start = keyframes[start].time;
280 let t_end = keyframes[end].time;
281 let dt = t_end - t_start;
282
283 let mut max_err = 0.0f32;
284 let mut max_idx = start + 1;
285
286 for i in (start + 1)..end {
287 let alpha = if dt > 1e-9 { (keyframes[i].time - t_start) / dt } else { 0.0 };
288 let interpolated = q_start.slerp(q_end, alpha);
289 let e = rotation_error_geodesic(keyframes[i].transform.rotation, interpolated);
290 if e > max_err {
291 max_err = e;
292 max_idx = i;
293 }
294 }
295
296 if max_err > epsilon {
297 rdp_recursive_rotations(keyframes, start, max_idx, epsilon, result);
298 rdp_recursive_rotations(keyframes, max_idx, end, epsilon, result);
299 }
300}
301
302pub fn rdp_reduce_track(track: &BoneTrack, base_pos_eps: f32, base_rot_eps: f32) -> BoneTrack {
304 if track.keyframes.is_empty() {
305 return track.clone();
306 }
307 let importance = track.importance.clamp(0.0, 1.0);
309 let pos_eps = base_pos_eps / (0.1 + 0.9 * importance);
310 let rot_eps = base_rot_eps / (0.1 + 0.9 * importance);
311
312 let pos_keep = rdp_simplify_positions(&track.keyframes, pos_eps);
314 let rot_keep = rdp_simplify_rotations(&track.keyframes, rot_eps);
316
317 let mut keep_set: HashSet<usize> = HashSet::new();
319 for &i in &pos_keep { keep_set.insert(i); }
320 for &i in &rot_keep { keep_set.insert(i); }
321 keep_set.insert(0);
323 keep_set.insert(track.keyframes.len() - 1);
324
325 let mut indices: Vec<usize> = keep_set.into_iter().collect();
326 indices.sort_unstable();
327
328 let new_keyframes: Vec<Keyframe> = indices.iter().map(|&i| track.keyframes[i].clone()).collect();
329 BoneTrack {
330 bone_index: track.bone_index,
331 keyframes: new_keyframes,
332 importance: track.importance,
333 }
334}
335
336#[derive(Debug, Clone)]
339pub struct PositionBounds {
340 pub min: Vec3,
341 pub max: Vec3,
342}
343
344impl PositionBounds {
345 pub fn from_track(track: &BoneTrack) -> Self {
346 if track.keyframes.is_empty() {
347 return Self { min: Vec3::ZERO, max: Vec3::ONE };
348 }
349 let mut min = Vec3::splat(f32::MAX);
350 let mut max = Vec3::splat(f32::MIN);
351 for kf in &track.keyframes {
352 min = min.min(kf.transform.position);
353 max = max.max(kf.transform.position);
354 }
355 let expand = (max - min) * 0.001 + Vec3::splat(1e-6);
357 Self { min: min - expand, max: max + expand }
358 }
359
360 pub fn range(&self) -> Vec3 {
361 self.max - self.min
362 }
363}
364
365pub fn quantize_position_16(pos: Vec3, bounds: &PositionBounds) -> [u16; 3] {
367 let range = bounds.range();
368 let norm = (pos - bounds.min) / range;
369 let qx = (norm.x.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
370 let qy = (norm.y.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
371 let qz = (norm.z.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16;
372 [qx, qy, qz]
373}
374
375pub fn dequantize_position_16(q: [u16; 3], bounds: &PositionBounds) -> Vec3 {
377 let range = bounds.range();
378 let nx = q[0] as f32 / 65535.0;
379 let ny = q[1] as f32 / 65535.0;
380 let nz = q[2] as f32 / 65535.0;
381 bounds.min + Vec3::new(nx, ny, nz) * range
382}
383
384#[derive(Debug, Clone, Copy)]
389pub struct CompressedQuat {
390 pub largest_component: u8, pub components: [i16; 3], }
393
394pub fn compress_quat_smallest3(q: Quat) -> CompressedQuat {
395 let q = if q.w < 0.0 { Quat::from_xyzw(-q.x, -q.y, -q.z, -q.w) } else { q };
397
398 let components = [q.w, q.x, q.y, q.z];
399 let abs = [q.w.abs(), q.x.abs(), q.y.abs(), q.z.abs()];
400
401 let mut largest = 0usize;
403 let mut largest_val = abs[0];
404 for i in 1..4 {
405 if abs[i] > largest_val {
406 largest_val = abs[i];
407 largest = i;
408 }
409 }
410
411 let mut small = [0.0f32; 3];
413 let mut si = 0;
414 for i in 0..4 {
415 if i != largest {
416 small[si] = components[i];
417 si += 1;
418 }
419 }
420
421 let scale = 32767.0 / SMALL3_SCALE;
424 CompressedQuat {
425 largest_component: largest as u8,
426 components: [
427 (small[0] * scale).round().clamp(-32767.0, 32767.0) as i16,
428 (small[1] * scale).round().clamp(-32767.0, 32767.0) as i16,
429 (small[2] * scale).round().clamp(-32767.0, 32767.0) as i16,
430 ],
431 }
432}
433
434pub fn decompress_quat_smallest3(cq: &CompressedQuat) -> Quat {
435 let inv_scale = SMALL3_SCALE / 32767.0;
436 let s0 = cq.components[0] as f32 * inv_scale;
437 let s1 = cq.components[1] as f32 * inv_scale;
438 let s2 = cq.components[2] as f32 * inv_scale;
439
440 let sum_sq = s0 * s0 + s1 * s1 + s2 * s2;
442 let largest = (1.0 - sum_sq).max(0.0).sqrt();
443
444 let (w, x, y, z) = match cq.largest_component {
445 0 => (largest, s0, s1, s2),
446 1 => (s0, largest, s1, s2),
447 2 => (s0, s1, largest, s2),
448 _ => (s0, s1, s2, largest),
449 };
450
451 Quat::from_xyzw(x, y, z, w).normalize()
452}
453
454pub fn quantize_scale_log8(s: f32) -> u8 {
459 let log_s = s.abs().max(1e-6).log2();
461 let norm = (log_s + 4.0) / 8.0; (norm.clamp(0.0, 1.0) * 255.0 + 0.5) as u8
463}
464
465pub fn dequantize_scale_log8(q: u8) -> f32 {
466 let norm = q as f32 / 255.0;
467 let log_s = norm * 8.0 - 4.0;
468 2.0f32.powf(log_s)
469}
470
471pub fn quantize_scale_vec_log8(s: Vec3) -> [u8; 3] {
472 [
473 quantize_scale_log8(s.x),
474 quantize_scale_log8(s.y),
475 quantize_scale_log8(s.z),
476 ]
477}
478
479pub fn dequantize_scale_vec_log8(q: [u8; 3]) -> Vec3 {
480 Vec3::new(
481 dequantize_scale_log8(q[0]),
482 dequantize_scale_log8(q[1]),
483 dequantize_scale_log8(q[2]),
484 )
485}
486
487#[derive(Debug, Clone)]
490pub struct CompressedKeyframe {
491 pub time_ticks: u32, pub position: [u16; 3],
493 pub rotation: CompressedQuat,
494 pub scale: [u8; 3],
495}
496
497#[derive(Debug, Clone)]
498pub struct CompressedTrack {
499 pub bone_index: u32,
500 pub pos_bounds: PositionBounds,
501 pub tick_rate: f32,
502 pub keyframes: Vec<CompressedKeyframe>,
503}
504
505impl CompressedTrack {
506 pub fn from_track(track: &BoneTrack, tick_rate: f32) -> Self {
507 let bounds = PositionBounds::from_track(track);
508 let keyframes = track.keyframes.iter().map(|kf| {
509 let ticks = (kf.time * tick_rate * 1000.0) as u32;
510 CompressedKeyframe {
511 time_ticks: ticks,
512 position: quantize_position_16(kf.transform.position, &bounds),
513 rotation: compress_quat_smallest3(kf.transform.rotation),
514 scale: quantize_scale_vec_log8(kf.transform.scale),
515 }
516 }).collect();
517 CompressedTrack {
518 bone_index: track.bone_index,
519 pos_bounds: bounds,
520 tick_rate,
521 keyframes,
522 }
523 }
524
525 pub fn to_track(&self) -> BoneTrack {
526 let keyframes = self.keyframes.iter().map(|ckf| {
527 let time = ckf.time_ticks as f32 / (self.tick_rate * 1000.0);
528 let position = dequantize_position_16(ckf.position, &self.pos_bounds);
529 let rotation = decompress_quat_smallest3(&ckf.rotation);
530 let scale = dequantize_scale_vec_log8(ckf.scale);
531 Keyframe { time, transform: Transform { position, rotation, scale } }
532 }).collect();
533 BoneTrack {
534 bone_index: self.bone_index,
535 keyframes,
536 importance: 1.0,
537 }
538 }
539
540 pub fn byte_size(&self) -> usize {
541 4 + 24 + 4 + self.keyframes.len() * 21
544 }
545}
546
547#[derive(Debug, Clone, Copy)]
551pub struct HermiteSegment {
552 pub t0: f32,
553 pub t1: f32,
554 pub p0: Vec3,
555 pub p1: Vec3,
556 pub m0: Vec3, pub m1: Vec3, }
559
560impl HermiteSegment {
561 pub fn evaluate(&self, t: f32) -> Vec3 {
562 let dt = self.t1 - self.t0;
563 let s = if dt > 1e-9 { (t - self.t0) / dt } else { 0.0 };
564 let s2 = s * s;
565 let s3 = s2 * s;
566 let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
568 let h10 = s3 - 2.0 * s2 + s;
569 let h01 = -2.0 * s3 + 3.0 * s2;
570 let h11 = s3 - s2;
571 self.p0 * h00 + self.m0 * (h10 * dt) + self.p1 * h01 + self.m1 * (h11 * dt)
572 }
573
574 pub fn max_error_vs_keyframes(&self, keyframes: &[Keyframe]) -> f32 {
575 let mut max_err = 0.0f32;
576 for kf in keyframes {
577 if kf.time >= self.t0 && kf.time <= self.t1 {
578 let approx = self.evaluate(kf.time);
579 let err = position_error_l2(approx, kf.transform.position);
580 if err > max_err { max_err = err; }
581 }
582 }
583 max_err
584 }
585}
586
587pub fn estimate_tangents_catmull_rom(keyframes: &[Keyframe]) -> Vec<Vec3> {
589 let n = keyframes.len();
590 let mut tangents = vec![Vec3::ZERO; n];
591 for i in 0..n {
592 if i == 0 {
593 if n > 1 {
594 let dt = keyframes[1].time - keyframes[0].time;
595 if dt > 1e-9 {
596 tangents[0] = (keyframes[1].transform.position - keyframes[0].transform.position) / dt;
597 }
598 }
599 } else if i == n - 1 {
600 let dt = keyframes[n-1].time - keyframes[n-2].time;
601 if dt > 1e-9 {
602 tangents[n-1] = (keyframes[n-1].transform.position - keyframes[n-2].transform.position) / dt;
603 }
604 } else {
605 let dt_prev = keyframes[i].time - keyframes[i-1].time;
606 let dt_next = keyframes[i+1].time - keyframes[i].time;
607 let dt_total = dt_prev + dt_next;
608 if dt_total > 1e-9 {
609 tangents[i] = (keyframes[i+1].transform.position - keyframes[i-1].transform.position) / dt_total;
610 }
611 }
612 }
613 tangents
614}
615
616pub fn build_hermite_spline(keyframes: &[Keyframe]) -> Vec<HermiteSegment> {
618 if keyframes.len() < 2 {
619 return Vec::new();
620 }
621 let tangents = estimate_tangents_catmull_rom(keyframes);
622 let mut segments = Vec::new();
623 for i in 0..(keyframes.len() - 1) {
624 segments.push(HermiteSegment {
625 t0: keyframes[i].time,
626 t1: keyframes[i+1].time,
627 p0: keyframes[i].transform.position,
628 p1: keyframes[i+1].transform.position,
629 m0: tangents[i],
630 m1: tangents[i+1],
631 });
632 }
633 segments
634}
635
636pub fn estimate_tangents_least_squares(keyframes: &[Keyframe]) -> Vec<Vec3> {
639 let n = keyframes.len();
640 let mut tangents = vec![Vec3::ZERO; n];
641 for i in 0..n {
642 if i == 0 || i == n - 1 {
643 if i == 0 && n > 1 {
645 let dt = keyframes[1].time - keyframes[0].time;
646 if dt > 1e-9 {
647 tangents[0] = (keyframes[1].transform.position - keyframes[0].transform.position) / dt;
648 }
649 } else if i == n - 1 && n > 1 {
650 let dt = keyframes[n-1].time - keyframes[n-2].time;
651 if dt > 1e-9 {
652 tangents[n-1] = (keyframes[n-1].transform.position - keyframes[n-2].transform.position) / dt;
653 }
654 }
655 } else {
656 let dt_m = keyframes[i].time - keyframes[i-1].time;
658 let dt_p = keyframes[i+1].time - keyframes[i].time;
659 let w_m = 1.0 / (dt_m * dt_m + 1e-9);
660 let w_p = 1.0 / (dt_p * dt_p + 1e-9);
661 let sum_w = w_m + w_p;
662 if sum_w > 1e-9 {
663 let slope_m = if dt_m > 1e-9 {
664 (keyframes[i].transform.position - keyframes[i-1].transform.position) / dt_m
665 } else {
666 Vec3::ZERO
667 };
668 let slope_p = if dt_p > 1e-9 {
669 (keyframes[i+1].transform.position - keyframes[i].transform.position) / dt_p
670 } else {
671 Vec3::ZERO
672 };
673 tangents[i] = (slope_m * w_m + slope_p * w_p) / sum_w;
674 }
675 }
676 }
677 tangents
678}
679
680pub fn hermite_reduce_track(track: &BoneTrack, max_error: f32) -> BoneTrack {
682 if track.keyframes.len() <= 2 {
683 return track.clone();
684 }
685 let keyframes = &track.keyframes;
686 let mut keep = vec![false; keyframes.len()];
687 keep[0] = true;
688 keep[keyframes.len() - 1] = true;
689
690 let mut changed = true;
692 while changed {
693 changed = false;
694 let kept: Vec<Keyframe> = keyframes.iter().enumerate()
696 .filter(|(i, _)| keep[*i])
697 .map(|(_, kf)| kf.clone())
698 .collect();
699 let tangents = estimate_tangents_least_squares(&kept);
700 for seg_i in 0..(kept.len().saturating_sub(1)) {
702 let seg = HermiteSegment {
703 t0: kept[seg_i].time,
704 t1: kept[seg_i + 1].time,
705 p0: kept[seg_i].transform.position,
706 p1: kept[seg_i + 1].transform.position,
707 m0: tangents[seg_i],
708 m1: tangents[seg_i + 1],
709 };
710 let in_range: Vec<Keyframe> = keyframes.iter()
712 .filter(|kf| kf.time > seg.t0 && kf.time < seg.t1)
713 .cloned()
714 .collect();
715 if in_range.is_empty() { continue; }
716 let err = seg.max_error_vs_keyframes(&in_range);
717 if err > max_error {
718 let mut worst_err = 0.0f32;
720 let mut worst_time = seg.t0;
721 for kf in &in_range {
722 let approx = seg.evaluate(kf.time);
723 let e = position_error_l2(approx, kf.transform.position);
724 if e > worst_err {
725 worst_err = e;
726 worst_time = kf.time;
727 }
728 }
729 if let Some(idx) = keyframes.iter().position(|kf| kf.time == worst_time) {
731 if !keep[idx] {
732 keep[idx] = true;
733 changed = true;
734 }
735 }
736 }
737 }
738 }
739
740 let new_keyframes: Vec<Keyframe> = keyframes.iter().enumerate()
741 .filter(|(i, _)| keep[*i])
742 .map(|(_, kf)| kf.clone())
743 .collect();
744 BoneTrack {
745 bone_index: track.bone_index,
746 keyframes: new_keyframes,
747 importance: track.importance,
748 }
749}
750
751#[derive(Debug, Clone)]
754pub struct ReferencePose {
755 pub transforms: Vec<Transform>, }
757
758impl ReferencePose {
759 pub fn new(n_bones: usize) -> Self {
760 Self { transforms: vec![Transform::identity(); n_bones] }
761 }
762
763 pub fn from_clip_frame0(clip: &AnimationClip, n_bones: usize) -> Self {
764 let mut transforms = vec![Transform::identity(); n_bones];
765 for track in &clip.tracks {
766 let idx = track.bone_index as usize;
767 if idx < n_bones {
768 if let Some(t) = track.keyframes.first() {
769 transforms[idx] = t.transform;
770 }
771 }
772 }
773 Self { transforms }
774 }
775}
776
777#[derive(Debug, Clone, Copy)]
779pub struct TransformDelta {
780 pub pos_delta: Vec3,
781 pub rot_delta: Quat,
782 pub scale_delta: Vec3,
783}
784
785impl TransformDelta {
786 pub fn compute(reference: &Transform, current: &Transform) -> Self {
787 let pos_delta = current.position - reference.position;
788 let rot_delta = reference.rotation.inverse() * current.rotation;
789 let scale_delta = current.scale - reference.scale;
790 Self { pos_delta, rot_delta, scale_delta }
791 }
792
793 pub fn apply(&self, reference: &Transform) -> Transform {
794 Transform {
795 position: reference.position + self.pos_delta,
796 rotation: reference.rotation * self.rot_delta,
797 scale: reference.scale + self.scale_delta,
798 }
799 }
800
801 pub fn is_near_zero(&self, pos_thresh: f32, rot_thresh: f32, scale_thresh: f32) -> bool {
802 self.pos_delta.length() < pos_thresh
803 && rotation_error_geodesic(self.rot_delta, Quat::IDENTITY) < rot_thresh
804 && self.scale_delta.length() < scale_thresh
805 }
806}
807
808#[derive(Debug, Clone)]
810pub enum EncodedDelta {
811 Zero, SmallPos([i8; 3]), FullPos(Vec3), SmallRot([i8; 4]), FullRot(Quat), SmallScale([i8; 3]), FullScale(Vec3), }
819
820pub fn encode_pos_delta(delta: Vec3, threshold: f32) -> EncodedDelta {
822 let len = delta.length();
823 if len < 1e-6 {
824 return EncodedDelta::Zero;
825 }
826 let cx = (delta.x * 200.0).round();
828 let cy = (delta.y * 200.0).round();
829 let cz = (delta.z * 200.0).round();
830 if cx.abs() <= 127.0 && cy.abs() <= 127.0 && cz.abs() <= 127.0 {
831 EncodedDelta::SmallPos([cx as i8, cy as i8, cz as i8])
832 } else {
833 EncodedDelta::FullPos(delta)
834 }
835}
836
837pub fn decode_pos_delta(enc: &EncodedDelta) -> Vec3 {
838 match enc {
839 EncodedDelta::Zero => Vec3::ZERO,
840 EncodedDelta::SmallPos(b) => Vec3::new(b[0] as f32 / 200.0, b[1] as f32 / 200.0, b[2] as f32 / 200.0),
841 EncodedDelta::FullPos(v) => *v,
842 _ => Vec3::ZERO,
843 }
844}
845
846#[derive(Debug, Clone)]
847pub struct DeltaFrame {
848 pub bone_index: u32,
849 pub time: f32,
850 pub delta: TransformDelta,
851 pub is_keyframe: bool, }
853
854#[derive(Debug, Clone)]
855pub struct DeltaCompressedTrack {
856 pub bone_index: u32,
857 pub reference: Transform,
858 pub frames: Vec<DeltaFrame>,
859}
860
861impl DeltaCompressedTrack {
862 pub fn from_track(track: &BoneTrack, reference: &Transform) -> Self {
863 let frames = track.keyframes.iter().enumerate().map(|(i, kf)| {
864 let delta = TransformDelta::compute(reference, &kf.transform);
865 DeltaFrame {
866 bone_index: track.bone_index,
867 time: kf.time,
868 delta,
869 is_keyframe: i == 0 || i == track.keyframes.len() - 1,
870 }
871 }).collect();
872 DeltaCompressedTrack {
873 bone_index: track.bone_index,
874 reference: *reference,
875 frames,
876 }
877 }
878
879 pub fn to_track(&self) -> BoneTrack {
880 let keyframes = self.frames.iter().map(|df| {
881 Keyframe {
882 time: df.time,
883 transform: df.delta.apply(&self.reference),
884 }
885 }).collect();
886 BoneTrack {
887 bone_index: self.bone_index,
888 keyframes,
889 importance: 1.0,
890 }
891 }
892
893 pub fn cull_zero_deltas(&mut self, pos_thresh: f32, rot_thresh: f32, scale_thresh: f32) {
895 self.frames.retain(|df| {
896 df.is_keyframe || !df.delta.is_near_zero(pos_thresh, rot_thresh, scale_thresh)
897 });
898 }
899}
900
901#[derive(Debug, Clone)]
904pub struct AnimationChunk {
905 pub chunk_id: u32,
906 pub start_frame: u32,
907 pub end_frame: u32,
908 pub tracks: Vec<CompressedTrack>,
909 pub byte_size: usize,
910}
911
912impl AnimationChunk {
913 pub fn new(chunk_id: u32, start_frame: u32, end_frame: u32) -> Self {
914 Self {
915 chunk_id,
916 start_frame,
917 end_frame,
918 tracks: Vec::new(),
919 byte_size: 0,
920 }
921 }
922
923 pub fn add_track(&mut self, track: CompressedTrack) {
924 self.byte_size += track.byte_size();
925 self.tracks.push(track);
926 }
927}
928
929#[derive(Debug, Clone)]
930pub struct StreamingAnimationAsset {
931 pub name: String,
932 pub total_frames: u32,
933 pub frame_rate: f32,
934 pub chunks: Vec<AnimationChunk>,
935 pub chunk_size: usize,
936 pub loaded_chunks: HashSet<u32>,
937}
938
939impl StreamingAnimationAsset {
940 pub fn new(name: &str, total_frames: u32, frame_rate: f32, chunk_size: usize) -> Self {
941 Self {
942 name: name.to_owned(),
943 total_frames,
944 frame_rate,
945 chunks: Vec::new(),
946 chunk_size,
947 loaded_chunks: HashSet::new(),
948 }
949 }
950
951 pub fn chunk_for_frame(&self, frame: u32) -> Option<u32> {
952 if self.chunk_size == 0 { return None; }
953 let chunk_id = frame / self.chunk_size as u32;
954 Some(chunk_id)
955 }
956
957 pub fn is_chunk_loaded(&self, chunk_id: u32) -> bool {
958 self.loaded_chunks.contains(&chunk_id)
959 }
960
961 pub fn mark_loaded(&mut self, chunk_id: u32) {
962 self.loaded_chunks.insert(chunk_id);
963 }
964
965 pub fn unload_chunk(&mut self, chunk_id: u32) {
966 self.loaded_chunks.remove(&chunk_id);
967 self.chunks.retain(|c| c.chunk_id != chunk_id);
968 }
969
970 pub fn prefetch_prediction(
972 &self,
973 current_frame: u32,
974 playback_speed: f32,
975 lookahead_seconds: f32,
976 ) -> Vec<u32> {
977 let lookahead_frames = (playback_speed.abs() * lookahead_seconds * self.frame_rate) as u32;
978 let end_frame = (current_frame + lookahead_frames).min(self.total_frames.saturating_sub(1));
979 let start_chunk = self.chunk_for_frame(current_frame).unwrap_or(0);
980 let end_chunk = self.chunk_for_frame(end_frame).unwrap_or(0);
981 let mut needed = Vec::new();
982 for c in start_chunk..=end_chunk {
983 if !self.is_chunk_loaded(c) {
984 needed.push(c);
985 }
986 }
987 needed
988 }
989
990 pub fn build_from_clip(clip: &AnimationClip, chunk_size: usize) -> Self {
991 let total_frames = (clip.duration * clip.frame_rate) as u32;
992 let mut asset = Self::new(&clip.name, total_frames, clip.frame_rate, chunk_size);
993 let n_chunks = (total_frames as usize + chunk_size - 1) / chunk_size;
994 for ci in 0..n_chunks {
995 let start = (ci * chunk_size) as u32;
996 let end = (((ci + 1) * chunk_size) as u32 - 1).min(total_frames - 1);
997 let mut chunk = AnimationChunk::new(ci as u32, start, end);
998 for track in &clip.tracks {
999 let t_start = start as f32 / clip.frame_rate;
1001 let t_end = end as f32 / clip.frame_rate;
1002 let kfs: Vec<Keyframe> = track.keyframes.iter()
1003 .filter(|kf| kf.time >= t_start - 0.001 && kf.time <= t_end + 0.001)
1004 .cloned()
1005 .collect();
1006 if !kfs.is_empty() {
1007 let sub_track = BoneTrack { bone_index: track.bone_index, keyframes: kfs, importance: track.importance };
1008 chunk.add_track(CompressedTrack::from_track(&sub_track, clip.frame_rate));
1009 }
1010 }
1011 asset.chunks.push(chunk);
1012 }
1013 asset
1014 }
1015}
1016
1017#[derive(Debug, Clone)]
1020pub struct BlendNode {
1021 pub name: String,
1022 pub clip: Option<AnimationClip>,
1023 pub children: Vec<BlendNode>,
1024 pub blend_weights: Vec<f32>,
1025}
1026
1027impl BlendNode {
1028 pub fn leaf(name: &str, clip: AnimationClip) -> Self {
1029 Self {
1030 name: name.to_owned(),
1031 clip: Some(clip),
1032 children: Vec::new(),
1033 blend_weights: Vec::new(),
1034 }
1035 }
1036
1037 pub fn blend(name: &str, children: Vec<BlendNode>, weights: Vec<f32>) -> Self {
1038 Self {
1039 name: name.to_owned(),
1040 clip: None,
1041 children,
1042 blend_weights: weights,
1043 }
1044 }
1045}
1046
1047pub fn extract_shared_base_pose(clips: &[&AnimationClip], n_bones: usize) -> ReferencePose {
1049 let mut avg_transforms = vec![Vec::new(); n_bones];
1050 for clip in clips {
1051 for track in &clip.tracks {
1052 let idx = track.bone_index as usize;
1053 if idx < n_bones {
1054 if let Some(kf) = track.keyframes.first() {
1055 avg_transforms[idx].push(kf.transform);
1056 }
1057 }
1058 }
1059 }
1060 let transforms = avg_transforms.iter().map(|ts| {
1061 if ts.is_empty() {
1062 Transform::identity()
1063 } else {
1064 let n = ts.len() as f32;
1066 let avg_pos = ts.iter().fold(Vec3::ZERO, |a, t| a + t.position) / n;
1067 let avg_scale = ts.iter().fold(Vec3::ZERO, |a, t| a + t.scale) / n;
1068 let mut avg_rot = ts[0].rotation;
1070 for t in ts.iter().skip(1) {
1071 avg_rot = avg_rot.slerp(t.rotation, 1.0 / n);
1072 }
1073 Transform { position: avg_pos, rotation: avg_rot.normalize(), scale: avg_scale }
1074 }
1075 }).collect();
1076 ReferencePose { transforms }
1077}
1078
1079#[derive(Debug, Clone)]
1080pub struct BlendTreeCompressed {
1081 pub base_pose: ReferencePose,
1082 pub clip_deltas: Vec<(String, Vec<DeltaCompressedTrack>)>,
1083}
1084
1085impl BlendTreeCompressed {
1086 pub fn compress(clips: &[&AnimationClip], n_bones: usize) -> Self {
1087 let base_pose = extract_shared_base_pose(clips, n_bones);
1088 let clip_deltas = clips.iter().map(|clip| {
1089 let tracks = clip.tracks.iter().map(|track| {
1090 let bone_idx = track.bone_index as usize;
1091 let reference = if bone_idx < base_pose.transforms.len() {
1092 base_pose.transforms[bone_idx]
1093 } else {
1094 Transform::identity()
1095 };
1096 DeltaCompressedTrack::from_track(track, &reference)
1097 }).collect();
1098 (clip.name.clone(), tracks)
1099 }).collect();
1100 Self { base_pose, clip_deltas }
1101 }
1102
1103 pub fn decompress_clip(&self, name: &str) -> Option<Vec<BoneTrack>> {
1104 let (_, delta_tracks) = self.clip_deltas.iter().find(|(n, _)| n == name)?;
1105 Some(delta_tracks.iter().map(|dt| dt.to_track()).collect())
1106 }
1107}
1108
1109#[derive(Debug, Clone)]
1112pub struct BoneMapping {
1113 pub source_bone: u32,
1114 pub target_bone: u32,
1115 pub scale_factor: f32,
1116 pub rotation_offset: Quat,
1117 pub position_offset: Vec3,
1118}
1119
1120impl BoneMapping {
1121 pub fn new(source: u32, target: u32) -> Self {
1122 Self {
1123 source_bone: source,
1124 target_bone: target,
1125 scale_factor: 1.0,
1126 rotation_offset: Quat::IDENTITY,
1127 position_offset: Vec3::ZERO,
1128 }
1129 }
1130
1131 pub fn with_scale(mut self, s: f32) -> Self {
1132 self.scale_factor = s;
1133 self
1134 }
1135
1136 pub fn with_rotation_offset(mut self, q: Quat) -> Self {
1137 self.rotation_offset = q;
1138 self
1139 }
1140}
1141
1142pub fn swing_twist_decompose(q: Quat, twist_axis: Vec3) -> (Quat, Quat) {
1144 let q_vec = Vec3::new(q.x, q.y, q.z);
1146 let proj = q_vec.dot(twist_axis) * twist_axis;
1147 let mut twist = Quat::from_xyzw(proj.x, proj.y, proj.z, q.w);
1148 if twist.length_squared() < 1e-10 {
1149 twist = Quat::IDENTITY;
1150 } else {
1151 twist = twist.normalize();
1152 }
1153 let swing = q * twist.inverse();
1154 (swing, twist)
1155}
1156
1157pub fn retarget_transform(t: &Transform, mapping: &BoneMapping) -> Transform {
1159 let (swing, twist) = swing_twist_decompose(t.rotation, Vec3::Y);
1160 Transform {
1162 position: t.position * mapping.scale_factor + mapping.position_offset,
1163 rotation: (mapping.rotation_offset * swing * twist).normalize(),
1164 scale: t.scale,
1165 }
1166}
1167
1168#[derive(Debug, Clone)]
1169pub struct RetargetingData {
1170 pub mappings: Vec<BoneMapping>,
1171 pub scale_factors: HashMap<u32, f32>,
1172}
1173
1174impl RetargetingData {
1175 pub fn new() -> Self {
1176 Self { mappings: Vec::new(), scale_factors: HashMap::new() }
1177 }
1178
1179 pub fn add_mapping(&mut self, m: BoneMapping) {
1180 self.mappings.push(m);
1181 }
1182
1183 pub fn retarget_clip(&self, source: &AnimationClip) -> AnimationClip {
1184 let mut result = AnimationClip::new(&source.name, source.frame_rate, source.duration);
1185 result.looping = source.looping;
1186 for track in &source.tracks {
1187 if let Some(mapping) = self.mappings.iter().find(|m| m.source_bone == track.bone_index) {
1188 let new_keyframes = track.keyframes.iter().map(|kf| {
1189 Keyframe {
1190 time: kf.time,
1191 transform: retarget_transform(&kf.transform, mapping),
1192 }
1193 }).collect();
1194 result.tracks.push(BoneTrack {
1195 bone_index: mapping.target_bone,
1196 keyframes: new_keyframes,
1197 importance: track.importance,
1198 });
1199 }
1200 }
1201 result
1202 }
1203}
1204
1205pub fn extract_additive_layer(
1209 clip: &AnimationClip,
1210 reference: &ReferencePose,
1211) -> AnimationClip {
1212 let mut additive = AnimationClip::new(
1213 &format!("{}_additive", clip.name),
1214 clip.frame_rate,
1215 clip.duration,
1216 );
1217 additive.looping = clip.looping;
1218
1219 for track in &clip.tracks {
1220 let bone_idx = track.bone_index as usize;
1221 let ref_t = if bone_idx < reference.transforms.len() {
1222 reference.transforms[bone_idx]
1223 } else {
1224 Transform::identity()
1225 };
1226
1227 let new_keyframes = track.keyframes.iter().map(|kf| {
1228 let pos_offset = kf.transform.position - ref_t.position;
1229 let rot_diff = ref_t.rotation.inverse() * kf.transform.rotation;
1230 let scale_mult = Vec3::new(
1231 kf.transform.scale.x / ref_t.scale.x.max(1e-6),
1232 kf.transform.scale.y / ref_t.scale.y.max(1e-6),
1233 kf.transform.scale.z / ref_t.scale.z.max(1e-6),
1234 );
1235 Keyframe {
1236 time: kf.time,
1237 transform: Transform {
1238 position: pos_offset,
1239 rotation: rot_diff.normalize(),
1240 scale: scale_mult,
1241 },
1242 }
1243 }).collect();
1244
1245 additive.tracks.push(BoneTrack {
1246 bone_index: track.bone_index,
1247 keyframes: new_keyframes,
1248 importance: track.importance,
1249 });
1250 }
1251 additive
1252}
1253
1254pub fn apply_additive_layer(
1256 base: &Transform,
1257 additive: &Transform,
1258 weight: f32,
1259) -> Transform {
1260 let pos = base.position + additive.position * weight;
1261 let rot_add = Quat::IDENTITY.slerp(additive.rotation, weight);
1262 let rot = (base.rotation * rot_add).normalize();
1263 let scale = base.scale * additive.scale.lerp(Vec3::ONE, 1.0 - weight);
1264 Transform { position: pos, rotation: rot, scale }
1265}
1266
1267pub fn apply_additive_clip_to_pose(
1269 base_pose: &mut Vec<Transform>,
1270 additive_clip: &AnimationClip,
1271 time: f32,
1272 weight: f32,
1273) {
1274 for track in &additive_clip.tracks {
1275 let idx = track.bone_index as usize;
1276 if idx < base_pose.len() {
1277 if let Some(add_t) = additive_clip.sample_at(track.bone_index, time) {
1278 base_pose[idx] = apply_additive_layer(&base_pose[idx], &add_t, weight);
1279 }
1280 }
1281 }
1282}
1283
1284#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1287pub enum BoneGroup {
1288 Spine,
1289 Arms,
1290 Legs,
1291 Hands,
1292 Fingers,
1293 Head,
1294 Face,
1295 Tail,
1296 Other,
1297}
1298
1299#[derive(Debug, Clone)]
1300pub struct BoneLodInfo {
1301 pub bone_index: u32,
1302 pub group: BoneGroup,
1303 pub min_lod_distance: f32, pub max_lod_distance: f32, }
1306
1307impl BoneLodInfo {
1308 pub fn new(bone_index: u32, group: BoneGroup) -> Self {
1309 let (min_d, max_d) = match group {
1310 BoneGroup::Fingers => (5.0, 15.0),
1311 BoneGroup::Face => (8.0, 20.0),
1312 BoneGroup::Hands => (10.0, 25.0),
1313 BoneGroup::Tail => (12.0, 30.0),
1314 BoneGroup::Head => (20.0, 50.0),
1315 BoneGroup::Arms | BoneGroup::Legs => (30.0, 80.0),
1316 BoneGroup::Spine => (50.0, 120.0),
1317 BoneGroup::Other => (15.0, 40.0),
1318 };
1319 Self { bone_index, group, min_lod_distance: min_d, max_lod_distance: max_d }
1320 }
1321
1322 pub fn lod_factor(&self, distance: f32) -> f32 {
1323 if distance <= self.min_lod_distance { 1.0 }
1324 else if distance >= self.max_lod_distance { 0.0 }
1325 else {
1326 1.0 - (distance - self.min_lod_distance) / (self.max_lod_distance - self.min_lod_distance)
1327 }
1328 }
1329
1330 pub fn should_skip(&self, distance: f32) -> bool {
1331 distance >= self.max_lod_distance
1332 }
1333}
1334
1335#[derive(Debug, Clone)]
1336pub struct LodAnimationVariant {
1337 pub lod_level: u32,
1338 pub distance_threshold: f32,
1339 pub clip: AnimationClip,
1340}
1341
1342pub fn create_lod_variants(
1344 clip: &AnimationClip,
1345 bone_lod_info: &[BoneLodInfo],
1346 lod_distances: &[f32], ) -> Vec<LodAnimationVariant> {
1348 lod_distances.iter().enumerate().map(|(lod_idx, &distance)| {
1349 let mut lod_clip = AnimationClip::new(
1350 &format!("{}_lod{}", clip.name, lod_idx),
1351 clip.frame_rate,
1352 clip.duration,
1353 );
1354 lod_clip.looping = clip.looping;
1355
1356 for track in &clip.tracks {
1357 let lod_info = bone_lod_info.iter().find(|b| b.bone_index == track.bone_index);
1358 let skip = lod_info.map_or(false, |b| b.should_skip(distance));
1359 if skip { continue; }
1360
1361 let lod_factor = lod_info.map_or(1.0, |b| b.lod_factor(distance));
1363 let base_eps_pos = 0.001;
1364 let base_eps_rot = 0.001;
1365 let eps_pos = base_eps_pos / lod_factor.max(0.01);
1366 let eps_rot = base_eps_rot / lod_factor.max(0.01);
1367
1368 let reduced = rdp_reduce_track(track, eps_pos, eps_rot);
1369 lod_clip.tracks.push(reduced);
1370 }
1371
1372 LodAnimationVariant {
1373 lod_level: lod_idx as u32,
1374 distance_threshold: distance,
1375 clip: lod_clip,
1376 }
1377 }).collect()
1378}
1379
1380pub fn select_lod_variant<'a>(
1382 variants: &'a [LodAnimationVariant],
1383 distance: f32,
1384) -> Option<&'a LodAnimationVariant> {
1385 variants.iter()
1387 .filter(|v| v.distance_threshold <= distance)
1388 .last()
1389 .or_else(|| variants.first())
1390}
1391
1392#[derive(Debug, Clone)]
1395pub struct CompressedAnimationClip {
1396 pub name: String,
1397 pub frame_rate: f32,
1398 pub duration: f32,
1399 pub looping: bool,
1400 pub tracks: Vec<CompressedTrack>,
1401 pub original_keyframe_count: usize,
1402 pub compressed_keyframe_count: usize,
1403 pub original_byte_size: usize,
1404 pub compressed_byte_size: usize,
1405}
1406
1407impl CompressedAnimationClip {
1408 pub fn compression_ratio(&self) -> f32 {
1409 if self.compressed_byte_size == 0 { return 0.0; }
1410 self.original_byte_size as f32 / self.compressed_byte_size as f32
1411 }
1412
1413 pub fn keyframe_reduction_ratio(&self) -> f32 {
1414 if self.compressed_keyframe_count == 0 { return 0.0; }
1415 self.original_keyframe_count as f32 / self.compressed_keyframe_count as f32
1416 }
1417}
1418
1419#[derive(Debug, Clone)]
1422pub struct CompressionErrorReport {
1423 pub clip_name: String,
1424 pub per_bone_max_pos_error: Vec<(u32, f32)>,
1425 pub per_bone_max_rot_error: Vec<(u32, f32)>,
1426 pub per_bone_max_scale_error: Vec<(u32, f32)>,
1427 pub global_max_pos_error: f32,
1428 pub global_max_rot_error: f32,
1429 pub global_rms_pos_error: f32,
1430 pub global_rms_rot_error: f32,
1431 pub total_original_keyframes: usize,
1432 pub total_compressed_keyframes: usize,
1433 pub byte_size_original: usize,
1434 pub byte_size_compressed: usize,
1435}
1436
1437impl CompressionErrorReport {
1438 pub fn new(clip_name: &str) -> Self {
1439 Self {
1440 clip_name: clip_name.to_owned(),
1441 per_bone_max_pos_error: Vec::new(),
1442 per_bone_max_rot_error: Vec::new(),
1443 per_bone_max_scale_error: Vec::new(),
1444 global_max_pos_error: 0.0,
1445 global_max_rot_error: 0.0,
1446 global_rms_pos_error: 0.0,
1447 global_rms_rot_error: 0.0,
1448 total_original_keyframes: 0,
1449 total_compressed_keyframes: 0,
1450 byte_size_original: 0,
1451 byte_size_compressed: 0,
1452 }
1453 }
1454}
1455
1456pub fn compute_error_report(
1457 original: &AnimationClip,
1458 compressed: &CompressedAnimationClip,
1459) -> CompressionErrorReport {
1460 let mut report = CompressionErrorReport::new(&original.name);
1461 report.total_original_keyframes = original.total_keyframes();
1462 report.total_compressed_keyframes = compressed.tracks.iter().map(|t| t.keyframes.len()).sum();
1463 report.byte_size_original = original.total_keyframes() * 40; report.byte_size_compressed = compressed.tracks.iter().map(|t| t.byte_size()).sum();
1466
1467 let mut pos_sq_sum = 0.0f64;
1468 let mut rot_sq_sum = 0.0f64;
1469 let mut sample_count = 0usize;
1470
1471 for orig_track in &original.tracks {
1472 let comp_track = match compressed.tracks.iter().find(|t| t.bone_index == orig_track.bone_index) {
1473 Some(t) => t,
1474 None => continue,
1475 };
1476 let decompressed = comp_track.to_track();
1477
1478 let mut bone_max_pos = 0.0f32;
1479 let mut bone_max_rot = 0.0f32;
1480 let mut bone_max_scale = 0.0f32;
1481
1482 for orig_kf in &orig_track.keyframes {
1483 let t = orig_kf.time;
1484 if let Some(decomp_t) = decompressed.keyframes.iter().enumerate().find_map(|(i, kf)| {
1486 if i == 0 || i == decompressed.keyframes.len() - 1 { return None; }
1487 let k0 = &decompressed.keyframes[i-1];
1488 let k1 = &decompressed.keyframes[i];
1489 if t >= k0.time && t <= k1.time {
1490 let dt = k1.time - k0.time;
1491 let a = if dt > 1e-9 { (t - k0.time) / dt } else { 0.0 };
1492 Some(k0.transform.lerp(&k1.transform, a))
1493 } else { None }
1494 }) {
1495 let pe = position_error_l2(orig_kf.transform.position, decomp_t.position);
1496 let re = rotation_error_geodesic(orig_kf.transform.rotation, decomp_t.rotation);
1497 let se = scale_error_l2(orig_kf.transform.scale, decomp_t.scale);
1498 bone_max_pos = bone_max_pos.max(pe);
1499 bone_max_rot = bone_max_rot.max(re);
1500 bone_max_scale = bone_max_scale.max(se);
1501 pos_sq_sum += (pe * pe) as f64;
1502 rot_sq_sum += (re * re) as f64;
1503 sample_count += 1;
1504 }
1505 }
1506
1507 report.per_bone_max_pos_error.push((orig_track.bone_index, bone_max_pos));
1508 report.per_bone_max_rot_error.push((orig_track.bone_index, bone_max_rot));
1509 report.per_bone_max_scale_error.push((orig_track.bone_index, bone_max_scale));
1510 report.global_max_pos_error = report.global_max_pos_error.max(bone_max_pos);
1511 report.global_max_rot_error = report.global_max_rot_error.max(bone_max_rot);
1512 }
1513
1514 if sample_count > 0 {
1515 report.global_rms_pos_error = ((pos_sq_sum / sample_count as f64).sqrt()) as f32;
1516 report.global_rms_rot_error = ((rot_sq_sum / sample_count as f64).sqrt()) as f32;
1517 }
1518
1519 report
1520}
1521
1522#[derive(Debug, Clone)]
1525pub struct CompressionSettings {
1526 pub pos_tolerance: f32,
1527 pub rot_tolerance_radians: f32,
1528 pub scale_tolerance: f32,
1529 pub use_rdp: bool,
1530 pub use_hermite_fitting: bool,
1531 pub use_quantization: bool,
1532 pub use_delta_compression: bool,
1533 pub tick_rate: f32,
1534 pub delta_pos_threshold: f32,
1535 pub delta_rot_threshold: f32,
1536 pub delta_scale_threshold: f32,
1537}
1538
1539impl Default for CompressionSettings {
1540 fn default() -> Self {
1541 Self {
1542 pos_tolerance: 0.001,
1543 rot_tolerance_radians: 0.001,
1544 scale_tolerance: 0.001,
1545 use_rdp: true,
1546 use_hermite_fitting: false,
1547 use_quantization: true,
1548 use_delta_compression: false,
1549 tick_rate: 30.0,
1550 delta_pos_threshold: 1e-5,
1551 delta_rot_threshold: 1e-4,
1552 delta_scale_threshold: 1e-4,
1553 }
1554 }
1555}
1556
1557pub struct AnimationCompressor {
1560 pub settings: CompressionSettings,
1561 pub bone_lod_info: Vec<BoneLodInfo>,
1562 pub retargeting: Option<RetargetingData>,
1563}
1564
1565impl AnimationCompressor {
1566 pub fn new() -> Self {
1567 Self {
1568 settings: CompressionSettings::default(),
1569 bone_lod_info: Vec::new(),
1570 retargeting: None,
1571 }
1572 }
1573
1574 pub fn with_settings(mut self, s: CompressionSettings) -> Self {
1575 self.settings = s;
1576 self
1577 }
1578
1579 pub fn with_bone_lod(mut self, info: Vec<BoneLodInfo>) -> Self {
1580 self.bone_lod_info = info;
1581 self
1582 }
1583
1584 pub fn with_retargeting(mut self, r: RetargetingData) -> Self {
1585 self.retargeting = Some(r);
1586 self
1587 }
1588
1589 pub fn compress(&self, clip: &AnimationClip) -> CompressedAnimationClip {
1591 let original_keyframe_count = clip.total_keyframes();
1592 let original_byte_size = original_keyframe_count * 40;
1593
1594 let mut compressed_tracks = Vec::new();
1595
1596 for track in &clip.tracks {
1597 let reduced = if self.settings.use_rdp {
1599 rdp_reduce_track(track, self.settings.pos_tolerance, self.settings.rot_tolerance_radians)
1600 } else {
1601 track.clone()
1602 };
1603
1604 let fitted = if self.settings.use_hermite_fitting {
1606 hermite_reduce_track(&reduced, self.settings.pos_tolerance)
1607 } else {
1608 reduced
1609 };
1610
1611 if self.settings.use_quantization {
1613 compressed_tracks.push(CompressedTrack::from_track(&fitted, self.settings.tick_rate));
1614 } else {
1615 compressed_tracks.push(CompressedTrack::from_track(&fitted, self.settings.tick_rate));
1616 }
1617 }
1618
1619 let compressed_keyframe_count = compressed_tracks.iter().map(|t| t.keyframes.len()).sum();
1620 let compressed_byte_size = compressed_tracks.iter().map(|t| t.byte_size()).sum();
1621
1622 CompressedAnimationClip {
1623 name: clip.name.clone(),
1624 frame_rate: clip.frame_rate,
1625 duration: clip.duration,
1626 looping: clip.looping,
1627 tracks: compressed_tracks,
1628 original_keyframe_count,
1629 compressed_keyframe_count,
1630 original_byte_size,
1631 compressed_byte_size,
1632 }
1633 }
1634
1635 pub fn decompress(&self, compressed: &CompressedAnimationClip) -> AnimationClip {
1637 let mut clip = AnimationClip::new(&compressed.name, compressed.frame_rate, compressed.duration);
1638 clip.looping = compressed.looping;
1639 for ct in &compressed.tracks {
1640 clip.tracks.push(ct.to_track());
1641 }
1642 clip
1643 }
1644
1645 pub fn error_analysis(
1647 &self,
1648 original: &AnimationClip,
1649 compressed: &CompressedAnimationClip,
1650 ) -> CompressionErrorReport {
1651 compute_error_report(original, compressed)
1652 }
1653
1654 pub fn batch_compress(&self, clips: &[&AnimationClip]) -> Vec<CompressedAnimationClip> {
1656 clips.iter().map(|c| self.compress(c)).collect()
1657 }
1658
1659 pub fn compress_with_lod(
1661 &self,
1662 clip: &AnimationClip,
1663 lod_distances: &[f32],
1664 ) -> Vec<CompressedAnimationClip> {
1665 let variants = create_lod_variants(clip, &self.bone_lod_info, lod_distances);
1666 variants.iter().map(|v| self.compress(&v.clip)).collect()
1667 }
1668
1669 pub fn build_streaming_asset(
1671 &self,
1672 clip: &AnimationClip,
1673 chunk_size: usize,
1674 ) -> StreamingAnimationAsset {
1675 StreamingAnimationAsset::build_from_clip(clip, chunk_size)
1676 }
1677}
1678
1679pub struct PoseEvaluator {
1683 pub n_bones: usize,
1684}
1685
1686impl PoseEvaluator {
1687 pub fn new(n_bones: usize) -> Self {
1688 Self { n_bones }
1689 }
1690
1691 pub fn evaluate(&self, clip: &AnimationClip, time: f32) -> Vec<Transform> {
1692 let mut pose = vec![Transform::identity(); self.n_bones];
1693 for track in &clip.tracks {
1694 let idx = track.bone_index as usize;
1695 if idx < self.n_bones {
1696 if let Some(t) = clip.sample_at(track.bone_index, time) {
1697 pose[idx] = t;
1698 }
1699 }
1700 }
1701 pose
1702 }
1703
1704 pub fn evaluate_compressed(&self, clip: &CompressedAnimationClip, time: f32) -> Vec<Transform> {
1705 let mut pose = vec![Transform::identity(); self.n_bones];
1706 for ct in &clip.tracks {
1707 let idx = ct.bone_index as usize;
1708 if idx >= self.n_bones || ct.keyframes.is_empty() { continue; }
1709 let decompressed = ct.to_track();
1710 let t_start = ct.keyframes.first().map(|k| k.time_ticks as f32 / (ct.tick_rate * 1000.0)).unwrap_or(0.0);
1712 let t_end = ct.keyframes.last().map(|k| k.time_ticks as f32 / (ct.tick_rate * 1000.0)).unwrap_or(0.0);
1713 let t_clamped = time.clamp(t_start, t_end);
1714 let kfs = &decompressed.keyframes;
1715 if kfs.len() == 1 {
1716 pose[idx] = kfs[0].transform;
1717 continue;
1718 }
1719 let seg_idx = kfs.partition_point(|kf| kf.time <= t_clamped).min(kfs.len() - 1);
1720 let seg_idx = seg_idx.max(1);
1721 let kf0 = &kfs[seg_idx - 1];
1722 let kf1 = &kfs[seg_idx];
1723 let dt = kf1.time - kf0.time;
1724 let alpha = if dt > 1e-9 { (t_clamped - kf0.time) / dt } else { 0.0 };
1725 pose[idx] = kf0.transform.lerp(&kf1.transform, alpha);
1726 }
1727 pose
1728 }
1729
1730 pub fn blend_poses(
1731 &self,
1732 pose_a: &[Transform],
1733 pose_b: &[Transform],
1734 weight: f32,
1735 ) -> Vec<Transform> {
1736 pose_a.iter().zip(pose_b.iter()).map(|(a, b)| a.lerp(b, weight)).collect()
1737 }
1738}
1739
1740#[derive(Debug, Clone, Default)]
1743pub struct BatchCompressionStats {
1744 pub total_clips: usize,
1745 pub total_original_bytes: usize,
1746 pub total_compressed_bytes: usize,
1747 pub total_original_keyframes: usize,
1748 pub total_compressed_keyframes: usize,
1749 pub avg_compression_ratio: f32,
1750 pub max_pos_error: f32,
1751 pub max_rot_error: f32,
1752 pub per_clip: Vec<(String, f32, f32)>, }
1754
1755impl BatchCompressionStats {
1756 pub fn compute(
1757 originals: &[&AnimationClip],
1758 compressed: &[CompressedAnimationClip],
1759 ) -> Self {
1760 let mut stats = Self::default();
1761 stats.total_clips = originals.len();
1762 for (orig, comp) in originals.iter().zip(compressed.iter()) {
1763 stats.total_original_bytes += orig.total_keyframes() * 40;
1764 stats.total_compressed_bytes += comp.compressed_byte_size;
1765 stats.total_original_keyframes += orig.total_keyframes();
1766 stats.total_compressed_keyframes += comp.compressed_keyframe_count;
1767 let ratio = if comp.compressed_byte_size > 0 {
1768 (orig.total_keyframes() * 40) as f32 / comp.compressed_byte_size as f32
1769 } else { 0.0 };
1770 stats.per_clip.push((orig.name.clone(), ratio, 0.0));
1771 }
1772 if stats.total_clips > 0 {
1773 let sum: f32 = stats.per_clip.iter().map(|(_, r, _)| r).sum();
1774 stats.avg_compression_ratio = sum / stats.total_clips as f32;
1775 }
1776 stats
1777 }
1778}
1779
1780#[derive(Debug, Clone)]
1783pub struct AdditiveLayer {
1784 pub name: String,
1785 pub clip: AnimationClip,
1786 pub weight: f32,
1787 pub mask: Vec<u32>, }
1789
1790impl AdditiveLayer {
1791 pub fn new(name: &str, clip: AnimationClip, weight: f32) -> Self {
1792 Self { name: name.to_owned(), clip, weight, mask: Vec::new() }
1793 }
1794
1795 pub fn with_mask(mut self, mask: Vec<u32>) -> Self {
1796 self.mask = mask;
1797 self
1798 }
1799
1800 pub fn is_masked(&self, bone_index: u32) -> bool {
1801 self.mask.is_empty() || self.mask.contains(&bone_index)
1802 }
1803}
1804
1805pub struct AdditiveLayerStack {
1806 pub base_pose: Vec<Transform>,
1807 pub layers: Vec<AdditiveLayer>,
1808}
1809
1810impl AdditiveLayerStack {
1811 pub fn new(n_bones: usize) -> Self {
1812 Self {
1813 base_pose: vec![Transform::identity(); n_bones],
1814 layers: Vec::new(),
1815 }
1816 }
1817
1818 pub fn push_layer(&mut self, layer: AdditiveLayer) {
1819 self.layers.push(layer);
1820 }
1821
1822 pub fn evaluate(&self, time: f32) -> Vec<Transform> {
1823 let mut pose = self.base_pose.clone();
1824 for layer in &self.layers {
1825 for (bone_idx, transform) in pose.iter_mut().enumerate() {
1826 if !layer.is_masked(bone_idx as u32) { continue; }
1827 if let Some(add_t) = layer.clip.sample_at(bone_idx as u32, time) {
1828 *transform = apply_additive_layer(transform, &add_t, layer.weight);
1829 }
1830 }
1831 }
1832 pose
1833 }
1834}
1835
1836#[derive(Debug, Clone, Copy, PartialEq)]
1839pub enum CurveChannel {
1840 PosX, PosY, PosZ,
1841 RotW, RotX, RotY, RotZ,
1842 ScaleX, ScaleY, ScaleZ,
1843}
1844
1845#[derive(Debug, Clone)]
1846pub struct ScalarKeyframe {
1847 pub time: f32,
1848 pub value: f32,
1849}
1850
1851#[derive(Debug, Clone)]
1852pub struct ScalarCurve {
1853 pub channel: CurveChannel,
1854 pub keyframes: Vec<ScalarKeyframe>,
1855}
1856
1857impl ScalarCurve {
1858 pub fn new(channel: CurveChannel) -> Self {
1859 Self { channel, keyframes: Vec::new() }
1860 }
1861
1862 pub fn sample(&self, t: f32) -> f32 {
1863 if self.keyframes.is_empty() { return 0.0; }
1864 if self.keyframes.len() == 1 { return self.keyframes[0].value; }
1865 let idx = self.keyframes.partition_point(|kf| kf.time <= t);
1866 if idx == 0 { return self.keyframes[0].value; }
1867 if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().value; }
1868 let k0 = &self.keyframes[idx - 1];
1869 let k1 = &self.keyframes[idx];
1870 let dt = k1.time - k0.time;
1871 let alpha = if dt > 1e-9 { (t - k0.time) / dt } else { 0.0 };
1872 k0.value + (k1.value - k0.value) * alpha
1873 }
1874
1875 pub fn rdp_reduce(&self, epsilon: f32) -> ScalarCurve {
1876 if self.keyframes.len() <= 2 { return self.clone(); }
1877 let n = self.keyframes.len();
1878 let mut keep = vec![false; n];
1879 keep[0] = true;
1880 keep[n-1] = true;
1881 scalar_rdp(&self.keyframes, 0, n-1, epsilon, &mut keep);
1882 ScalarCurve {
1883 channel: self.channel,
1884 keyframes: self.keyframes.iter().enumerate()
1885 .filter(|(i, _)| keep[*i])
1886 .map(|(_, kf)| kf.clone())
1887 .collect(),
1888 }
1889 }
1890}
1891
1892fn scalar_rdp(kfs: &[ScalarKeyframe], start: usize, end: usize, eps: f32, keep: &mut Vec<bool>) {
1893 if end - start < 2 { return; }
1894 let t0 = kfs[start].time;
1895 let t1 = kfs[end].time;
1896 let v0 = kfs[start].value;
1897 let v1 = kfs[end].value;
1898 let dt = t1 - t0;
1899
1900 let mut max_dist = 0.0f32;
1901 let mut max_idx = start + 1;
1902 for i in (start+1)..end {
1903 let alpha = if dt > 1e-9 { (kfs[i].time - t0) / dt } else { 0.0 };
1904 let interp = v0 + (v1 - v0) * alpha;
1905 let d = (kfs[i].value - interp).abs();
1906 if d > max_dist { max_dist = d; max_idx = i; }
1907 }
1908
1909 if max_dist > eps {
1910 keep[max_idx] = true;
1911 scalar_rdp(kfs, start, max_idx, eps, keep);
1912 scalar_rdp(kfs, max_idx, end, eps, keep);
1913 }
1914}
1915
1916pub fn decompose_track_to_scalar_curves(track: &BoneTrack) -> Vec<ScalarCurve> {
1918 let channels = [
1919 CurveChannel::PosX, CurveChannel::PosY, CurveChannel::PosZ,
1920 CurveChannel::RotW, CurveChannel::RotX, CurveChannel::RotY, CurveChannel::RotZ,
1921 CurveChannel::ScaleX, CurveChannel::ScaleY, CurveChannel::ScaleZ,
1922 ];
1923 channels.iter().map(|&ch| {
1924 let mut curve = ScalarCurve::new(ch);
1925 for kf in &track.keyframes {
1926 let value = match ch {
1927 CurveChannel::PosX => kf.transform.position.x,
1928 CurveChannel::PosY => kf.transform.position.y,
1929 CurveChannel::PosZ => kf.transform.position.z,
1930 CurveChannel::RotW => kf.transform.rotation.w,
1931 CurveChannel::RotX => kf.transform.rotation.x,
1932 CurveChannel::RotY => kf.transform.rotation.y,
1933 CurveChannel::RotZ => kf.transform.rotation.z,
1934 CurveChannel::ScaleX => kf.transform.scale.x,
1935 CurveChannel::ScaleY => kf.transform.scale.y,
1936 CurveChannel::ScaleZ => kf.transform.scale.z,
1937 };
1938 curve.keyframes.push(ScalarKeyframe { time: kf.time, value });
1939 }
1940 curve
1941 }).collect()
1942}
1943
1944pub fn average_quaternions(quats: &[(Quat, f32)]) -> Quat {
1948 if quats.is_empty() { return Quat::IDENTITY; }
1949 if quats.len() == 1 { return quats[0].0; }
1950 let mut result = quats[0].0;
1951 let total_w: f32 = quats.iter().map(|(_, w)| w).sum();
1952 if total_w < 1e-9 { return Quat::IDENTITY; }
1953 let mut acc_w = quats[0].1 / total_w;
1954 for &(q, w) in quats.iter().skip(1) {
1955 let t = (w / total_w) / (acc_w + w / total_w).max(1e-9);
1956 result = result.slerp(q, t);
1957 acc_w += w / total_w;
1958 }
1959 result.normalize()
1960}
1961
1962pub struct BitWriter {
1965 pub data: Vec<u8>,
1966 pub bit_pos: usize,
1967}
1968
1969impl BitWriter {
1970 pub fn new() -> Self {
1971 Self { data: Vec::new(), bit_pos: 0 }
1972 }
1973
1974 pub fn write_bits(&mut self, value: u64, n_bits: usize) {
1975 for i in 0..n_bits {
1976 let bit = ((value >> (n_bits - 1 - i)) & 1) as u8;
1977 let byte_idx = self.bit_pos / 8;
1978 let bit_offset = 7 - (self.bit_pos % 8);
1979 if byte_idx >= self.data.len() {
1980 self.data.push(0);
1981 }
1982 self.data[byte_idx] |= bit << bit_offset;
1983 self.bit_pos += 1;
1984 }
1985 }
1986
1987 pub fn write_u16(&mut self, v: u16) { self.write_bits(v as u64, 16); }
1988 pub fn write_u8(&mut self, v: u8) { self.write_bits(v as u64, 8); }
1989 pub fn write_i16(&mut self, v: i16) { self.write_bits(v as u16 as u64, 16); }
1990 pub fn write_u32(&mut self, v: u32) { self.write_bits(v as u64, 32); }
1991
1992 pub fn byte_size(&self) -> usize {
1993 (self.bit_pos + 7) / 8
1994 }
1995}
1996
1997pub struct BitReader<'a> {
1998 pub data: &'a [u8],
1999 pub bit_pos: usize,
2000}
2001
2002impl<'a> BitReader<'a> {
2003 pub fn new(data: &'a [u8]) -> Self {
2004 Self { data, bit_pos: 0 }
2005 }
2006
2007 pub fn read_bits(&mut self, n_bits: usize) -> u64 {
2008 let mut value = 0u64;
2009 for i in 0..n_bits {
2010 let byte_idx = self.bit_pos / 8;
2011 let bit_offset = 7 - (self.bit_pos % 8);
2012 if byte_idx >= self.data.len() { break; }
2013 let bit = ((self.data[byte_idx] >> bit_offset) & 1) as u64;
2014 value |= bit << (n_bits - 1 - i);
2015 self.bit_pos += 1;
2016 }
2017 value
2018 }
2019
2020 pub fn read_u16(&mut self) -> u16 { self.read_bits(16) as u16 }
2021 pub fn read_u8(&mut self) -> u8 { self.read_bits(8) as u8 }
2022 pub fn read_i16(&mut self) -> i16 { self.read_bits(16) as i16 }
2023 pub fn read_u32(&mut self) -> u32 { self.read_bits(32) as u32 }
2024}
2025
2026pub fn serialize_compressed_track(track: &CompressedTrack) -> Vec<u8> {
2028 let mut w = BitWriter::new();
2029 w.write_u32(track.bone_index);
2030 let bounds_data: [f32; 6] = [
2032 track.pos_bounds.min.x, track.pos_bounds.min.y, track.pos_bounds.min.z,
2033 track.pos_bounds.max.x, track.pos_bounds.max.y, track.pos_bounds.max.z,
2034 ];
2035 for &f in &bounds_data {
2036 w.write_u32(f.to_bits());
2037 }
2038 w.write_u32(track.tick_rate.to_bits());
2039 w.write_u32(track.keyframes.len() as u32);
2040 for kf in &track.keyframes {
2041 w.write_u32(kf.time_ticks);
2042 w.write_u16(kf.position[0]);
2043 w.write_u16(kf.position[1]);
2044 w.write_u16(kf.position[2]);
2045 w.write_u8(kf.rotation.largest_component);
2046 w.write_i16(kf.rotation.components[0]);
2047 w.write_i16(kf.rotation.components[1]);
2048 w.write_i16(kf.rotation.components[2]);
2049 w.write_u8(kf.scale[0]);
2050 w.write_u8(kf.scale[1]);
2051 w.write_u8(kf.scale[2]);
2052 }
2053 w.data
2054}
2055
2056pub fn deserialize_compressed_track(data: &[u8]) -> CompressedTrack {
2057 let mut r = BitReader::new(data);
2058 let bone_index = r.read_u32();
2059 let min_x = f32::from_bits(r.read_u32());
2060 let min_y = f32::from_bits(r.read_u32());
2061 let min_z = f32::from_bits(r.read_u32());
2062 let max_x = f32::from_bits(r.read_u32());
2063 let max_y = f32::from_bits(r.read_u32());
2064 let max_z = f32::from_bits(r.read_u32());
2065 let tick_rate = f32::from_bits(r.read_u32());
2066 let n_kf = r.read_u32() as usize;
2067 let mut keyframes = Vec::with_capacity(n_kf);
2068 for _ in 0..n_kf {
2069 let time_ticks = r.read_u32();
2070 let pos = [r.read_u16(), r.read_u16(), r.read_u16()];
2071 let largest_component = r.read_u8();
2072 let c0 = r.read_i16();
2073 let c1 = r.read_i16();
2074 let c2 = r.read_i16();
2075 let sc = [r.read_u8(), r.read_u8(), r.read_u8()];
2076 keyframes.push(CompressedKeyframe {
2077 time_ticks,
2078 position: pos,
2079 rotation: CompressedQuat { largest_component, components: [c0, c1, c2] },
2080 scale: sc,
2081 });
2082 }
2083 CompressedTrack {
2084 bone_index,
2085 pos_bounds: PositionBounds {
2086 min: Vec3::new(min_x, min_y, min_z),
2087 max: Vec3::new(max_x, max_y, max_z),
2088 },
2089 tick_rate,
2090 keyframes,
2091 }
2092}
2093
2094pub struct AnimationRegistry {
2097 pub clips: HashMap<String, AnimationClip>,
2098 pub compressed: HashMap<String, CompressedAnimationClip>,
2099 pub streaming: HashMap<String, StreamingAnimationAsset>,
2100}
2101
2102impl AnimationRegistry {
2103 pub fn new() -> Self {
2104 Self {
2105 clips: HashMap::new(),
2106 compressed: HashMap::new(),
2107 streaming: HashMap::new(),
2108 }
2109 }
2110
2111 pub fn register(&mut self, clip: AnimationClip) {
2112 self.clips.insert(clip.name.clone(), clip);
2113 }
2114
2115 pub fn compress_all(&mut self, compressor: &AnimationCompressor) {
2116 let names: Vec<String> = self.clips.keys().cloned().collect();
2117 for name in names {
2118 if let Some(clip) = self.clips.get(&name) {
2119 let c = compressor.compress(clip);
2120 self.compressed.insert(name, c);
2121 }
2122 }
2123 }
2124
2125 pub fn build_streaming_all(&mut self, compressor: &AnimationCompressor, chunk_size: usize) {
2126 let names: Vec<String> = self.clips.keys().cloned().collect();
2127 for name in names {
2128 if let Some(clip) = self.clips.get(&name) {
2129 let s = compressor.build_streaming_asset(clip, chunk_size);
2130 self.streaming.insert(name, s);
2131 }
2132 }
2133 }
2134
2135 pub fn get_clip(&self, name: &str) -> Option<&AnimationClip> {
2136 self.clips.get(name)
2137 }
2138
2139 pub fn get_compressed(&self, name: &str) -> Option<&CompressedAnimationClip> {
2140 self.compressed.get(name)
2141 }
2142}
2143
2144pub struct CompressionPipeline {
2147 pub compressor: AnimationCompressor,
2148 pub registry: AnimationRegistry,
2149 pub stats: BatchCompressionStats,
2150}
2151
2152impl CompressionPipeline {
2153 pub fn new() -> Self {
2154 Self {
2155 compressor: AnimationCompressor::new(),
2156 registry: AnimationRegistry::new(),
2157 stats: BatchCompressionStats::default(),
2158 }
2159 }
2160
2161 pub fn add_clip(&mut self, clip: AnimationClip) {
2162 self.registry.register(clip);
2163 }
2164
2165 pub fn run(&mut self) {
2166 self.registry.compress_all(&self.compressor);
2167 let originals: Vec<&AnimationClip> = self.registry.clips.values().collect();
2168 let compressed: Vec<&CompressedAnimationClip> = self.registry.compressed.values().collect();
2169 let orig_refs: Vec<&AnimationClip> = originals.iter().map(|c| *c).collect();
2170 let comp_vals: Vec<CompressedAnimationClip> = compressed.iter().map(|c| (*c).clone()).collect();
2171 self.stats = BatchCompressionStats::compute(&orig_refs, &comp_vals);
2172 }
2173
2174 pub fn report(&self) -> String {
2175 format!(
2176 "Compression pipeline: {} clips, {:.2}x avg ratio, {}/{} keyframes",
2177 self.stats.total_clips,
2178 self.stats.avg_compression_ratio,
2179 self.stats.total_compressed_keyframes,
2180 self.stats.total_original_keyframes,
2181 )
2182 }
2183}
2184
2185#[derive(Debug, Clone)]
2189pub struct SpringDamper {
2190 pub position: Vec3,
2191 pub velocity: Vec3,
2192 pub stiffness: f32,
2193 pub damping: f32,
2194}
2195
2196impl SpringDamper {
2197 pub fn new(stiffness: f32, damping: f32) -> Self {
2198 Self {
2199 position: Vec3::ZERO,
2200 velocity: Vec3::ZERO,
2201 stiffness,
2202 damping,
2203 }
2204 }
2205
2206 pub fn update(&mut self, target: Vec3, dt: f32) -> Vec3 {
2207 let force = (target - self.position) * self.stiffness - self.velocity * self.damping;
2208 self.velocity += force * dt;
2209 self.position += self.velocity * dt;
2210 self.position
2211 }
2212}
2213
2214pub fn ik_two_bone(
2216 root: Vec3,
2217 mid: Vec3,
2218 end: Vec3,
2219 target: Vec3,
2220 pole: Vec3,
2221 upper_len: f32,
2222 lower_len: f32,
2223) -> (Quat, Quat) {
2224 let total_len = upper_len + lower_len;
2225 let to_target = target - root;
2226 let target_dist = to_target.length().min(total_len * 0.9999);
2227
2228 let cos_a = (upper_len * upper_len + target_dist * target_dist - lower_len * lower_len)
2230 / (2.0 * upper_len * target_dist + 1e-9);
2231 let cos_a = cos_a.clamp(-1.0, 1.0);
2232 let angle_a = cos_a.acos();
2233
2234 let dir_to_target = if to_target.length() > 1e-6 { to_target.normalize() } else { Vec3::Y };
2236
2237 let pole_dir = (pole - root).normalize();
2239 let perp = dir_to_target.cross(pole_dir);
2240 let bend_dir = if perp.length() > 1e-6 {
2241 perp.normalize().cross(dir_to_target).normalize()
2242 } else {
2243 Vec3::Z
2244 };
2245
2246 let mid_offset = dir_to_target * (upper_len * cos_a) + bend_dir * (upper_len * angle_a.sin());
2247 let new_mid = root + mid_offset;
2248
2249 let upper_rot = Quat::from_rotation_arc(Vec3::Y, (new_mid - root).normalize());
2251 let lower_dir = (target - new_mid).normalize();
2252 let lower_rot = Quat::from_rotation_arc((new_mid - root).normalize(), lower_dir);
2253
2254 (upper_rot, lower_rot)
2255}
2256
2257pub fn extract_frame_range(
2261 clip: &AnimationClip,
2262 t_start: f32,
2263 t_end: f32,
2264) -> AnimationClip {
2265 let duration = t_end - t_start;
2266 let mut result = AnimationClip::new(
2267 &format!("{}_range_{:.2}_{:.2}", clip.name, t_start, t_end),
2268 clip.frame_rate,
2269 duration.max(0.0),
2270 );
2271 result.looping = clip.looping;
2272 for track in &clip.tracks {
2273 let kfs: Vec<Keyframe> = track.keyframes.iter()
2274 .filter(|kf| kf.time >= t_start && kf.time <= t_end)
2275 .map(|kf| Keyframe { time: kf.time - t_start, transform: kf.transform })
2276 .collect();
2277 if !kfs.is_empty() {
2278 result.tracks.push(BoneTrack {
2279 bone_index: track.bone_index,
2280 keyframes: kfs,
2281 importance: track.importance,
2282 });
2283 }
2284 }
2285 result
2286}
2287
2288pub fn mirror_animation(clip: &AnimationClip, bone_mirror_map: &HashMap<u32, u32>) -> AnimationClip {
2290 let mut mirrored = AnimationClip::new(
2291 &format!("{}_mirror", clip.name),
2292 clip.frame_rate,
2293 clip.duration,
2294 );
2295 mirrored.looping = clip.looping;
2296 for track in &clip.tracks {
2297 let target_bone = *bone_mirror_map.get(&track.bone_index).unwrap_or(&track.bone_index);
2298 let new_kfs: Vec<Keyframe> = track.keyframes.iter().map(|kf| {
2299 let mut pos = kf.transform.position;
2300 pos.x = -pos.x; let rot = kf.transform.rotation;
2303 let mirrored_rot = Quat::from_xyzw(-rot.x, rot.y, rot.z, -rot.w).normalize();
2304 Keyframe {
2305 time: kf.time,
2306 transform: Transform { position: pos, rotation: mirrored_rot, scale: kf.transform.scale },
2307 }
2308 }).collect();
2309 mirrored.tracks.push(BoneTrack {
2310 bone_index: target_bone,
2311 keyframes: new_kfs,
2312 importance: track.importance,
2313 });
2314 }
2315 mirrored
2316}
2317
2318#[derive(Debug, Clone)]
2321pub struct ReductionStats {
2322 pub original_count: usize,
2323 pub reduced_count: usize,
2324 pub reduction_percent: f32,
2325}
2326
2327impl ReductionStats {
2328 pub fn compute(original: &BoneTrack, reduced: &BoneTrack) -> Self {
2329 let orig = original.keyframes.len();
2330 let red = reduced.keyframes.len();
2331 let pct = if orig > 0 { (1.0 - red as f32 / orig as f32) * 100.0 } else { 0.0 };
2332 Self { original_count: orig, reduced_count: red, reduction_percent: pct }
2333 }
2334}
2335
2336pub fn retarget_track_swing_twist(
2340 track: &BoneTrack,
2341 mapping: &BoneMapping,
2342 twist_axis: Vec3,
2343) -> BoneTrack {
2344 let new_kfs: Vec<Keyframe> = track.keyframes.iter().map(|kf| {
2345 let (swing, twist) = swing_twist_decompose(kf.transform.rotation, twist_axis);
2346 let new_rot = (mapping.rotation_offset * swing * twist).normalize();
2348 Keyframe {
2349 time: kf.time,
2350 transform: Transform {
2351 position: kf.transform.position * mapping.scale_factor + mapping.position_offset,
2352 rotation: new_rot,
2353 scale: kf.transform.scale,
2354 },
2355 }
2356 }).collect();
2357 BoneTrack {
2358 bone_index: mapping.target_bone,
2359 keyframes: new_kfs,
2360 importance: track.importance,
2361 }
2362}
2363
2364#[derive(Debug, Clone)]
2367pub struct AnimationEvent {
2368 pub time: f32,
2369 pub name: String,
2370 pub params: HashMap<String, f32>,
2371}
2372
2373impl AnimationEvent {
2374 pub fn new(time: f32, name: &str) -> Self {
2375 Self { time, name: name.to_owned(), params: HashMap::new() }
2376 }
2377
2378 pub fn with_param(mut self, key: &str, value: f32) -> Self {
2379 self.params.insert(key.to_owned(), value);
2380 self
2381 }
2382}
2383
2384pub struct AnimationEventTrack {
2385 pub events: Vec<AnimationEvent>,
2386}
2387
2388impl AnimationEventTrack {
2389 pub fn new() -> Self {
2390 Self { events: Vec::new() }
2391 }
2392
2393 pub fn add(&mut self, event: AnimationEvent) {
2394 self.events.push(event);
2395 self.events.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
2396 }
2397
2398 pub fn events_in_range(&self, t_start: f32, t_end: f32) -> Vec<&AnimationEvent> {
2399 self.events.iter().filter(|e| e.time >= t_start && e.time < t_end).collect()
2400 }
2401}
2402
2403pub fn preset_lossless() -> CompressionSettings {
2406 CompressionSettings {
2407 pos_tolerance: 0.0,
2408 rot_tolerance_radians: 0.0,
2409 scale_tolerance: 0.0,
2410 use_rdp: false,
2411 use_hermite_fitting: false,
2412 use_quantization: false,
2413 use_delta_compression: false,
2414 ..Default::default()
2415 }
2416}
2417
2418pub fn preset_high_quality() -> CompressionSettings {
2419 CompressionSettings {
2420 pos_tolerance: 0.0005,
2421 rot_tolerance_radians: 0.0005,
2422 scale_tolerance: 0.001,
2423 use_rdp: true,
2424 use_hermite_fitting: true,
2425 use_quantization: true,
2426 use_delta_compression: false,
2427 ..Default::default()
2428 }
2429}
2430
2431pub fn preset_medium_quality() -> CompressionSettings {
2432 CompressionSettings {
2433 pos_tolerance: 0.002,
2434 rot_tolerance_radians: 0.002,
2435 scale_tolerance: 0.005,
2436 use_rdp: true,
2437 use_hermite_fitting: false,
2438 use_quantization: true,
2439 use_delta_compression: true,
2440 ..Default::default()
2441 }
2442}
2443
2444pub fn preset_low_quality() -> CompressionSettings {
2445 CompressionSettings {
2446 pos_tolerance: 0.01,
2447 rot_tolerance_radians: 0.01,
2448 scale_tolerance: 0.02,
2449 use_rdp: true,
2450 use_hermite_fitting: false,
2451 use_quantization: true,
2452 use_delta_compression: true,
2453 ..Default::default()
2454 }
2455}
2456
2457pub struct AdaptiveCompressor {
2461 pub target_max_pos_error: f32,
2462 pub target_max_rot_error: f32,
2463 pub n_passes: usize,
2464}
2465
2466impl AdaptiveCompressor {
2467 pub fn new(target_pos: f32, target_rot: f32) -> Self {
2468 Self { target_max_pos_error: target_pos, target_max_rot_error: target_rot, n_passes: 8 }
2469 }
2470
2471 pub fn compress(&self, clip: &AnimationClip) -> CompressedAnimationClip {
2472 let mut lo = 0.0f32;
2473 let mut hi = 0.1f32;
2474 let mut best: Option<CompressedAnimationClip> = None;
2475
2476 for _ in 0..self.n_passes {
2477 let mid = (lo + hi) / 2.0;
2478 let settings = CompressionSettings {
2479 pos_tolerance: mid,
2480 rot_tolerance_radians: mid * 2.0,
2481 use_rdp: true,
2482 use_quantization: true,
2483 ..Default::default()
2484 };
2485 let compressor = AnimationCompressor::new().with_settings(settings);
2486 let compressed = compressor.compress(clip);
2487 let report = compute_error_report(clip, &compressed);
2488
2489 if report.global_max_pos_error <= self.target_max_pos_error
2490 && report.global_max_rot_error <= self.target_max_rot_error
2491 {
2492 best = Some(compressed);
2493 lo = mid; } else {
2495 hi = mid; }
2497 }
2498
2499 best.unwrap_or_else(|| {
2500 let compressor = AnimationCompressor::new();
2501 compressor.compress(clip)
2502 })
2503 }
2504}
2505
2506pub fn compute_bone_importance_from_velocity(track: &BoneTrack) -> f32 {
2510 if track.keyframes.len() < 2 { return 0.5; }
2511 let mut total_vel = 0.0f32;
2512 for i in 1..track.keyframes.len() {
2513 let dt = track.keyframes[i].time - track.keyframes[i-1].time;
2514 if dt < 1e-9 { continue; }
2515 let dp = (track.keyframes[i].transform.position - track.keyframes[i-1].transform.position).length();
2516 let dr = rotation_error_geodesic(
2517 track.keyframes[i].transform.rotation,
2518 track.keyframes[i-1].transform.rotation,
2519 );
2520 total_vel += dp / dt + dr / dt * 0.1;
2521 }
2522 let avg_vel = total_vel / (track.keyframes.len() - 1) as f32;
2523 (avg_vel / 10.0).min(1.0)
2525}
2526
2527pub fn dedup_keyframes(track: &mut BoneTrack, eps: f32) {
2531 if track.keyframes.len() < 2 { return; }
2532 let mut keep = vec![true; track.keyframes.len()];
2533 keep[0] = true;
2534 keep[track.keyframes.len() - 1] = true;
2535 for i in 1..track.keyframes.len() - 1 {
2536 let prev = &track.keyframes[i-1];
2537 let curr = &track.keyframes[i];
2538 let pos_same = position_error_l2(prev.transform.position, curr.transform.position) < eps;
2539 let rot_same = rotation_error_geodesic(prev.transform.rotation, curr.transform.rotation) < eps;
2540 let scale_same = scale_error_l2(prev.transform.scale, curr.transform.scale) < eps;
2541 if pos_same && rot_same && scale_same {
2542 keep[i] = false;
2543 }
2544 }
2545 track.keyframes = track.keyframes.iter().enumerate()
2546 .filter(|(i, _)| keep[*i])
2547 .map(|(_, kf)| kf.clone())
2548 .collect();
2549}
2550
2551pub fn sort_keyframes(track: &mut BoneTrack) {
2553 track.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap_or(std::cmp::Ordering::Equal));
2554}
2555
2556pub fn normalize_track_rotations(track: &mut BoneTrack) {
2558 for kf in &mut track.keyframes {
2559 kf.transform.rotation = kf.transform.rotation.normalize();
2560 }
2561}
2562
2563pub fn fix_quaternion_continuity(track: &mut BoneTrack) {
2565 for i in 1..track.keyframes.len() {
2566 let prev = track.keyframes[i-1].transform.rotation;
2567 let curr = track.keyframes[i].transform.rotation;
2568 if prev.dot(curr) < 0.0 {
2569 track.keyframes[i].transform.rotation = Quat::from_xyzw(
2570 -curr.x, -curr.y, -curr.z, -curr.w
2571 );
2572 }
2573 }
2574}
2575
2576pub fn stitch_clips(
2580 clip_a: &AnimationClip,
2581 clip_b: &AnimationClip,
2582 crossfade_duration: f32,
2583) -> AnimationClip {
2584 let total_duration = clip_a.duration + clip_b.duration - crossfade_duration;
2585 let mut result = AnimationClip::new(
2586 &format!("{}_{}", clip_a.name, clip_b.name),
2587 clip_a.frame_rate,
2588 total_duration,
2589 );
2590
2591 let all_bones: HashSet<u32> = clip_a.tracks.iter().map(|t| t.bone_index)
2592 .chain(clip_b.tracks.iter().map(|t| t.bone_index))
2593 .collect();
2594
2595 let offset = clip_a.duration - crossfade_duration;
2596
2597 for &bone in &all_bones {
2598 let mut new_kfs: Vec<Keyframe> = Vec::new();
2599
2600 if let Some(track_a) = clip_a.tracks.iter().find(|t| t.bone_index == bone) {
2602 for kf in &track_a.keyframes {
2603 new_kfs.push(kf.clone());
2604 }
2605 }
2606
2607 if let Some(track_b) = clip_b.tracks.iter().find(|t| t.bone_index == bone) {
2609 for kf in &track_b.keyframes {
2610 let t = kf.time + offset;
2611 if kf.time < crossfade_duration {
2613 let alpha = kf.time / crossfade_duration.max(1e-9);
2614 if let Some(a_t) = clip_a.sample_at(bone, clip_a.duration - crossfade_duration + kf.time) {
2615 let blended = a_t.lerp(&kf.transform, alpha);
2616 new_kfs.push(Keyframe { time: t, transform: blended });
2617 } else {
2618 new_kfs.push(Keyframe { time: t, transform: kf.transform });
2619 }
2620 } else {
2621 new_kfs.push(Keyframe { time: t, transform: kf.transform });
2622 }
2623 }
2624 }
2625
2626 new_kfs.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
2627 new_kfs.dedup_by(|a, b| (a.time - b.time).abs() < 1e-6);
2628
2629 result.tracks.push(BoneTrack {
2630 bone_index: bone,
2631 keyframes: new_kfs,
2632 importance: 1.0,
2633 });
2634 }
2635 result
2636}
2637
2638pub fn resample_clip(clip: &AnimationClip, new_frame_rate: f32) -> AnimationClip {
2642 let dt = 1.0 / new_frame_rate;
2643 let n_frames = (clip.duration * new_frame_rate).ceil() as usize + 1;
2644 let mut result = AnimationClip::new(
2645 &format!("{}_resampled_{}", clip.name, new_frame_rate as u32),
2646 new_frame_rate,
2647 clip.duration,
2648 );
2649 result.looping = clip.looping;
2650
2651 for track in &clip.tracks {
2652 let mut new_kfs = Vec::with_capacity(n_frames);
2653 for fi in 0..n_frames {
2654 let t = (fi as f32 * dt).min(clip.duration);
2655 if let Some(tf) = clip.sample_at(track.bone_index, t) {
2656 new_kfs.push(Keyframe { time: t, transform: tf });
2657 }
2658 }
2659 result.tracks.push(BoneTrack {
2660 bone_index: track.bone_index,
2661 keyframes: new_kfs,
2662 importance: track.importance,
2663 });
2664 }
2665 result
2666}
2667
2668pub fn evaluate_blend_node(node: &BlendNode, time: f32, n_bones: usize) -> Vec<Transform> {
2672 if let Some(clip) = &node.clip {
2673 let evaluator = PoseEvaluator::new(n_bones);
2674 return evaluator.evaluate(clip, time);
2675 }
2676
2677 if node.children.is_empty() {
2678 return vec![Transform::identity(); n_bones];
2679 }
2680
2681 let total_weight: f32 = node.blend_weights.iter().sum();
2682 if total_weight < 1e-9 {
2683 return vec![Transform::identity(); n_bones];
2684 }
2685
2686 let mut result: Vec<Transform> = vec![Transform::identity(); n_bones];
2687 let mut accumulated_weight = 0.0f32;
2688
2689 for (child, &weight) in node.children.iter().zip(node.blend_weights.iter()) {
2690 let child_pose = evaluate_blend_node(child, time, n_bones);
2691 let norm_weight = weight / total_weight;
2692 let t = norm_weight / (accumulated_weight + norm_weight).max(1e-9);
2693 for (r, c) in result.iter_mut().zip(child_pose.iter()) {
2694 *r = r.lerp(c, t);
2695 }
2696 accumulated_weight += norm_weight;
2697 }
2698 result
2699}
2700
2701#[derive(Debug, Clone)]
2704pub struct SkeletonPose {
2705 pub local_transforms: Vec<Transform>,
2706 pub world_transforms: Vec<Transform>,
2707 pub parent_indices: Vec<Option<usize>>,
2708}
2709
2710impl SkeletonPose {
2711 pub fn new(n_bones: usize, parent_indices: Vec<Option<usize>>) -> Self {
2712 Self {
2713 local_transforms: vec![Transform::identity(); n_bones],
2714 world_transforms: vec![Transform::identity(); n_bones],
2715 parent_indices,
2716 }
2717 }
2718
2719 pub fn compute_world_transforms(&mut self) {
2720 let n = self.local_transforms.len();
2721 for i in 0..n {
2722 let local = self.local_transforms[i];
2723 self.world_transforms[i] = if let Some(parent) = self.parent_indices[i] {
2724 let parent_world = self.world_transforms[parent];
2725 let local_mat = local.to_mat4();
2726 let parent_mat = parent_world.to_mat4();
2727 let world_mat = parent_mat * local_mat;
2728 let (scale, rot, pos) = decompose_mat4(world_mat);
2729 Transform { position: pos, rotation: rot, scale }
2730 } else {
2731 local
2732 };
2733 }
2734 }
2735
2736 pub fn to_skinning_matrices(&self, inverse_bind_poses: &[Mat4]) -> Vec<Mat4> {
2737 self.world_transforms.iter().zip(inverse_bind_poses.iter()).map(|(world, ibp)| {
2738 world.to_mat4() * *ibp
2739 }).collect()
2740 }
2741}
2742
2743fn decompose_mat4(m: Mat4) -> (Vec3, Quat, Vec3) {
2744 let pos = Vec3::new(m.w_axis.x, m.w_axis.y, m.w_axis.z);
2745 let sx = Vec3::new(m.x_axis.x, m.x_axis.y, m.x_axis.z).length();
2746 let sy = Vec3::new(m.y_axis.x, m.y_axis.y, m.y_axis.z).length();
2747 let sz = Vec3::new(m.z_axis.x, m.z_axis.y, m.z_axis.z).length();
2748 let scale = Vec3::new(sx, sy, sz);
2749 let rot_mat = Mat4::from_cols(
2750 m.x_axis / sx,
2751 m.y_axis / sy,
2752 m.z_axis / sz,
2753 Vec4::W,
2754 );
2755 let rot = Quat::from_mat4(&rot_mat).normalize();
2756 (scale, rot, pos)
2757}
2758
2759pub struct ClipRetargeter {
2763 pub source_bind_pose: Vec<Transform>,
2764 pub target_bind_pose: Vec<Transform>,
2765 pub mappings: Vec<BoneMapping>,
2766}
2767
2768impl ClipRetargeter {
2769 pub fn new(
2770 source_bind: Vec<Transform>,
2771 target_bind: Vec<Transform>,
2772 mappings: Vec<BoneMapping>,
2773 ) -> Self {
2774 Self { source_bind_pose: source_bind, target_bind_pose: target_bind, mappings }
2775 }
2776
2777 pub fn retarget_keyframe(&self, source_tf: &Transform, mapping: &BoneMapping) -> Transform {
2778 let src_bone = mapping.source_bone as usize;
2779 let tgt_bone = mapping.target_bone as usize;
2780 if src_bone >= self.source_bind_pose.len() || tgt_bone >= self.target_bind_pose.len() {
2781 return *source_tf;
2782 }
2783 let src_bind = &self.source_bind_pose[src_bone];
2784 let tgt_bind = &self.target_bind_pose[tgt_bone];
2785
2786 let local_rot = src_bind.rotation.inverse() * source_tf.rotation;
2788
2789 let new_rot = (tgt_bind.rotation * local_rot).normalize();
2791
2792 let new_pos = tgt_bind.position + (source_tf.position - src_bind.position) * mapping.scale_factor;
2794
2795 Transform { position: new_pos, rotation: new_rot, scale: source_tf.scale }
2796 }
2797
2798 pub fn retarget_clip(&self, source: &AnimationClip) -> AnimationClip {
2799 let mut result = AnimationClip::new(&source.name, source.frame_rate, source.duration);
2800 result.looping = source.looping;
2801 for track in &source.tracks {
2802 if let Some(mapping) = self.mappings.iter().find(|m| m.source_bone == track.bone_index) {
2803 let new_kfs: Vec<Keyframe> = track.keyframes.iter().map(|kf| {
2804 Keyframe {
2805 time: kf.time,
2806 transform: self.retarget_keyframe(&kf.transform, mapping),
2807 }
2808 }).collect();
2809 result.tracks.push(BoneTrack {
2810 bone_index: mapping.target_bone,
2811 keyframes: new_kfs,
2812 importance: track.importance,
2813 });
2814 }
2815 }
2816 result
2817 }
2818}
2819
2820pub fn print_compression_report(report: &CompressionErrorReport) {
2823 let _ = format!(
2824 "=== Compression Report: {} ===\n\
2825 Max Position Error: {:.6} m\n\
2826 Max Rotation Error: {:.6} rad\n\
2827 RMS Position Error: {:.6} m\n\
2828 RMS Rotation Error: {:.6} rad\n\
2829 Keyframes: {} -> {} ({:.1}% reduction)\n\
2830 Bytes: {} -> {} ({:.2}x ratio)",
2831 report.clip_name,
2832 report.global_max_pos_error,
2833 report.global_max_rot_error,
2834 report.global_rms_pos_error,
2835 report.global_rms_rot_error,
2836 report.total_original_keyframes,
2837 report.total_compressed_keyframes,
2838 if report.total_original_keyframes > 0 {
2839 (1.0 - report.total_compressed_keyframes as f32 / report.total_original_keyframes as f32) * 100.0
2840 } else { 0.0 },
2841 report.byte_size_original,
2842 report.byte_size_compressed,
2843 if report.byte_size_compressed > 0 {
2844 report.byte_size_original as f32 / report.byte_size_compressed as f32
2845 } else { 0.0 },
2846 );
2847}
2848
2849pub fn encode_compressed_clip(clip: &CompressedAnimationClip) -> Vec<u8> {
2852 let mut data = Vec::new();
2853 let name_bytes = clip.name.as_bytes();
2855 data.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
2856 data.extend_from_slice(name_bytes);
2857 data.extend_from_slice(&clip.frame_rate.to_bits().to_le_bytes());
2858 data.extend_from_slice(&clip.duration.to_bits().to_le_bytes());
2859 data.extend_from_slice(&(clip.looping as u8).to_le_bytes());
2860 data.extend_from_slice(&(clip.tracks.len() as u32).to_le_bytes());
2861 for track in &clip.tracks {
2863 let track_bytes = serialize_compressed_track(track);
2864 data.extend_from_slice(&(track_bytes.len() as u32).to_le_bytes());
2865 data.extend_from_slice(&track_bytes);
2866 }
2867 data
2868}
2869
2870pub fn decode_compressed_clip(data: &[u8]) -> Option<CompressedAnimationClip> {
2871 if data.len() < 4 { return None; }
2872 let mut pos = 0usize;
2873
2874 let name_len = u32::from_le_bytes(data[pos..pos+4].try_into().ok()?) as usize;
2875 pos += 4;
2876 if pos + name_len > data.len() { return None; }
2877 let name = std::str::from_utf8(&data[pos..pos+name_len]).ok()?.to_owned();
2878 pos += name_len;
2879
2880 if pos + 9 > data.len() { return None; }
2881 let frame_rate = f32::from_bits(u32::from_le_bytes(data[pos..pos+4].try_into().ok()?));
2882 pos += 4;
2883 let duration = f32::from_bits(u32::from_le_bytes(data[pos..pos+4].try_into().ok()?));
2884 pos += 4;
2885 let looping = data[pos] != 0;
2886 pos += 1;
2887
2888 if pos + 4 > data.len() { return None; }
2889 let n_tracks = u32::from_le_bytes(data[pos..pos+4].try_into().ok()?) as usize;
2890 pos += 4;
2891
2892 let mut tracks = Vec::new();
2893 for _ in 0..n_tracks {
2894 if pos + 4 > data.len() { return None; }
2895 let track_len = u32::from_le_bytes(data[pos..pos+4].try_into().ok()?) as usize;
2896 pos += 4;
2897 if pos + track_len > data.len() { return None; }
2898 let track = deserialize_compressed_track(&data[pos..pos+track_len]);
2899 tracks.push(track);
2900 pos += track_len;
2901 }
2902
2903 let original_kf = tracks.iter().map(|t| t.keyframes.len()).sum();
2904 let compressed_bytes = tracks.iter().map(|t| t.byte_size()).sum();
2905
2906 Some(CompressedAnimationClip {
2907 name,
2908 frame_rate,
2909 duration,
2910 looping,
2911 tracks,
2912 original_keyframe_count: original_kf,
2913 compressed_keyframe_count: original_kf,
2914 original_byte_size: compressed_bytes,
2915 compressed_byte_size: compressed_bytes,
2916 })
2917}
2918
2919pub fn build_sample_walk_clip(n_bones: u32, n_frames: usize, frame_rate: f32) -> AnimationClip {
2923 let duration = n_frames as f32 / frame_rate;
2924 let mut clip = AnimationClip::new("walk", frame_rate, duration);
2925 clip.looping = true;
2926
2927 for bone in 0..n_bones {
2928 let importance = if bone < 5 { 1.0 } else { 0.5 };
2929 let mut track = BoneTrack::new(bone, importance);
2930 for fi in 0..n_frames {
2931 let t = fi as f32 / frame_rate;
2932 let phase = t * std::f32::consts::TAU / duration;
2933 let pos = Vec3::new(
2934 (phase * (bone as f32 + 1.0)).sin() * 0.1,
2935 (phase * 2.0 + bone as f32).cos() * 0.05,
2936 0.0,
2937 );
2938 let rot = Quat::from_rotation_y((phase * 0.5 + bone as f32 * 0.1).sin() * 0.3);
2939 let scale = Vec3::ONE;
2940 track.push(Keyframe { time: t, transform: Transform { position: pos, rotation: rot, scale } });
2941 }
2942 clip.tracks.push(track);
2943 }
2944 clip
2945}
2946
2947#[derive(Debug, Clone, Copy)]
2951pub struct ChannelMask {
2952 pub position: bool,
2953 pub rotation: bool,
2954 pub scale: bool,
2955}
2956
2957impl Default for ChannelMask {
2958 fn default() -> Self {
2959 Self { position: true, rotation: true, scale: true }
2960 }
2961}
2962
2963impl ChannelMask {
2964 pub fn rotation_only() -> Self {
2965 Self { position: false, rotation: true, scale: false }
2966 }
2967
2968 pub fn no_scale() -> Self {
2969 Self { position: true, rotation: true, scale: false }
2970 }
2971}
2972
2973pub fn compress_track_masked(
2975 track: &BoneTrack,
2976 settings: &CompressionSettings,
2977 mask: ChannelMask,
2978 tick_rate: f32,
2979) -> CompressedTrack {
2980 let bounds = PositionBounds::from_track(track);
2981 let keyframes = track.keyframes.iter().map(|kf| {
2982 let ticks = (kf.time * tick_rate * 1000.0) as u32;
2983 let position = if mask.position {
2984 quantize_position_16(kf.transform.position, &bounds)
2985 } else {
2986 [32767, 32767, 32767] };
2988 let rotation = if mask.rotation {
2989 compress_quat_smallest3(kf.transform.rotation)
2990 } else {
2991 CompressedQuat { largest_component: 0, components: [0, 0, 0] }
2992 };
2993 let scale = if mask.scale {
2994 quantize_scale_vec_log8(kf.transform.scale)
2995 } else {
2996 [128, 128, 128] };
2998 CompressedKeyframe { time_ticks: ticks, position, rotation, scale }
2999 }).collect();
3000 CompressedTrack { bone_index: track.bone_index, pos_bounds: bounds, tick_rate, keyframes }
3001}
3002
3003pub struct LodStreamingManager {
3006 pub assets: HashMap<String, Vec<LodAnimationVariant>>,
3007 pub camera_distance_cache: HashMap<String, f32>,
3008}
3009
3010impl LodStreamingManager {
3011 pub fn new() -> Self {
3012 Self {
3013 assets: HashMap::new(),
3014 camera_distance_cache: HashMap::new(),
3015 }
3016 }
3017
3018 pub fn register_lod_variants(&mut self, name: &str, variants: Vec<LodAnimationVariant>) {
3019 self.assets.insert(name.to_owned(), variants);
3020 }
3021
3022 pub fn update_distance(&mut self, name: &str, distance: f32) {
3023 self.camera_distance_cache.insert(name.to_owned(), distance);
3024 }
3025
3026 pub fn get_active_variant(&self, name: &str) -> Option<&LodAnimationVariant> {
3027 let variants = self.assets.get(name)?;
3028 let &distance = self.camera_distance_cache.get(name).unwrap_or(&0.0);
3029 select_lod_variant(variants, distance)
3030 }
3031
3032 pub fn evaluate_pose(&self, name: &str, time: f32, n_bones: usize) -> Option<Vec<Transform>> {
3033 let variant = self.get_active_variant(name)?;
3034 let evaluator = PoseEvaluator::new(n_bones);
3035 Some(evaluator.evaluate(&variant.clip, time))
3036 }
3037}
3038
3039pub struct CompressionCache {
3042 pub cache: HashMap<u64, CompressedAnimationClip>,
3043}
3044
3045impl CompressionCache {
3046 pub fn new() -> Self {
3047 Self { cache: HashMap::new() }
3048 }
3049
3050 pub fn key(clip_name: &str, settings: &CompressionSettings) -> u64 {
3051 let mut h = 0u64;
3053 for b in clip_name.bytes() {
3054 h = h.wrapping_mul(31).wrapping_add(b as u64);
3055 }
3056 h = h.wrapping_add((settings.pos_tolerance.to_bits() as u64) << 32);
3057 h = h.wrapping_add(settings.rot_tolerance_radians.to_bits() as u64);
3058 h
3059 }
3060
3061 pub fn get(&self, key: u64) -> Option<&CompressedAnimationClip> {
3062 self.cache.get(&key)
3063 }
3064
3065 pub fn insert(&mut self, key: u64, clip: CompressedAnimationClip) {
3066 self.cache.insert(key, clip);
3067 }
3068
3069 pub fn get_or_compress(
3070 &mut self,
3071 clip: &AnimationClip,
3072 settings: &CompressionSettings,
3073 ) -> CompressedAnimationClip {
3074 let key = Self::key(&clip.name, settings);
3075 if let Some(cached) = self.cache.get(&key) {
3076 return cached.clone();
3077 }
3078 let compressor = AnimationCompressor::new().with_settings(settings.clone());
3079 let compressed = compressor.compress(clip);
3080 self.cache.insert(key, compressed.clone());
3081 compressed
3082 }
3083}
3084
3085pub fn keyframe_density_histogram(
3089 clip: &AnimationClip,
3090 n_buckets: usize,
3091) -> Vec<usize> {
3092 let mut counts = vec![0usize; n_buckets];
3093 if clip.duration < 1e-9 || n_buckets == 0 { return counts; }
3094 for track in &clip.tracks {
3095 for kf in &track.keyframes {
3096 let bucket = ((kf.time / clip.duration) * n_buckets as f32) as usize;
3097 let bucket = bucket.min(n_buckets - 1);
3098 counts[bucket] += 1;
3099 }
3100 }
3101 counts
3102}
3103
3104pub struct CompressionBundle {
3107 pub compressed: CompressedAnimationClip,
3108 pub error_report: CompressionErrorReport,
3109 pub lod_variants: Vec<CompressedAnimationClip>,
3110 pub streaming_asset: StreamingAnimationAsset,
3111}
3112
3113impl CompressionBundle {
3114 pub fn build(
3115 clip: &AnimationClip,
3116 settings: CompressionSettings,
3117 lod_distances: &[f32],
3118 bone_lod_info: Vec<BoneLodInfo>,
3119 chunk_size: usize,
3120 ) -> Self {
3121 let compressor = AnimationCompressor::new()
3122 .with_settings(settings)
3123 .with_bone_lod(bone_lod_info);
3124 let compressed = compressor.compress(clip);
3125 let error_report = compressor.error_analysis(clip, &compressed);
3126 let lod_variants = compressor.compress_with_lod(clip, lod_distances);
3127 let streaming_asset = compressor.build_streaming_asset(clip, chunk_size);
3128 Self { compressed, error_report, lod_variants, streaming_asset }
3129 }
3130
3131 pub fn summary(&self) -> String {
3132 format!(
3133 "Clip '{}': {:.2}x compression, {} LOD variants, {} chunks streamed",
3134 self.compressed.name,
3135 self.compressed.compression_ratio(),
3136 self.lod_variants.len(),
3137 self.streaming_asset.chunks.len(),
3138 )
3139 }
3140}
3141
3142pub fn run_compression_test() -> BatchCompressionStats {
3145 let clips: Vec<AnimationClip> = vec![
3147 build_sample_walk_clip(20, 120, 30.0),
3148 build_sample_walk_clip(10, 60, 24.0),
3149 build_sample_walk_clip(30, 240, 60.0),
3150 ];
3151
3152 let compressor = AnimationCompressor::new().with_settings(preset_medium_quality());
3153 let clip_refs: Vec<&AnimationClip> = clips.iter().collect();
3154 let compressed = compressor.batch_compress(&clip_refs);
3155
3156 BatchCompressionStats::compute(&clip_refs, &compressed)
3157}
3158
3159#[derive(Debug, Clone)]
3162pub struct SkinWeight {
3163 pub bone_index: u8,
3164 pub weight: f32,
3165}
3166
3167#[derive(Debug, Clone)]
3168pub struct SkinWeightSet {
3169 pub weights: Vec<SkinWeight>,
3170}
3171
3172impl SkinWeightSet {
3173 pub fn new() -> Self {
3174 Self { weights: Vec::new() }
3175 }
3176
3177 pub fn normalize(&mut self) {
3178 let total: f32 = self.weights.iter().map(|w| w.weight).sum();
3179 if total > 1e-9 {
3180 for w in &mut self.weights {
3181 w.weight /= total;
3182 }
3183 }
3184 }
3185
3186 pub fn quantize_u8(&self) -> Vec<(u8, u8)> {
3188 let mut quantized: Vec<(u8, u8)> = self.weights.iter().map(|w| {
3189 (w.bone_index, (w.weight * 255.0).round() as u8)
3190 }).collect();
3191 let sum: u32 = quantized.iter().map(|(_, w)| *w as u32).sum();
3193 if sum > 0 && sum != 255 {
3194 if let Some(max_idx) = quantized.iter().enumerate().max_by_key(|(_, (_, w))| *w).map(|(i, _)| i) {
3196 let diff = 255i32 - sum as i32;
3197 quantized[max_idx].1 = (quantized[max_idx].1 as i32 + diff).max(0).min(255) as u8;
3198 }
3199 }
3200 quantized
3201 }
3202}
3203
3204#[derive(Debug, Clone, PartialEq)]
3207pub enum CompressionJobStatus {
3208 Pending,
3209 Running,
3210 Complete,
3211 Failed(String),
3212}
3213
3214#[derive(Debug, Clone)]
3215pub struct CompressionJob {
3216 pub id: u64,
3217 pub clip_name: String,
3218 pub settings: CompressionSettings,
3219 pub status: CompressionJobStatus,
3220 pub result: Option<CompressedAnimationClip>,
3221}
3222
3223impl CompressionJob {
3224 pub fn new(id: u64, clip_name: &str, settings: CompressionSettings) -> Self {
3225 Self {
3226 id,
3227 clip_name: clip_name.to_owned(),
3228 settings,
3229 status: CompressionJobStatus::Pending,
3230 result: None,
3231 }
3232 }
3233}
3234
3235pub struct CompressionJobQueue {
3236 pub pending: VecDeque<CompressionJob>,
3237 pub running: Option<CompressionJob>,
3238 pub completed: Vec<CompressionJob>,
3239 pub registry: AnimationRegistry,
3240}
3241
3242impl CompressionJobQueue {
3243 pub fn new() -> Self {
3244 Self {
3245 pending: VecDeque::new(),
3246 running: None,
3247 completed: Vec::new(),
3248 registry: AnimationRegistry::new(),
3249 }
3250 }
3251
3252 pub fn enqueue(&mut self, job: CompressionJob) {
3253 self.pending.push_back(job);
3254 }
3255
3256 pub fn tick(&mut self) {
3257 if self.running.is_some() { return; }
3258 if let Some(mut job) = self.pending.pop_front() {
3259 job.status = CompressionJobStatus::Running;
3260 if let Some(clip) = self.registry.clips.get(&job.clip_name) {
3261 let compressor = AnimationCompressor::new().with_settings(job.settings.clone());
3262 let compressed = compressor.compress(clip);
3263 job.result = Some(compressed);
3264 job.status = CompressionJobStatus::Complete;
3265 } else {
3266 job.status = CompressionJobStatus::Failed(format!("Clip '{}' not found", job.clip_name));
3267 }
3268 self.completed.push(job);
3269 }
3270 }
3271
3272 pub fn results(&self) -> impl Iterator<Item = &CompressedAnimationClip> {
3273 self.completed.iter().filter_map(|j| j.result.as_ref())
3274 }
3275}
3276
3277pub struct PiecewiseLinearCurve {
3280 pub times: Vec<f32>,
3281 pub values: Vec<f32>,
3282}
3283
3284impl PiecewiseLinearCurve {
3285 pub fn new(times: Vec<f32>, values: Vec<f32>) -> Self {
3286 Self { times, values }
3287 }
3288
3289 pub fn sample(&self, t: f32) -> f32 {
3290 if self.times.is_empty() { return 0.0; }
3291 if self.times.len() == 1 { return self.values[0]; }
3292 let idx = self.times.partition_point(|&ti| ti <= t);
3293 if idx == 0 { return self.values[0]; }
3294 if idx >= self.times.len() { return *self.values.last().unwrap(); }
3295 let t0 = self.times[idx - 1];
3296 let t1 = self.times[idx];
3297 let v0 = self.values[idx - 1];
3298 let v1 = self.values[idx];
3299 let dt = t1 - t0;
3300 let alpha = if dt > 1e-9 { (t - t0) / dt } else { 0.0 };
3301 v0 + (v1 - v0) * alpha
3302 }
3303
3304 pub fn reduce_rdp(&self, eps: f32) -> Self {
3305 if self.times.len() <= 2 {
3306 return Self::new(self.times.clone(), self.values.clone());
3307 }
3308 let kfs: Vec<ScalarKeyframe> = self.times.iter().zip(self.values.iter())
3309 .map(|(&t, &v)| ScalarKeyframe { time: t, value: v })
3310 .collect();
3311 let curve = ScalarCurve { channel: CurveChannel::PosX, keyframes: kfs };
3312 let reduced = curve.rdp_reduce(eps);
3313 Self::new(
3314 reduced.keyframes.iter().map(|k| k.time).collect(),
3315 reduced.keyframes.iter().map(|k| k.value).collect(),
3316 )
3317 }
3318}
3319
3320#[derive(Debug, Clone)]
3323pub struct BoneHierarchy {
3324 pub n_bones: usize,
3325 pub parent_indices: Vec<Option<u32>>,
3326 pub bone_names: Vec<String>,
3327}
3328
3329impl BoneHierarchy {
3330 pub fn new(n_bones: usize) -> Self {
3331 Self {
3332 n_bones,
3333 parent_indices: vec![None; n_bones],
3334 bone_names: (0..n_bones).map(|i| format!("bone_{}", i)).collect(),
3335 }
3336 }
3337
3338 pub fn set_parent(&mut self, bone: u32, parent: u32) {
3339 if (bone as usize) < self.n_bones {
3340 self.parent_indices[bone as usize] = Some(parent);
3341 }
3342 }
3343
3344 pub fn root_bones(&self) -> Vec<u32> {
3345 self.parent_indices.iter().enumerate()
3346 .filter(|(_, p)| p.is_none())
3347 .map(|(i, _)| i as u32)
3348 .collect()
3349 }
3350
3351 pub fn children_of(&self, bone: u32) -> Vec<u32> {
3352 self.parent_indices.iter().enumerate()
3353 .filter(|(_, &p)| p == Some(bone))
3354 .map(|(i, _)| i as u32)
3355 .collect()
3356 }
3357
3358 pub fn depth_of(&self, bone: u32) -> u32 {
3359 let mut depth = 0;
3360 let mut current = bone as usize;
3361 for _ in 0..self.n_bones {
3362 match self.parent_indices[current] {
3363 Some(p) => { depth += 1; current = p as usize; }
3364 None => break,
3365 }
3366 }
3367 depth
3368 }
3369}
3370
3371pub fn compute_hierarchy_importance(hierarchy: &BoneHierarchy) -> Vec<f32> {
3374 let mut importance = vec![0.5f32; hierarchy.n_bones];
3375 for i in 0..hierarchy.n_bones {
3376 let depth = hierarchy.depth_of(i as u32);
3377 let n_children = hierarchy.children_of(i as u32).len();
3379 let child_factor = if n_children == 0 { 0.5 } else { 1.0 };
3380 let depth_factor = (1.0 / (1.0 + depth as f32 * 0.1)).max(0.1);
3381 importance[i] = (depth_factor * child_factor).min(1.0);
3382 }
3383 importance
3384}
3385
3386pub fn clip_bounding_box(clip: &AnimationClip) -> (Vec3, Vec3) {
3390 let mut min = Vec3::splat(f32::MAX);
3391 let mut max = Vec3::splat(f32::MIN);
3392 for track in &clip.tracks {
3393 for kf in &track.keyframes {
3394 min = min.min(kf.transform.position);
3395 max = max.max(kf.transform.position);
3396 }
3397 }
3398 (min, max)
3399}
3400
3401pub fn clip_has_motion(clip: &AnimationClip, eps: f32) -> bool {
3403 for track in &clip.tracks {
3404 for kf in &track.keyframes {
3405 if position_error_l2(kf.transform.position, Vec3::ZERO) > eps { return true; }
3406 if rotation_error_geodesic(kf.transform.rotation, Quat::IDENTITY) > eps { return true; }
3407 }
3408 }
3409 false
3410}
3411
3412pub fn scale_clip_positions(clip: &mut AnimationClip, scale: f32) {
3414 for track in &mut clip.tracks {
3415 for kf in &mut track.keyframes {
3416 kf.transform.position *= scale;
3417 }
3418 }
3419}
3420
3421pub fn time_scale_clip(clip: &mut AnimationClip, time_scale: f32) {
3423 if time_scale.abs() < 1e-9 { return; }
3424 clip.duration /= time_scale;
3425 clip.frame_rate *= time_scale;
3426 for track in &mut clip.tracks {
3427 for kf in &mut track.keyframes {
3428 kf.time /= time_scale;
3429 }
3430 }
3431}
3432
3433pub fn reverse_clip(clip: &AnimationClip) -> AnimationClip {
3435 let mut reversed = clip.clone();
3436 reversed.name = format!("{}_reversed", clip.name);
3437 for track in &mut reversed.tracks {
3438 track.keyframes.reverse();
3439 for kf in &mut track.keyframes {
3440 kf.time = clip.duration - kf.time;
3441 }
3442 track.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
3443 }
3444 reversed
3445}
3446
3447pub fn bake_additive_into_base(
3449 base: &AnimationClip,
3450 additive: &AnimationClip,
3451 weight: f32,
3452) -> AnimationClip {
3453 let mut result = base.clone();
3454 result.name = format!("{}_baked", base.name);
3455 let evaluator = PoseEvaluator::new(MAX_BONES);
3456 for track in &mut result.tracks {
3457 for kf in &mut track.keyframes {
3458 if let Some(add_t) = additive.sample_at(track.bone_index, kf.time) {
3459 kf.transform = apply_additive_layer(&kf.transform, &add_t, weight);
3460 }
3461 }
3462 }
3463 result
3464}
3465
3466pub fn validate_round_trip(
3469 clip: &AnimationClip,
3470 compressor: &AnimationCompressor,
3471 max_acceptable_pos_error: f32,
3472 max_acceptable_rot_error: f32,
3473) -> Result<CompressionErrorReport, String> {
3474 let compressed = compressor.compress(clip);
3475 let report = compute_error_report(clip, &compressed);
3476 if report.global_max_pos_error > max_acceptable_pos_error {
3477 return Err(format!(
3478 "Position error {:.6} exceeds threshold {:.6}",
3479 report.global_max_pos_error, max_acceptable_pos_error
3480 ));
3481 }
3482 if report.global_max_rot_error > max_acceptable_rot_error {
3483 return Err(format!(
3484 "Rotation error {:.6} exceeds threshold {:.6}",
3485 report.global_max_rot_error, max_acceptable_rot_error
3486 ));
3487 }
3488 Ok(report)
3489}
3490
3491#[derive(Debug, Clone)]
3494pub struct AnimationExportManifest {
3495 pub clips: Vec<String>,
3496 pub total_compressed_bytes: usize,
3497 pub total_original_bytes: usize,
3498 pub export_time_ms: u64,
3499 pub settings: CompressionSettings,
3500}
3501
3502impl AnimationExportManifest {
3503 pub fn build(
3504 clips: &[&AnimationClip],
3505 compressed: &[CompressedAnimationClip],
3506 settings: CompressionSettings,
3507 export_time_ms: u64,
3508 ) -> Self {
3509 Self {
3510 clips: clips.iter().map(|c| c.name.clone()).collect(),
3511 total_original_bytes: clips.iter().map(|c| c.total_keyframes() * 40).sum(),
3512 total_compressed_bytes: compressed.iter().map(|c| c.compressed_byte_size).sum(),
3513 export_time_ms,
3514 settings,
3515 }
3516 }
3517
3518 pub fn compression_ratio(&self) -> f32 {
3519 if self.total_compressed_bytes == 0 { 0.0 }
3520 else { self.total_original_bytes as f32 / self.total_compressed_bytes as f32 }
3521 }
3522}
3523
3524pub struct RotationHermiteSpline {
3528 pub times: Vec<f32>,
3529 pub rotations: Vec<Quat>,
3530 pub tangents: Vec<Vec3>, }
3532
3533impl RotationHermiteSpline {
3534 pub fn from_keyframes(kfs: &[Keyframe]) -> Self {
3535 let times: Vec<f32> = kfs.iter().map(|k| k.time).collect();
3536 let rotations: Vec<Quat> = kfs.iter().map(|k| k.transform.rotation).collect();
3537 let n = times.len();
3538 let mut tangents = vec![Vec3::ZERO; n];
3539
3540 for i in 0..n {
3541 if i == 0 || i == n - 1 { continue; }
3542 let dt_p = times[i+1] - times[i];
3543 let dt_m = times[i] - times[i-1];
3544 let dt = times[i+1] - times[i-1];
3545 if dt < 1e-9 { continue; }
3546 let log_p = quat_log(rotations[i].inverse() * rotations[i+1]);
3548 let log_m = quat_log(rotations[i-1].inverse() * rotations[i]);
3549 tangents[i] = (log_m / dt_m + log_p / dt_p) * 0.5;
3550 }
3551
3552 Self { times, rotations, tangents }
3553 }
3554
3555 pub fn sample(&self, t: f32) -> Quat {
3556 if self.times.is_empty() { return Quat::IDENTITY; }
3557 let idx = self.times.partition_point(|&ti| ti <= t);
3558 if idx == 0 { return self.rotations[0]; }
3559 if idx >= self.times.len() { return *self.rotations.last().unwrap(); }
3560 let t0 = self.times[idx-1];
3561 let t1 = self.times[idx];
3562 let dt = t1 - t0;
3563 let s = if dt > 1e-9 { (t - t0) / dt } else { 0.0 };
3564 let q0 = self.rotations[idx-1];
3565 let q1 = self.rotations[idx];
3566 q0.slerp(q1, s)
3567 }
3568}
3569
3570fn quat_log(q: Quat) -> Vec3 {
3571 let len = Vec3::new(q.x, q.y, q.z).length();
3572 if len < 1e-9 { return Vec3::ZERO; }
3573 let angle = 2.0 * len.atan2(q.w);
3574 Vec3::new(q.x, q.y, q.z) * (angle / len)
3575}
3576
3577fn quat_exp(v: Vec3) -> Quat {
3578 let angle = v.length();
3579 if angle < 1e-9 { return Quat::IDENTITY; }
3580 let axis = v / angle;
3581 Quat::from_axis_angle(axis, angle)
3582}
3583
3584pub fn blend_n_poses(poses: &[Vec<Transform>], weights: &[f32]) -> Vec<Transform> {
3587 assert!(!poses.is_empty());
3588 let n_bones = poses[0].len();
3589 let total_w: f32 = weights.iter().sum();
3590 if total_w < 1e-9 {
3591 return vec![Transform::identity(); n_bones];
3592 }
3593
3594 let mut result = vec![Transform::identity(); n_bones];
3595 let mut acc_w = 0.0f32;
3596
3597 for (pose, &w) in poses.iter().zip(weights.iter()) {
3598 let norm_w = w / total_w;
3599 let t = norm_w / (acc_w + norm_w).max(1e-9);
3600 for (r, p) in result.iter_mut().zip(pose.iter()) {
3601 *r = r.lerp(p, t);
3602 }
3603 acc_w += norm_w;
3604 }
3605 result
3606}
3607
3608pub fn quantization_error_worst_case(range: f32, bits: u32) -> f32 {
3612 let n_steps = (1u64 << bits) as f32;
3613 range / n_steps / 2.0
3614}
3615
3616pub fn quantization_error_rotation_16bit() -> f32 {
3617 let range = 2.0 * SMALL3_SCALE;
3619 quantization_error_worst_case(range, 15) }
3621
3622pub fn quantization_error_position_16bit(range: f32) -> f32 {
3623 quantization_error_worst_case(range, 16)
3624}
3625
3626pub fn quantization_error_scale_8bit() -> f32 {
3627 quantization_error_worst_case(8.0, 8)
3629}
3630
3631#[derive(Debug, Clone)]
3634pub struct RiggedAnimationSet {
3635 pub skeleton: BoneHierarchy,
3636 pub bind_pose: Vec<Transform>,
3637 pub clips: Vec<AnimationClip>,
3638 pub compressed_clips: Vec<CompressedAnimationClip>,
3639 pub lod_info: Vec<BoneLodInfo>,
3640}
3641
3642impl RiggedAnimationSet {
3643 pub fn new(skeleton: BoneHierarchy, bind_pose: Vec<Transform>) -> Self {
3644 Self {
3645 skeleton,
3646 bind_pose,
3647 clips: Vec::new(),
3648 compressed_clips: Vec::new(),
3649 lod_info: Vec::new(),
3650 }
3651 }
3652
3653 pub fn add_clip(&mut self, clip: AnimationClip) {
3654 self.clips.push(clip);
3655 }
3656
3657 pub fn compress_all(&mut self, settings: CompressionSettings) {
3658 let compressor = AnimationCompressor::new()
3659 .with_settings(settings)
3660 .with_bone_lod(self.lod_info.clone());
3661 let clip_refs: Vec<&AnimationClip> = self.clips.iter().collect();
3662 self.compressed_clips = compressor.batch_compress(&clip_refs);
3663 }
3664
3665 pub fn evaluate_pose(&self, clip_name: &str, time: f32) -> Vec<Transform> {
3666 let clip = self.compressed_clips.iter().find(|c| c.name == clip_name);
3668 if let Some(c) = clip {
3669 let evaluator = PoseEvaluator::new(self.skeleton.n_bones);
3670 return evaluator.evaluate_compressed(c, time);
3671 }
3672 if let Some(c) = self.clips.iter().find(|c| c.name == clip_name) {
3674 let evaluator = PoseEvaluator::new(self.skeleton.n_bones);
3675 return evaluator.evaluate(c, time);
3676 }
3677 vec![Transform::identity(); self.skeleton.n_bones]
3678 }
3679}
3680
3681#[derive(Debug, Clone, Copy, PartialEq)]
3684pub enum ConstraintType {
3685 LookAt, Aim, OrientLike, CopyLocation,
3686 LimitRotation, LimitLocation, LimitScale,
3687 StretchTo, TrackTo, ClampTo, SplineIK,
3688}
3689
3690#[derive(Debug, Clone)]
3691pub struct BoneConstraint {
3692 pub bone_index: u32,
3693 pub constraint_type: ConstraintType,
3694 pub target_bone: Option<u32>,
3695 pub influence: f32,
3696 pub min: Vec3,
3697 pub max: Vec3,
3698 pub enabled: bool,
3699}
3700
3701impl BoneConstraint {
3702 pub fn look_at(bone: u32, target: u32) -> Self {
3703 Self { bone_index: bone, constraint_type: ConstraintType::LookAt, target_bone: Some(target), influence: 1.0, min: Vec3::NEG_ONE, max: Vec3::ONE, enabled: true }
3704 }
3705
3706 pub fn limit_rotation(bone: u32, min: Vec3, max: Vec3) -> Self {
3707 Self { bone_index: bone, constraint_type: ConstraintType::LimitRotation, target_bone: None, influence: 1.0, min, max, enabled: true }
3708 }
3709
3710 pub fn apply_look_at(bone_transform: &Transform, target_world_pos: Vec3, up: Vec3) -> Transform {
3711 let dir = (target_world_pos - bone_transform.position).normalize();
3712 let right = up.cross(dir).normalize();
3713 let up_correct = dir.cross(right);
3714 let rot = Quat::from_mat4(&Mat4::from_cols(
3715 Vec4::new(right.x, right.y, right.z, 0.0),
3716 Vec4::new(up_correct.x, up_correct.y, up_correct.z, 0.0),
3717 Vec4::new(dir.x, dir.y, dir.z, 0.0),
3718 Vec4::W,
3719 )).normalize();
3720 Transform { rotation: rot, ..*bone_transform }
3721 }
3722
3723 pub fn apply_limit_rotation(t: &Transform, min: Vec3, max: Vec3) -> Transform {
3724 let (yaw, pitch, roll) = quat_to_euler_yxz(t.rotation);
3725 let clamped_rot = euler_yxz_to_quat(yaw.clamp(min.y, max.y), pitch.clamp(min.x, max.x), roll.clamp(min.z, max.z));
3726 Transform { rotation: clamped_rot, ..*t }
3727 }
3728}
3729
3730fn quat_to_euler_yxz(q: Quat) -> (f32, f32, f32) {
3731 let sinr_cosp = 2.0 * (q.w * q.x + q.y * q.z);
3732 let cosr_cosp = 1.0 - 2.0 * (q.x * q.x + q.y * q.y);
3733 let roll = sinr_cosp.atan2(cosr_cosp);
3734 let sinp = 2.0 * (q.w * q.y - q.z * q.x);
3735 let pitch = if sinp.abs() >= 1.0 { sinp.signum() * std::f32::consts::FRAC_PI_2 } else { sinp.asin() };
3736 let siny_cosp = 2.0 * (q.w * q.z + q.x * q.y);
3737 let cosy_cosp = 1.0 - 2.0 * (q.y * q.y + q.z * q.z);
3738 let yaw = siny_cosp.atan2(cosy_cosp);
3739 (yaw, pitch, roll)
3740}
3741
3742fn euler_yxz_to_quat(yaw: f32, pitch: f32, roll: f32) -> Quat {
3743 let cy = (yaw * 0.5).cos(); let sy = (yaw * 0.5).sin();
3744 let cp = (pitch * 0.5).cos(); let sp = (pitch * 0.5).sin();
3745 let cr = (roll * 0.5).cos(); let sr = (roll * 0.5).sin();
3746 Quat::from_xyzw(
3747 cy * sp * cr + sy * cp * sr,
3748 sy * cp * cr - cy * sp * sr,
3749 cy * cp * sr - sy * sp * cr,
3750 cy * cp * cr + sy * sp * sr,
3751 ).normalize()
3752}
3753
3754#[derive(Debug, Clone, PartialEq)]
3757pub enum BlendStrategy { Linear, Cubic, Additive, Override }
3758
3759#[derive(Debug, Clone)]
3760pub struct AnimationStateEntry {
3761 pub name: String,
3762 pub clip_name: String,
3763 pub speed: f32,
3764 pub looping: bool,
3765 pub blend_in_time: f32,
3766 pub blend_out_time: f32,
3767}
3768
3769impl AnimationStateEntry {
3770 pub fn new(name: &str, clip_name: &str) -> Self {
3771 Self { name: name.to_owned(), clip_name: clip_name.to_owned(), speed: 1.0, looping: false, blend_in_time: 0.2, blend_out_time: 0.2 }
3772 }
3773}
3774
3775#[derive(Debug, Clone)]
3776pub struct StateTransition {
3777 pub from: String, pub to: String,
3778 pub duration: f32, pub blend_strategy: BlendStrategy,
3779}
3780
3781pub struct LayeredAnimationStateMachine {
3782 pub states: HashMap<String, AnimationStateEntry>,
3783 pub transitions: Vec<StateTransition>,
3784 pub current_state: Option<String>,
3785 pub next_state: Option<String>,
3786 pub blend_alpha: f32,
3787 pub elapsed: f32,
3788}
3789
3790impl LayeredAnimationStateMachine {
3791 pub fn new() -> Self {
3792 Self { states: HashMap::new(), transitions: Vec::new(), current_state: None, next_state: None, blend_alpha: 0.0, elapsed: 0.0 }
3793 }
3794
3795 pub fn add_state(&mut self, s: AnimationStateEntry) { self.states.insert(s.name.clone(), s); }
3796 pub fn add_transition(&mut self, t: StateTransition) { self.transitions.push(t); }
3797
3798 pub fn trigger(&mut self, name: &str) {
3799 if self.states.contains_key(name) { self.next_state = Some(name.to_owned()); self.blend_alpha = 0.0; }
3800 }
3801
3802 pub fn update(&mut self, dt: f32) -> f32 {
3803 self.elapsed += dt;
3804 if let Some(ref next) = self.next_state.clone() {
3805 let dur = self.transitions.iter()
3806 .find(|t| self.current_state.as_deref() == Some(&t.from) && t.to == *next)
3807 .map_or(0.2, |t| t.duration);
3808 self.blend_alpha = (self.blend_alpha + dt / dur.max(0.001)).min(1.0);
3809 if self.blend_alpha >= 1.0 {
3810 self.current_state = Some(next.clone());
3811 self.next_state = None;
3812 self.blend_alpha = 0.0;
3813 }
3814 }
3815 self.blend_alpha
3816 }
3817
3818 pub fn evaluate_pose(&self, registry: &AnimationRegistry, time: f32, n_bones: usize) -> Vec<Transform> {
3819 let evaluator = PoseEvaluator::new(n_bones);
3820 let eval_state = |name: &str| -> Vec<Transform> {
3821 if let Some(state) = self.states.get(name) {
3822 if let Some(clip) = registry.get_compressed(&state.clip_name) {
3823 return evaluator.evaluate_compressed(clip, time * state.speed);
3824 }
3825 if let Some(clip) = registry.get_clip(&state.clip_name) {
3826 return evaluator.evaluate(clip, time * state.speed);
3827 }
3828 }
3829 vec![Transform::identity(); n_bones]
3830 };
3831 let cur = self.current_state.as_deref().unwrap_or("");
3832 let nxt = self.next_state.as_deref().unwrap_or(cur);
3833 let pose_a = eval_state(cur);
3834 if self.blend_alpha < 1e-6 || cur == nxt { return pose_a; }
3835 let pose_b = eval_state(nxt);
3836 evaluator.blend_poses(&pose_a, &pose_b, self.blend_alpha)
3837 }
3838}
3839
3840pub struct AnimationFrameCache {
3843 pub cache: HashMap<(String, u32), Vec<Transform>>,
3844 pub max_entries: usize,
3845 pub access_order: VecDeque<(String, u32)>,
3846}
3847
3848impl AnimationFrameCache {
3849 pub fn new(max_entries: usize) -> Self {
3850 Self { cache: HashMap::new(), max_entries, access_order: VecDeque::new() }
3851 }
3852
3853 pub fn get_or_compute(&mut self, clip: &AnimationClip, frame: u32, n_bones: usize) -> Vec<Transform> {
3854 let key = (clip.name.clone(), frame);
3855 if let Some(pose) = self.cache.get(&key) {
3856 self.access_order.retain(|k| k != &key);
3857 self.access_order.push_back(key);
3858 return pose.clone();
3859 }
3860 let t = frame as f32 / clip.frame_rate;
3861 let evaluator = PoseEvaluator::new(n_bones);
3862 let pose = evaluator.evaluate(clip, t);
3863 if self.cache.len() >= self.max_entries {
3864 if let Some(oldest) = self.access_order.pop_front() { self.cache.remove(&oldest); }
3865 }
3866 self.cache.insert(key.clone(), pose.clone());
3867 self.access_order.push_back(key);
3868 pose
3869 }
3870
3871 pub fn invalidate(&mut self, clip_name: &str) {
3872 self.cache.retain(|(name, _), _| name != clip_name);
3873 self.access_order.retain(|(name, _)| name != clip_name);
3874 }
3875
3876 pub fn memory_usage_estimate(&self) -> usize {
3877 self.cache.values().map(|p| p.len() * std::mem::size_of::<Transform>()).sum()
3878 }
3879}
3880
3881pub fn pose_diff(pose_a: &[Transform], pose_b: &[Transform]) -> Vec<TransformDelta> {
3884 pose_a.iter().zip(pose_b.iter()).map(|(a, b)| TransformDelta::compute(a, b)).collect()
3885}
3886
3887pub fn poses_equal(pose_a: &[Transform], pose_b: &[Transform], eps: f32) -> bool {
3888 if pose_a.len() != pose_b.len() { return false; }
3889 pose_a.iter().zip(pose_b.iter()).all(|(a, b)| {
3890 position_error_l2(a.position, b.position) < eps
3891 && rotation_error_geodesic(a.rotation, b.rotation) < eps
3892 && scale_error_l2(a.scale, b.scale) < eps
3893 })
3894}
3895
3896pub fn extrapolate_pose(pose: &[Transform], prev_pose: &[Transform], dt: f32, frame_dt: f32) -> Vec<Transform> {
3897 let factor = if frame_dt > 1e-9 { dt / frame_dt } else { 0.0 };
3898 pose.iter().zip(prev_pose.iter()).map(|(cur, prev)| {
3899 let pos_vel = cur.position - prev.position;
3900 let rot_vel = prev.rotation.inverse() * cur.rotation;
3901 let extra_rot = Quat::IDENTITY.slerp(rot_vel, factor);
3902 Transform {
3903 position: cur.position + pos_vel * factor,
3904 rotation: (cur.rotation * extra_rot).normalize(),
3905 scale: cur.scale,
3906 }
3907 }).collect()
3908}
3909
3910#[derive(Debug, Clone)]
3913pub struct MotionPose {
3914 pub positions: Vec<Vec3>,
3915 pub velocities: Vec<Vec3>,
3916 pub trajectory: Vec<Vec3>,
3917 pub clip_name: String,
3918 pub frame: u32,
3919}
3920
3921impl MotionPose {
3922 pub fn distance(&self, other: &MotionPose, pw: f32, vw: f32, tw: f32) -> f32 {
3923 let pe: f32 = self.positions.iter().zip(other.positions.iter()).map(|(a,b)| (*a - *b).length_squared()).sum::<f32>().sqrt();
3924 let ve: f32 = self.velocities.iter().zip(other.velocities.iter()).map(|(a,b)| (*a - *b).length_squared()).sum::<f32>().sqrt();
3925 let te: f32 = self.trajectory.iter().zip(other.trajectory.iter()).map(|(a,b)| (*a - *b).length_squared()).sum::<f32>().sqrt();
3926 pe * pw + ve * vw + te * tw
3927 }
3928}
3929
3930pub struct MotionMatchingDb {
3931 pub poses: Vec<MotionPose>,
3932}
3933
3934impl MotionMatchingDb {
3935 pub fn new() -> Self { Self { poses: Vec::new() } }
3936
3937 pub fn add(&mut self, p: MotionPose) { self.poses.push(p); }
3938
3939 pub fn find_best(&self, query: &MotionPose, pw: f32, vw: f32, tw: f32) -> Option<&MotionPose> {
3940 self.poses.iter().min_by(|a, b| {
3941 a.distance(query, pw, vw, tw).partial_cmp(&b.distance(query, pw, vw, tw)).unwrap_or(std::cmp::Ordering::Equal)
3942 })
3943 }
3944
3945 pub fn build_from_clip(clip: &AnimationClip, n_bones: usize, traj_steps: usize, traj_dt: f32) -> Self {
3946 let mut db = Self::new();
3947 let frames = (clip.duration * clip.frame_rate) as u32;
3948 let ev = PoseEvaluator::new(n_bones);
3949 for frame in 0..frames {
3950 let t = frame as f32 / clip.frame_rate;
3951 let pose = ev.evaluate(clip, t);
3952 let pose_prev = ev.evaluate(clip, (t - 1.0 / clip.frame_rate).max(0.0));
3953 let positions: Vec<Vec3> = pose.iter().map(|b| b.position).collect();
3954 let velocities: Vec<Vec3> = pose.iter().zip(pose_prev.iter()).map(|(c,p)| c.position - p.position).collect();
3955 let trajectory: Vec<Vec3> = (1..=traj_steps).map(|si| {
3956 let ft = (t + si as f32 * traj_dt).min(clip.duration);
3957 ev.evaluate(clip, ft).get(0).map_or(Vec3::ZERO, |b| b.position)
3958 }).collect();
3959 db.add(MotionPose { positions, velocities, trajectory, clip_name: clip.name.clone(), frame });
3960 }
3961 db
3962 }
3963}
3964
3965#[derive(Debug, Clone)]
3968pub struct FootIKSolver {
3969 pub left_foot_bone: u32,
3970 pub right_foot_bone: u32,
3971 pub left_plant_threshold: f32,
3972 pub right_plant_threshold: f32,
3973 pub ik_blend: f32,
3974 pub left_planted: bool,
3975 pub right_planted: bool,
3976 pub left_plant_pos: Vec3,
3977 pub right_plant_pos: Vec3,
3978}
3979
3980impl FootIKSolver {
3981 pub fn new(left: u32, right: u32) -> Self {
3982 Self { left_foot_bone: left, right_foot_bone: right, left_plant_threshold: 0.05, right_plant_threshold: 0.05, ik_blend: 1.0, left_planted: false, right_planted: false, left_plant_pos: Vec3::ZERO, right_plant_pos: Vec3::ZERO }
3983 }
3984
3985 pub fn update(&mut self, pose: &[Transform], prev_pose: &[Transform]) {
3986 let left_vel = if (self.left_foot_bone as usize) < pose.len() {
3987 let prev = prev_pose.get(self.left_foot_bone as usize).copied().unwrap_or_default();
3988 position_error_l2(pose[self.left_foot_bone as usize].position, prev.position)
3989 } else { 1.0 };
3990 let right_vel = if (self.right_foot_bone as usize) < pose.len() {
3991 let prev = prev_pose.get(self.right_foot_bone as usize).copied().unwrap_or_default();
3992 position_error_l2(pose[self.right_foot_bone as usize].position, prev.position)
3993 } else { 1.0 };
3994 if left_vel < self.left_plant_threshold && !self.left_planted {
3995 self.left_planted = true;
3996 self.left_plant_pos = pose.get(self.left_foot_bone as usize).map_or(Vec3::ZERO, |t| t.position);
3997 } else if left_vel >= self.left_plant_threshold { self.left_planted = false; }
3998 if right_vel < self.right_plant_threshold && !self.right_planted {
3999 self.right_planted = true;
4000 self.right_plant_pos = pose.get(self.right_foot_bone as usize).map_or(Vec3::ZERO, |t| t.position);
4001 } else if right_vel >= self.right_plant_threshold { self.right_planted = false; }
4002 }
4003}
4004
4005pub struct FabrikChain {
4008 pub bone_indices: Vec<u32>,
4009 pub target: Vec3,
4010 pub iterations: usize,
4011 pub tolerance: f32,
4012 pub bone_lengths: Vec<f32>,
4013}
4014
4015impl FabrikChain {
4016 pub fn new(bone_indices: Vec<u32>, bone_lengths: Vec<f32>, target: Vec3) -> Self {
4017 Self { bone_indices, target, iterations: 10, tolerance: 0.001, bone_lengths }
4018 }
4019
4020 pub fn solve(&self, pose: &mut Vec<Transform>) {
4021 let n = self.bone_indices.len();
4022 if n == 0 || n > self.bone_lengths.len() + 1 { return; }
4023 let mut positions: Vec<Vec3> = self.bone_indices.iter()
4024 .map(|&bi| pose.get(bi as usize).map_or(Vec3::ZERO, |t| t.position))
4025 .collect();
4026 let root = positions[0];
4027 let total_len: f32 = self.bone_lengths.iter().sum();
4028 let dist = (self.target - root).length();
4029 if dist > total_len {
4030 let dir = (self.target - root).normalize();
4031 for i in 1..n { positions[i] = positions[i-1] + dir * self.bone_lengths[i-1]; }
4032 } else {
4033 for _ in 0..self.iterations {
4034 positions[n-1] = self.target;
4035 for i in (0..n-1).rev() {
4036 let dir = (positions[i] - positions[i+1]).normalize();
4037 positions[i] = positions[i+1] + dir * self.bone_lengths[i];
4038 }
4039 positions[0] = root;
4040 for i in 0..n-1 {
4041 let dir = (positions[i+1] - positions[i]).normalize();
4042 positions[i+1] = positions[i] + dir * self.bone_lengths[i];
4043 }
4044 if (positions[n-1] - self.target).length() < self.tolerance { break; }
4045 }
4046 }
4047 for i in 0..n-1 {
4048 let bi = self.bone_indices[i] as usize;
4049 if bi >= pose.len() { continue; }
4050 let dir = (positions[i+1] - positions[i]).normalize();
4051 pose[bi].position = positions[i];
4052 pose[bi].rotation = Quat::from_rotation_arc(Vec3::Y, dir);
4053 }
4054 }
4055}
4056
4057pub fn serialize_pose(pose: &[Transform]) -> Vec<u8> {
4060 let mut out = Vec::with_capacity(4 + pose.len() * 40);
4061 out.extend_from_slice(&(pose.len() as u32).to_le_bytes());
4062 for t in pose {
4063 out.extend_from_slice(&t.position.x.to_bits().to_le_bytes());
4064 out.extend_from_slice(&t.position.y.to_bits().to_le_bytes());
4065 out.extend_from_slice(&t.position.z.to_bits().to_le_bytes());
4066 out.extend_from_slice(&t.rotation.x.to_bits().to_le_bytes());
4067 out.extend_from_slice(&t.rotation.y.to_bits().to_le_bytes());
4068 out.extend_from_slice(&t.rotation.z.to_bits().to_le_bytes());
4069 out.extend_from_slice(&t.rotation.w.to_bits().to_le_bytes());
4070 out.extend_from_slice(&t.scale.x.to_bits().to_le_bytes());
4071 out.extend_from_slice(&t.scale.y.to_bits().to_le_bytes());
4072 out.extend_from_slice(&t.scale.z.to_bits().to_le_bytes());
4073 }
4074 out
4075}
4076
4077pub fn deserialize_pose(data: &[u8]) -> Option<Vec<Transform>> {
4078 if data.len() < 4 { return None; }
4079 let n = u32::from_le_bytes(data[0..4].try_into().ok()?) as usize;
4080 if data.len() < 4 + n * 40 { return None; }
4081 let mut pose = Vec::with_capacity(n);
4082 for i in 0..n {
4083 let b = 4 + i * 40;
4084 let r = |s: usize| f32::from_bits(u32::from_le_bytes(data[s..s+4].try_into().unwrap_or([0u8;4])));
4085 pose.push(Transform {
4086 position: Vec3::new(r(b), r(b+4), r(b+8)),
4087 rotation: Quat::from_xyzw(r(b+12), r(b+16), r(b+20), r(b+24)),
4088 scale: Vec3::new(r(b+28), r(b+32), r(b+36)),
4089 });
4090 }
4091 Some(pose)
4092}
4093
4094#[derive(Debug, Clone)]
4097pub struct AnimationMetrics {
4098 pub clip_name: String,
4099 pub total_frames: u32,
4100 pub active_bones: usize,
4101 pub total_keyframes: usize,
4102 pub avg_keyframes_per_bone: f32,
4103 pub duration_secs: f32,
4104 pub has_root_motion: bool,
4105 pub max_bone_velocity: f32,
4106 pub root_displacement: Vec3,
4107}
4108
4109impl AnimationMetrics {
4110 pub fn compute(clip: &AnimationClip) -> Self {
4111 let total_kf: usize = clip.tracks.iter().map(|t| t.keyframes.len()).sum();
4112 let active = clip.tracks.len();
4113 let root_disp = clip.tracks.first().map(|track| {
4114 let fp = track.keyframes.first().map_or(Vec3::ZERO, |k| k.transform.position);
4115 let lp = track.keyframes.last().map_or(Vec3::ZERO, |k| k.transform.position);
4116 lp - fp
4117 }).unwrap_or(Vec3::ZERO);
4118 let mut max_vel = 0.0f32;
4119 for track in &clip.tracks {
4120 for i in 1..track.keyframes.len() {
4121 let dt = track.keyframes[i].time - track.keyframes[i-1].time;
4122 if dt < 1e-9 { continue; }
4123 let v = (track.keyframes[i].transform.position - track.keyframes[i-1].transform.position).length() / dt;
4124 if v > max_vel { max_vel = v; }
4125 }
4126 }
4127 Self {
4128 clip_name: clip.name.clone(),
4129 total_frames: (clip.duration * clip.frame_rate) as u32,
4130 active_bones: active,
4131 total_keyframes: total_kf,
4132 avg_keyframes_per_bone: if active > 0 { total_kf as f32 / active as f32 } else { 0.0 },
4133 duration_secs: clip.duration,
4134 has_root_motion: root_disp.length() > 0.01,
4135 max_bone_velocity: max_vel,
4136 root_displacement: root_disp,
4137 }
4138 }
4139}
4140
4141#[derive(Debug, Clone)]
4144pub struct SkeletonMask {
4145 pub bone_weights: Vec<f32>,
4146}
4147
4148impl SkeletonMask {
4149 pub fn new(n: usize) -> Self { Self { bone_weights: vec![1.0; n] } }
4150
4151 pub fn upper_body(n: usize, upper_start: usize) -> Self {
4152 let mut m = Self::new(n);
4153 for i in 0..upper_start.min(n) { m.bone_weights[i] = 0.0; }
4154 m
4155 }
4156
4157 pub fn lower_body(n: usize, upper_start: usize) -> Self {
4158 let mut m = Self::new(n);
4159 for i in upper_start..n { m.bone_weights[i] = 0.0; }
4160 m
4161 }
4162
4163 pub fn apply(&self, base: &[Transform], layer: &[Transform]) -> Vec<Transform> {
4164 base.iter().zip(layer.iter()).enumerate().map(|(i, (b, l))| {
4165 b.lerp(l, self.bone_weights.get(i).copied().unwrap_or(0.0))
4166 }).collect()
4167 }
4168}
4169
4170pub struct CompressedPoseStream {
4173 pub bone_count: usize,
4174 pub pos_bounds: Vec<PositionBounds>,
4175 pub frames: Vec<Vec<CompressedKeyframe>>,
4176}
4177
4178impl CompressedPoseStream {
4179 pub fn new(bone_count: usize) -> Self {
4180 Self {
4181 bone_count,
4182 pos_bounds: vec![PositionBounds { min: Vec3::splat(-10.0), max: Vec3::splat(10.0) }; bone_count],
4183 frames: Vec::new(),
4184 }
4185 }
4186
4187 pub fn push_pose(&mut self, pose: &[Transform]) {
4188 let frame: Vec<CompressedKeyframe> = (0..self.bone_count.min(pose.len())).map(|bi| {
4189 CompressedKeyframe {
4190 time_ticks: 0,
4191 position: quantize_position_16(pose[bi].position, &self.pos_bounds[bi]),
4192 rotation: compress_quat_smallest3(pose[bi].rotation),
4193 scale: quantize_scale_vec_log8(pose[bi].scale),
4194 }
4195 }).collect();
4196 self.frames.push(frame);
4197 }
4198
4199 pub fn decode_frame(&self, fi: usize) -> Option<Vec<Transform>> {
4200 let frame = self.frames.get(fi)?;
4201 Some(frame.iter().enumerate().map(|(bi, ckf)| Transform {
4202 position: dequantize_position_16(ckf.position, &self.pos_bounds[bi]),
4203 rotation: decompress_quat_smallest3(&ckf.rotation),
4204 scale: dequantize_scale_vec_log8(ckf.scale),
4205 }).collect())
4206 }
4207
4208 pub fn byte_size(&self) -> usize { self.frames.iter().map(|f| f.len() * 21).sum() }
4209}
4210
4211pub struct AnimationLodManager {
4214 pub lod_distances: [f32; MAX_LOD_LEVELS],
4215 pub update_rates: [f32; MAX_LOD_LEVELS],
4216 pub current_lods: HashMap<u32, usize>,
4217}
4218
4219impl AnimationLodManager {
4220 pub fn new() -> Self {
4221 Self {
4222 lod_distances: [10.0, 25.0, 60.0, 150.0],
4223 update_rates: [60.0, 30.0, 15.0, 5.0],
4224 current_lods: HashMap::new(),
4225 }
4226 }
4227
4228 pub fn update_entity(&mut self, entity_id: u32, distance: f32) {
4229 let lod = self.lod_distances.iter().position(|&d| distance < d).unwrap_or(MAX_LOD_LEVELS - 1);
4230 self.current_lods.insert(entity_id, lod);
4231 }
4232
4233 pub fn should_update(&self, entity_id: u32, frame: u32) -> bool {
4234 let lod = self.current_lods.get(&entity_id).copied().unwrap_or(0);
4235 let period = (60.0 / self.update_rates[lod]) as u32;
4236 frame % period.max(1) == 0
4237 }
4238
4239 pub fn bone_mask(&self, entity_id: u32) -> u64 {
4240 match self.current_lods.get(&entity_id).copied().unwrap_or(0) {
4241 0 => u64::MAX,
4242 1 => 0x00FFFFFFFFFFFFFF,
4243 2 => 0x000000FFFFFFFFFF,
4244 _ => 0x000000000000FFFF,
4245 }
4246 }
4247}
4248
4249pub struct AdditiveBlendTree {
4252 pub base_clip: String,
4253 pub additive_layers: Vec<(String, f32, SkeletonMask)>,
4254}
4255
4256impl AdditiveBlendTree {
4257 pub fn new(base_clip: &str) -> Self {
4258 Self { base_clip: base_clip.to_owned(), additive_layers: Vec::new() }
4259 }
4260
4261 pub fn add_layer(&mut self, clip: &str, weight: f32, mask: SkeletonMask) {
4262 self.additive_layers.push((clip.to_owned(), weight, mask));
4263 }
4264
4265 pub fn evaluate(&self, registry: &AnimationRegistry, time: f32, n_bones: usize) -> Vec<Transform> {
4266 let ev = PoseEvaluator::new(n_bones);
4267 let mut pose = if let Some(clip) = registry.get_compressed(&self.base_clip) {
4268 ev.evaluate_compressed(clip, time)
4269 } else if let Some(clip) = registry.get_clip(&self.base_clip) {
4270 ev.evaluate(clip, time)
4271 } else {
4272 vec![Transform::identity(); n_bones]
4273 };
4274
4275 for (clip_name, weight, mask) in &self.additive_layers {
4276 let add_pose = if let Some(clip) = registry.get_compressed(clip_name) {
4277 ev.evaluate_compressed(clip, time)
4278 } else if let Some(clip) = registry.get_clip(clip_name) {
4279 ev.evaluate(clip, time)
4280 } else {
4281 continue;
4282 };
4283 let blended = mask.apply(&pose, &add_pose);
4284 for (b, bl) in pose.iter_mut().zip(blended.into_iter()) {
4285 *b = b.lerp(&bl, *weight);
4286 }
4287 }
4288 pose
4289 }
4290}
4291
4292pub fn clamp_velocity_per_frame(
4295 pose: &mut Vec<Transform>,
4296 prev_pose: &[Transform],
4297 max_pos_vel: f32,
4298 max_rot_vel: f32,
4299 dt: f32,
4300) {
4301 for (i, t) in pose.iter_mut().enumerate() {
4302 let prev = prev_pose.get(i).copied().unwrap_or_default();
4303 let pos_vel = (t.position - prev.position).length() / dt.max(1e-9);
4304 if pos_vel > max_pos_vel {
4305 let clamped_step = (t.position - prev.position).normalize() * max_pos_vel * dt;
4306 t.position = prev.position + clamped_step;
4307 }
4308 let rot_vel = rotation_error_geodesic(t.rotation, prev.rotation) / dt.max(1e-9);
4309 if rot_vel > max_rot_vel {
4310 let blend_t = max_rot_vel * dt / rot_vel.max(1e-9);
4311 t.rotation = prev.rotation.slerp(t.rotation, blend_t.min(1.0));
4312 }
4313 }
4314}
4315
4316#[derive(Debug, Clone)]
4319pub struct ClipMetadata {
4320 pub name: String,
4321 pub category: String,
4322 pub tags: Vec<String>,
4323 pub created_at: u64,
4324 pub author: String,
4325 pub source_file: String,
4326 pub frame_rate: f32,
4327 pub duration: f32,
4328 pub looping: bool,
4329 pub has_root_motion: bool,
4330 pub compression_ratio: f32,
4331}
4332
4333impl ClipMetadata {
4334 pub fn from_clip(clip: &AnimationClip, compressed: &CompressedAnimationClip) -> Self {
4335 let metrics = AnimationMetrics::compute(clip);
4336 Self {
4337 name: clip.name.clone(),
4338 category: String::new(),
4339 tags: Vec::new(),
4340 created_at: 0,
4341 author: String::new(),
4342 source_file: String::new(),
4343 frame_rate: clip.frame_rate,
4344 duration: clip.duration,
4345 looping: clip.looping,
4346 has_root_motion: metrics.has_root_motion,
4347 compression_ratio: compressed.compression_ratio(),
4348 }
4349 }
4350}
4351
4352pub struct ClipLibrary {
4355 pub clips: BTreeMap<String, AnimationClip>,
4356 pub metadata: BTreeMap<String, ClipMetadata>,
4357 pub compressed: BTreeMap<String, CompressedAnimationClip>,
4358 pub compressor: AnimationCompressor,
4359}
4360
4361impl ClipLibrary {
4362 pub fn new() -> Self {
4363 Self {
4364 clips: BTreeMap::new(),
4365 metadata: BTreeMap::new(),
4366 compressed: BTreeMap::new(),
4367 compressor: AnimationCompressor::new(),
4368 }
4369 }
4370
4371 pub fn import(&mut self, clip: AnimationClip) {
4372 let name = clip.name.clone();
4373 let c = self.compressor.compress(&clip);
4374 let meta = ClipMetadata::from_clip(&clip, &c);
4375 self.clips.insert(name.clone(), clip);
4376 self.compressed.insert(name.clone(), c);
4377 self.metadata.insert(name, meta);
4378 }
4379
4380 pub fn search_by_tag(&self, tag: &str) -> Vec<&ClipMetadata> {
4381 self.metadata.values().filter(|m| m.tags.iter().any(|t| t == tag)).collect()
4382 }
4383
4384 pub fn search_by_duration(&self, min: f32, max: f32) -> Vec<&ClipMetadata> {
4385 self.metadata.values().filter(|m| m.duration >= min && m.duration <= max).collect()
4386 }
4387
4388 pub fn total_compressed_size(&self) -> usize {
4389 self.compressed.values().map(|c| c.compressed_byte_size).sum()
4390 }
4391}
4392
4393#[derive(Debug, Clone, Copy, PartialEq)]
4396pub enum InterpolationMode { Step, Linear, CubicHermite, CatmullRom }
4397
4398pub fn sample_track_with_mode(track: &BoneTrack, t: f32, mode: InterpolationMode) -> Option<Transform> {
4399 if track.keyframes.is_empty() { return None; }
4400 if track.keyframes.len() == 1 { return Some(track.keyframes[0].transform); }
4401 let tc = t.clamp(track.keyframes.first().unwrap().time, track.keyframes.last().unwrap().time);
4402 let idx = track.keyframes.partition_point(|kf| kf.time <= tc).min(track.keyframes.len() - 1).max(1);
4403 let kf0 = &track.keyframes[idx - 1];
4404 let kf1 = &track.keyframes[idx];
4405 let dt = kf1.time - kf0.time;
4406 let alpha = if dt > 1e-9 { (tc - kf0.time) / dt } else { 0.0 };
4407 match mode {
4408 InterpolationMode::Step => Some(kf0.transform),
4409 InterpolationMode::Linear => Some(kf0.transform.lerp(&kf1.transform, alpha)),
4410 InterpolationMode::CubicHermite | InterpolationMode::CatmullRom => {
4411 let tan0 = if idx >= 2 {
4412 let prev = &track.keyframes[idx-2];
4413 let dtp = kf0.time - prev.time;
4414 if dtp > 1e-9 { (kf1.transform.position - prev.transform.position) / (dtp + dt) } else { Vec3::ZERO }
4415 } else { if dt > 1e-9 { (kf1.transform.position - kf0.transform.position) / dt } else { Vec3::ZERO } };
4416 let tan1 = if idx < track.keyframes.len() - 1 {
4417 let next = &track.keyframes[idx+1];
4418 let dtn = next.time - kf1.time;
4419 if dtn > 1e-9 { (next.transform.position - kf0.transform.position) / (dt + dtn) } else { Vec3::ZERO }
4420 } else { if dt > 1e-9 { (kf1.transform.position - kf0.transform.position) / dt } else { Vec3::ZERO } };
4421 let seg = HermiteSegment { t0: kf0.time, t1: kf1.time, p0: kf0.transform.position, p1: kf1.transform.position, m0: tan0, m1: tan1 };
4422 Some(Transform { position: seg.evaluate(tc), rotation: kf0.transform.rotation.slerp(kf1.transform.rotation, alpha), scale: kf0.transform.scale.lerp(kf1.transform.scale, alpha) })
4423 }
4424 }
4425}
4426
4427pub struct QualityAnalyzer {
4430 pub sample_rate: f32,
4431 pub test_cases: Vec<(AnimationClip, CompressionSettings)>,
4432}
4433
4434impl QualityAnalyzer {
4435 pub fn new(sample_rate: f32) -> Self {
4436 Self { sample_rate, test_cases: Vec::new() }
4437 }
4438
4439 pub fn add_test(&mut self, clip: AnimationClip, settings: CompressionSettings) {
4440 self.test_cases.push((clip, settings));
4441 }
4442
4443 pub fn run_all(&self) -> Vec<CompressionErrorReport> {
4444 self.test_cases.iter().map(|(clip, settings)| {
4445 let compressor = AnimationCompressor::new().with_settings(settings.clone());
4446 let compressed = compressor.compress(clip);
4447 compute_error_report(clip, &compressed)
4448 }).collect()
4449 }
4450
4451 pub fn worst_case<'a>(&self, reports: &'a [CompressionErrorReport]) -> Option<&'a CompressionErrorReport> {
4452 reports.iter().max_by(|a, b| a.global_max_pos_error.partial_cmp(&b.global_max_pos_error).unwrap_or(std::cmp::Ordering::Equal))
4453 }
4454
4455 pub fn passes_threshold(&self, reports: &[CompressionErrorReport], max_pos: f32, max_rot: f32) -> bool {
4456 reports.iter().all(|r| r.global_max_pos_error <= max_pos && r.global_max_rot_error <= max_rot)
4457 }
4458}
4459
4460pub struct ProceduralBreathing {
4463 pub chest_bone: u32,
4464 pub spine_bone: u32,
4465 pub rate: f32,
4466 pub intensity: f32,
4467 pub phase: f32,
4468}
4469
4470impl ProceduralBreathing {
4471 pub fn new(chest_bone: u32, spine_bone: u32) -> Self {
4472 Self { chest_bone, spine_bone, rate: 0.25, intensity: 0.03, phase: 0.0 }
4473 }
4474
4475 pub fn update(&mut self, dt: f32) {
4476 self.phase = (self.phase + dt * self.rate * std::f32::consts::TAU).rem_euclid(std::f32::consts::TAU);
4477 }
4478
4479 pub fn apply(&self, pose: &mut Vec<Transform>) {
4480 let t = self.phase.sin() * 0.5 + 0.5;
4481 let sa = t * self.intensity;
4482 if (self.chest_bone as usize) < pose.len() {
4483 pose[self.chest_bone as usize].scale += Vec3::new(sa * 0.5, sa, sa * 0.5);
4484 }
4485 if (self.spine_bone as usize) < pose.len() {
4486 let bend_rot = Quat::from_rotation_x(t * self.intensity * 0.5);
4487 pose[self.spine_bone as usize].rotation = (pose[self.spine_bone as usize].rotation * bend_rot).normalize();
4488 }
4489 }
4490}
4491
4492pub struct ProceduralHeadLook {
4495 pub head_bone: u32,
4496 pub neck_bone: u32,
4497 pub target: Vec3,
4498 pub blend: f32,
4499 pub max_angle: f32,
4500 pub current_blend: f32,
4501}
4502
4503impl ProceduralHeadLook {
4504 pub fn new(head: u32, neck: u32) -> Self {
4505 Self { head_bone: head, neck_bone: neck, target: Vec3::ZERO, blend: 1.0, max_angle: std::f32::consts::FRAC_PI_2, current_blend: 0.0 }
4506 }
4507
4508 pub fn update(&mut self, dt: f32) {
4509 self.current_blend = (self.current_blend + dt * 3.0).min(self.blend);
4510 }
4511
4512 pub fn apply(&self, pose: &mut Vec<Transform>) {
4513 if (self.head_bone as usize) >= pose.len() { return; }
4514 let head = pose[self.head_bone as usize];
4515 let dir = self.target - head.position;
4516 if dir.length_squared() < 1e-9 { return; }
4517 let new_rot = Quat::from_rotation_arc(Vec3::Z, dir.normalize());
4518 pose[self.head_bone as usize].rotation = head.rotation.slerp(new_rot, self.current_blend);
4519 }
4520}
4521
4522