Implement animation root motion deltas (#84)
Implements root motion and adds a dash attack. Reviewed-on: #84 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #84.
This commit is contained in:
@@ -14,6 +14,7 @@ use crate::{
|
||||
},
|
||||
math::fixed_transform::FixedMat4,
|
||||
math::int::FGi32,
|
||||
math::vec3::FGVec3,
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -55,6 +56,31 @@ impl BakedFighterAnimations {
|
||||
pub fn frame_count(&self, kind: AnimKind) -> Option<u32> {
|
||||
self.clip(kind).map(|clip| clip.frames.len() as u32)
|
||||
}
|
||||
pub fn root_motion_cycle_delta(
|
||||
&self,
|
||||
kind: AnimKind,
|
||||
bone: &str,
|
||||
model_scale: FGi32,
|
||||
) -> Option<FGVec3> {
|
||||
let last = self.frame_count(kind)?.saturating_sub(1);
|
||||
let first = FighterAnimationFrame {
|
||||
kind,
|
||||
frame: 0,
|
||||
repeat: false,
|
||||
};
|
||||
let last = FighterAnimationFrame {
|
||||
kind,
|
||||
frame: last,
|
||||
repeat: false,
|
||||
};
|
||||
Some(
|
||||
self.sample_bone_model_transform(&last, bone, model_scale)?
|
||||
.get_translation()
|
||||
- self
|
||||
.sample_bone_model_transform(&first, bone, model_scale)?
|
||||
.get_translation(),
|
||||
)
|
||||
}
|
||||
/// Samples one bone in model space without depending on the runtime's
|
||||
/// previous-frame buffers. Tooling uses this when the user scrubs between
|
||||
/// unrelated frames.
|
||||
@@ -95,6 +121,12 @@ pub struct FighterBoneMatrices {
|
||||
previous_local_matrices: Vec<FixedMat4>,
|
||||
model_matrices: Vec<Option<FixedMat4>>,
|
||||
presentation_kind: Option<AnimKind>,
|
||||
previous_animation_frame: Option<FighterAnimationFrame>,
|
||||
/// The raw, scaled model-space root translation used as the baseline for
|
||||
/// root-motion extraction. This is kept separately because the published
|
||||
/// pose has the root translation removed.
|
||||
previous_root_motion_translation: Option<FGVec3>,
|
||||
root_motion_delta: crate::math::vec3::FGVec3,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default)]
|
||||
@@ -111,7 +143,11 @@ impl FighterBoneMatrices {
|
||||
let Some(pose) = baked.sample_pose(frame) else {
|
||||
return false;
|
||||
};
|
||||
self.set_pose(
|
||||
self.set_pose_with_frame(
|
||||
*frame,
|
||||
"root",
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
frame.kind,
|
||||
&baked.bone_names,
|
||||
&baked.parents,
|
||||
@@ -158,6 +194,11 @@ impl FighterBoneMatrices {
|
||||
pub fn get_current_index(&self, index: usize) -> Option<FixedMat4> {
|
||||
self.matrices.get(index).copied()
|
||||
}
|
||||
/// Translation of the configured root-motion bone since the previous
|
||||
/// simulation animation frame, in scaled model-space units.
|
||||
pub fn root_motion_delta(&self) -> FGVec3 {
|
||||
self.root_motion_delta
|
||||
}
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, FixedMat4)> {
|
||||
self.bone_names
|
||||
.iter()
|
||||
@@ -180,6 +221,7 @@ impl FighterBoneMatrices {
|
||||
.unwrap_or(current);
|
||||
Some(interpolate_transform(previous, current, overstep_fraction))
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn set_pose(
|
||||
&mut self,
|
||||
kind: AnimKind,
|
||||
@@ -188,8 +230,44 @@ impl FighterBoneMatrices {
|
||||
local: &[FixedMat4],
|
||||
model_scale: FGi32,
|
||||
) {
|
||||
self.sample_pose(kind, bone_names, parents, local, model_scale, true);
|
||||
self.set_pose_with_frame(
|
||||
FighterAnimationFrame::new(kind, false),
|
||||
"",
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
kind,
|
||||
bone_names,
|
||||
parents,
|
||||
local,
|
||||
model_scale,
|
||||
);
|
||||
}
|
||||
pub(super) fn set_pose_with_frame(
|
||||
&mut self,
|
||||
animation_frame: FighterAnimationFrame,
|
||||
root_motion_bone: &str,
|
||||
root_motion_cycle_delta: FGVec3,
|
||||
root_motion_loop_frame_count: Option<u32>,
|
||||
kind: AnimKind,
|
||||
bone_names: &[String],
|
||||
parents: &[Option<usize>],
|
||||
local: &[FixedMat4],
|
||||
model_scale: FGi32,
|
||||
) {
|
||||
self.sample_pose(
|
||||
animation_frame,
|
||||
root_motion_bone,
|
||||
root_motion_cycle_delta,
|
||||
root_motion_loop_frame_count,
|
||||
kind,
|
||||
bone_names,
|
||||
parents,
|
||||
local,
|
||||
model_scale,
|
||||
true,
|
||||
);
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(super) fn resample_pose(
|
||||
&mut self,
|
||||
kind: AnimKind,
|
||||
@@ -198,10 +276,49 @@ impl FighterBoneMatrices {
|
||||
local: &[FixedMat4],
|
||||
model_scale: FGi32,
|
||||
) {
|
||||
self.sample_pose(kind, bone_names, parents, local, model_scale, false);
|
||||
self.resample_pose_with_frame(
|
||||
FighterAnimationFrame::new(kind, false),
|
||||
"",
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
kind,
|
||||
bone_names,
|
||||
parents,
|
||||
local,
|
||||
model_scale,
|
||||
);
|
||||
}
|
||||
pub(super) fn resample_pose_with_frame(
|
||||
&mut self,
|
||||
animation_frame: FighterAnimationFrame,
|
||||
root_motion_bone: &str,
|
||||
_root_motion_cycle_delta: FGVec3,
|
||||
_root_motion_loop_frame_count: Option<u32>,
|
||||
kind: AnimKind,
|
||||
bone_names: &[String],
|
||||
parents: &[Option<usize>],
|
||||
local: &[FixedMat4],
|
||||
model_scale: FGi32,
|
||||
) {
|
||||
self.sample_pose(
|
||||
animation_frame,
|
||||
root_motion_bone,
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
kind,
|
||||
bone_names,
|
||||
parents,
|
||||
local,
|
||||
model_scale,
|
||||
false,
|
||||
);
|
||||
}
|
||||
fn sample_pose(
|
||||
&mut self,
|
||||
animation_frame: FighterAnimationFrame,
|
||||
root_motion_bone: &str,
|
||||
root_motion_cycle_delta: FGVec3,
|
||||
root_motion_loop_frame_count: Option<u32>,
|
||||
kind: AnimKind,
|
||||
bone_names: &[String],
|
||||
parents: &[Option<usize>],
|
||||
@@ -219,6 +336,7 @@ impl FighterBoneMatrices {
|
||||
parents.len(),
|
||||
"baked bone parents do not match names"
|
||||
);
|
||||
let animation_changed = self.presentation_kind != Some(kind);
|
||||
if self.bone_names.is_empty() {
|
||||
self.bone_names.extend_from_slice(bone_names);
|
||||
self.parents.extend_from_slice(parents);
|
||||
@@ -235,9 +353,19 @@ impl FighterBoneMatrices {
|
||||
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);
|
||||
self.local_matrices.clear();
|
||||
self.local_matrices.extend_from_slice(local);
|
||||
|
||||
let root_index = if root_motion_bone.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
self.bone_names
|
||||
.iter()
|
||||
.position(|name| name == root_motion_bone)
|
||||
.expect("configured root motion bone should exist"),
|
||||
)
|
||||
};
|
||||
self.model_matrices.resize(local.len(), None);
|
||||
self.model_matrices.fill(None);
|
||||
for index in 0..local.len() {
|
||||
@@ -252,12 +380,57 @@ impl FighterBoneMatrices {
|
||||
[FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE],
|
||||
],
|
||||
};
|
||||
let raw_root_motion_translation = root_index.map(|root_index| {
|
||||
model_scale
|
||||
.mul(
|
||||
self.model_matrices[root_index]
|
||||
.expect("baked bone hierarchy should be valid"),
|
||||
)
|
||||
.get_translation()
|
||||
});
|
||||
|
||||
// Gameplay owns the root's displacement. Keep rotation and scale in
|
||||
// the animation, but remove its translation before publishing the pose
|
||||
// used by visuals and gameplay volumes.
|
||||
if let Some(root_index) = root_index {
|
||||
self.local_matrices[root_index].translate(FGVec3::ZERO);
|
||||
self.model_matrices.fill(None);
|
||||
for index in 0..local.len() {
|
||||
resolve_model_transform(index, parents, &self.local_matrices, &mut self.model_matrices)
|
||||
.expect("baked bone hierarchy should be valid");
|
||||
}
|
||||
}
|
||||
self.matrices.clear();
|
||||
self.matrices.extend(
|
||||
self.model_matrices.iter().map(|matrix| {
|
||||
model_scale.mul(matrix.expect("baked bone hierarchy should be valid"))
|
||||
}),
|
||||
);
|
||||
self.root_motion_delta = if advance_history
|
||||
&& !animation_changed
|
||||
&& self.previous_animation_frame.is_some()
|
||||
&& let (Some(current), Some(previous)) = (
|
||||
raw_root_motion_translation,
|
||||
self.previous_root_motion_translation,
|
||||
)
|
||||
{
|
||||
let mut delta = current - previous;
|
||||
if animation_frame.repeat
|
||||
&& self.previous_animation_frame.is_some_and(|previous| {
|
||||
previous.frame.saturating_add(1) == animation_frame.frame
|
||||
})
|
||||
&& root_motion_loop_frame_count == Some(animation_frame.frame)
|
||||
{
|
||||
delta = delta + root_motion_cycle_delta;
|
||||
}
|
||||
delta
|
||||
} else {
|
||||
FGVec3::ZERO
|
||||
};
|
||||
self.previous_root_motion_translation = raw_root_motion_translation;
|
||||
if advance_history {
|
||||
self.previous_animation_frame = Some(animation_frame);
|
||||
}
|
||||
// A state transition is a discontinuity, not an animation frame to
|
||||
// interpolate from the previous clip. Keep the presentation pose at
|
||||
// the new clip's first/current frame immediately.
|
||||
@@ -344,8 +517,18 @@ fn sample_fighter_bone_matrices(
|
||||
let manifest = manifests
|
||||
.get(&fighter.manifest)
|
||||
.expect("fighter manifest should be loaded before the match starts");
|
||||
let root_motion_cycle_delta = baked
|
||||
.root_motion_cycle_delta(frame.kind, &manifest.root_motion_bone, manifest.model_scale)
|
||||
.unwrap_or(FGVec3::ZERO);
|
||||
let root_motion_loop_frame_count = baked
|
||||
.frame_count(frame.kind)
|
||||
.map(|count| count.saturating_sub(1).max(1));
|
||||
if advance_history {
|
||||
matrices.set_pose(
|
||||
matrices.set_pose_with_frame(
|
||||
*frame,
|
||||
&manifest.root_motion_bone,
|
||||
root_motion_cycle_delta,
|
||||
root_motion_loop_frame_count,
|
||||
frame.kind,
|
||||
&baked.bone_names,
|
||||
&baked.parents,
|
||||
@@ -353,7 +536,11 @@ fn sample_fighter_bone_matrices(
|
||||
manifest.model_scale,
|
||||
);
|
||||
} else {
|
||||
matrices.resample_pose(
|
||||
matrices.resample_pose_with_frame(
|
||||
*frame,
|
||||
&manifest.root_motion_bone,
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
frame.kind,
|
||||
&baked.bone_names,
|
||||
&baked.parents,
|
||||
|
||||
Reference in New Issue
Block a user