Skip to content

Commit 5cf7e9e

Browse files
committed
feat(ltk_anim): add CompressedEvaluator for efficient sequential playback of compressed animations
1 parent 443ae7f commit 5cf7e9e

File tree

5 files changed

+334
-157
lines changed

5 files changed

+334
-157
lines changed

crates/ltk_anim/src/asset/compressed/evaluate.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -309,10 +309,9 @@ impl JumpFrame for JumpFrameU32 {
309309
}
310310
}
311311

312-
/// Hot frame evaluator state
312+
/// Hot frame evaluator state (internal)
313313
#[derive(Clone, Debug)]
314-
pub struct HotFrameEvaluator {
315-
#[allow(dead_code)]
314+
pub(crate) struct HotFrameEvaluator {
316315
pub last_evaluation_time: f32,
317316
pub cursor: usize,
318317
pub hot_frames: Vec<JointHotFrame>,
@@ -327,7 +326,6 @@ impl HotFrameEvaluator {
327326
}
328327
}
329328

330-
#[allow(dead_code)]
331329
pub fn reset(&mut self) {
332330
self.last_evaluation_time = -1.0;
333331
self.cursor = 0;
Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
1+
//! Stateful evaluator for compressed animations
2+
//!
3+
//! Provides efficient sequential playback by maintaining hot frame state
4+
//! between evaluations, only reinitializing when seeking.
5+
6+
use super::{
7+
evaluate::{
8+
compress_time, decompress_vector3, HotFrameEvaluator, JointHotFrame, JumpFrame,
9+
JumpFrameU16, JumpFrameU32, QuaternionHotFrame, VectorHotFrame,
10+
},
11+
frame::TransformType,
12+
read::AnimationFlags,
13+
Compressed,
14+
};
15+
use crate::quantized;
16+
use glam::{Quat, Vec3};
17+
use std::collections::HashMap;
18+
use std::mem::size_of;
19+
20+
/// A stateful evaluator for compressed animations.
21+
///
22+
/// This evaluator maintains hot frame state between evaluations, making it
23+
/// efficient for sequential playback. It only reinitializes from jump caches
24+
/// when seeking backwards or jumping too far forward.
25+
///
26+
/// # Example
27+
///
28+
/// ```ignore
29+
/// let animation = Compressed::from_reader(&mut reader)?;
30+
/// let mut evaluator = CompressedEvaluator::new(&animation);
31+
///
32+
/// // Efficient sequential playback
33+
/// for frame in 0..100 {
34+
/// let time = frame as f32 / 30.0;
35+
/// let pose = evaluator.evaluate(time);
36+
/// // Use pose...
37+
/// }
38+
/// ```
39+
pub struct CompressedEvaluator<'a> {
40+
animation: &'a Compressed,
41+
state: HotFrameEvaluator,
42+
}
43+
44+
impl<'a> CompressedEvaluator<'a> {
45+
/// Creates a new evaluator for the given animation.
46+
pub fn new(animation: &'a Compressed) -> Self {
47+
Self {
48+
state: HotFrameEvaluator::new(animation.joint_count()),
49+
animation,
50+
}
51+
}
52+
53+
/// Resets the evaluator state, forcing reinitialization on next evaluate.
54+
pub fn reset(&mut self) {
55+
self.state.reset();
56+
}
57+
58+
/// Evaluates the animation at the given time.
59+
///
60+
/// Returns a map of joint hash -> (rotation, translation, scale).
61+
///
62+
/// This method is optimized for sequential playback. When evaluating
63+
/// times in order, hot frames are updated incrementally. Seeking backwards
64+
/// or jumping too far forward triggers reinitialization from jump caches.
65+
pub fn evaluate(&mut self, time: f32) -> HashMap<u32, (Quat, Vec3, Vec3)> {
66+
let time = time.clamp(0.0, self.animation.duration);
67+
let parametrized = self
68+
.animation
69+
.flags
70+
.contains(AnimationFlags::UseKeyframeParametrization);
71+
72+
// Update hot frames
73+
self.update_hot_frames(time);
74+
75+
let compressed_time = compress_time(time, self.animation.duration);
76+
77+
self.animation
78+
.joints
79+
.iter()
80+
.enumerate()
81+
.map(|(id, &hash)| {
82+
(
83+
hash,
84+
self.state.hot_frames[id].sample(compressed_time, parametrized),
85+
)
86+
})
87+
.collect()
88+
}
89+
90+
/// Updates hot frames for the given evaluation time.
91+
fn update_hot_frames(&mut self, time: f32) {
92+
// Check if we need to reinitialize from jump cache
93+
let needs_reinit = self.state.last_evaluation_time < 0.0
94+
|| self.state.last_evaluation_time > time
95+
|| (self.animation.jump_cache_count > 0
96+
&& (time - self.state.last_evaluation_time)
97+
> self.animation.duration / self.animation.jump_cache_count as f32);
98+
99+
if needs_reinit {
100+
self.initialize_from_jump_cache(time);
101+
}
102+
103+
// Walk through frames to update hot frames
104+
let compressed_time = compress_time(time, self.animation.duration);
105+
self.advance_cursor(compressed_time);
106+
107+
self.state.last_evaluation_time = time;
108+
}
109+
110+
/// Initializes hot frames from jump cache for the given time.
111+
fn initialize_from_jump_cache(&mut self, time: f32) {
112+
if self.animation.jump_cache_count == 0 || self.animation.duration <= 0.0 {
113+
return;
114+
}
115+
116+
// Get cache id based on time
117+
let jump_cache_id = ((self.animation.jump_cache_count as f32
118+
* (time / self.animation.duration)) as usize)
119+
.min(self.animation.jump_cache_count - 1);
120+
121+
self.state.cursor = 0;
122+
123+
if self.animation.frames.len() < 0x10001 {
124+
self.init_from_cache::<JumpFrameU16>(jump_cache_id);
125+
} else {
126+
self.init_from_cache::<JumpFrameU32>(jump_cache_id);
127+
}
128+
129+
self.state.cursor += 1;
130+
}
131+
132+
fn init_from_cache<J: JumpFrame>(&mut self, jump_cache_id: usize) {
133+
let cache_start = jump_cache_id * size_of::<J>() * self.animation.joints.len();
134+
135+
for joint_id in 0..self.animation.joints.len() {
136+
let offset = cache_start + joint_id * size_of::<J>();
137+
let Some(bytes) = self
138+
.animation
139+
.jump_caches
140+
.get(offset..offset + size_of::<J>())
141+
else {
142+
continue;
143+
};
144+
let jump_frame: &J = bytemuck::from_bytes(bytes);
145+
self.init_joint_hot_frame(joint_id, jump_frame);
146+
}
147+
}
148+
149+
fn init_joint_hot_frame<J: JumpFrame>(&mut self, joint_id: usize, jump_frame: &J) {
150+
let mut hot_frame = JointHotFrame::default();
151+
152+
// Initialize rotation hot frames
153+
for (i, &frame_idx) in jump_frame.rotation_keys().iter().enumerate() {
154+
self.state.cursor = self.state.cursor.max(frame_idx);
155+
if let Some(frame) = self.animation.frames.get(frame_idx) {
156+
hot_frame.rotation[i] = QuaternionHotFrame {
157+
time: frame.time(),
158+
value: quantized::decompress_quat_u16(&frame.value()),
159+
};
160+
}
161+
}
162+
163+
// Initialize translation hot frames
164+
for (i, &frame_idx) in jump_frame.translation_keys().iter().enumerate() {
165+
self.state.cursor = self.state.cursor.max(frame_idx);
166+
if let Some(frame) = self.animation.frames.get(frame_idx) {
167+
hot_frame.translation[i] = VectorHotFrame {
168+
time: frame.time(),
169+
value: decompress_vector3(
170+
&frame.value(),
171+
self.animation.translation_min,
172+
self.animation.translation_max,
173+
),
174+
};
175+
}
176+
}
177+
178+
// Initialize scale hot frames
179+
for (i, &frame_idx) in jump_frame.scale_keys().iter().enumerate() {
180+
self.state.cursor = self.state.cursor.max(frame_idx);
181+
if let Some(frame) = self.animation.frames.get(frame_idx) {
182+
hot_frame.scale[i] = VectorHotFrame {
183+
time: frame.time(),
184+
value: decompress_vector3(
185+
&frame.value(),
186+
self.animation.scale_min,
187+
self.animation.scale_max,
188+
),
189+
};
190+
}
191+
}
192+
193+
// Rotate quaternions along shortest path
194+
for i in 1..4 {
195+
if hot_frame.rotation[i].value.dot(hot_frame.rotation[0].value) < 0.0 {
196+
hot_frame.rotation[i].value = -hot_frame.rotation[i].value;
197+
}
198+
}
199+
200+
self.state.hot_frames[joint_id] = hot_frame;
201+
}
202+
203+
/// Advances the cursor through frames, updating hot frames as needed.
204+
fn advance_cursor(&mut self, compressed_time: u16) {
205+
while self.state.cursor < self.animation.frames.len() {
206+
let frame = &self.animation.frames[self.state.cursor];
207+
let joint_id = frame.joint_id() as usize;
208+
let transform_type = frame.transform_type();
209+
210+
// Check if we need this frame yet
211+
let hot_frame = &self.state.hot_frames[joint_id];
212+
let needs_update = match transform_type {
213+
TransformType::Rotation => compressed_time >= hot_frame.rotation[2].time,
214+
TransformType::Translation => compressed_time >= hot_frame.translation[2].time,
215+
TransformType::Scale => compressed_time >= hot_frame.scale[2].time,
216+
};
217+
218+
if !needs_update {
219+
break;
220+
}
221+
222+
// Fetch the new frame
223+
match transform_type {
224+
TransformType::Rotation => {
225+
self.fetch_rotation_frame(joint_id, frame.time(), &frame.value())
226+
}
227+
TransformType::Translation => {
228+
self.fetch_translation_frame(joint_id, frame.time(), &frame.value())
229+
}
230+
TransformType::Scale => {
231+
self.fetch_scale_frame(joint_id, frame.time(), &frame.value())
232+
}
233+
}
234+
235+
self.state.cursor += 1;
236+
}
237+
}
238+
239+
/// Fetches a new rotation frame, shifting the hot frame window.
240+
fn fetch_rotation_frame(&mut self, joint_id: usize, time: u16, value: &[u16; 3]) {
241+
let hot_frame = &mut self.state.hot_frames[joint_id];
242+
243+
// Shift frames: [P0, P1, P2, P3] -> [P1, P2, P3, new]
244+
hot_frame.rotation[0] = hot_frame.rotation[1];
245+
hot_frame.rotation[1] = hot_frame.rotation[2];
246+
hot_frame.rotation[2] = hot_frame.rotation[3];
247+
hot_frame.rotation[3] = QuaternionHotFrame {
248+
time,
249+
value: quantized::decompress_quat_u16(value),
250+
};
251+
252+
// Rotate along shortest path
253+
for i in 1..4 {
254+
if hot_frame.rotation[i].value.dot(hot_frame.rotation[0].value) < 0.0 {
255+
hot_frame.rotation[i].value = -hot_frame.rotation[i].value;
256+
}
257+
}
258+
}
259+
260+
/// Fetches a new translation frame, shifting the hot frame window.
261+
fn fetch_translation_frame(&mut self, joint_id: usize, time: u16, value: &[u16; 3]) {
262+
let hot_frame = &mut self.state.hot_frames[joint_id];
263+
264+
// Shift frames: [P0, P1, P2, P3] -> [P1, P2, P3, new]
265+
hot_frame.translation[0] = hot_frame.translation[1];
266+
hot_frame.translation[1] = hot_frame.translation[2];
267+
hot_frame.translation[2] = hot_frame.translation[3];
268+
269+
hot_frame.translation[3] = VectorHotFrame {
270+
time,
271+
value: decompress_vector3(
272+
value,
273+
self.animation.translation_min,
274+
self.animation.translation_max,
275+
),
276+
};
277+
}
278+
279+
/// Fetches a new scale frame, shifting the hot frame window.
280+
fn fetch_scale_frame(&mut self, joint_id: usize, time: u16, value: &[u16; 3]) {
281+
let hot_frame = &mut self.state.hot_frames[joint_id];
282+
283+
// Shift frames: [P0, P1, P2, P3] -> [P1, P2, P3, new]
284+
hot_frame.scale[0] = hot_frame.scale[1];
285+
hot_frame.scale[1] = hot_frame.scale[2];
286+
hot_frame.scale[2] = hot_frame.scale[3];
287+
288+
hot_frame.scale[3] = VectorHotFrame {
289+
time,
290+
value: decompress_vector3(value, self.animation.scale_min, self.animation.scale_max),
291+
};
292+
}
293+
}

0 commit comments

Comments
 (0)