From 42ff80e55e30756821c47a1fda335c9e4efa89f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=81lex=20Rom=C3=A1n=20N=C3=BA=C3=B1ez?= Date: Sun, 27 Sep 2026 13:54:28 +0200 Subject: [PATCH] Fix bone resampling breaking hitbox interpolation. --- src/fighter/baked_animation/mod.rs | 27 +++++- src/fighter/baked_animation/runtime.rs | 128 ++++++++++++++++++++----- src/fighter/mod.rs | 2 +- 3 files changed, 132 insertions(+), 25 deletions(-) diff --git a/src/fighter/baked_animation/mod.rs b/src/fighter/baked_animation/mod.rs index 1c95527..2f87f62 100644 --- a/src/fighter/baked_animation/mod.rs +++ b/src/fighter/baked_animation/mod.rs @@ -8,7 +8,7 @@ pub use crate::math::fixed_transform::FixedMat4; pub use asset_baking::{BakedAnimationAssetProcessingPlugin, BakedAnimationPlugin}; pub use runtime::{ BakedAnimationClip, BakedFighterAnimations, FighterBoneIndexMap, FighterBoneMatrices, - update_fighter_bone_matrices, + resample_fighter_bone_matrices, update_fighter_bone_matrices, }; pub const BAKED_ANIMATION_FPS: u32 = 60; @@ -275,6 +275,31 @@ mod tests { assert_eq!(capacities, matrices.buffer_capacities()); } + #[test] + fn same_frame_resample_preserves_previous_gameplay_pose() { + let names = ["root".into()]; + let parents = [None]; + let mut pose_a = FixedMat4::IDENTITY; + pose_a.translate(FGVec3::lit("1", "0", "0")); + let mut pose_b = FixedMat4::IDENTITY; + pose_b.translate(FGVec3::lit("2", "0", "0")); + let mut snapped_pose = FixedMat4::IDENTITY; + snapped_pose.translate(FGVec3::lit("3", "0", "0")); + let mut matrices = FighterBoneMatrices::default(); + + matrices.set_pose(AnimKind::Wait, &names, &parents, &[pose_a], FGi32::ONE); + matrices.set_pose(AnimKind::Wait, &names, &parents, &[pose_b], FGi32::ONE); + matrices.resample_pose( + AnimKind::Landing, + &names, + &parents, + &[snapped_pose], + FGi32::ONE, + ); + + assert_eq!(matrices.get_index(0), Some((pose_a, snapped_pose))); + } + #[test] fn model_scale_affects_gameplay_bones_without_changing_visual_locals() { let mut root = FixedMat4::IDENTITY; diff --git a/src/fighter/baked_animation/runtime.rs b/src/fighter/baked_animation/runtime.rs index 18b65b6..fe05f0b 100644 --- a/src/fighter/baked_animation/runtime.rs +++ b/src/fighter/baked_animation/runtime.rs @@ -144,6 +144,27 @@ impl FighterBoneMatrices { parents: &[Option], local: &[FixedMat4], model_scale: FGi32, + ) { + self.sample_pose(kind, bone_names, parents, local, model_scale, true); + } + pub(super) fn resample_pose( + &mut self, + kind: AnimKind, + bone_names: &[String], + parents: &[Option], + local: &[FixedMat4], + model_scale: FGi32, + ) { + self.sample_pose(kind, bone_names, parents, local, model_scale, false); + } + fn sample_pose( + &mut self, + kind: AnimKind, + bone_names: &[String], + parents: &[Option], + local: &[FixedMat4], + model_scale: FGi32, + advance_history: bool, ) { assert_eq!( bone_names.len(), @@ -167,9 +188,11 @@ impl FighterBoneMatrices { self.local_matrices.reserve(local.len()); self.previous_local_matrices.reserve(local.len()); self.model_matrices.reserve(local.len()); - std::mem::swap(&mut self.matrices, &mut self.previous_frame_matrices); + if advance_history { + std::mem::swap(&mut self.matrices, &mut self.previous_frame_matrices); + std::mem::swap(&mut self.local_matrices, &mut self.previous_local_matrices); + } let animation_changed = self.presentation_kind != Some(kind); - std::mem::swap(&mut self.local_matrices, &mut self.previous_local_matrices); self.local_matrices.clear(); self.local_matrices.extend_from_slice(local); self.model_matrices.resize(local.len(), None); @@ -215,19 +238,70 @@ pub fn update_fighter_bone_matrices( manifests: Res>, ) { 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" + sample_fighter_bone_matrices( + frame, + animations, + fighter, + &mut matrices, + &mut bone_indices, + &baked_assets, + &manifests, + true, ); - 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"); + } +} + +/// Resamples the current pose after collision-driven state transitions without +/// advancing the previous-frame pose used for swept gameplay volumes. +pub fn resample_fighter_bone_matrices( + mut fighters: Query<( + &FighterAnimationFrame, + &FighterAnimations, + &Fighter, + &mut FighterBoneMatrices, + &mut FighterBoneIndexMap, + )>, + baked_assets: Res>, + manifests: Res>, +) { + for (frame, animations, fighter, mut matrices, mut bone_indices) in &mut fighters { + sample_fighter_bone_matrices( + frame, + animations, + fighter, + &mut matrices, + &mut bone_indices, + &baked_assets, + &manifests, + false, + ); + } +} + +fn sample_fighter_bone_matrices( + frame: &FighterAnimationFrame, + animations: &FighterAnimations, + fighter: &Fighter, + matrices: &mut FighterBoneMatrices, + bone_indices: &mut FighterBoneIndexMap, + baked_assets: &Assets, + manifests: &Assets, + advance_history: bool, +) { + 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"); + if advance_history { matrices.set_pose( frame.kind, &baked.bone_names, @@ -235,15 +309,23 @@ pub fn update_fighter_bone_matrices( 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)), - ); - } + } else { + matrices.resample_pose( + frame.kind, + &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)), + ); } } diff --git a/src/fighter/mod.rs b/src/fighter/mod.rs index c52df73..5289d39 100644 --- a/src/fighter/mod.rs +++ b/src/fighter/mod.rs @@ -183,7 +183,7 @@ impl Plugin for FighterPlugin { GgrsSchedule, ( (blast_zones::apply_blast_zones, ecb::update_ecb, ecb::decrement_ecb_lockout, state::state_collision_interrupt_system).chain(), - baked_animation::update_fighter_bone_matrices, + baked_animation::resample_fighter_bone_matrices, ) .chain() .in_set(GameplaySet::Collision),