Skip to content

Commit 1a2df30

Browse files
authored
Merge pull request #81 from DexalGT/main
feat(ltk_anim): implement animation reading/writing and evaluation
2 parents eb92869 + ac8137b commit 1a2df30

File tree

18 files changed

+1794
-35
lines changed

18 files changed

+1794
-35
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,4 +16,5 @@ Cargo.lock
1616
.direnv
1717
result
1818

19-
bin
19+
bin
20+
Reference-Folder/

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ zstd = { version = "0.13", default-features = false }
2323
ruzstd = { version = "0.8" }
2424

2525
paste = "1.0.15"
26+
bytemuck = { version = "1.14", features = ["derive"] }
2627

2728
serde = { version = "1.0.204", features = ["derive"] }
2829
indexmap = "2.7.0"

crates/ltk_anim/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ thiserror = { workspace = true }
1111
byteorder = { workspace = true }
1212
bitflags = { workspace = true }
1313
num_enum = { workspace = true }
14+
bytemuck = { workspace = true }
1415

1516
glam = { workspace = true }
1617

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
//! Compressed animation evaluation
2+
//!
3+
//! Implements hot frame-based Catmull-Rom interpolation for compressed animations.
4+
5+
use glam::{Quat, Vec3};
6+
7+
/// Decompresses a compressed time value to actual time
8+
#[allow(dead_code)]
9+
pub fn decompress_time(compressed_time: u16, duration: f32) -> f32 {
10+
(compressed_time as f32 / u16::MAX as f32) * duration
11+
}
12+
13+
/// Compresses a time value to u16 range
14+
pub fn compress_time(time: f32, duration: f32) -> u16 {
15+
if duration <= 0.0 {
16+
return 0;
17+
}
18+
((time / duration) * u16::MAX as f32) as u16
19+
}
20+
21+
/// Decompresses a vector from quantized u16 components
22+
pub fn decompress_vector3(value: &[u16; 3], min: Vec3, max: Vec3) -> Vec3 {
23+
let scale = max - min;
24+
Vec3::new(
25+
(value[0] as f32 / u16::MAX as f32) * scale.x + min.x,
26+
(value[1] as f32 / u16::MAX as f32) * scale.y + min.y,
27+
(value[2] as f32 / u16::MAX as f32) * scale.z + min.z,
28+
)
29+
}
30+
31+
/// A hot frame for vector transforms (translation/scale)
32+
#[derive(Clone, Copy, Debug, Default)]
33+
pub struct VectorHotFrame {
34+
pub time: u16,
35+
pub value: Vec3,
36+
}
37+
38+
/// A hot frame for quaternion transforms (rotation)
39+
#[derive(Clone, Copy, Debug)]
40+
pub struct QuaternionHotFrame {
41+
pub time: u16,
42+
pub value: Quat,
43+
}
44+
45+
impl Default for QuaternionHotFrame {
46+
fn default() -> Self {
47+
Self {
48+
time: 0,
49+
value: Quat::IDENTITY,
50+
}
51+
}
52+
}
53+
54+
/// Joint hot frame state containing 4 control points for each transform
55+
#[derive(Clone, Debug, Default)]
56+
pub struct JointHotFrame {
57+
pub rotation: [QuaternionHotFrame; 4],
58+
pub translation: [VectorHotFrame; 4],
59+
pub scale: [VectorHotFrame; 4],
60+
}
61+
62+
impl JointHotFrame {
63+
/// Samples all transforms at the given time
64+
///
65+
/// Returns (rotation, translation, scale)
66+
pub fn sample(&self, time: u16, parametrized: bool) -> (Quat, Vec3, Vec3) {
67+
if parametrized {
68+
(
69+
self.sample_rotation_parametrized(time),
70+
self.sample_translation_parametrized(time),
71+
self.sample_scale_parametrized(time),
72+
)
73+
} else {
74+
(
75+
self.sample_rotation_uniform(time),
76+
self.sample_translation_uniform(time),
77+
self.sample_scale_uniform(time),
78+
)
79+
}
80+
}
81+
82+
/// Samples rotation using uniform Catmull-Rom interpolation
83+
fn sample_rotation_uniform(&self, time: u16) -> Quat {
84+
let t_d = self.rotation[2].time.saturating_sub(self.rotation[1].time);
85+
if t_d == 0 {
86+
return self.rotation[1].value;
87+
}
88+
let amount = (time.saturating_sub(self.rotation[1].time)) as f32 / t_d as f32;
89+
90+
interpolate_quat_catmull(
91+
amount,
92+
0.5,
93+
0.5,
94+
self.rotation[0].value,
95+
self.rotation[1].value,
96+
self.rotation[2].value,
97+
self.rotation[3].value,
98+
)
99+
}
100+
101+
/// Samples translation using uniform Catmull-Rom interpolation
102+
fn sample_translation_uniform(&self, time: u16) -> Vec3 {
103+
let t_d = self.translation[2]
104+
.time
105+
.saturating_sub(self.translation[1].time);
106+
if t_d == 0 {
107+
return self.translation[1].value;
108+
}
109+
let amount = (time.saturating_sub(self.translation[1].time)) as f32 / t_d as f32;
110+
111+
interpolate_vec3_catmull(
112+
amount,
113+
0.5,
114+
0.5,
115+
self.translation[0].value,
116+
self.translation[1].value,
117+
self.translation[2].value,
118+
self.translation[3].value,
119+
)
120+
}
121+
122+
/// Samples scale using uniform Catmull-Rom interpolation
123+
fn sample_scale_uniform(&self, time: u16) -> Vec3 {
124+
let t_d = self.scale[2].time.saturating_sub(self.scale[1].time);
125+
if t_d == 0 {
126+
return self.scale[1].value;
127+
}
128+
let amount = (time.saturating_sub(self.scale[1].time)) as f32 / t_d as f32;
129+
130+
interpolate_vec3_catmull(
131+
amount,
132+
0.5,
133+
0.5,
134+
self.scale[0].value,
135+
self.scale[1].value,
136+
self.scale[2].value,
137+
self.scale[3].value,
138+
)
139+
}
140+
141+
/// Samples rotation using parametrized Catmull-Rom interpolation
142+
fn sample_rotation_parametrized(&self, time: u16) -> Quat {
143+
let (amount, scale_in, scale_out) = create_keyframe_weights(
144+
time,
145+
self.rotation[0].time,
146+
self.rotation[1].time,
147+
self.rotation[2].time,
148+
self.rotation[3].time,
149+
);
150+
151+
interpolate_quat_catmull(
152+
amount,
153+
scale_in,
154+
scale_out,
155+
self.rotation[0].value,
156+
self.rotation[1].value,
157+
self.rotation[2].value,
158+
self.rotation[3].value,
159+
)
160+
}
161+
162+
/// Samples translation using parametrized Catmull-Rom interpolation
163+
fn sample_translation_parametrized(&self, time: u16) -> Vec3 {
164+
let (amount, scale_in, scale_out) = create_keyframe_weights(
165+
time,
166+
self.translation[0].time,
167+
self.translation[1].time,
168+
self.translation[2].time,
169+
self.translation[3].time,
170+
);
171+
172+
interpolate_vec3_catmull(
173+
amount,
174+
scale_in,
175+
scale_out,
176+
self.translation[0].value,
177+
self.translation[1].value,
178+
self.translation[2].value,
179+
self.translation[3].value,
180+
)
181+
}
182+
183+
/// Samples scale using parametrized Catmull-Rom interpolation
184+
fn sample_scale_parametrized(&self, time: u16) -> Vec3 {
185+
let (amount, scale_in, scale_out) = create_keyframe_weights(
186+
time,
187+
self.scale[0].time,
188+
self.scale[1].time,
189+
self.scale[2].time,
190+
self.scale[3].time,
191+
);
192+
193+
interpolate_vec3_catmull(
194+
amount,
195+
scale_in,
196+
scale_out,
197+
self.scale[0].value,
198+
self.scale[1].value,
199+
self.scale[2].value,
200+
self.scale[3].value,
201+
)
202+
}
203+
}
204+
205+
const SLERP_EPSILON: f32 = 0.000001;
206+
207+
/// Creates Catmull-Rom keyframe weights for parametrized interpolation
208+
fn create_keyframe_weights(time: u16, t0: u16, t1: u16, t2: u16, t3: u16) -> (f32, f32, f32) {
209+
let t_d = t2.saturating_sub(t1) as f32;
210+
let amount = time.saturating_sub(t1) as f32 / (t_d + SLERP_EPSILON);
211+
let scale_in = t_d / (t2.saturating_sub(t0) as f32 + SLERP_EPSILON);
212+
let scale_out = t_d / (t3.saturating_sub(t1) as f32 + SLERP_EPSILON);
213+
(amount, scale_in, scale_out)
214+
}
215+
216+
/// Creates Catmull-Rom weights for interpolation
217+
fn create_catmull_rom_weights(amount: f32, ease_in: f32, ease_out: f32) -> (f32, f32, f32, f32) {
218+
let m0 = (((2.0 - amount) * amount) - 1.0) * (amount * ease_in);
219+
let m1 = ((((2.0 - ease_out) * amount) + (ease_out - 3.0)) * (amount * amount)) + 1.0;
220+
let m2 = ((((3.0 - ease_in * 2.0) + ((ease_in - 2.0) * amount)) * amount) + ease_in) * amount;
221+
let m3 = ((amount - 1.0) * amount) * (amount * ease_out);
222+
(m0, m1, m2, m3)
223+
}
224+
225+
/// Interpolates Vec3 using Catmull-Rom spline
226+
fn interpolate_vec3_catmull(
227+
amount: f32,
228+
tau20: f32,
229+
tau31: f32,
230+
p0: Vec3,
231+
p1: Vec3,
232+
p2: Vec3,
233+
p3: Vec3,
234+
) -> Vec3 {
235+
let (m0, m1, m2, m3) = create_catmull_rom_weights(amount, tau20, tau31);
236+
Vec3::new(
237+
m1 * p1.x + m0 * p0.x + m3 * p3.x + m2 * p2.x,
238+
m1 * p1.y + m0 * p0.y + m3 * p3.y + m2 * p2.y,
239+
m1 * p1.z + m0 * p0.z + m3 * p3.z + m2 * p2.z,
240+
)
241+
}
242+
243+
/// Interpolates Quaternion using Catmull-Rom spline
244+
fn interpolate_quat_catmull(
245+
amount: f32,
246+
tau20: f32,
247+
tau31: f32,
248+
p0: Quat,
249+
p1: Quat,
250+
p2: Quat,
251+
p3: Quat,
252+
) -> Quat {
253+
let (m0, m1, m2, m3) = create_catmull_rom_weights(amount, tau20, tau31);
254+
Quat::from_xyzw(
255+
m1 * p1.x + m0 * p0.x + m3 * p3.x + m2 * p2.x,
256+
m1 * p1.y + m0 * p0.y + m3 * p3.y + m2 * p2.y,
257+
m1 * p1.z + m0 * p0.z + m3 * p3.z + m2 * p2.z,
258+
m1 * p1.w + m0 * p0.w + m3 * p3.w + m2 * p2.w,
259+
)
260+
.normalize()
261+
}
262+
263+
/// Trait for jump frames that provide frame indices for hot frame initialization
264+
pub trait JumpFrame: bytemuck::Pod {
265+
fn rotation_keys(&self) -> [usize; 4];
266+
fn translation_keys(&self) -> [usize; 4];
267+
fn scale_keys(&self) -> [usize; 4];
268+
}
269+
270+
/// Jump frame with 16-bit keys (used when frame_count < 0x10001)
271+
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
272+
#[repr(C)]
273+
pub struct JumpFrameU16 {
274+
pub rotation_keys: [u16; 4],
275+
pub translation_keys: [u16; 4],
276+
pub scale_keys: [u16; 4],
277+
}
278+
279+
impl JumpFrame for JumpFrameU16 {
280+
fn rotation_keys(&self) -> [usize; 4] {
281+
self.rotation_keys.map(|k| k as usize)
282+
}
283+
fn translation_keys(&self) -> [usize; 4] {
284+
self.translation_keys.map(|k| k as usize)
285+
}
286+
fn scale_keys(&self) -> [usize; 4] {
287+
self.scale_keys.map(|k| k as usize)
288+
}
289+
}
290+
291+
/// Jump frame with 32-bit keys (used when frame_count >= 0x10001)
292+
#[derive(Clone, Copy, Debug, bytemuck::Pod, bytemuck::Zeroable)]
293+
#[repr(C)]
294+
pub struct JumpFrameU32 {
295+
pub rotation_keys: [u32; 4],
296+
pub translation_keys: [u32; 4],
297+
pub scale_keys: [u32; 4],
298+
}
299+
300+
impl JumpFrame for JumpFrameU32 {
301+
fn rotation_keys(&self) -> [usize; 4] {
302+
self.rotation_keys.map(|k| k as usize)
303+
}
304+
fn translation_keys(&self) -> [usize; 4] {
305+
self.translation_keys.map(|k| k as usize)
306+
}
307+
fn scale_keys(&self) -> [usize; 4] {
308+
self.scale_keys.map(|k| k as usize)
309+
}
310+
}
311+
312+
/// Hot frame evaluator state (internal)
313+
#[derive(Clone, Debug)]
314+
pub(crate) struct HotFrameEvaluator {
315+
pub last_evaluation_time: f32,
316+
pub cursor: usize,
317+
pub hot_frames: Vec<JointHotFrame>,
318+
}
319+
320+
impl HotFrameEvaluator {
321+
pub fn new(joint_count: usize) -> Self {
322+
Self {
323+
last_evaluation_time: -1.0,
324+
cursor: 0,
325+
hot_frames: vec![JointHotFrame::default(); joint_count],
326+
}
327+
}
328+
329+
pub fn reset(&mut self) {
330+
self.last_evaluation_time = -1.0;
331+
self.cursor = 0;
332+
for hf in &mut self.hot_frames {
333+
*hf = JointHotFrame::default();
334+
}
335+
}
336+
}

0 commit comments

Comments
 (0)