Initial work on ledge & improved baking of root motion. (#90)
Reviewed-on: #90 Co-authored-by: Álex Román Núñez <eirexe@eirteam.moe>
This commit was merged in pull request #90.
This commit is contained in:
@@ -21,6 +21,9 @@ use crate::{
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct BakedAnimationClip {
|
||||
pub frames: Vec<Vec<FixedMat4>>,
|
||||
/// Unscaled model-space position of the configured root-motion bone at
|
||||
/// each baked frame.
|
||||
pub root_positions: Vec<FGVec3>,
|
||||
}
|
||||
|
||||
#[derive(Asset, bevy::reflect::TypePath, Clone, Debug, Serialize, Deserialize)]
|
||||
@@ -29,11 +32,22 @@ pub struct BakedFighterAnimations {
|
||||
pub frame_rate: u32,
|
||||
pub bone_names: Vec<String>,
|
||||
pub parents: Vec<Option<usize>>,
|
||||
pub(super) root_bone_index: usize,
|
||||
pub(super) clips: Vec<BakedAnimationClip>,
|
||||
pub(super) clip_indices: HashMap<AnimKind, usize>,
|
||||
}
|
||||
|
||||
impl BakedFighterAnimations {
|
||||
fn sample_index(&self, frame: &FighterAnimationFrame) -> Option<usize> {
|
||||
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
|
||||
};
|
||||
Some((frame_index as usize).min(frames.len().saturating_sub(1)))
|
||||
}
|
||||
pub fn contains_bone(&self, bone: &str) -> bool {
|
||||
self.bone_names.iter().any(|name| name == bone)
|
||||
}
|
||||
@@ -42,17 +56,20 @@ impl BakedFighterAnimations {
|
||||
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)
|
||||
let clip = self.clip(frame.kind)?;
|
||||
clip.frames.get(self.sample_index(frame)?).map(Vec::as_slice)
|
||||
}
|
||||
pub fn sample_root_position(
|
||||
&self,
|
||||
frame: &FighterAnimationFrame,
|
||||
model_scale: FGi32,
|
||||
) -> Option<FGVec3> {
|
||||
let clip = self.clip(frame.kind)?;
|
||||
let position = *clip.root_positions.get(self.sample_index(frame)?)?;
|
||||
Some(position * model_scale)
|
||||
}
|
||||
pub fn initial_root_position(&self, kind: AnimKind, model_scale: FGi32) -> Option<FGVec3> {
|
||||
self.sample_root_position(&FighterAnimationFrame::new(kind, false), model_scale)
|
||||
}
|
||||
pub fn frame_count(&self, kind: AnimKind) -> Option<u32> {
|
||||
self.clip(kind).map(|clip| clip.frames.len() as u32)
|
||||
@@ -60,27 +77,10 @@ impl BakedFighterAnimations {
|
||||
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(),
|
||||
)
|
||||
let clip = self.clip(kind)?;
|
||||
Some((*clip.root_positions.last()? - *clip.root_positions.first()?) * model_scale)
|
||||
}
|
||||
/// Samples one bone in model space without depending on the runtime's
|
||||
/// previous-frame buffers. Tooling uses this when the user scrubs between
|
||||
@@ -124,13 +124,12 @@ pub struct FighterBoneMatrices {
|
||||
model_matrices: Vec<Option<FixedMat4>>,
|
||||
presentation_kind: Option<AnimKind>,
|
||||
previous_animation_frame: Option<FighterAnimationFrame>,
|
||||
root_motion_position: Option<FGVec3>,
|
||||
/// 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,
|
||||
cached_root_motion_kind: Option<AnimKind>,
|
||||
cached_root_motion_cycle_delta: FGVec3,
|
||||
}
|
||||
|
||||
#[derive(Component, Clone, Debug, Default)]
|
||||
@@ -149,7 +148,8 @@ impl FighterBoneMatrices {
|
||||
};
|
||||
self.set_pose_with_frame(
|
||||
*frame,
|
||||
"root",
|
||||
Some(baked.root_bone_index),
|
||||
baked.sample_root_position(frame, model_scale),
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
frame.kind,
|
||||
@@ -203,6 +203,11 @@ impl FighterBoneMatrices {
|
||||
pub fn root_motion_delta(&self) -> FGVec3 {
|
||||
self.root_motion_delta
|
||||
}
|
||||
/// Current raw, scaled model-space position of the configured root-motion
|
||||
/// bone. The published skeleton has this translation removed.
|
||||
pub fn root_motion_position(&self) -> Option<FGVec3> {
|
||||
self.root_motion_position
|
||||
}
|
||||
pub fn iter(&self) -> impl Iterator<Item = (&str, FixedMat4)> {
|
||||
self.bone_names
|
||||
.iter()
|
||||
@@ -236,7 +241,8 @@ impl FighterBoneMatrices {
|
||||
) {
|
||||
self.set_pose_with_frame(
|
||||
FighterAnimationFrame::new(kind, false),
|
||||
"",
|
||||
None,
|
||||
None,
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
kind,
|
||||
@@ -249,7 +255,8 @@ impl FighterBoneMatrices {
|
||||
pub(super) fn set_pose_with_frame(
|
||||
&mut self,
|
||||
animation_frame: FighterAnimationFrame,
|
||||
root_motion_bone: &str,
|
||||
root_bone_index: Option<usize>,
|
||||
root_motion_position: Option<FGVec3>,
|
||||
root_motion_cycle_delta: FGVec3,
|
||||
root_motion_loop_frame_count: Option<u32>,
|
||||
kind: AnimKind,
|
||||
@@ -260,7 +267,8 @@ impl FighterBoneMatrices {
|
||||
) {
|
||||
self.sample_pose(
|
||||
animation_frame,
|
||||
root_motion_bone,
|
||||
root_bone_index,
|
||||
root_motion_position,
|
||||
root_motion_cycle_delta,
|
||||
root_motion_loop_frame_count,
|
||||
kind,
|
||||
@@ -282,7 +290,8 @@ impl FighterBoneMatrices {
|
||||
) {
|
||||
self.resample_pose_with_frame(
|
||||
FighterAnimationFrame::new(kind, false),
|
||||
"",
|
||||
None,
|
||||
None,
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
kind,
|
||||
@@ -295,7 +304,8 @@ impl FighterBoneMatrices {
|
||||
pub(super) fn resample_pose_with_frame(
|
||||
&mut self,
|
||||
animation_frame: FighterAnimationFrame,
|
||||
root_motion_bone: &str,
|
||||
root_bone_index: Option<usize>,
|
||||
root_motion_position: Option<FGVec3>,
|
||||
_root_motion_cycle_delta: FGVec3,
|
||||
_root_motion_loop_frame_count: Option<u32>,
|
||||
kind: AnimKind,
|
||||
@@ -306,7 +316,8 @@ impl FighterBoneMatrices {
|
||||
) {
|
||||
self.sample_pose(
|
||||
animation_frame,
|
||||
root_motion_bone,
|
||||
root_bone_index,
|
||||
root_motion_position,
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
kind,
|
||||
@@ -320,7 +331,8 @@ impl FighterBoneMatrices {
|
||||
fn sample_pose(
|
||||
&mut self,
|
||||
animation_frame: FighterAnimationFrame,
|
||||
root_motion_bone: &str,
|
||||
root_index: Option<usize>,
|
||||
raw_root_motion_translation: Option<FGVec3>,
|
||||
root_motion_cycle_delta: FGVec3,
|
||||
root_motion_loop_frame_count: Option<u32>,
|
||||
kind: AnimKind,
|
||||
@@ -360,16 +372,6 @@ impl FighterBoneMatrices {
|
||||
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"),
|
||||
)
|
||||
};
|
||||
let model_scale = FixedMat4 {
|
||||
cols: [
|
||||
[model_scale, FGi32::ZERO, FGi32::ZERO, FGi32::ZERO],
|
||||
@@ -378,21 +380,8 @@ impl FighterBoneMatrices {
|
||||
[FGi32::ZERO, FGi32::ZERO, FGi32::ZERO, FGi32::ONE],
|
||||
],
|
||||
};
|
||||
// Root motion only needs the configured bone's model transform. Resolve
|
||||
// that ancestry first, then do the normal full-pose pass once below.
|
||||
// Previously the complete hierarchy was resolved once before removing
|
||||
// root translation and once afterwards.
|
||||
let raw_root_motion_translation = root_index.map(|root_index| {
|
||||
self.model_matrices.resize(local.len(), None);
|
||||
self.model_matrices.fill(None);
|
||||
let root = resolve_model_transform(root_index, parents, local, &mut self.model_matrices)
|
||||
.expect("baked bone hierarchy should be valid");
|
||||
model_scale.mul(root).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.
|
||||
// Gameplay always owns root displacement. Keep rotation and scale in
|
||||
// the pose while removing the configured root bone's translation.
|
||||
if let Some(root_index) = root_index {
|
||||
self.local_matrices[root_index].translate(FGVec3::ZERO);
|
||||
}
|
||||
@@ -419,7 +408,8 @@ impl FighterBoneMatrices {
|
||||
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
|
||||
previous.kind == animation_frame.kind
|
||||
&& previous.frame.saturating_add(1) == animation_frame.frame
|
||||
})
|
||||
&& root_motion_loop_frame_count == Some(animation_frame.frame)
|
||||
{
|
||||
@@ -429,6 +419,7 @@ impl FighterBoneMatrices {
|
||||
} else {
|
||||
FGVec3::ZERO
|
||||
};
|
||||
self.root_motion_position = raw_root_motion_translation;
|
||||
self.previous_root_motion_translation = raw_root_motion_translation;
|
||||
if advance_history {
|
||||
self.previous_animation_frame = Some(animation_frame);
|
||||
@@ -519,23 +510,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 = if matrices.cached_root_motion_kind == Some(frame.kind) {
|
||||
matrices.cached_root_motion_cycle_delta
|
||||
} else {
|
||||
let delta = baked
|
||||
.root_motion_cycle_delta(frame.kind, &manifest.root_motion_bone, manifest.model_scale)
|
||||
.unwrap_or(FGVec3::ZERO);
|
||||
matrices.cached_root_motion_kind = Some(frame.kind);
|
||||
matrices.cached_root_motion_cycle_delta = delta;
|
||||
delta
|
||||
};
|
||||
let root_motion_position = baked.sample_root_position(frame, manifest.model_scale);
|
||||
let root_motion_cycle_delta = baked
|
||||
.root_motion_cycle_delta(frame.kind, 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_with_frame(
|
||||
*frame,
|
||||
&manifest.root_motion_bone,
|
||||
Some(baked.root_bone_index),
|
||||
root_motion_position,
|
||||
root_motion_cycle_delta,
|
||||
root_motion_loop_frame_count,
|
||||
frame.kind,
|
||||
@@ -547,7 +533,8 @@ fn sample_fighter_bone_matrices(
|
||||
} else {
|
||||
matrices.resample_pose_with_frame(
|
||||
*frame,
|
||||
&manifest.root_motion_bone,
|
||||
Some(baked.root_bone_index),
|
||||
root_motion_position,
|
||||
FGVec3::ZERO,
|
||||
None,
|
||||
frame.kind,
|
||||
@@ -568,7 +555,7 @@ fn sample_fighter_bone_matrices(
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_model_transform(
|
||||
pub(super) fn resolve_model_transform(
|
||||
index: usize,
|
||||
parents: &[Option<usize>],
|
||||
local: &[FixedMat4],
|
||||
|
||||
Reference in New Issue
Block a user