diff --git a/crates/skate-game/src/animation_pose.rs b/crates/skate-game/src/animation_pose.rs index 55a46dc0..13031498 100644 --- a/crates/skate-game/src/animation_pose.rs +++ b/crates/skate-game/src/animation_pose.rs @@ -4,7 +4,7 @@ use skate_core::animation::{ output::{self, NativeMatrix, Sqt}, playback_tree::PoseCommand, pose_add, pose_blend, pose_mirror, - pose_sample::{sample_key_vbr, select_frames}, + pose_sample::{sample_key, select_frames}, pose_trajectory::{self, LoopTransform}, }; use skate_data::{ @@ -163,7 +163,10 @@ fn sample_clip(clip: &ClipFrames, time: f32) -> Result, String> { .zip(&clip.frames[selection.second]) .enumerate() .map(|(bone, (&first, &second))| { - let mut sample = sample_key_vbr(sqt(first), sqt(second), selection); + // ClipFrames already contains independently decoded keys. The raw + // VBR block-boundary compatibility path substitutes the next key, + // which skips then holds a frame when applied to this flat cache. + let mut sample = sample_key(sqt(first), sqt(second), selection); sample.translation[3] = f32::from_bits(clip.channel_weights[bone]); sample }) @@ -178,7 +181,7 @@ fn sample_bone(clip: &ClipFrames, time: f32, bone: usize) -> Result true, 0.0, )?; - let mut sample = sample_key_vbr( + let mut sample = sample_key( sqt(clip.frames[selection.first][bone]), sqt(clip.frames[selection.second][bone]), selection, diff --git a/crates/skate-game/src/tests/animation_pose.rs b/crates/skate-game/src/tests/animation_pose.rs index f615a528..8a75c850 100644 --- a/crates/skate-game/src/tests/animation_pose.rs +++ b/crates/skate-game/src/tests/animation_pose.rs @@ -1,5 +1,55 @@ use super::*; +#[test] +fn decoded_clip_interpolates_across_compression_boundaries() { + // Uniform motion makes a skipped key or a held pose unambiguous. Exercise + // both full-pose sampling and the trajectory's previous-frame sampler. + for fps in [30.0_f32, 60.0] { + let clip = ClipFrames { + name: "BOUNDARY_RAMP".into(), + source_offset: 0, + fps_bits: fps.to_bits(), + loop_translation_bits: [0; 3], + loop_rotation_bits: [0, 0, 0, 1.0_f32.to_bits()], + channel_animation: true, + channel_weights: vec![0.25_f32.to_bits()], + frames: (0..=17) + .map(|frame| { + let half_angle = frame as f32 * 0.02; + vec![ + [ + 1.0, + 1.0, + 1.0, + 0.0, + 0.0, + half_angle.sin(), + half_angle.cos(), + frame as f32, + 0.0, + 0.0, + ] + .map(f32::to_bits), + ] + }) + .collect(), + }; + for quarter in 0..=68 { + let frame = quarter as f32 * 0.25; + let time = frame / fps; + let pose = sample_clip(&clip, time).unwrap()[0]; + assert_eq!(pose, sample_bone(&clip, time, 0).unwrap()); + assert!( + (pose.translation[0] - frame).abs() < 0.0001, + "{fps} fps, frame {frame}: skipped/held decoded key: {pose:?}" + ); + let angle = 2.0 * pose.rotation[2].atan2(pose.rotation[3]); + assert!((angle - frame * 0.04).abs() < 0.0001); + assert_eq!(pose.translation[3], 0.25); + } + } +} + #[test] #[ignore = "requires the user's decoded stock animation assets"] fn stock_pose_commands_sample_complete_bones_and_preserve_tree_order() {