Fixes #19 Splits the baked animation implementation into focused fixed-transform, runtime, visual-binding, and asset-baking modules behind the existing public facade. Preserves public APIs, serialized field order, TypePath, plugin registration, schedules, and runtime behavior. Reviewed-on: #38 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #38.
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{BAKED_ANIMATION_FPS};
|
||||
|
||||
use crate::{
|
||||
fighter::{
|
||||
Fighter,
|
||||
animation::{AnimKind, FighterAnimationFrame},
|
||||
manifest::FighterManifest,
|
||||
visual::FighterAnimations,
|
||||
},
|
||||
math::int::FGi32,
|
||||
math::fixed_transform::FixedMat4
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BakedAnimationClip {
|
||||
pub frames: Vec<Vec<FixedMat4>>,
|
||||
}
|
||||
|
||||
#[derive(Asset, bevy::reflect::TypePath, Clone, Debug, Serialize, Deserialize)]
|
||||
#[type_path = "shinespark::fighter::baked_animation"]
|
||||
pub struct BakedFighterAnimations {
|
||||
pub frame_rate: u32,
|
||||
pub bone_names: Vec<String>,
|
||||
pub parents: Vec<Option<usize>>,
|
||||
pub(super) clips: Vec<BakedAnimationClip>,
|
||||
pub(super) clip_indices: HashMap<AnimKind, usize>,
|
||||
}
|
||||
|
||||
impl BakedFighterAnimations {
|
||||
pub fn contains_bone(&self, bone: &str) -> bool {
|
||||
self.bone_names.iter().any(|name| name == bone)
|
||||
}
|
||||
pub fn has_unique_bone_names(&self) -> bool {
|
||||
let mut names = std::collections::HashSet::with_capacity(self.bone_names.len());
|
||||
self.bone_names.iter().all(|name| names.insert(name))
|
||||
}
|
||||
pub fn sample_pose(&self, frame: &FighterAnimationFrame) -> Option<&[FixedMat4]> {
|
||||
let frames = &self.clip(frame.kind)?.frames;
|
||||
let frame_index = if frame.repeat {
|
||||
let loop_frame_count = frames.len().saturating_sub(1).max(1) as u32;
|
||||
frame.frame % loop_frame_count
|
||||
} else {
|
||||
frame.frame
|
||||
};
|
||||
frames
|
||||
.get(frame_index as usize)
|
||||
.or_else(|| frames.last())
|
||||
.map(Vec::as_slice)
|
||||
}
|
||||
pub fn frame_count(&self, kind: AnimKind) -> Option<u32> {
|
||||
self.clip(kind).map(|clip| clip.frames.len() as u32)
|
||||
}
|
||||
pub(super) fn clip(&self, kind: AnimKind) -> Option<&BakedAnimationClip> {
|
||||
self.clip_indices
|
||||
.get(&kind)
|
||||
.and_then(|index| self.clips.get(*index))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default)]
|
||||
pub struct FighterBoneMatrices {
|
||||
bone_names: Vec<String>,
|
||||
parents: Vec<Option<usize>>,
|
||||
matrices: Vec<FixedMat4>,
|
||||
previous_frame_matrices: Vec<FixedMat4>,
|
||||
local_matrices: Vec<FixedMat4>,
|
||||
previous_local_matrices: Vec<FixedMat4>,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default)]
|
||||
pub struct FighterBoneIndexMap(pub HashMap<String, usize>);
|
||||
|
||||
impl FighterBoneMatrices {
|
||||
pub fn get(&self, bone: &str) -> Option<(FixedMat4, FixedMat4)> {
|
||||
let index = self.bone_names.iter().position(|name| name == bone)?;
|
||||
Some((
|
||||
*self.previous_frame_matrices.get(index)?,
|
||||
*self.matrices.get(index)?,
|
||||
))
|
||||
}
|
||||
pub fn get_current(&self, bone: &str) -> Option<FixedMat4> {
|
||||
let index = self.bone_names.iter().position(|name| name == bone)?;
|
||||
self.matrices.get(index).copied()
|
||||
}
|
||||
pub fn get_index(&self, index: usize) -> Option<(FixedMat4, FixedMat4)> {
|
||||
Some((
|
||||
*self.previous_frame_matrices.get(index)?,
|
||||
*self.matrices.get(index)?,
|
||||
))
|
||||
}
|
||||
pub fn get_current_index(&self, index: usize) -> Option<FixedMat4> {
|
||||
self.matrices.get(index).copied()
|
||||
}
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, FixedMat4)> {
|
||||
self.bone_names
|
||||
.iter()
|
||||
.map(String::as_str)
|
||||
.zip(self.matrices.iter().copied())
|
||||
}
|
||||
pub fn local(&self, index: usize) -> Option<FixedMat4> {
|
||||
self.local_matrices.get(index).copied()
|
||||
}
|
||||
pub(super) fn presentation_local(
|
||||
&self,
|
||||
index: usize,
|
||||
overstep_fraction: f32,
|
||||
) -> Option<Transform> {
|
||||
let current = self.local_matrices.get(index).copied()?;
|
||||
let previous = self
|
||||
.previous_local_matrices
|
||||
.get(index)
|
||||
.copied()
|
||||
.unwrap_or(current);
|
||||
Some(interpolate_transform(previous, current, overstep_fraction))
|
||||
}
|
||||
pub(super) fn set_pose(
|
||||
&mut self,
|
||||
bone_names: &[String],
|
||||
parents: &[Option<usize>],
|
||||
local: &[FixedMat4],
|
||||
model_scale: FGi32,
|
||||
) {
|
||||
assert_eq!(
|
||||
bone_names.len(),
|
||||
local.len(),
|
||||
"baked animation pose does not match its bone names"
|
||||
);
|
||||
assert_eq!(
|
||||
bone_names.len(),
|
||||
parents.len(),
|
||||
"baked bone parents do not match names"
|
||||
);
|
||||
if self.bone_names.is_empty() {
|
||||
self.bone_names.extend_from_slice(bone_names);
|
||||
self.parents.extend_from_slice(parents);
|
||||
} else {
|
||||
assert_eq!(self.bone_names, bone_names, "baked bone names changed");
|
||||
assert_eq!(self.parents, parents, "baked bone parents changed");
|
||||
}
|
||||
std::mem::swap(&mut self.matrices, &mut self.previous_frame_matrices);
|
||||
std::mem::swap(&mut self.local_matrices, &mut self.previous_local_matrices);
|
||||
self.local_matrices.clear();
|
||||
self.local_matrices.extend_from_slice(local);
|
||||
let mut model = vec![None; local.len()];
|
||||
for index in 0..local.len() {
|
||||
resolve_model_transform(index, parents, local, &mut model)
|
||||
.expect("baked bone hierarchy should be valid");
|
||||
}
|
||||
let model_scale = FixedMat4 {
|
||||
cols: [
|
||||
[model_scale, FGi32::ZERO, FGi32::ZERO, FGi32::ZERO],
|
||||
[FGi32::ZERO, model_scale, FGi32::ZERO, FGi32::ZERO],
|
||||
[FGi32::ZERO, FGi32::ZERO, model_scale, FGi32::ZERO],
|
||||
[FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE],
|
||||
],
|
||||
};
|
||||
self.matrices = model
|
||||
.into_iter()
|
||||
.map(|matrix| model_scale.mul(matrix.expect("baked bone hierarchy should be valid")))
|
||||
.collect();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_fighter_bone_matrices(
|
||||
mut fighters: Query<(
|
||||
&FighterAnimationFrame,
|
||||
&FighterAnimations,
|
||||
&Fighter,
|
||||
&mut FighterBoneMatrices,
|
||||
&mut FighterBoneIndexMap,
|
||||
)>,
|
||||
baked_assets: Res<Assets<BakedFighterAnimations>>,
|
||||
manifests: Res<Assets<FighterManifest>>,
|
||||
) {
|
||||
for (frame, animations, fighter, mut matrices, mut bone_indices) in &mut fighters {
|
||||
let baked = baked_assets
|
||||
.get(&animations.baked)
|
||||
.expect("fighter baked animation asset should be loaded before the match starts");
|
||||
assert_eq!(
|
||||
baked.frame_rate, BAKED_ANIMATION_FPS,
|
||||
"fighter baked animation rate does not match the simulation rate"
|
||||
);
|
||||
let pose = baked
|
||||
.sample_pose(frame)
|
||||
.expect("fighter animation frame should select a baked pose");
|
||||
let manifest = manifests
|
||||
.get(&fighter.manifest)
|
||||
.expect("fighter manifest should be loaded before the match starts");
|
||||
matrices.set_pose(
|
||||
&baked.bone_names,
|
||||
&baked.parents,
|
||||
pose,
|
||||
manifest.model_scale,
|
||||
);
|
||||
if bone_indices.0.is_empty() {
|
||||
bone_indices.0.extend(
|
||||
baked
|
||||
.bone_names
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, name)| (name.clone(), index)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_model_transform(
|
||||
index: usize,
|
||||
parents: &[Option<usize>],
|
||||
local: &[FixedMat4],
|
||||
model: &mut [Option<FixedMat4>],
|
||||
) -> std::io::Result<FixedMat4> {
|
||||
if let Some(value) = model[index] {
|
||||
return Ok(value);
|
||||
}
|
||||
let value = match parents[index] {
|
||||
Some(parent) => resolve_model_transform(parent, parents, local, model)?.mul(local[index]),
|
||||
None => local[index],
|
||||
};
|
||||
model[index] = Some(value);
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
pub(super) fn interpolate_transform(
|
||||
previous: FixedMat4,
|
||||
current: FixedMat4,
|
||||
factor: f32,
|
||||
) -> Transform {
|
||||
let (previous_scale, previous_rotation, previous_translation) =
|
||||
previous.to_mat4().to_scale_rotation_translation();
|
||||
let (current_scale, current_rotation, current_translation) =
|
||||
current.to_mat4().to_scale_rotation_translation();
|
||||
let factor = factor.clamp(0.0, 1.0);
|
||||
Transform {
|
||||
translation: previous_translation.lerp(current_translation, factor),
|
||||
rotation: previous_rotation.slerp(current_rotation, factor),
|
||||
scale: previous_scale.lerp(current_scale, factor),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user