## Summary - split development dynamic linking from production builds - add headless processed-asset generation - add a separate production CI cache and static Bevy/Rust linkage verification - keep custom debug features enabled for production builds behind a feature gate Reviewed-on: #46 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
251 lines
9.0 KiB
Rust
251 lines
9.0 KiB
Rust
//! Deterministic, fighter-local skeletal animation samples for gameplay and rendering.
|
|
|
|
mod asset_baking;
|
|
mod runtime;
|
|
mod visual_binding;
|
|
|
|
pub use asset_baking::{BakedAnimationAssetProcessingPlugin, BakedAnimationPlugin};
|
|
pub use crate::math::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"));
|
|
}
|
|
}
|