Revert "Use Kimodo body motion for regular 360 flips"

This reverts commit c8cfb0b7fd.
This commit is contained in:
chazmm committed 2026-09-07 05:15:49 +01:00
1 parent c8cfb0b7fd
commit a5fab9828a
5 files changed
+3 -409

No files matched your search

+2 -13
View File
@@ -12,31 +12,21 @@ use skate_data::{
animation_frames::{AnimationFrames, ClipFrames, SampleWords},
};
use std::path::Path;
mod custom_tricks;
pub(crate) struct PoseEvaluator {
pub frames: AnimationFrames,
tricks: custom_tricks::Replacements,
}
impl PoseEvaluator {
pub fn load(asset_root: &Path) -> Result<Self, String> {
let mut evaluator = Self::from_banks(&AnimationBanks::load(asset_root)?)?;
evaluator.load_custom_tricks(asset_root)?;
Ok(evaluator)
Self::from_banks(&AnimationBanks::load(asset_root)?)
}
pub fn from_banks(banks: &AnimationBanks) -> Result<Self, String> {
Ok(Self {
frames: AnimationFrames::from_banks(banks)?,
tricks: Default::default(),
})
}
pub fn load_custom_tricks(&mut self, root: &Path) -> Result<(), String> {
self.tricks = custom_tricks::Replacements::load(root, &self.frames)?;
Ok(())
}
/// Executes the ordered stock tree. This uses the native immediate ACS
/// arithmetic; the host does not recreate packed animation job commands.
pub fn evaluate(&self, commands: &[PoseCommand]) -> Result<Vec<Sqt>, String> {
@@ -90,8 +80,7 @@ impl PoseEvaluator {
previous_time,
loops,
} => {
let stock = self.frames.clip(name)?;
let clip = self.tricks.clip(&stock.name).unwrap_or(stock);
let clip = self.frames.clip(name)?;
let mut pose = sample_clip(clip, *time)?;
if self.frames.has_trajectory {
let previous = sample_bone(clip, *previous_time, 0)?;
@@ -1,230 +0,0 @@
//! Optional 360-flip body replacement, baked into the stock G/A clip slots.
//! The stock graph still owns timing, attributes, trajectory and mirroring.
use super::*;
use bevy::math::{Mat4, Quat, Vec3};
use serde::Deserialize;
use std::collections::BTreeMap;
#[derive(Default)]
pub(super) struct Replacements(BTreeMap<String, ClipFrames>);
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct File {
version: u32,
bone_names: Vec<String>,
ground_last_frame: usize,
air_first_frame: usize,
/// Absolute native-space joint matrices, column major; not Blender axes.
frames: Vec<Vec<[f32; 16]>>,
}
impl Replacements {
pub fn load(root: &Path, frames: &AnimationFrames) -> Result<Self, String> {
let path = root.join("private/custom/360flip.json");
let text = match std::fs::read_to_string(&path) {
Ok(text) => text,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Self::default()),
Err(e) => return Err(format!("{}: {e}", path.display())),
};
Self::parse(&text, frames).map_err(|e| format!("{}: {e}", path.display()))
}
fn parse(text: &str, frames: &AnimationFrames) -> Result<Self, String> {
let file: File = serde_json::from_str(text).map_err(|e| e.to_string())?;
file.validate(&frames.bone_names)?;
let bind = &frames.named_pose("RIG_TPOSE")?.samples;
let index = |name: &str| {
frames
.bone_names
.iter()
.position(|n| n == name)
.ok_or_else(|| format!("360-flip skeleton is missing {name}"))
};
let hips = index("HIPS")?;
let board = index("SKATEBOARD_ROOT")?;
let helpers = [
(index("RIGHTTOEBASE_REPARENTED")?, index("RIGHTTOEBASE")?),
(index("LEFTTOEBASE_REPARENTED")?, index("LEFTTOEBASE")?),
(index("RIGHTHAND_REPARENTED")?, index("RIGHTHAND")?),
(index("LEFTHAND_REPARENTED")?, index("LEFTHAND")?),
];
let mut replacements = BTreeMap::new();
// Ordinary ollie 360 flips only. Nollie and dark-catch slots stay stock.
for style in ["D", "GONZ", "HSU"] {
for height in ["LOW", "HIGH"] {
for phase in ["G", "A"] {
let name = format!("360FLIP_{style}_{height}_{phase}");
let stock = frames.clip(&name)?;
let mut baked = stock.frames.clone();
let (start, end) = if phase == "G" {
(0, file.ground_last_frame)
} else {
(file.air_first_frame, file.frames.len() - 1)
};
for (i, row) in baked.iter_mut().enumerate() {
let position = start as f32
+ (end - start) as f32 * i as f32
/ (stock.frames.len() - 1).max(1) as f32;
// Match the stock entry/exit poses over three source
// frames, without restarting the fade at the G/A seam.
let distance = position.min((file.frames.len() - 1) as f32 - position);
let t = (distance / 3.0).clamp(0.0, 1.0);
let weight = t * t * (3.0 - 2.0 * t);
if weight == 0.0 {
continue;
}
let mut globals = file.sample(position);
// Restore board hierarchy from THIS stock style/height
// and frame. Reparented IK targets use that moving board.
let mut stock_globals = vec![Mat4::IDENTITY; row.len()];
for bone in 1..row.len() {
let parent = frames.parents[bone];
let local =
matrix(pose_add::add(sqt(row[bone]), sqt(bind[bone]), true));
stock_globals[bone] = if parent > 0 {
stock_globals[parent as usize] * local
} else {
local
};
}
globals[0] = Mat4::IDENTITY;
for bone in board..helpers.iter().map(|p| p.0).min().unwrap() {
globals[bone] = stock_globals[bone];
}
for &(helper, joint) in &helpers {
globals[helper] = globals[joint];
}
for bone in (hips..board).chain(helpers.iter().map(|p| p.0)) {
let parent = frames.parents[bone];
let local = if parent > 0 {
globals[parent as usize].inverse() * globals[bone]
} else {
globals[bone]
};
let replacement = remove_reference(local, sqt(bind[bone]));
let blended =
pose_blend::blend_sample(sqt(row[bone]), replacement, weight);
row[bone] = words(blended);
}
}
replacements.insert(
name.clone(),
ClipFrames {
name,
source_offset: stock.source_offset,
fps_bits: stock.fps_bits,
loop_translation_bits: stock.loop_translation_bits,
loop_rotation_bits: stock.loop_rotation_bits,
channel_animation: stock.channel_animation,
channel_weights: stock.channel_weights.clone(),
frames: baked,
},
);
}
}
}
Ok(Self(replacements))
}
pub fn clip(&self, name: &str) -> Option<&ClipFrames> {
self.0.get(name)
}
}
impl File {
fn validate(&self, names: &[String]) -> Result<(), String> {
if self.version != 1
|| self.bone_names != names
|| self.frames.len() < 4
|| self.frames.len() > 600
|| self.ground_last_frame == 0
|| self.ground_last_frame.checked_add(1) != Some(self.air_first_frame)
|| self.air_first_frame >= self.frames.len() - 1
{
return Err("Invalid 360-flip format, skeleton or ground/air split".into());
}
for row in &self.frames {
if row.len() != names.len() {
return Err("Incomplete 360-flip frame".into());
}
for values in row {
let m = Mat4::from_cols_array(values);
let (scale, rotation, _) = m.to_scale_rotation_translation();
if !m.is_finite()
|| (m.w_axis.w - 1.0).abs() > 0.001
|| [m.x_axis.w, m.y_axis.w, m.z_axis.w]
.iter()
.any(|w| w.abs() > 0.001)
|| scale.min_element() < 0.95
|| scale.max_element() > 1.05
|| !rotation.is_finite()
|| (rotation.length_squared() - 1.0).abs() > 0.002
{
return Err("Invalid 360-flip joint transform".into());
}
}
}
Ok(())
}
fn sample(&self, position: f32) -> Vec<Mat4> {
let first = position.floor() as usize;
let second = (first + 1).min(self.frames.len() - 1);
self.frames[first]
.iter()
.zip(&self.frames[second])
.map(|(a, b)| {
let (sa, qa, ta) = Mat4::from_cols_array(a).to_scale_rotation_translation();
let (sb, qb, tb) = Mat4::from_cols_array(b).to_scale_rotation_translation();
let t = position.fract();
Mat4::from_scale_rotation_translation(
sa.lerp(sb, t),
qa.slerp(qb, t),
ta.lerp(tb, t),
)
})
.collect()
}
}
fn matrix(s: Sqt) -> Mat4 {
Mat4::from_scale_rotation_translation(
Vec3::from_slice(&s.scale),
Quat::from_array(s.rotation),
Vec3::from_slice(&s.translation),
)
}
fn remove_reference(local: Mat4, reference: Sqt) -> Sqt {
let (scale, rotation, translation) = local.to_scale_rotation_translation();
let inverse = Quat::from_array(reference.rotation).inverse();
let scale = scale / Vec3::from_slice(&reference.scale);
// AddSQT rotates translation without applying the reference scale.
let translation = inverse * (translation - Vec3::from_slice(&reference.translation));
Sqt {
scale: [scale.x, scale.y, scale.z, 1.0],
rotation: (inverse * rotation).normalize().to_array(),
translation: [translation.x, translation.y, translation.z, 1.0],
}
}
fn words(s: Sqt) -> SampleWords {
[
s.scale[0],
s.scale[1],
s.scale[2],
s.rotation[0],
s.rotation[1],
s.rotation[2],
s.rotation[3],
s.translation[0],
s.translation[1],
s.translation[2],
]
.map(f32::to_bits)
}
#[cfg(test)]
#[path = "custom_tricks_tests.rs"]
mod tests;
@@ -1,123 +0,0 @@
use super::*;
#[test]
fn removing_reference_roundtrips_native_add_including_scaled_reference() {
let reference = Sqt {
scale: [1.2, 0.8, 1.1, 1.0],
rotation: Quat::from_rotation_y(0.8).to_array(),
translation: [0.2, 0.9, -0.3, 1.0],
};
let motion = Sqt {
scale: [0.9, 1.1, 1.0, 1.0],
rotation: Quat::from_rotation_x(-0.3).to_array(),
translation: [-0.4, 0.1, 0.2, 1.0],
};
let actual = remove_reference(matrix(pose_add::add(motion, reference, true)), reference);
assert!(matrix(actual).abs_diff_eq(matrix(motion), 0.00001));
}
#[test]
fn replacement_rejects_wrong_skeleton_split_and_nonfinite_samples() {
let names = vec!["HIPS".to_string()];
let mut file = File {
version: 1,
bone_names: names.clone(),
ground_last_frame: 1,
air_first_frame: 2,
frames: vec![vec![Mat4::IDENTITY.to_cols_array()]; 4],
};
assert!(file.validate(&names).is_ok());
assert!(file.validate(&["HEAD".into()]).is_err());
file.air_first_frame = 3;
assert!(file.validate(&names).is_err());
file.air_first_frame = 2;
file.frames[1][0][12] = f32::NAN;
assert!(file.validate(&names).is_err());
}
#[test]
#[ignore = "requires private stock banks and exported Kimodo body"]
fn kimodo_360flip_preserves_stock_board_timing_channels_and_other_tricks() {
let root = std::env::var_os("SKATE3_ASSET_ROOT").expect("Set SKATE3_ASSET_ROOT");
let root = Path::new(&root);
let banks = AnimationBanks::load(root).unwrap();
let stock = PoseEvaluator::from_banks(&banks).unwrap();
let custom = PoseEvaluator::load(root).unwrap();
assert_eq!(custom.tricks.0.len(), 12);
let command = |name: &str, time| PoseCommand::Clip {
name: name.into(),
time,
previous_time: 0.0,
loops: 0,
};
for (name, replacement) in &custom.tricks.0 {
let original = stock.frames.clip(name).unwrap();
assert_eq!(replacement.fps_bits, original.fps_bits);
assert_eq!(replacement.frames.len(), original.frames.len());
assert_eq!(replacement.channel_weights, original.channel_weights);
assert_eq!(replacement.loop_rotation_bits, original.loop_rotation_bits);
assert_eq!(
replacement.loop_translation_bits,
original.loop_translation_bits
);
for (a, b) in original.frames.iter().zip(&replacement.frames) {
for bone in std::iter::once(0).chain(25..32) {
assert_eq!(a[bone], b[bone], "{name} changed board/trajectory {bone}");
}
}
if name.ends_with("_G") {
assert_eq!(original.frames[0], replacement.frames[0]);
}
if name.ends_with("_A") {
assert_eq!(original.frames.last(), replacement.frames.last());
}
assert_ne!(
original.frames[original.frames.len() / 2][5],
replacement.frames[replacement.frames.len() / 2][5],
"{name} did not replace the torso"
);
for fraction in [0.0, 0.125, 0.5, 0.875, 1.0] {
let time =
fraction * (original.frames.len() - 1) as f32 / f32::from_bits(original.fps_bits);
let a = stock.evaluate(&[command(name, time)]).unwrap();
let b = custom.evaluate(&[command(name, time)]).unwrap();
for (a, b) in a.iter().zip(&b) {
assert_eq!(a.translation[3], b.translation[3]);
}
for mirrored in [false, true] {
let mut commands = vec![
command(name, time),
PoseCommand::Pose {
name: "RIG_TPOSE".into(),
},
PoseCommand::Add { motion_is_a: true },
];
if mirrored {
commands.push(PoseCommand::Mirror { trajectory_mode: 0 });
}
let pose = custom.evaluate(&commands).unwrap();
let hierarchy = custom.hierarchy(&pose).unwrap();
assert!(hierarchy.iter().flatten().flatten().all(|v| v.is_finite()));
if !mirrored && name.starts_with("360FLIP_D_HIGH") {
if let Some(folder) = std::env::var_os("SKATE3_CUSTOM_CAPTURE") {
let folder = Path::new(&folder);
std::fs::create_dir_all(folder).unwrap();
let value = serde_json::json!({"names":custom.frames.bone_names,
"pose":hierarchy,"root":Mat4::IDENTITY.to_cols_array_2d(),"board_state":1});
std::fs::write(
folder.join(format!("{name}_{}.json", (fraction * 1000.) as u32)),
serde_json::to_string(&value).unwrap(),
)
.unwrap();
}
}
}
}
}
for name in ["N_360FLIP_HIGH_G", "N_360FLIP_HIGH_A", "R_IDLE_HCOM_000"] {
assert_eq!(
stock.evaluate(&[command(name, 0.1)]).unwrap(),
custom.evaluate(&[command(name, 0.1)]).unwrap()
);
}
}
+1 -2
View File
@@ -82,8 +82,7 @@ impl SkaterAnimation {
pro_skater: &[u8],
) -> Result<Self, String> {
let banks = skate_data::animation_banks::AnimationBanks::load(root)?;
let mut evaluator = PoseEvaluator::from_banks(&banks)?;
evaluator.load_custom_tricks(root)?;
let evaluator = PoseEvaluator::from_banks(&banks)?;
let state = state::AnimationState::new(true);
let count = evaluator.frames.bone_names.len();
let mut motion = MotionHost::from_graph(