//! Deterministic, fighter-local skeletal animation samples for gameplay and rendering. mod asset_baking; mod fixed_transform; mod runtime; mod visual_binding; pub use asset_baking::BakedAnimationPlugin; pub use fixed_transform::FixedMat4; pub use runtime::{ BakedAnimationClip, BakedFighterAnimations, FighterBoneIndexMap, FighterBoneMatrices, update_fighter_bone_matrices, }; pub const BAKED_ANIMATION_FPS: u32 = 60; #[cfg(test)] mod tests { use super::*; use crate::fighter::{animation::AnimKind, manifest::FighterManifest}; use crate::math::{int::FGi32, vec3::FGVec3}; use asset_baking::{ Channel, ChannelValues, bake_gltf_document, baked_frame_count, sample_vec3, }; use std::collections::HashMap; #[test] fn configured_fighter_clips_bake_to_fixed_point_frames() { let manifest: FighterManifest = ron::from_str(include_str!( "../../../assets/fighters/test/test.fighter.ron" )) .unwrap(); let (document, buffers, _) = gltf::import(concat!( env!("CARGO_MANIFEST_DIR"), "/assets/fighters/test/test_fighter.gltf" )) .unwrap(); let baked = bake_gltf_document(document, buffers, &manifest.animations).unwrap(); assert_eq!(baked.frame_rate, BAKED_ANIMATION_FPS); assert_eq!(baked.frame_count(AnimKind::Wait), Some(121)); assert_eq!(baked.frame_count(AnimKind::Walk), Some(33)); assert_eq!(baked.frame_count(AnimKind::Run), Some(33)); assert_eq!(baked.frame_count(AnimKind::Dash), Some(21)); assert_eq!(baked.frame_count(AnimKind::Landing), Some(9)); assert_eq!(baked.frame_count(AnimKind::JumpBack), Some(57)); assert_eq!(baked.clip_indices.len(), manifest.animations.len()); assert_eq!( baked.clip(AnimKind::Wait).unwrap().frames[0].len(), baked.bone_names.len() ); assert_eq!(baked.parents.len(), baked.bone_names.len()); } #[test] fn pose_sampling_wraps_repeating_frames_and_clamps_finished_frames() { let first = FixedMat4::IDENTITY; let mut second = FixedMat4::IDENTITY; second.translate(FGVec3::lit("2", "0", "0")); let mut terminal = FixedMat4::IDENTITY; terminal.translate(FGVec3::lit("4", "0", "0")); let baked = BakedFighterAnimations { frame_rate: BAKED_ANIMATION_FPS, bone_names: vec!["root".into()], parents: vec![None], clips: vec![BakedAnimationClip { frames: vec![vec![first], vec![second], vec![terminal]], }], clip_indices: HashMap::from([(AnimKind::Wait, 0)]), }; let repeating = crate::fighter::animation::FighterAnimationFrame { kind: AnimKind::Wait, frame: 3, repeat: true, }; let finished = crate::fighter::animation::FighterAnimationFrame { kind: AnimKind::Wait, frame: 4, repeat: false, }; assert_eq!(baked.sample_pose(&repeating), Some(&[second][..])); assert_eq!(baked.sample_pose(&finished), Some(&[terminal][..])); } #[test] fn frame_count_snaps_source_frame_rounding_error_to_the_bake_grid() { assert_eq!(baked_frame_count(0.533333361, 60), 33); assert_eq!(baked_frame_count(0.333333343, 60), 21); assert_eq!(baked_frame_count(0.133333340, 60), 9); assert_eq!(baked_frame_count(2.0, 60), 121); } #[test] fn linear_source_channel_is_resampled_between_thirty_hz_keys() { let channel = Channel { node: 0, property: gltf::animation::Property::Translation, interpolation: gltf::animation::Interpolation::Linear, times: vec![0.0, 1.0 / 30.0], values: ChannelValues::Vec3(vec![[0.0, 0.0, 0.0], [2.0, 0.0, 0.0]]), }; let ChannelValues::Vec3(values) = &channel.values else { unreachable!() }; assert_eq!(sample_vec3(&channel, values, 1.0 / 60.0), [1.0, 0.0, 0.0]); } #[test] fn presentation_pose_interpolates_previous_and_current_local_transforms() { let mut previous = FixedMat4::IDENTITY; previous.translate(FGVec3::lit("2", "0", "0")); let mut current = FixedMat4::IDENTITY; current.translate(FGVec3::lit("6", "0", "0")); let transform = runtime::interpolate_transform(previous, current, 0.25); assert_eq!( transform.translation, bevy::prelude::Vec3::new(3.0, 0.0, 0.0) ); } #[test] fn bone_matrix_component_keeps_names_aligned_with_the_current_pose() { let first = FixedMat4::IDENTITY; let mut second = FixedMat4::IDENTITY; second.translate(FGVec3::lit("0", "3", "0")); let mut matrices = FighterBoneMatrices::default(); matrices.set_pose( &["root".into(), "hand".into()], &[None, Some(0)], &[first, second], FGi32::ONE, ); assert_eq!(matrices.get_current("hand"), Some(second)); assert_eq!( matrices.iter().collect::>(), vec![("root", first), ("hand", second)] ); } #[test] fn local_pose_reconstructs_model_space_for_gameplay() { let mut root = FixedMat4::IDENTITY; root.translate(FGVec3::lit("10", "0", "0")); let mut hand = FixedMat4::IDENTITY; hand.translate(FGVec3::lit("0", "3", "0")); let mut matrices = FighterBoneMatrices::default(); matrices.set_pose( &["root".into(), "hand".into()], &[None, Some(0)], &[root, hand], FGi32::ONE, ); assert_eq!(matrices.local(1), Some(hand)); assert_eq!( matrices.get_current("hand").unwrap().get_translation(), FGVec3::lit("10", "3", "0") ); assert_eq!( matrices .local(1) .unwrap() .to_mat4() .transform_point3(bevy::prelude::Vec3::ZERO), bevy::prelude::Vec3::new(0.0, 3.0, 0.0) ); } #[test] fn model_scale_affects_gameplay_bones_without_changing_visual_locals() { let mut root = FixedMat4::IDENTITY; root.translate(FGVec3::lit("1", "0", "0")); let mut hand = FixedMat4::IDENTITY; hand.translate(FGVec3::lit("0", "2", "0")); let mut matrices = FighterBoneMatrices::default(); matrices.set_pose( &["root".into(), "hand".into()], &[None, Some(0)], &[root, hand], FGi32::lit("10"), ); assert_eq!(matrices.local(1), Some(hand)); assert_eq!( matrices.get_current("hand").unwrap().get_translation(), FGVec3::lit("10", "20", "0") ); } #[test] fn fixed_matrix_transforms_points() { let transform = FixedMat4 { cols: [ [FGi32::lit("2"), FGi32::ZERO, FGi32::ZERO, FGi32::ZERO], [FGi32::ZERO, FGi32::lit("3"), FGi32::ZERO, FGi32::ZERO], [FGi32::ZERO, FGi32::ZERO, FGi32::lit("4"), FGi32::ZERO], [ FGi32::lit("10"), FGi32::lit("-5"), FGi32::lit("1.5"), FGi32::ONE, ], ], }; assert_eq!( transform.transform_point(FGVec3::lit("1.25", "-2", "0.5")), FGVec3::lit("12.5", "-11", "3.5") ); } #[test] fn fixed_matrix_rotates_points_around_y() { let transform = FixedMat4 { cols: [ [FGi32::ONE, FGi32::ZERO, FGi32::ZERO, FGi32::ZERO], [FGi32::ZERO, FGi32::ONE, FGi32::ZERO, FGi32::ZERO], [FGi32::ZERO, FGi32::ZERO, FGi32::ONE, FGi32::ZERO], [ FGi32::lit("10"), FGi32::lit("2"), FGi32::lit("-3"), FGi32::ONE, ], ], }; assert_eq!( transform .rotate_y(5) .transform_point(FGVec3::lit("1", "0", "0")), FGVec3::lit("10", "2", "-4") ); } #[test] fn fixed_matrix_builders_set_euler_rotation_and_translation() { let transform = FixedMat4::IDENTITY .with_rotation(FGVec3::lit("0", "0", "1.5707963267948966")) .with_translation(FGVec3::lit("10", "2", "-3")); assert_eq!( transform.transform_point(FGVec3::lit("1", "0", "0")), FGVec3::lit("10", "3", "-3") ); assert_eq!(transform.get_translation(), FGVec3::lit("10", "2", "-3")); } #[test] fn fixed_matrix_rotation_builder_preserves_translation() { let transform = FixedMat4::IDENTITY .with_translation(FGVec3::lit("4", "5", "6")) .with_rotation(FGVec3::lit("0", "0", "1.5707963267948966")); assert_eq!(transform.get_translation(), FGVec3::lit("4", "5", "6")); } }