This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
//! 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<_>>(),
|
||||
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"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user